794 lines
22 KiB
Dart
794 lines
22 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/services.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:pdf/pdf.dart';
|
|
import 'package:pdf/widgets.dart' as pw;
|
|
|
|
import '../models/work_order.dart';
|
|
|
|
enum PdfReportType {
|
|
client(
|
|
label: 'Cliente',
|
|
description: 'Incluye mano de obra, repuestos y valores.',
|
|
fileSegment: 'cliente',
|
|
),
|
|
certifier(
|
|
label: 'Empresa certificadora',
|
|
description: 'Omite repuestos y toda la información económica.',
|
|
fileSegment: 'certificadora',
|
|
);
|
|
|
|
const PdfReportType({
|
|
required this.label,
|
|
required this.description,
|
|
required this.fileSegment,
|
|
});
|
|
|
|
final String label;
|
|
final String description;
|
|
final String fileSegment;
|
|
}
|
|
|
|
class PdfService {
|
|
static const _reportFilePrefix = 'OT_v12_';
|
|
|
|
bool isCurrentReport(String filePath, {PdfReportType? type}) {
|
|
final expectedPrefix = type == null
|
|
? _reportFilePrefix
|
|
: '$_reportFilePrefix${type.fileSegment}_';
|
|
return p.basename(filePath).startsWith(expectedPrefix);
|
|
}
|
|
|
|
Future<String> generate(
|
|
WorkOrder order, {
|
|
required PdfReportType type,
|
|
}) async {
|
|
final reportBytes = await build(order, type: type);
|
|
final documents = await getApplicationDocumentsDirectory();
|
|
final reportDirectory = Directory(
|
|
p.join(documents.path, 'ot_movil', 'reports'),
|
|
);
|
|
await reportDirectory.create(recursive: true);
|
|
final shortId = order.id.replaceAll('-', '').substring(0, 8);
|
|
final reportPath = p.join(
|
|
reportDirectory.path,
|
|
'$_reportFilePrefix${type.fileSegment}_'
|
|
'${DateFormat('yyyyMMdd_HHmm').format(DateTime.now())}_$shortId.pdf',
|
|
);
|
|
await File(reportPath).writeAsBytes(reportBytes, flush: true);
|
|
return reportPath;
|
|
}
|
|
|
|
Future<Uint8List> build(
|
|
WorkOrder order, {
|
|
required PdfReportType type,
|
|
}) async {
|
|
final document = pw.Document(
|
|
title: 'Orden de trabajo - ${order.title}',
|
|
author: 'SMoya Gasfiter',
|
|
subject: 'Informe de mantenimiento para ${type.label.toLowerCase()}',
|
|
);
|
|
final logoData = await rootBundle.load(
|
|
'assets/pdf/simon_moya_report_logo.png',
|
|
);
|
|
final logo = pw.MemoryImage(
|
|
logoData.buffer.asUint8List(
|
|
logoData.offsetInBytes,
|
|
logoData.lengthInBytes,
|
|
),
|
|
);
|
|
final signatureData = await rootBundle.load(
|
|
'assets/pdf/simon_moya_signature.png',
|
|
);
|
|
final signature = pw.MemoryImage(
|
|
signatureData.buffer.asUint8List(
|
|
signatureData.offsetInBytes,
|
|
signatureData.lengthInBytes,
|
|
),
|
|
);
|
|
final taskPhotos = <String, _TaskImages>{};
|
|
for (final task in order.tasks) {
|
|
taskPhotos[task.id] = await _loadTaskImages(task);
|
|
}
|
|
|
|
const primary = PdfColor.fromInt(0xFF0B5C5E);
|
|
const dark = PdfColor.fromInt(0xFF163334);
|
|
const muted = PdfColor.fromInt(0xFF5F6F70);
|
|
const pale = PdfColor.fromInt(0xFFE7F1F0);
|
|
final dateFormat = DateFormat('dd/MM/yyyy HH:mm');
|
|
final priceFormat = NumberFormat.decimalPattern('es_CL');
|
|
|
|
document.addPage(
|
|
pw.MultiPage(
|
|
pageTheme: pw.PageTheme(
|
|
pageFormat: PdfPageFormat.a4,
|
|
margin: const pw.EdgeInsets.fromLTRB(38, 42, 38, 42),
|
|
),
|
|
header: (context) => _reportHeader(
|
|
order,
|
|
logo,
|
|
type: type,
|
|
primary: primary,
|
|
dark: dark,
|
|
muted: muted,
|
|
),
|
|
footer: (context) => _reportFooter(context, muted),
|
|
build: (context) => [
|
|
pw.SizedBox(height: 12),
|
|
pw.Text(
|
|
order.title,
|
|
style: pw.TextStyle(
|
|
color: dark,
|
|
fontSize: 21,
|
|
fontWeight: pw.FontWeight.bold,
|
|
),
|
|
),
|
|
pw.SizedBox(height: 6),
|
|
pw.Text(
|
|
'Orden de trabajo finalizada',
|
|
style: const pw.TextStyle(color: primary, fontSize: 9),
|
|
),
|
|
pw.SizedBox(height: 15),
|
|
pw.Container(
|
|
padding: const pw.EdgeInsets.symmetric(horizontal: 10, vertical: 9),
|
|
decoration: pw.BoxDecoration(
|
|
color: pale,
|
|
borderRadius: pw.BorderRadius.circular(7),
|
|
),
|
|
child: pw.Row(
|
|
children: [
|
|
_summaryItem(
|
|
'Creada',
|
|
dateFormat.format(order.createdAt.toLocal()),
|
|
dark,
|
|
muted,
|
|
),
|
|
_summaryDivider(),
|
|
_summaryItem(
|
|
'Finalizada',
|
|
dateFormat.format(
|
|
(order.completedAt ?? order.updatedAt).toLocal(),
|
|
),
|
|
dark,
|
|
muted,
|
|
),
|
|
_summaryDivider(),
|
|
_summaryItem(
|
|
'Contenido',
|
|
'${order.tasks.length} tareas - ${order.totalPhotos} fotos',
|
|
dark,
|
|
muted,
|
|
),
|
|
if (type == PdfReportType.client) ...[
|
|
_summaryDivider(),
|
|
_summaryItem(
|
|
'Total OT',
|
|
'\$${priceFormat.format(order.totalCost)}',
|
|
dark,
|
|
muted,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
pw.SizedBox(height: 20),
|
|
...order.tasks.expand(
|
|
(task) => _taskWidgets(
|
|
task,
|
|
taskPhotos[task.id]!,
|
|
primary: primary,
|
|
dark: dark,
|
|
muted: muted,
|
|
pale: pale,
|
|
priceFormat: priceFormat,
|
|
includeParts: type == PdfReportType.client,
|
|
),
|
|
),
|
|
if (type == PdfReportType.client) ...[
|
|
pw.SizedBox(height: 2),
|
|
_costSummary(
|
|
order,
|
|
primary: primary,
|
|
dark: dark,
|
|
muted: muted,
|
|
pale: pale,
|
|
priceFormat: priceFormat,
|
|
),
|
|
],
|
|
pw.SizedBox(height: 8),
|
|
_signatureBlock(signature, dark: dark, muted: muted),
|
|
],
|
|
),
|
|
);
|
|
|
|
return document.save();
|
|
}
|
|
|
|
pw.Widget _reportHeader(
|
|
WorkOrder order,
|
|
pw.ImageProvider logo, {
|
|
required PdfReportType type,
|
|
required PdfColor primary,
|
|
required PdfColor dark,
|
|
required PdfColor muted,
|
|
}) {
|
|
return pw.Container(
|
|
height: 78,
|
|
padding: const pw.EdgeInsets.only(bottom: 10),
|
|
decoration: pw.BoxDecoration(
|
|
border: pw.Border(bottom: pw.BorderSide(color: primary, width: 1.5)),
|
|
),
|
|
child: pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
pw.SizedBox(
|
|
width: 175,
|
|
height: 67,
|
|
child: pw.Image(
|
|
logo,
|
|
fit: pw.BoxFit.cover,
|
|
alignment: pw.Alignment.center,
|
|
),
|
|
),
|
|
pw.SizedBox(
|
|
width: 245,
|
|
child: pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
|
children: [
|
|
pw.Text(
|
|
'Informe para ${type.label.toLowerCase()}',
|
|
style: pw.TextStyle(color: muted, fontSize: 9),
|
|
),
|
|
pw.SizedBox(height: 7),
|
|
_headerDetail(
|
|
'Cliente',
|
|
order.details.client.isEmpty
|
|
? 'No informado'
|
|
: order.details.client,
|
|
dark,
|
|
muted,
|
|
),
|
|
pw.SizedBox(height: 3),
|
|
_headerDetail(
|
|
'Dirección',
|
|
order.details.address.isEmpty
|
|
? 'No informada'
|
|
: order.details.address,
|
|
dark,
|
|
muted,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
pw.Widget _reportFooter(pw.Context context, PdfColor muted) {
|
|
return pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
pw.Text(
|
|
'Generado por SMoya Gasfiter',
|
|
style: pw.TextStyle(color: muted, fontSize: 8),
|
|
),
|
|
pw.Text(
|
|
'Página ${context.pageNumber} de ${context.pagesCount}',
|
|
style: pw.TextStyle(color: muted, fontSize: 8),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
pw.Widget _headerDetail(
|
|
String label,
|
|
String value,
|
|
PdfColor dark,
|
|
PdfColor muted,
|
|
) {
|
|
return pw.RichText(
|
|
textAlign: pw.TextAlign.right,
|
|
text: pw.TextSpan(
|
|
children: [
|
|
pw.TextSpan(
|
|
text: '${label.toUpperCase()}: ',
|
|
style: pw.TextStyle(
|
|
color: muted,
|
|
fontSize: 6.5,
|
|
fontWeight: pw.FontWeight.bold,
|
|
letterSpacing: .35,
|
|
),
|
|
),
|
|
pw.TextSpan(
|
|
text: value,
|
|
style: pw.TextStyle(
|
|
color: dark,
|
|
fontSize: 7.5,
|
|
fontWeight: pw.FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
pw.Widget _summaryItem(
|
|
String label,
|
|
String value,
|
|
PdfColor dark,
|
|
PdfColor muted,
|
|
) {
|
|
return pw.Expanded(
|
|
child: pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
pw.Text(
|
|
label.toUpperCase(),
|
|
style: pw.TextStyle(
|
|
color: muted,
|
|
fontSize: 6.5,
|
|
fontWeight: pw.FontWeight.bold,
|
|
letterSpacing: .55,
|
|
),
|
|
),
|
|
pw.SizedBox(height: 3),
|
|
pw.Text(
|
|
value,
|
|
style: pw.TextStyle(
|
|
color: dark,
|
|
fontSize: 8,
|
|
fontWeight: pw.FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
pw.Widget _summaryDivider() {
|
|
return pw.Container(
|
|
height: 24,
|
|
margin: const pw.EdgeInsets.symmetric(horizontal: 8),
|
|
decoration: const pw.BoxDecoration(
|
|
border: pw.Border(
|
|
left: pw.BorderSide(color: PdfColor.fromInt(0xFFB8CFCD)),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
List<pw.Widget> _taskWidgets(
|
|
WorkTask task,
|
|
_TaskImages images, {
|
|
required PdfColor primary,
|
|
required PdfColor dark,
|
|
required PdfColor muted,
|
|
required PdfColor pale,
|
|
required NumberFormat priceFormat,
|
|
required bool includeParts,
|
|
}) {
|
|
return [
|
|
pw.NewPage(freeSpace: includeParts && task.parts.isNotEmpty ? 220 : 145),
|
|
pw.Container(
|
|
padding: const pw.EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
decoration: pw.BoxDecoration(
|
|
color: dark,
|
|
borderRadius: pw.BorderRadius.circular(5),
|
|
),
|
|
child: pw.Row(
|
|
children: [
|
|
pw.Container(
|
|
width: 23,
|
|
height: 23,
|
|
alignment: pw.Alignment.center,
|
|
decoration: pw.BoxDecoration(
|
|
color: primary,
|
|
shape: pw.BoxShape.circle,
|
|
),
|
|
child: pw.Text(
|
|
'${task.position + 1}',
|
|
style: pw.TextStyle(
|
|
color: PdfColors.white,
|
|
fontSize: 10,
|
|
fontWeight: pw.FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
pw.SizedBox(width: 9),
|
|
pw.Expanded(
|
|
child: pw.Text(
|
|
task.title,
|
|
style: pw.TextStyle(
|
|
color: PdfColors.white,
|
|
fontSize: 13,
|
|
fontWeight: pw.FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
pw.SizedBox(height: 10),
|
|
pw.Text(
|
|
'OBSERVACIÓN',
|
|
style: pw.TextStyle(
|
|
color: primary,
|
|
fontSize: 8,
|
|
fontWeight: pw.FontWeight.bold,
|
|
letterSpacing: .8,
|
|
),
|
|
),
|
|
pw.SizedBox(height: 4),
|
|
pw.Container(
|
|
width: double.infinity,
|
|
padding: const pw.EdgeInsets.all(11),
|
|
decoration: pw.BoxDecoration(
|
|
color: pale,
|
|
borderRadius: pw.BorderRadius.circular(4),
|
|
),
|
|
child: pw.Text(
|
|
task.observation,
|
|
style: pw.TextStyle(color: dark, fontSize: 10, lineSpacing: 3),
|
|
),
|
|
),
|
|
if (includeParts && task.parts.isNotEmpty) ...[
|
|
pw.SizedBox(height: 13),
|
|
..._partsWidgets(
|
|
task.parts,
|
|
task.partsTotal,
|
|
primary: primary,
|
|
dark: dark,
|
|
muted: muted,
|
|
pale: pale,
|
|
priceFormat: priceFormat,
|
|
),
|
|
],
|
|
pw.SizedBox(height: 15),
|
|
..._photoSection(
|
|
'ANTES DE LA MANTENCIÓN',
|
|
images.before,
|
|
primary: primary,
|
|
muted: muted,
|
|
),
|
|
pw.SizedBox(height: 14),
|
|
..._photoSection(
|
|
'DESPUÉS DE LA MANTENCIÓN',
|
|
images.after,
|
|
primary: primary,
|
|
muted: muted,
|
|
),
|
|
pw.SizedBox(height: 28),
|
|
];
|
|
}
|
|
|
|
List<pw.Widget> _partsWidgets(
|
|
List<TaskPart> parts,
|
|
int subtotal, {
|
|
required PdfColor primary,
|
|
required PdfColor dark,
|
|
required PdfColor muted,
|
|
required PdfColor pale,
|
|
required NumberFormat priceFormat,
|
|
}) {
|
|
final widgets = <pw.Widget>[
|
|
pw.Row(
|
|
children: [
|
|
pw.Text(
|
|
'REPUESTOS UTILIZADOS',
|
|
style: pw.TextStyle(
|
|
color: primary,
|
|
fontSize: 8,
|
|
fontWeight: pw.FontWeight.bold,
|
|
letterSpacing: .7,
|
|
),
|
|
),
|
|
pw.Spacer(),
|
|
pw.Text(
|
|
'${parts.length} ${parts.length == 1 ? 'repuesto' : 'repuestos'}',
|
|
style: pw.TextStyle(color: muted, fontSize: 8),
|
|
),
|
|
],
|
|
),
|
|
pw.SizedBox(height: 6),
|
|
pw.Container(
|
|
padding: const pw.EdgeInsets.symmetric(horizontal: 9, vertical: 6),
|
|
decoration: pw.BoxDecoration(
|
|
color: pale,
|
|
borderRadius: pw.BorderRadius.circular(3),
|
|
),
|
|
child: pw.Row(
|
|
children: [
|
|
pw.Expanded(
|
|
child: pw.Text(
|
|
'DESCRIPCIÓN',
|
|
style: pw.TextStyle(
|
|
color: muted,
|
|
fontSize: 6.5,
|
|
fontWeight: pw.FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
pw.SizedBox(
|
|
width: 90,
|
|
child: pw.Text(
|
|
'PRECIO',
|
|
textAlign: pw.TextAlign.right,
|
|
style: pw.TextStyle(
|
|
color: muted,
|
|
fontSize: 6.5,
|
|
fontWeight: pw.FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
for (final part in parts)
|
|
pw.Container(
|
|
padding: const pw.EdgeInsets.symmetric(horizontal: 9, vertical: 6),
|
|
decoration: const pw.BoxDecoration(
|
|
border: pw.Border(
|
|
bottom: pw.BorderSide(
|
|
color: PdfColor.fromInt(0xFFD6E0DF),
|
|
width: .6,
|
|
),
|
|
),
|
|
),
|
|
child: pw.Row(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
pw.Expanded(
|
|
child: pw.Text(
|
|
part.name,
|
|
style: pw.TextStyle(color: dark, fontSize: 8),
|
|
),
|
|
),
|
|
pw.SizedBox(
|
|
width: 90,
|
|
child: pw.Text(
|
|
'\$${priceFormat.format(part.price)}',
|
|
textAlign: pw.TextAlign.right,
|
|
style: pw.TextStyle(
|
|
color: dark,
|
|
fontSize: 8,
|
|
fontWeight: pw.FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
pw.Container(
|
|
padding: const pw.EdgeInsets.only(top: 7),
|
|
child: pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.end,
|
|
children: [
|
|
pw.Text(
|
|
'Subtotal: ',
|
|
style: pw.TextStyle(color: muted, fontSize: 8),
|
|
),
|
|
pw.Text(
|
|
'\$${priceFormat.format(subtotal)}',
|
|
style: pw.TextStyle(
|
|
color: primary,
|
|
fontSize: 9,
|
|
fontWeight: pw.FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
];
|
|
if (parts.length <= 8) {
|
|
return [
|
|
pw.Inseparable(
|
|
child: pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
|
|
children: widgets,
|
|
),
|
|
),
|
|
];
|
|
}
|
|
return widgets;
|
|
}
|
|
|
|
List<pw.Widget> _photoSection(
|
|
String title,
|
|
List<pw.ImageProvider> images, {
|
|
required PdfColor primary,
|
|
required PdfColor muted,
|
|
}) {
|
|
final widgets = <pw.Widget>[
|
|
pw.Row(
|
|
children: [
|
|
pw.Text(
|
|
title,
|
|
style: pw.TextStyle(
|
|
color: primary,
|
|
fontSize: 8,
|
|
fontWeight: pw.FontWeight.bold,
|
|
letterSpacing: .7,
|
|
),
|
|
),
|
|
pw.SizedBox(width: 8),
|
|
pw.Text(
|
|
'${images.length} ${images.length == 1 ? 'foto' : 'fotos'}',
|
|
style: pw.TextStyle(color: muted, fontSize: 8),
|
|
),
|
|
],
|
|
),
|
|
pw.SizedBox(height: 7),
|
|
];
|
|
|
|
for (var index = 0; index < images.length; index += 3) {
|
|
widgets.add(
|
|
pw.Row(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
for (var offset = 0; offset < 3; offset++) ...[
|
|
pw.Expanded(
|
|
child: index + offset < images.length
|
|
? _pdfImage(images[index + offset])
|
|
: pw.SizedBox(),
|
|
),
|
|
if (offset < 2) pw.SizedBox(width: 8),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
widgets.add(pw.SizedBox(height: 8));
|
|
}
|
|
return widgets;
|
|
}
|
|
|
|
pw.Widget _costSummary(
|
|
WorkOrder order, {
|
|
required PdfColor primary,
|
|
required PdfColor dark,
|
|
required PdfColor muted,
|
|
required PdfColor pale,
|
|
required NumberFormat priceFormat,
|
|
}) {
|
|
pw.Widget costRow(String label, int value, {bool emphasize = false}) {
|
|
return pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
pw.Text(
|
|
label,
|
|
style: pw.TextStyle(
|
|
color: emphasize ? dark : muted,
|
|
fontSize: emphasize ? 10 : 9,
|
|
fontWeight: emphasize ? pw.FontWeight.bold : pw.FontWeight.normal,
|
|
),
|
|
),
|
|
pw.Text(
|
|
'\$${priceFormat.format(value)}',
|
|
style: pw.TextStyle(
|
|
color: emphasize ? primary : dark,
|
|
fontSize: emphasize ? 11 : 9,
|
|
fontWeight: pw.FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
return pw.Inseparable(
|
|
child: pw.Container(
|
|
width: double.infinity,
|
|
padding: const pw.EdgeInsets.all(12),
|
|
decoration: pw.BoxDecoration(
|
|
color: pale,
|
|
borderRadius: pw.BorderRadius.circular(6),
|
|
),
|
|
child: pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
|
|
children: [
|
|
pw.Text(
|
|
'RESUMEN DE COSTOS',
|
|
style: pw.TextStyle(
|
|
color: primary,
|
|
fontSize: 8,
|
|
fontWeight: pw.FontWeight.bold,
|
|
letterSpacing: .7,
|
|
),
|
|
),
|
|
pw.SizedBox(height: 9),
|
|
costRow('Costo de mano de obra', order.laborCost),
|
|
pw.SizedBox(height: 6),
|
|
costRow('Total de repuestos', order.totalPartsCost),
|
|
pw.SizedBox(height: 8),
|
|
pw.Divider(color: muted, thickness: .5),
|
|
pw.SizedBox(height: 6),
|
|
costRow('Valor total de la OT', order.totalCost, emphasize: true),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
pw.Widget _signatureBlock(
|
|
pw.ImageProvider signature, {
|
|
required PdfColor dark,
|
|
required PdfColor muted,
|
|
}) {
|
|
return pw.Inseparable(
|
|
child: pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
|
children: [
|
|
pw.SizedBox(width: double.infinity, height: 8),
|
|
pw.Text(
|
|
'Atentamente',
|
|
style: pw.TextStyle(color: muted, fontSize: 8),
|
|
),
|
|
pw.SizedBox(height: 2),
|
|
pw.SizedBox(
|
|
width: 105,
|
|
height: 78,
|
|
child: pw.Image(signature, fit: pw.BoxFit.contain),
|
|
),
|
|
pw.Text(
|
|
'Simón Moya Salinas',
|
|
style: pw.TextStyle(
|
|
color: dark,
|
|
fontSize: 10,
|
|
fontWeight: pw.FontWeight.bold,
|
|
),
|
|
),
|
|
pw.SizedBox(height: 2),
|
|
pw.Text(
|
|
'C.I.: 9.990.302-3 - Instalador de gas autorizado SEC',
|
|
style: pw.TextStyle(color: dark, fontSize: 8),
|
|
),
|
|
pw.SizedBox(height: 2),
|
|
pw.Text(
|
|
'WhatsApp: 933922780',
|
|
style: pw.TextStyle(color: muted, fontSize: 8),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
pw.Widget _pdfImage(pw.ImageProvider image) {
|
|
return pw.Container(
|
|
height: 115,
|
|
decoration: pw.BoxDecoration(
|
|
borderRadius: pw.BorderRadius.circular(4),
|
|
border: pw.Border.all(
|
|
color: const PdfColor.fromInt(0xFFD6E0DF),
|
|
width: .7,
|
|
),
|
|
),
|
|
child: pw.Center(child: pw.Image(image, fit: pw.BoxFit.contain)),
|
|
);
|
|
}
|
|
|
|
Future<_TaskImages> _loadTaskImages(WorkTask task) async {
|
|
final before = <pw.ImageProvider>[];
|
|
final after = <pw.ImageProvider>[];
|
|
for (final photo in task.photos) {
|
|
final file = File(photo.path);
|
|
if (!await file.exists()) continue;
|
|
final Uint8List bytes = await file.readAsBytes();
|
|
final provider = pw.MemoryImage(bytes);
|
|
if (photo.kind == PhotoKind.before) {
|
|
before.add(provider);
|
|
} else {
|
|
after.add(provider);
|
|
}
|
|
}
|
|
return _TaskImages(before: before, after: after);
|
|
}
|
|
}
|
|
|
|
class _TaskImages {
|
|
const _TaskImages({required this.before, required this.after});
|
|
|
|
final List<pw.ImageProvider> before;
|
|
final List<pw.ImageProvider> after;
|
|
}
|