Files
Handwerks_app/services/pdf_export_service.dart
JUSN 9ddce354c0 Feature
ein paar feature aber datenbank macht probleme wenn man aufträge speichern möchge
2026-04-05 12:47:57 +02:00

283 lines
8.7 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:io';
import 'dart:typed_data';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
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';
import '../models/auftrag_status.dart';
import '../models/dokument_typ.dart';
import '../models/zahlungs_status.dart';
import 'sepa_qr_data.dart';
class PdfExportService {
static final _datDe = DateFormat('dd.MM.yyyy');
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;
final docTitel = auftrag.dokumentTyp.pdfTitel;
final epc = SepaQrData.buildEpcString(auftrag);
doc.addPage(
pw.MultiPage(
pageFormat: PdfPageFormat.a4,
margin: const pw.EdgeInsets.all(40),
build: (ctx) => [
pw.Header(
level: 0,
child: pw.Text(
docTitel,
style: pw.TextStyle(
fontSize: 22,
fontWeight: pw.FontWeight.bold,
),
),
),
pw.SizedBox(height: 8),
pw.Text(
'Dokument-Nr.: $nr',
style: pw.TextStyle(
fontSize: 12,
fontWeight: pw.FontWeight.bold,
),
),
pw.SizedBox(height: 6),
pw.Text(
'Bearbeitungsstatus: ${auftrag.status.labelDe} · Zahlung: '
'${auftrag.zahlungsStatus.labelDe}',
style: const pw.TextStyle(fontSize: 10),
),
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.kundenAdresse.isNotEmpty) ...[
pw.SizedBox(height: 4),
pw.Text(
'Adresse: ${auftrag.kundenAdresse}',
style: const pw.TextStyle(fontSize: 11),
),
],
if (auftrag.kundenEmail.isNotEmpty) ...[
pw.SizedBox(height: 4),
pw.Text(
'E-Mail: ${auftrag.kundenEmail}',
style: const pw.TextStyle(fontSize: 11),
),
],
if (auftrag.ustIdKunde.trim().isNotEmpty) ...[
pw.SizedBox(height: 4),
pw.Text(
'USt-IdNr. Kunde: ${auftrag.ustIdKunde.trim()}',
style: const pw.TextStyle(fontSize: 11),
),
],
if (auftrag.leistungsDatum != null) ...[
pw.SizedBox(height: 8),
pw.Text(
'Leistungsdatum: ${_datDe.format(auftrag.leistungsDatum!)}',
style: const pw.TextStyle(fontSize: 11),
),
],
if (auftrag.faelligAm != null) ...[
pw.SizedBox(height: 4),
pw.Text(
'Zahlungsziel / Fälligkeit: ${_datDe.format(auftrag.faelligAm!)}',
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,
),
),
],
if (auftrag.skontoText.trim().isNotEmpty) ...[
pw.SizedBox(height: 8),
pw.Text(
'Skonto / Zahlungsbedingungen: ${auftrag.skontoText.trim()}',
style: const pw.TextStyle(fontSize: 10),
),
],
pw.SizedBox(height: 12),
_steuerHinweiseBlock(auftrag),
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,
),
],
if (epc != null) ...[
pw.SizedBox(height: 24),
pw.Text(
'SEPA-Zahlung (QR-Code für Banking-App)',
style: pw.TextStyle(
fontSize: 11,
fontWeight: pw.FontWeight.bold,
),
),
pw.SizedBox(height: 8),
pw.BarcodeWidget(
data: epc,
barcode: pw.Barcode.qrCode(),
drawText: false,
width: 132,
height: 132,
),
],
pw.SizedBox(height: 28),
pw.Text(
'Hinweis: Dieses Dokument dient der Dokumentation und ersetzt keine '
'steuerliche oder rechtliche Beratung.',
style: pw.TextStyle(fontSize: 8, color: PdfColors.grey700),
),
],
),
);
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 pw.Widget _steuerHinweiseBlock(Auftrag a) {
final parts = <pw.Widget>[];
if (a.kleinunternehmer) {
parts.add(
pw.Text(
'§19 UStG: Als Kleinunternehmer wird keine Umsatzsteuer berechnet.',
style: const pw.TextStyle(fontSize: 9),
),
);
}
if (a.reverseCharge) {
parts.add(
pw.Text(
'Steuerschuldnerschaft des Leistungsempfängers (Reverse Charge) '
'Umsatzsteuer liegt bei Ihnen.',
style: const pw.TextStyle(fontSize: 9),
),
);
}
if (parts.isEmpty) {
return pw.SizedBox(height: 0);
}
return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
'Steuerliche Hinweise',
style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold),
),
pw.SizedBox(height: 4),
...parts.expand((w) => [w, pw.SizedBox(height: 2)]),
],
);
}
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;
}
/// HTTP-URL oder Appwrite-Datei-ID (über [fileLoader]).
static Future<Uint8List?> ladeReferenz(
String ref, {
Future<Uint8List?> Function(String fileId)? fileLoader,
}) async {
if (ref.startsWith('http://') || ref.startsWith('https://')) {
return ladeUrl(ref);
}
if (fileLoader != null) {
try {
return await fileLoader(ref);
} 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',
),
);
}
}