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 buildPdf({ required Auftrag auftrag, required List 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 = []; 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 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 ladeReferenz( String ref, { Future 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 teilen(File file, {String? rechnungsnummer}) async { await SharePlus.instance.share( ShareParams( files: [XFile(file.path)], subject: rechnungsnummer != null && rechnungsnummer.isNotEmpty ? 'Rechnung $rechnungsnummer' : 'Rechnung PDF', ), ); } }