Files
PlayMaker/lib/pages/pdf_export_service.dart
2026-04-14 17:19:21 +01:00

374 lines
16 KiB
Dart

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<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;
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<dynamic> myPlayers = myTeamId != null ? await supabase.from('members').select().eq('team_id', myTeamId).eq('type', 'Jogador') : [];
List<dynamic> 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<String, Map<String, dynamic>> statsMap = {};
for (var s in statsData) {
statsMap[s['member_id'].toString()] = s;
}
List<List<String>> myTeamTable = _buildTeamTableData(myPlayers, statsMap);
List<List<String>> 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<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, 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<List<String>> 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('', 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),
),
]
)
);
}
}