From fb85566e3f0eacc8880bf19b89e2fb3842622e8a Mon Sep 17 00:00:00 2001 From: 230404 <230404@epvc.pt> Date: Tue, 14 Apr 2026 17:19:21 +0100 Subject: [PATCH] fazer pdf --- lib/controllers/placar_controller.dart | 42 +- lib/pages/gamePage.dart | 38 +- lib/pages/pdf_export_service.dart | 374 ++++++++++++++++++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 112 +++++- pubspec.yaml | 2 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 10 files changed, 544 insertions(+), 35 deletions(-) create mode 100644 lib/pages/pdf_export_service.dart diff --git a/lib/controllers/placar_controller.dart b/lib/controllers/placar_controller.dart index 317d9f8..9cf37af 100644 --- a/lib/controllers/placar_controller.dart +++ b/lib/controllers/placar_controller.dart @@ -78,7 +78,6 @@ class PlacarController extends ChangeNotifier { String? pendingPlayerId; List matchShots = []; - // Lista para o Histórico de Jogadas List playByPlay = []; ValueNotifier durationNotifier = ValueNotifier(const Duration(minutes: 10)); @@ -113,7 +112,6 @@ class PlacarController extends ChangeNotifier { gameWasAlreadyFinished = gameResponse['status'] == 'Terminado'; - // CARREGAR HISTÓRICO DA BASE DE DADOS if (gameResponse['play_by_play'] != null) { playByPlay = List.from(gameResponse['play_by_play']); } else { @@ -147,6 +145,7 @@ class PlacarController extends ChangeNotifier { "stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 0, "fls": s['fls'] ?? 0, "fgm": s['fgm'] ?? 0, "fga": s['fga'] ?? 0, "ftm": s['ftm'] ?? 0, "fta": s['fta'] ?? 0, "orb": s['orb'] ?? 0, "drb": s['drb'] ?? 0, + "p2m": s['p2m'] ?? 0, "p2a": s['p2a'] ?? 0, "p3m": s['p3m'] ?? 0, "p3a": s['p3a'] ?? 0, }; myFouls += (s['fls'] as int? ?? 0); } @@ -166,6 +165,7 @@ class PlacarController extends ChangeNotifier { "stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 0, "fls": s['fls'] ?? 0, "fgm": s['fgm'] ?? 0, "fga": s['fga'] ?? 0, "ftm": s['ftm'] ?? 0, "fta": s['fta'] ?? 0, "orb": s['orb'] ?? 0, "drb": s['drb'] ?? 0, + "p2m": s['p2m'] ?? 0, "p2a": s['p2a'] ?? 0, "p3m": s['p3m'] ?? 0, "p3a": s['p3a'] ?? 0, }; opponentFouls += (s['fls'] as int? ?? 0); } @@ -204,7 +204,8 @@ class PlacarController extends ChangeNotifier { playerStats[id] = { "pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, - "fls": 0, "fgm": 0, "fga": 0, "ftm": 0, "fta": 0, "orb": 0, "drb": 0 + "fls": 0, "fgm": 0, "fga": 0, "ftm": 0, "fta": 0, "orb": 0, "drb": 0, + "p2m": 0, "p2a": 0, "p3m": 0, "p3a": 0 }; if (isMyTeam) { @@ -231,7 +232,7 @@ class PlacarController extends ChangeNotifier { 'playerStats': playerStats, 'myCourt': myCourt, 'myBench': myBench, 'oppCourt': oppCourt, 'oppBench': oppBench, 'matchShots': matchShots.map((s) => s.toJson()).toList(), - 'playByPlay': playByPlay, // Guarda o histórico no telemóvel + 'playByPlay': playByPlay, }; await prefs.setString('backup_$gameId', jsonEncode(backupData)); } catch (e) { @@ -357,13 +358,8 @@ class PlacarController extends ChangeNotifier { String name = playerNames[playerId] ?? "Jogador"; matchShots.add(ShotRecord( - relativeX: relativeX, - relativeY: relativeY, - isMake: isMake, - playerId: playerId, - playerName: name, - zone: zone, - points: points + relativeX: relativeX, relativeY: relativeY, isMake: isMake, + playerId: playerId, playerName: name, zone: zone, points: points )); String finalAction = isMake ? "add_pts_$points" : "miss_$points"; @@ -440,8 +436,11 @@ class PlacarController extends ChangeNotifier { int pts = int.parse(action.split("_").last); if (isOpponent) opponentScore += pts; else myScore += pts; stats["pts"] = stats["pts"]! + pts; - if (pts == 2 || pts == 3) { stats["fgm"] = stats["fgm"]! + 1; stats["fga"] = stats["fga"]! + 1; } + + if (pts == 2) { stats["fgm"] = stats["fgm"]! + 1; stats["fga"] = stats["fga"]! + 1; stats["p2m"] = stats["p2m"]! + 1; stats["p2a"] = stats["p2a"]! + 1; } + if (pts == 3) { stats["fgm"] = stats["fgm"]! + 1; stats["fga"] = stats["fga"]! + 1; stats["p3m"] = stats["p3m"]! + 1; stats["p3a"] = stats["p3a"]! + 1; } if (pts == 1) { stats["ftm"] = stats["ftm"]! + 1; stats["fta"] = stats["fta"]! + 1; } + logText = "marcou $pts pontos 🏀"; } else if (action.startsWith("sub_pts_")) { @@ -449,9 +448,18 @@ class PlacarController extends ChangeNotifier { if (isOpponent) { opponentScore = (opponentScore - pts < 0) ? 0 : opponentScore - pts; } else { myScore = (myScore - pts < 0) ? 0 : myScore - pts; } stats["pts"] = (stats["pts"]! - pts < 0) ? 0 : stats["pts"]! - pts; - if (pts == 2 || pts == 3) { + + if (pts == 2) { if (stats["fgm"]! > 0) stats["fgm"] = stats["fgm"]! - 1; if (stats["fga"]! > 0) stats["fga"] = stats["fga"]! - 1; + if (stats["p2m"]! > 0) stats["p2m"] = stats["p2m"]! - 1; + if (stats["p2a"]! > 0) stats["p2a"] = stats["p2a"]! - 1; + } + if (pts == 3) { + if (stats["fgm"]! > 0) stats["fgm"] = stats["fgm"]! - 1; + if (stats["fga"]! > 0) stats["fga"] = stats["fga"]! - 1; + if (stats["p3m"]! > 0) stats["p3m"] = stats["p3m"]! - 1; + if (stats["p3a"]! > 0) stats["p3a"] = stats["p3a"]! - 1; } if (pts == 1) { if (stats["ftm"]! > 0) stats["ftm"] = stats["ftm"]! - 1; @@ -460,12 +468,12 @@ class PlacarController extends ChangeNotifier { logText = "teve $pts pontos retirados ❌"; } else if (action == "miss_1") { stats["fta"] = stats["fta"]! + 1; logText = "falhou lance livre ❌"; } - else if (action == "miss_2" || action == "miss_3") { stats["fga"] = stats["fga"]! + 1; logText = "falhou lançamento ❌"; } + else if (action == "miss_2") { stats["fga"] = stats["fga"]! + 1; stats["p2a"] = stats["p2a"]! + 1; logText = "falhou lançamento de 2 ❌"; } + else if (action == "miss_3") { stats["fga"] = stats["fga"]! + 1; stats["p3a"] = stats["p3a"]! + 1; logText = "falhou lançamento de 3 ❌"; } else if (action == "add_orb") { stats["orb"] = stats["orb"]! + 1; stats["rbs"] = stats["rbs"]! + 1; logText = "ganhou ressalto ofensivo 🔄"; } else if (action == "add_drb") { stats["drb"] = stats["drb"]! + 1; stats["rbs"] = stats["rbs"]! + 1; logText = "ganhou ressalto defensivo 🛡️"; } else if (action == "add_ast") { stats["ast"] = stats["ast"]! + 1; - if (playByPlay.isNotEmpty && playByPlay[0].contains("marcou") && !playByPlay[0].contains("Assistência")) { playByPlay[0] = "${playByPlay[0]} (Assistência: $name 🤝)"; _saveLocalBackup(); @@ -531,7 +539,6 @@ class PlacarController extends ChangeNotifier { if (mvpScore > maxMvpScore && mvpScore > 0) { maxMvpScore = mvpScore; mvpName = pName; } }); - // ATUALIZA O JOGO COM OS NOVOS ESTADOS E COM O HISTÓRICO DE JOGADAS! await supabase.from('games').update({ 'my_score': myScore, 'opponent_score': opponentScore, @@ -545,7 +552,7 @@ class PlacarController extends ChangeNotifier { 'top_rbs_name': topRbsName, 'top_def_name': topDefName, 'mvp_name': mvpName, - 'play_by_play': playByPlay, // Envia o histórico para a base de dados + 'play_by_play': playByPlay, }).eq('id', gameId); if (isGameFinishedNow && !gameWasAlreadyFinished && myTeamDbId != null && oppTeamDbId != null) { @@ -579,6 +586,7 @@ class PlacarController extends ChangeNotifier { batchStats.add({ 'game_id': gameId, 'member_id': playerId, 'team_id': isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!, 'pts': stats['pts'], 'rbs': stats['rbs'], 'ast': stats['ast'], 'stl': stats['stl'], 'blk': stats['blk'], 'tov': stats['tov'], 'fls': stats['fls'], 'fgm': stats['fgm'], 'fga': stats['fga'], 'ftm': stats['ftm'], 'fta': stats['fta'], 'orb': stats['orb'], 'drb': stats['drb'], + 'p2m': stats['p2m'], 'p2a': stats['p2a'], 'p3m': stats['p3m'], 'p3a': stats['p3a'], }); } }); diff --git a/lib/pages/gamePage.dart b/lib/pages/gamePage.dart index 71097ee..102b02d 100644 --- a/lib/pages/gamePage.dart +++ b/lib/pages/gamePage.dart @@ -6,8 +6,8 @@ import '../controllers/team_controller.dart'; import '../controllers/game_controller.dart'; import '../models/game_model.dart'; import '../utils/size_extension.dart'; +import 'pdf_export_service.dart'; -// --- CARD DE EXIBIÇÃO DO JOGO --- class GameResultCard extends StatelessWidget { final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season; final String? myTeamLogo, opponentTeamLogo; @@ -44,13 +44,35 @@ class GameResultCard extends StatelessWidget { Expanded(child: _buildTeamInfo(opponentTeam, Colors.grey.shade600, opponentTeamLogo, sf, textColor)), ], ), + Positioned( top: -10 * sf, right: -10 * sf, - child: IconButton( - icon: Icon(Icons.delete_outline, color: Colors.grey.shade400, size: 22 * sf), - splashRadius: 20 * sf, - onPressed: () => _showDeleteConfirmation(context), + child: Row( + children: [ + IconButton( + icon: Icon(Icons.picture_as_pdf, color: AppTheme.primaryRed.withOpacity(0.8), size: 22 * sf), + splashRadius: 20 * sf, + tooltip: 'Gerar PDF', + onPressed: () async { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('A gerar PDF...'), duration: Duration(seconds: 1))); + await PdfExportService.generateAndPrintBoxScore( + gameId: gameId, + myTeam: myTeam, + opponentTeam: opponentTeam, + myScore: myScore, + opponentScore: opponentScore, + season: season, + ); + }, + ), + IconButton( + icon: Icon(Icons.delete_outline, color: Colors.grey.shade400, size: 22 * sf), + splashRadius: 20 * sf, + tooltip: 'Eliminar Jogo', + onPressed: () => _showDeleteConfirmation(context), + ), + ], ), ), ], @@ -141,7 +163,6 @@ class GameResultCard extends StatelessWidget { ); } -// --- POPUP DE CRIAÇÃO --- class CreateGameDialogManual extends StatefulWidget { final TeamController teamController; final GameController gameController; @@ -280,7 +301,6 @@ class _CreateGameDialogManualState extends State { } } -// --- PÁGINA PRINCIPAL DOS JOGOS --- class GamePage extends StatefulWidget { const GamePage({super.key}); @@ -347,13 +367,11 @@ class _GamePageState extends State { myTeamLogo: myLogo, opponentTeamLogo: oppLogo, sf: context.sf, - onDelete: () async { + onDelete: () async { bool success = await gameController.deleteGame(game.id); if (context.mounted) { if (success) { - // 👇 ISTO FORÇA A LISTA A ATUALIZAR IMEDIATAMENTE 👇 setState(() {}); - ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Jogo eliminado com sucesso!'), backgroundColor: Colors.green) ); diff --git a/lib/pages/pdf_export_service.dart b/lib/pages/pdf_export_service.dart new file mode 100644 index 0000000..6230705 --- /dev/null +++ b/lib/pages/pdf_export_service.dart @@ -0,0 +1,374 @@ +import 'dart:typed_data'; +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 PdfExportService { + static Future 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; + + final gameData = await supabase.from('games').select().eq('id', gameId).single(); + + final teamsData = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]); + String? myTeamId, oppTeamId; + for (var t in teamsData) { + if (t['name'] == myTeam) myTeamId = t['id'].toString(); + if (t['name'] == opponentTeam) oppTeamId = t['id'].toString(); + } + + List myPlayers = myTeamId != null ? await supabase.from('members').select().eq('team_id', myTeamId).eq('type', 'Jogador') : []; + List oppPlayers = oppTeamId != null ? await supabase.from('members').select().eq('team_id', oppTeamId).eq('type', 'Jogador') : []; + + 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; + } + + List> myTeamTable = _buildTeamTableData(myPlayers, statsMap); + List> oppTeamTable = _buildTeamTableData(oppPlayers, statsMap); + + final pdf = pw.Document(); + +pdf.addPage( + pw.Page( // 1. Trocado de MultiPage para Page + pageFormat: PdfPageFormat.a4.landscape, + margin: const pw.EdgeInsets.all(16), // Margens ligeiramente reduzidas para aproveitar o espaço + build: (pw.Context context) { + // 2. Envolvemos tudo num FittedBox + return pw.FittedBox( + fit: pw.BoxFit.scaleDown, // Reduz o tamanho apenas se não couber na página + child: pw.Container( + // Fixamos a largura do contentor à largura útil da página + width: PdfPageFormat.a4.landscape.availableWidth, + // 3. Colocamos todos os elementos dentro de uma Column + 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: 24, fontWeight: pw.FontWeight.bold)), + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.end, + children: [ + pw.Text('$myTeam vs $opponentTeam', style: pw.TextStyle(fontSize: 16, fontWeight: pw.FontWeight.bold)), + pw.Text('Resultado: $myScore - $opponentScore', style: const pw.TextStyle(fontSize: 14)), + pw.Text('Época: $season', style: const pw.TextStyle(fontSize: 12)), + ] + ) + ] + ), + pw.SizedBox(height: 15), // Espaçamentos verticais um pouco mais otimizados + + pw.Text('Equipa: $myTeam', style: pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold, color: const PdfColor.fromInt(0xFFA00000))), + pw.SizedBox(height: 4), + _buildPdfTable(myTeamTable, const PdfColor.fromInt(0xFFA00000)), + + pw.SizedBox(height: 15), + + pw.Text('Equipa: $opponentTeam', style: pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold, color: PdfColors.grey700)), + pw.SizedBox(height: 4), + _buildPdfTable(oppTeamTable, PdfColors.grey700), + + pw.SizedBox(height: 15), + + 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'] ?? '---'), + ] + ), + ], + ), + ), + ); + }, + ), + ); + + await Printing.layoutPdf( + onLayout: (PdfPageFormat format) async => pdf.save(), + name: 'BoxScore_${myTeam}_vs_${opponentTeam}.pdf', + ); + } + + 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, tTr = 0, tStl = 0, tAst = 0, tTov = 0, tBlk = 0; + int tP3m = 0, tP2m = 0, tP3a = 0, tP2a = 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(); + String name = p['name'] ?? 'Desconhecido'; + String number = p['number']?.toString() ?? '-'; + + var stat = statsMap[id] ?? {}; + + int pts = stat['pts'] ?? 0; + int fgm = stat['fgm'] ?? 0; + int fga = stat['fga'] ?? 0; + int ftm = stat['ftm'] ?? 0; + int fta = stat['fta'] ?? 0; + int p2m = stat['p2m'] ?? 0; + int p2a = stat['p2a'] ?? 0; + int p3m = stat['p3m'] ?? 0; + int p3a = stat['p3a'] ?? 0; + int fls = stat['fls'] ?? 0; + int orb = stat['orb'] ?? 0; + int drb = stat['drb'] ?? 0; + int tr = orb + drb; + int stl = stat['stl'] ?? 0; + int ast = stat['ast'] ?? 0; + int tov = stat['tov'] ?? 0; + int blk = stat['blk'] ?? 0; + + tPts += pts; tFgm += fgm; tFga += fga; tFtm += ftm; tFta += fta; + tFls += fls; tOrb += orb; tDrb += drb; tTr += tr; tStl += stl; + tAst += ast; tTov += tov; tBlk += blk; + tP3m += p3m; tP2m += p2m; tP3a += p3a; tP2a += p2a; + + String p2Pct = p2a > 0 ? ((p2m / p2a) * 100).toStringAsFixed(0) + '%' : '0%'; + String p3Pct = p3a > 0 ? ((p3m / p3a) * 100).toStringAsFixed(0) + '%' : '0%'; + String globalPct = fga > 0 ? ((fgm / fga) * 100).toStringAsFixed(0) + '%' : '0%'; + String llPct = fta > 0 ? ((ftm / fta) * 100).toStringAsFixed(0) + '%' : '0%'; + + tableData.add([ + number, name, pts.toString(), + p2m.toString(), p2a.toString(), p2Pct, + p3m.toString(), p3a.toString(), p3Pct, + fgm.toString(), fga.toString(), globalPct, + ftm.toString(), fta.toString(), llPct, + fls.toString(), orb.toString(), drb.toString(), tr.toString(), + stl.toString(), ast.toString(), tov.toString(), blk.toString() + ]); + } + + if (tableData.isEmpty) { + tableData.add([ + '-', 'Sem jogadores registados', '0', + '0', '0', '0%', + '0', '0', '0%', + '0', '0', '0%', + '0', '0', '0%', + '0', '0', '0', '0', '0', '0', '0', '0' + ]); + } + + String tP2Pct = tP2a > 0 ? ((tP2m / tP2a) * 100).toStringAsFixed(0) + '%' : '0%'; + String tP3Pct = tP3a > 0 ? ((tP3m / tP3a) * 100).toStringAsFixed(0) + '%' : '0%'; + String tGlobalPct = tFga > 0 ? ((tFgm / tFga) * 100).toStringAsFixed(0) + '%' : '0%'; + String tLlPct = tFta > 0 ? ((tFtm / tFta) * 100).toStringAsFixed(0) + '%' : '0%'; + + tableData.add([ + '', 'TOTAIS', tPts.toString(), + tP2m.toString(), tP2a.toString(), tP2Pct, + tP3m.toString(), tP3a.toString(), tP3Pct, + tFgm.toString(), tFga.toString(), tGlobalPct, + tFtm.toString(), tFta.toString(), tLlPct, + tFls.toString(), tOrb.toString(), tDrb.toString(), tTr.toString(), + tStl.toString(), tAst.toString(), tTov.toString(), tBlk.toString() + ]); + + return tableData; + } + + static pw.Widget _buildPdfTable(List> data, PdfColor headerColor) { + final headerStyle = pw.TextStyle(color: PdfColors.white, fontWeight: pw.FontWeight.bold, fontSize: 8); + final subHeaderStyle = pw.TextStyle(color: PdfColors.white, fontWeight: pw.FontWeight.bold, fontSize: 7); + final cellStyle = const pw.TextStyle(fontSize: 8); + + // Agora usamos apenas 15 colunas principais na tabela. + // Os grupos (2P, 3P, etc.) são subdivididos INTERNAMENTE para evitar erros de colSpan. + return pw.Table( + border: pw.TableBorder.all(color: PdfColors.grey400, width: 0.5), + columnWidths: { + 0: const pw.FlexColumnWidth(1.2), // Nº + 1: const pw.FlexColumnWidth(5.0), // NOME (Maior para caber nomes como S.Gilgeous-alexander) + 2: const pw.FlexColumnWidth(1.5), // PT + 3: const pw.FlexColumnWidth(4.5), // 2 PONTOS (Grupo de 3) + 4: const pw.FlexColumnWidth(4.5), // 3 PONTOS (Grupo de 3) + 5: const pw.FlexColumnWidth(4.5), // GLOBAL (Grupo de 3) + 6: const pw.FlexColumnWidth(4.5), // L. LIVRES (Grupo de 3) + 7: const pw.FlexColumnWidth(1.5), // FLS + 8: const pw.FlexColumnWidth(1.5), // RO + 9: const pw.FlexColumnWidth(1.5), // RD + 10: const pw.FlexColumnWidth(1.5), // TR + 11: const pw.FlexColumnWidth(1.5), // BR + 12: const pw.FlexColumnWidth(1.5), // AS + 13: const pw.FlexColumnWidth(1.5), // BP + 14: const pw.FlexColumnWidth(1.5), // BLK + }, + children: [ + // --- LINHA 1: CABEÇALHOS --- + pw.TableRow( + decoration: pw.BoxDecoration(color: headerColor), + children: [ + _simpleHeader('Nº', subHeaderStyle), + _simpleHeader('NOME', subHeaderStyle, align: pw.Alignment.centerLeft), + _simpleHeader('PT', subHeaderStyle), + _groupHeader('2 PONTOS', headerStyle, subHeaderStyle), + _groupHeader('3 PONTOS', headerStyle, subHeaderStyle), + _groupHeader('GLOBAL', headerStyle, subHeaderStyle), + _groupHeader('L. LIVRES', headerStyle, subHeaderStyle), + _simpleHeader('FLS', subHeaderStyle), + _simpleHeader('RO', subHeaderStyle), + _simpleHeader('RD', subHeaderStyle), + _simpleHeader('TR', subHeaderStyle), + _simpleHeader('BR', subHeaderStyle), + _simpleHeader('AS', subHeaderStyle), + _simpleHeader('BP', subHeaderStyle), + _simpleHeader('BLK', subHeaderStyle), + ], + ), + // --- LINHAS 2+: DADOS --- + ...data.map((row) { + bool isTotais = row[1] == 'TOTAIS'; + var rowStyle = isTotais ? pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold) : cellStyle; + return pw.TableRow( + decoration: pw.BoxDecoration( + color: isTotais ? PdfColors.grey200 : PdfColors.white, + ), + children: [ + _simpleData(row[0], rowStyle), + _simpleData(row[1], rowStyle, align: pw.Alignment.centerLeft), + _simpleData(row[2], rowStyle), + _groupData(row[3], row[4], row[5], rowStyle), // 2P: C, T, % + _groupData(row[6], row[7], row[8], rowStyle), // 3P: C, T, % + _groupData(row[9], row[10], row[11], rowStyle), // GLOBAL: C, T, % + _groupData(row[12], row[13], row[14], rowStyle), // L. LIVRES: C, T, % + _simpleData(row[15], rowStyle), + _simpleData(row[16], rowStyle), + _simpleData(row[17], rowStyle), + _simpleData(row[18], rowStyle), + _simpleData(row[19], rowStyle), + _simpleData(row[20], rowStyle), + _simpleData(row[21], rowStyle), + _simpleData(row[22], rowStyle), + ], + ); + }), + ], + ); + } + + // ==== WIDGETS AUXILIARES PARA RESOLVER A ESTRUTURA DO PDF ==== + + // Cabeçalho simples (Colunas que não se dividem) + static pw.Widget _simpleHeader(String text, pw.TextStyle style, {pw.Alignment align = pw.Alignment.center}) { + return pw.Container( + alignment: align, + padding: const pw.EdgeInsets.symmetric(vertical: 2, horizontal: 2), + child: pw.Text(text, style: style), + ); + } + + // Dados simples + static pw.Widget _simpleData(String text, pw.TextStyle style, {pw.Alignment align = pw.Alignment.center}) { + return pw.Container( + alignment: align, + padding: const pw.EdgeInsets.symmetric(vertical: 3, horizontal: 2), + child: pw.Text(text, style: style), + ); + } + + // Cria a divisão do Cabeçalho (O falso ColSpan que une "2 PONTOS" sobre "C | T | %") + 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, child: pw.Text('C', style: sStyle))), + pw.Container(width: 0.5, height: 10, color: PdfColors.white), // Divisória vertical manual + pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, child: pw.Text('T', style: sStyle))), + pw.Container(width: 0.5, height: 10, color: PdfColors.white), // Divisória vertical manual + pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, 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: 3), + child: pw.Text(c, style: style), + ), + ), + pw.Container(width: 0.5, height: 12, color: PdfColors.grey400), // Divisória cinza + pw.Expanded( + child: pw.Container( + alignment: pw.Alignment.center, + padding: const pw.EdgeInsets.symmetric(vertical: 3), + child: pw.Text(t, style: style), + ), + ), + pw.Container(width: 0.5, height: 12, color: PdfColors.grey400), // Divisória cinza + pw.Expanded( + child: pw.Container( + alignment: pw.Alignment.center, + padding: const pw.EdgeInsets.symmetric(vertical: 3), + child: pw.Text(pct, style: style), + ), + ), + ], + ); +} + 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(4), + 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(6), + child: pw.Text(value, style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold), textAlign: pw.TextAlign.center), + ), + ] + ) + ); + } +} \ No newline at end of file diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e12c657..f35d186 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { @@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) gtk_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); gtk_plugin_register_with_registrar(gtk_registrar); + g_autoptr(FlPluginRegistrar) printing_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin"); + printing_plugin_register_with_registrar(printing_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 4453582..56309a9 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux gtk + printing url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 7ebbc2f..a5003c3 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import app_links import file_selector_macos import path_provider_foundation +import printing import shared_preferences_foundation import sqflite_darwin import url_launcher_macos @@ -16,6 +17,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 90a8561..0daf6be 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" async: dependency: transitive description: @@ -49,6 +57,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + barcode: + dependency: transitive + description: + name: barcode + sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4" + url: "https://pub.dev" + source: hosted + version: "2.2.9" + bidi: + dependency: transitive + description: + name: bidi + sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d" + url: "https://pub.dev" + source: hosted + version: "2.0.13" boolean_selector: dependency: transitive description: @@ -85,10 +109,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" clock: dependency: transitive description: @@ -296,6 +320,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.dev" + source: hosted + version: "4.8.0" image_cropper: dependency: "direct main" description: @@ -436,18 +468,18 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: @@ -488,6 +520,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: transitive description: @@ -536,6 +576,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + pdf: + dependency: "direct main" + description: + name: pdf + sha256: e47a275b267873d5944ad5f5ff0dcc7ac2e36c02b3046a0ffac9b72fd362c44b + url: "https://pub.dev" + source: hosted + version: "3.12.0" + pdf_widget_wrapper: + dependency: transitive + description: + name: pdf_widget_wrapper + sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" platform: dependency: transitive description: @@ -560,6 +624,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" postgrest: dependency: transitive description: @@ -568,6 +640,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.0" + printing: + dependency: "direct main" + description: + name: printing + sha256: "689170c9ddb1bda85826466ba80378aa8993486d3c959a71cd7d2d80cb606692" + url: "https://pub.dev" + source: hosted + version: "5.14.3" provider: dependency: "direct main" description: @@ -576,6 +656,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5+1" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" realtime_client: dependency: transitive description: @@ -785,10 +873,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.7" typed_data: dependency: transitive description: @@ -917,6 +1005,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" yet_another_json_isolate: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index eb3fb20..d8392a8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,8 @@ dependencies: shimmer: ^3.0.0 cached_network_image: ^3.4.1 shared_preferences: ^2.5.4 + printing: ^5.14.3 + pdf: ^3.12.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 5bbd4c3..4c1d908 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -15,6 +16,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("AppLinksPluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + PrintingPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PrintingPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 79001bc..5a8cfd6 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links file_selector_windows + printing url_launcher_windows )