374 lines
16 KiB
Dart
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('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),
|
|
),
|
|
]
|
|
)
|
|
);
|
|
}
|
|
} |