Files
Handwerks_app/services/pdf_export_service.dart
2026-04-03 20:42:47 +02:00

139 lines
4.4 KiB
Dart

import 'dart:io';
import 'dart:typed_data';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:share_plus/share_plus.dart' show ShareParams, SharePlus, XFile;
import '../models/auftrag.dart';
class PdfExportService {
static Future<File> buildPdf({
required Auftrag auftrag,
required List<Uint8List?> fotoBytes,
Uint8List? unterschriftBytes,
}) async {
final doc = pw.Document();
final nr = auftrag.rechnungsnummer.isEmpty
? 'Entwurf'
: auftrag.rechnungsnummer;
doc.addPage(
pw.MultiPage(
pageFormat: PdfPageFormat.a4,
margin: const pw.EdgeInsets.all(40),
build: (ctx) => [
pw.Header(
level: 0,
child: pw.Text(
'Rechnung',
style: pw.TextStyle(
fontSize: 22,
fontWeight: pw.FontWeight.bold,
),
),
),
pw.SizedBox(height: 8),
pw.Text('Rechnungs-Nr.: $nr',
style: pw.TextStyle(
fontSize: 12,
fontWeight: pw.FontWeight.bold,
)),
pw.SizedBox(height: 16),
pw.Text('Leistung / Titel: ${auftrag.titel}',
style: const pw.TextStyle(fontSize: 14)),
pw.SizedBox(height: 8),
pw.Text('Kunde: ${auftrag.kundenName}',
style: const pw.TextStyle(fontSize: 14)),
if (auftrag.kundenEmail.isNotEmpty) ...[
pw.SizedBox(height: 4),
pw.Text('E-Mail: ${auftrag.kundenEmail}',
style: const pw.TextStyle(fontSize: 11)),
],
if (auftrag.betragText.isNotEmpty) ...[
pw.SizedBox(height: 12),
pw.Text('Betrag (Brutto): ${auftrag.betragText}',
style: pw.TextStyle(
fontSize: 14,
fontWeight: pw.FontWeight.bold,
)),
],
pw.SizedBox(height: 16),
pw.Text('Beschreibung / Positionen:',
style: pw.TextStyle(
fontSize: 12,
fontWeight: pw.FontWeight.bold,
)),
pw.SizedBox(height: 6),
pw.Text(
auftrag.beschreibung.isEmpty ? '' : auftrag.beschreibung,
style: const pw.TextStyle(fontSize: 11),
),
pw.SizedBox(height: 20),
if (fotoBytes.any((b) => b != null && b.isNotEmpty)) ...[
pw.Text('Fotos',
style: pw.TextStyle(
fontSize: 12,
fontWeight: pw.FontWeight.bold,
)),
pw.SizedBox(height: 8),
for (final b in fotoBytes)
if (b != null && b.isNotEmpty)
pw.Padding(
padding: const pw.EdgeInsets.only(bottom: 12),
child: pw.Image(
pw.MemoryImage(b),
fit: pw.BoxFit.contain,
height: 200,
),
),
],
if (unterschriftBytes != null && unterschriftBytes.isNotEmpty) ...[
pw.SizedBox(height: 16),
pw.Text('Unterschrift Kunde',
style: pw.TextStyle(
fontSize: 12,
fontWeight: pw.FontWeight.bold,
)),
pw.SizedBox(height: 8),
pw.Image(
pw.MemoryImage(unterschriftBytes),
height: 100,
fit: pw.BoxFit.contain,
),
],
],
),
);
final bytes = await doc.save();
final dir = await getTemporaryDirectory();
final safeNr = nr.replaceAll(RegExp(r'[^\w\-]'), '_');
final name = 'rechnung_${safeNr.isEmpty ? auftrag.id.substring(0, 8) : safeNr}.pdf';
final file = File('${dir.path}/$name');
await file.writeAsBytes(bytes);
return file;
}
static Future<Uint8List?> ladeUrl(String url) async {
try {
final r = await http.get(Uri.parse(url));
if (r.statusCode == 200) return r.bodyBytes;
} catch (_) {}
return null;
}
static Future<void> teilen(File file, {String? rechnungsnummer}) async {
await SharePlus.instance.share(
ShareParams(
files: [XFile(file.path)],
subject: rechnungsnummer != null && rechnungsnummer.isNotEmpty
? 'Rechnung $rechnungsnummer'
: 'Rechnung PDF',
),
);
}
}