ein paar feature aber datenbank macht probleme wenn man aufträge speichern möchge
This commit is contained in:
2026-04-05 12:47:57 +02:00
parent e1d4bb7edf
commit 9ddce354c0
32 changed files with 3931 additions and 612 deletions

View File

@@ -2,14 +2,21 @@ 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,
@@ -19,6 +26,8 @@ class PdfExportService {
final nr = auftrag.rechnungsnummer.isEmpty
? 'Entwurf'
: auftrag.rechnungsnummer;
final docTitel = auftrag.dokumentTyp.pdfTitel;
final epc = SepaQrData.buildEpcString(auftrag);
doc.addPage(
pw.MultiPage(
@@ -28,7 +37,7 @@ class PdfExportService {
pw.Header(
level: 0,
child: pw.Text(
'Rechnung',
docTitel,
style: pw.TextStyle(
fontSize: 22,
fontWeight: pw.FontWeight.bold,
@@ -36,36 +45,91 @@ class PdfExportService {
),
),
pw.SizedBox(height: 8),
pw.Text('Rechnungs-Nr.: $nr',
style: pw.TextStyle(
fontSize: 12,
fontWeight: pw.FontWeight.bold,
)),
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.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)),
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)),
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,
)),
],
pw.SizedBox(height: 16),
pw.Text('Beschreibung / Positionen:',
pw.Text(
'Betrag (Brutto): ${auftrag.betragText}',
style: pw.TextStyle(
fontSize: 12,
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,
@@ -73,11 +137,13 @@ class PdfExportService {
),
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.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)
@@ -92,11 +158,13 @@ class PdfExportService {
],
if (unterschriftBytes != null && unterschriftBytes.isNotEmpty) ...[
pw.SizedBox(height: 16),
pw.Text('Unterschrift Kunde',
style: pw.TextStyle(
fontSize: 12,
fontWeight: pw.FontWeight.bold,
)),
pw.Text(
'Unterschrift Kunde',
style: pw.TextStyle(
fontSize: 12,
fontWeight: pw.FontWeight.bold,
),
),
pw.SizedBox(height: 8),
pw.Image(
pw.MemoryImage(unterschriftBytes),
@@ -104,6 +172,30 @@ class PdfExportService {
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),
),
],
),
);
@@ -111,12 +203,48 @@ class PdfExportService {
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 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));
@@ -125,6 +253,22 @@ class PdfExportService {
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(