import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_email_sender/flutter_email_sender.dart'; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:signature/signature.dart'; import 'package:speech_to_text/speech_to_text.dart' as stt; import '../../models/auftrag.dart'; import '../../models/auftrag_status.dart'; import '../../models/dokument_typ.dart'; import '../../models/zahlungs_status.dart'; import '../../services/auftrag_repository.dart'; import '../../services/auftrag_storage_service.dart'; import '../../services/pdf_export_service.dart'; import '../../services/pdf_history_service.dart'; import '../../services/position_from_text_parser.dart'; import '../../services/workflow_metrics_service.dart'; import '../../theme/app_theme.dart'; class AuftragBearbeitenScreen extends StatefulWidget { const AuftragBearbeitenScreen({super.key, this.auftragId}); final String? auftragId; @override State createState() => _AuftragBearbeitenScreenState(); } class _AuftragBearbeitenScreenState extends State { final _repo = AuftragRepository(); final _storage = AuftragStorageService(); final _rechnungsnummer = TextEditingController(); final _titel = TextEditingController(); final _beschreibung = TextEditingController(); final _kunde = TextEditingController(); final _kundenAdresse = TextEditingController(); final _kundenEmail = TextEditingController(); final _betrag = TextEditingController(); late final String _docId; late final SignatureController _signatur; final _picker = ImagePicker(); bool _isNeu = true; bool _loading = true; bool _saving = false; List _fotoUrls = []; String? _unterschriftUrl; bool _signaturVorhanden = false; AuftragStatus _status = AuftragStatus.offen; DokumentTyp _dokumentTyp = DokumentTyp.rechnung; ZahlungsStatus _zahlungsStatus = ZahlungsStatus.offen; DateTime? _faelligAm; DateTime? _leistungsDatum; bool _kleinunternehmer = false; bool _reverseCharge = false; final _skonto = TextEditingController(); final _ustId = TextEditingController(); final _iban = TextEditingController(); final _bic = TextEditingController(); final _kontoinhaber = TextEditingController(); final stt.SpeechToText _speech = stt.SpeechToText(); bool _speechReady = false; bool _speechListening = false; static final _datDe = DateFormat('dd.MM.yyyy'); @override void initState() { super.initState(); _signatur = SignatureController( penStrokeWidth: 2.5, penColor: Colors.black, exportBackgroundColor: Colors.white, ); _docId = widget.auftragId ?? _repo.neueId(); if (widget.auftragId != null) { _laden(); } else { _loading = false; } if (!kIsWeb) { _initSpeech(); } } void _initSpeech() { _speech.initialize( onError: (_) {}, onStatus: (s) { if (s == 'done' || s == 'notListening') { if (mounted) setState(() => _speechListening = false); } }, ).then((ok) { if (mounted) setState(() => _speechReady = ok); }); } Future _laden() async { final a = await _repo.get(widget.auftragId!); if (!mounted) return; if (a == null) { setState(() => _loading = false); _snack('Auftrag nicht gefunden.'); return; } _isNeu = false; _rechnungsnummer.text = a.rechnungsnummer; _titel.text = a.titel; _beschreibung.text = a.beschreibung; _kunde.text = a.kundenName; _kundenAdresse.text = a.kundenAdresse; _kundenEmail.text = a.kundenEmail; _betrag.text = a.betragText; _fotoUrls = List.from(a.fotoUrls); _unterschriftUrl = a.unterschriftUrl; _signaturVorhanden = a.hasUnterschrift; _status = a.status; _dokumentTyp = a.dokumentTyp; _zahlungsStatus = a.zahlungsStatus; _faelligAm = a.faelligAm; _leistungsDatum = a.leistungsDatum; _kleinunternehmer = a.kleinunternehmer; _reverseCharge = a.reverseCharge; _skonto.text = a.skontoText; _ustId.text = a.ustIdKunde; _iban.text = a.ibanVerkaeufer; _bic.text = a.bicVerkaeufer; _kontoinhaber.text = a.kontoinhaberVerkaeufer; setState(() => _loading = false); } @override void dispose() { _rechnungsnummer.dispose(); _titel.dispose(); _beschreibung.dispose(); _kunde.dispose(); _kundenAdresse.dispose(); _kundenEmail.dispose(); _betrag.dispose(); _skonto.dispose(); _ustId.dispose(); _iban.dispose(); _bic.dispose(); _kontoinhaber.dispose(); _signatur.dispose(); if (_speechListening) { _speech.stop(); } super.dispose(); } void _snack(String t) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t))); } Future _bytesFuerReferenz(String ref) { return PdfExportService.ladeReferenz( ref, fileLoader: (id) => _storage.getFileBytes(id), ); } Widget _fotoVorschau(String ref) { if (ref.startsWith('http://') || ref.startsWith('https://')) { return ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network( ref, width: 88, height: 88, fit: BoxFit.cover, errorBuilder: (context, error, stack) => Container( width: 88, height: 88, color: const Color(0xFF2C2C2C), child: const Icon(Icons.broken_image, color: Colors.white54), ), ), ); } return FutureBuilder( future: _storage.getFileBytes(ref), builder: (context, snap) { if (snap.connectionState != ConnectionState.done) { return Container( width: 88, height: 88, color: const Color(0xFF2C2C2C), alignment: Alignment.center, child: const SizedBox( width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2), ), ); } final b = snap.data; if (b == null || b.isEmpty) { return Container( width: 88, height: 88, color: const Color(0xFF2C2C2C), child: const Icon(Icons.broken_image, color: Colors.white54), ); } return ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.memory(b, width: 88, height: 88, fit: BoxFit.cover), ); }, ); } Auftrag _aktuellerAuftrag(String? sigUrl) { var rn = _rechnungsnummer.text.trim(); if (rn.isEmpty) { final compact = _docId.replaceAll('-', ''); final suffix = compact.length >= 6 ? compact.substring(0, 6) : compact.padRight(6, '0'); rn = 'RE-${DateTime.now().year}-$suffix'; } return Auftrag( id: _docId, titel: _titel.text.trim(), beschreibung: _beschreibung.text.trim(), kundenName: _kunde.text.trim(), kundenAdresse: _kundenAdresse.text.trim(), kundenEmail: _kundenEmail.text.trim(), rechnungsnummer: rn, betragText: _betrag.text.trim(), fotoUrls: _fotoUrls, unterschriftUrl: sigUrl, createdAt: null, status: _status, dokumentTyp: _dokumentTyp, zahlungsStatus: _zahlungsStatus, faelligAm: _faelligAm, leistungsDatum: _leistungsDatum, kleinunternehmer: _kleinunternehmer, reverseCharge: _reverseCharge, skontoText: _skonto.text.trim(), ustIdKunde: _ustId.text.trim(), ibanVerkaeufer: _iban.text.trim(), bicVerkaeufer: _bic.text.trim(), kontoinhaberVerkaeufer: _kontoinhaber.text.trim(), ); } Future _speichernIntern({bool zeigeSnacks = true}) async { final titel = _titel.text.trim(); if (titel.isEmpty) { if (zeigeSnacks) { _snack('Bitte Leistung / Kurztitel eintragen.'); } return false; } if (_rechnungsnummer.text.trim().isEmpty) { final compact = _docId.replaceAll('-', ''); final suffix = compact.length >= 6 ? compact.substring(0, 6) : compact.padRight(6, '0'); _rechnungsnummer.text = 'RE-${DateTime.now().year}-$suffix'; } try { String? sigUrl = _unterschriftUrl; if (_signatur.isNotEmpty) { final png = await _signatur.toPngBytes(); if (png != null && png.isNotEmpty) { sigUrl = await _storage.hochladenUnterschrift(_docId, png); _signatur.clear(); _signaturVorhanden = true; _unterschriftUrl = sigUrl; } } final auftrag = _aktuellerAuftrag(sigUrl); await _repo.speichern(id: _docId, daten: auftrag, isNeu: _isNeu); await WorkflowMetricsService.recordDokumentGespeichert(auftrag.dokumentTyp); _isNeu = false; if (mounted) setState(() {}); if (zeigeSnacks) _snack('Gespeichert.'); return true; } catch (e) { if (zeigeSnacks) _snack('Speichern fehlgeschlagen: $e'); return false; } } Future _speichernTapped() async { setState(() => _saving = true); try { await _speichernIntern(); } finally { if (mounted) setState(() => _saving = false); } } Future _fotoKamera() async { if (kIsWeb) { _snack('Kamera ist im Browser nicht verfügbar.'); return; } final x = await _picker.pickImage( source: ImageSource.camera, imageQuality: 85, ); if (x == null) return; setState(() => _saving = true); try { final bytes = await x.readAsBytes(); final name = '${DateTime.now().millisecondsSinceEpoch}_cam.jpg'; final id = await _storage.hochladenFoto(_docId, bytes, name); _fotoUrls.add(id); setState(() {}); } catch (e) { _snack('Kamera: $e'); } finally { if (mounted) setState(() => _saving = false); } } Future _fotosHinzufuegen() async { final files = await _picker.pickMultiImage(imageQuality: 85); if (files.isEmpty) return; setState(() => _saving = true); try { var i = _fotoUrls.length; for (final x in files) { final bytes = await x.readAsBytes(); final name = '${DateTime.now().millisecondsSinceEpoch}_$i.jpg'; final id = await _storage.hochladenFoto(_docId, bytes, name); _fotoUrls.add(id); i++; } setState(() {}); } catch (e) { _snack('Foto-Upload: $e'); } finally { if (mounted) setState(() => _saving = false); } } Future _fotoEntfernen(int index) async { final id = _fotoUrls[index]; setState(() => _fotoUrls.removeAt(index)); await _storage.loescheDatei(id); } Future _kopieMitTyp(DokumentTyp typ, {required String numPrefix}) async { final ok = await _speichernIntern(zeigeSnacks: true); if (!ok || !mounted) return; final base = _aktuellerAuftrag(_unterschriftUrl); final newId = _repo.neueId(); final compact = newId.replaceAll('-', ''); final suffix = compact.length >= 6 ? compact.substring(0, 6) : compact.padRight(6, '0'); final newRn = '$numPrefix-${DateTime.now().year}-$suffix'; final body = typ == DokumentTyp.mahnung ? 'Mahnung zu ${base.rechnungsnummer}.\n\n${base.beschreibung}' : base.beschreibung; final copy = Auftrag( id: newId, titel: base.titel, beschreibung: body, kundenName: base.kundenName, kundenAdresse: base.kundenAdresse, kundenEmail: base.kundenEmail, rechnungsnummer: newRn, betragText: base.betragText, fotoUrls: const [], unterschriftUrl: null, createdAt: null, status: base.status, dokumentTyp: typ, zahlungsStatus: ZahlungsStatus.offen, faelligAm: base.faelligAm, leistungsDatum: base.leistungsDatum, kleinunternehmer: base.kleinunternehmer, reverseCharge: base.reverseCharge, skontoText: base.skontoText, ustIdKunde: base.ustIdKunde, ibanVerkaeufer: base.ibanVerkaeufer, bicVerkaeufer: base.bicVerkaeufer, kontoinhaberVerkaeufer: base.kontoinhaberVerkaeufer, ); try { await _repo.speichern(id: newId, daten: copy, isNeu: true); await WorkflowMetricsService.recordDokumentGespeichert(copy.dokumentTyp); if (!mounted) return; await Navigator.of(context).push( MaterialPageRoute( builder: (_) => AuftragBearbeitenScreen(auftragId: newId), ), ); } catch (e) { _snack('Kopie fehlgeschlagen: $e'); } } Future _toggleSpeech() async { if (!_speechReady) { _snack('Spracheingabe nicht verfügbar (Rechte / Gerät).'); return; } if (_speechListening) { await _speech.stop(); if (mounted) setState(() => _speechListening = false); return; } setState(() => _speechListening = true); await _speech.listen( onResult: (r) { if (!mounted) return; if (r.finalResult) { final t = r.recognizedWords.trim(); if (t.isEmpty) return; setState(() { final cur = _beschreibung.text; _beschreibung.text = cur.trim().isEmpty ? t : '$cur\n$t'; _beschreibung.selection = TextSelection.collapsed( offset: _beschreibung.text.length, ); }); } }, localeId: 'de_DE', listenOptions: stt.SpeechListenOptions( listenMode: stt.ListenMode.dictation, ), ); } Future _schnellpositionDialog() async { final c = TextEditingController(); final go = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Schnellposition'), content: TextField( controller: c, autofocus: true, decoration: const InputDecoration( hintText: 'z. B. 2h Elektro oder 50€ Material Kabel', ), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Abbrechen'), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('Übernehmen'), ), ], ), ); if (go != true || !mounted) return; final line = PositionFromTextParser.vorschlagZeile(c.text); c.dispose(); if (line == null) { _snack('Kein erkennbares Muster.'); return; } setState(() { final cur = _beschreibung.text.trim(); _beschreibung.text = cur.isEmpty ? line : '$cur\n$line'; }); } Future _pickLeistungsdatum() async { final now = DateTime.now(); final d = await showDatePicker( context: context, initialDate: _leistungsDatum ?? now, firstDate: DateTime(now.year - 5), lastDate: DateTime(now.year + 2), ); if (d != null) setState(() => _leistungsDatum = d); } Future _pickFaelligAm() async { final now = DateTime.now(); final d = await showDatePicker( context: context, initialDate: _faelligAm ?? now, firstDate: DateTime(now.year - 2), lastDate: DateTime(now.year + 5), ); if (d != null) setState(() => _faelligAm = d); } Future _pdfDateiErzeugen() async { final titel = _titel.text.trim(); if (titel.isEmpty) { _snack('Leistung / Kurztitel eintragen (für die PDF).'); return null; } String? sigUrl = _unterschriftUrl; Uint8List? sigBytes; if (_signatur.isNotEmpty) { sigBytes = await _signatur.toPngBytes(); } if ((sigBytes == null || sigBytes.isEmpty) && sigUrl != null && sigUrl.isNotEmpty) { sigBytes = await _bytesFuerReferenz(sigUrl); } final fotoBytes = []; for (final ref in _fotoUrls) { fotoBytes.add(await _bytesFuerReferenz(ref)); } final auftrag = _aktuellerAuftrag(sigUrl); return PdfExportService.buildPdf( auftrag: auftrag, fotoBytes: fotoBytes, unterschriftBytes: sigBytes, ); } Future _pdfErstellenUndSenden() async { setState(() => _saving = true); try { final ok = await _speichernIntern(zeigeSnacks: true); if (!ok) return; final file = await _pdfDateiErzeugen(); if (file == null) return; final rn = _rechnungsnummer.text.trim(); await PdfExportService.teilen(file, rechnungsnummer: rn); await PdfHistoryService.add( title: _titel.text.trim().isEmpty ? 'Auftrag' : _titel.text.trim(), rechnungsnummer: rn, ); await WorkflowMetricsService.recordPdfExported(); if (mounted) _snack('PDF erstellt – Teilen geöffnet.'); } catch (e) { _snack('PDF / Teilen: $e'); } finally { if (mounted) setState(() => _saving = false); } } Future _emailMitPdf() async { setState(() => _saving = true); try { final ok = await _speichernIntern(zeigeSnacks: true); if (!ok) return; final file = await _pdfDateiErzeugen(); if (file == null) return; final empf = _kundenEmail.text.trim(); final rn = _rechnungsnummer.text.trim().isEmpty ? 'Rechnung' : _rechnungsnummer.text.trim(); await FlutterEmailSender.send( Email( subject: 'Rechnung $rn', body: 'Guten Tag,\n\nanbei die Rechnung als PDF.\n\nMit freundlichen Grüßen', recipients: empf.contains('@') ? [empf] : [], attachmentPaths: [file.path], ), ); await PdfHistoryService.add( title: _titel.text.trim().isEmpty ? 'Auftrag' : _titel.text.trim(), rechnungsnummer: rn, ); await WorkflowMetricsService.recordPdfExported(); } catch (e) { _snack('E-Mail: $e (Mail-App / Konto prüfen)'); } finally { if (mounted) setState(() => _saving = false); } } Widget _fotoSlot({ required IconData icon, required String label, required VoidCallback? onTap, }) { return Material( color: Colors.transparent, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Container( width: 88, height: 88, alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade700, width: 1.5), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, color: Colors.grey.shade600, size: 28), const SizedBox(height: 4), Text( label, style: TextStyle(fontSize: 10, color: Colors.grey.shade600), ), ], ), ), ), ); } Widget _fotoLeerHinzufuegen() { return _fotoSlot( icon: Icons.add_photo_alternate_outlined, label: 'Galerie', onTap: _saving ? null : _fotosHinzufuegen, ); } Widget _fotoKameraSlot() { return _fotoSlot( icon: Icons.photo_camera_outlined, label: 'Kamera', onTap: _saving ? null : _fotoKamera, ); } @override Widget build(BuildContext context) { if (_loading) { return Scaffold( backgroundColor: AppTheme.background, body: const Center(child: CircularProgressIndicator()), ); } return Scaffold( backgroundColor: AppTheme.background, appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.of(context).pop(), ), title: Text(_isNeu ? 'Neuer Auftrag' : 'Auftrag bearbeiten'), actions: [ PopupMenuButton( enabled: !_saving, icon: const Icon(Icons.more_vert), color: AppTheme.card, onSelected: (v) { if (v == 'mahnung') { _kopieMitTyp(DokumentTyp.mahnung, numPrefix: 'MAH'); } else if (v == 'angebot') { _kopieMitTyp(DokumentTyp.angebot, numPrefix: 'AN'); } else if (v == 'rechnung') { _kopieMitTyp(DokumentTyp.rechnung, numPrefix: 'RE'); } }, itemBuilder: (context) => const [ PopupMenuItem( value: 'mahnung', child: Text('Neue Mahnung (Kopie)'), ), PopupMenuItem( value: 'angebot', child: Text('Kopie als Angebot'), ), PopupMenuItem( value: 'rechnung', child: Text('Kopie als Rechnung'), ), ], ), TextButton( onPressed: _saving ? null : _speichernTapped, child: const Text( 'Speichern', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w600, ), ), ), ], ), body: Stack( children: [ ListView( padding: const EdgeInsets.fromLTRB(20, 8, 20, 120), children: [ TextField( controller: _kunde, textCapitalization: TextCapitalization.words, decoration: const InputDecoration( labelText: 'Kunde', hintText: 'z. B. Müller GmbH', ), ), const SizedBox(height: 14), TextField( controller: _kundenAdresse, textCapitalization: TextCapitalization.sentences, decoration: const InputDecoration( labelText: 'Adresse', hintText: 'Hauptstr. 12, 67655 Kaiserslautern', ), ), const SizedBox(height: 14), TextField( controller: _titel, textCapitalization: TextCapitalization.sentences, decoration: const InputDecoration( labelText: 'Leistung / Kurztitel', hintText: 'z. B. Elektroinstallation Küche', ), ), const SizedBox(height: 18), Text( 'Status', style: Theme.of(context).textTheme.titleSmall?.copyWith( color: Colors.grey.shade400, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: AuftragStatus.values.map((s) { final sel = _status == s; return FilterChip( label: Text(s.labelDe), selected: sel, onSelected: (_) { if (_saving) return; setState(() => _status = s); }, selectedColor: AppTheme.accentCyan.withValues(alpha: 0.35), checkmarkColor: Colors.black, labelStyle: TextStyle( color: sel ? Colors.black : Colors.white70, fontWeight: sel ? FontWeight.w600 : FontWeight.w400, ), ); }).toList(), ), const SizedBox(height: 20), Text( 'Dokumenttyp', style: Theme.of(context).textTheme.titleSmall?.copyWith( color: Colors.grey.shade400, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: DokumentTyp.values.map((t) { final sel = _dokumentTyp == t; return FilterChip( label: Text(t.labelDe, style: const TextStyle(fontSize: 12)), selected: sel, onSelected: (_) { if (_saving) return; setState(() => _dokumentTyp = t); }, selectedColor: AppTheme.accentCyan.withValues(alpha: 0.35), checkmarkColor: Colors.black, labelStyle: TextStyle( color: sel ? Colors.black : Colors.white70, fontWeight: sel ? FontWeight.w600 : FontWeight.w400, ), ); }).toList(), ), const SizedBox(height: 18), Text( 'Zahlung', style: Theme.of(context).textTheme.titleSmall?.copyWith( color: Colors.grey.shade400, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: ZahlungsStatus.values.map((z) { final sel = _zahlungsStatus == z; return FilterChip( label: Text(z.labelDe), selected: sel, onSelected: (_) { if (_saving) return; setState(() => _zahlungsStatus = z); }, selectedColor: AppTheme.accentCyan.withValues(alpha: 0.35), checkmarkColor: Colors.black, labelStyle: TextStyle( color: sel ? Colors.black : Colors.white70, fontWeight: sel ? FontWeight.w600 : FontWeight.w400, ), ); }).toList(), ), const SizedBox(height: 10), Row( children: [ Expanded( child: OutlinedButton( onPressed: _saving ? null : _pickLeistungsdatum, child: Text( _leistungsDatum == null ? 'Leistungsdatum' : 'Leistung: ${_datDe.format(_leistungsDatum!)}', overflow: TextOverflow.ellipsis, ), ), ), const SizedBox(width: 10), Expanded( child: OutlinedButton( onPressed: _saving ? null : _pickFaelligAm, child: Text( _faelligAm == null ? 'Fällig am' : 'Fällig: ${_datDe.format(_faelligAm!)}', overflow: TextOverflow.ellipsis, ), ), ), ], ), const SizedBox(height: 6), Align( alignment: Alignment.centerRight, child: TextButton( onPressed: _saving ? null : () => setState(() { _leistungsDatum = null; _faelligAm = null; }), child: const Text('Daten löschen'), ), ), const SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( 'Beschreibung / Positionen', style: Theme.of(context).textTheme.titleSmall?.copyWith( color: Colors.grey.shade400, fontWeight: FontWeight.w600, ), ), ), IconButton( tooltip: _speechListening ? 'Stopp' : 'Spracheingabe', onPressed: _saving ? null : _toggleSpeech, icon: Icon( _speechListening ? Icons.mic : Icons.mic_none_outlined, color: _speechListening ? AppTheme.accentCyan : Colors.grey, ), ), TextButton( onPressed: _saving ? null : _schnellpositionDialog, child: const Text('Schnellposition'), ), ], ), const SizedBox(height: 6), TextField( controller: _beschreibung, minLines: 4, maxLines: 8, textCapitalization: TextCapitalization.sentences, decoration: const InputDecoration( labelText: 'Text & Positionen', hintText: 'Beschreibung eingeben…', alignLabelWithHint: true, ), ), Theme( data: Theme.of(context).copyWith( dividerColor: Colors.transparent, listTileTheme: const ListTileThemeData( iconColor: AppTheme.accentCyan, ), ), child: ExpansionTile( tilePadding: EdgeInsets.zero, title: Text( 'Steuern, Skonto & SEPA-QR', style: TextStyle( color: Colors.grey.shade400, fontWeight: FontWeight.w500, ), ), children: [ SwitchListTile( contentPadding: EdgeInsets.zero, title: const Text('Kleinunternehmer §19 UStG'), value: _kleinunternehmer, onChanged: _saving ? null : (v) => setState(() => _kleinunternehmer = v), ), SwitchListTile( contentPadding: EdgeInsets.zero, title: const Text('Reverse Charge'), value: _reverseCharge, onChanged: _saving ? null : (v) => setState(() => _reverseCharge = v), ), TextField( controller: _skonto, decoration: const InputDecoration( labelText: 'Skonto / Zahlungsbedingungen', hintText: 'z. B. 2% bei Zahlung innerhalb 14 Tagen', ), ), const SizedBox(height: 12), TextField( controller: _ustId, autocorrect: false, decoration: const InputDecoration( labelText: 'USt-IdNr. Kunde (optional)', hintText: 'DE123456789', ), ), const SizedBox(height: 12), TextField( controller: _iban, autocorrect: false, decoration: const InputDecoration( labelText: 'Ihre IBAN (für QR-Zahlung im PDF)', hintText: 'DE…', ), ), const SizedBox(height: 12), TextField( controller: _bic, autocorrect: false, decoration: const InputDecoration( labelText: 'BIC (optional, für QR)', ), ), const SizedBox(height: 12), TextField( controller: _kontoinhaber, textCapitalization: TextCapitalization.words, decoration: const InputDecoration( labelText: 'Kontoinhaber (Empfängername QR)', ), ), const SizedBox(height: 8), ], ), ), const SizedBox(height: 22), Text( 'Fotos', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Colors.white, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 10), SizedBox( height: 92, child: ListView( scrollDirection: Axis.horizontal, children: [ for (var i = 0; i < _fotoUrls.length; i++) Padding( padding: const EdgeInsets.only(right: 10), child: Stack( clipBehavior: Clip.none, children: [ _fotoVorschau(_fotoUrls[i]), Positioned( top: -6, right: -6, child: IconButton.filled( style: IconButton.styleFrom( padding: const EdgeInsets.all(2), minimumSize: const Size(26, 26), backgroundColor: Colors.black87, ), onPressed: _saving ? null : () => _fotoEntfernen(i), icon: const Icon(Icons.close, size: 14), ), ), ], ), ), _fotoLeerHinzufuegen(), Padding( padding: const EdgeInsets.only(left: 10), child: _fotoKameraSlot(), ), ], ), ), const SizedBox(height: 22), Text( 'Unterschrift Kunde', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Colors.white, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 6), if (_signaturVorhanden && _signatur.isEmpty) Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( 'Es liegt bereits eine Unterschrift. Neu zeichnen und ' 'speichern ersetzt sie.', style: TextStyle( fontSize: 12, color: Colors.grey.shade500, ), ), ), ClipRRect( borderRadius: BorderRadius.circular(12), child: SizedBox( height: 160, child: Signature( key: const Key('signatur'), controller: _signatur, backgroundColor: Colors.grey.shade300, ), ), ), Align( alignment: Alignment.centerLeft, child: TextButton.icon( onPressed: _saving ? null : () => _signatur.clear(), icon: const Icon(Icons.clear, size: 18), label: const Text('Unterschrift leeren'), style: TextButton.styleFrom(foregroundColor: Colors.grey), ), ), Theme( data: Theme.of(context).copyWith( dividerColor: Colors.transparent, listTileTheme: const ListTileThemeData( iconColor: AppTheme.accentCyan, ), ), child: ExpansionTile( tilePadding: EdgeInsets.zero, title: Text( 'Rechnung & Kontakt', style: TextStyle( color: Colors.grey.shade400, fontWeight: FontWeight.w500, ), ), children: [ TextField( controller: _rechnungsnummer, decoration: const InputDecoration( labelText: 'Rechnungsnummer', hintText: 'Wird beim Speichern gesetzt, wenn leer', ), ), const SizedBox(height: 12), TextField( controller: _kundenEmail, keyboardType: TextInputType.emailAddress, autocorrect: false, decoration: const InputDecoration( labelText: 'Kunden-E-Mail', hintText: 'Für „Per E-Mail senden“', ), ), const SizedBox(height: 12), TextField( controller: _betrag, keyboardType: TextInputType.text, decoration: const InputDecoration( labelText: 'Betrag brutto (€)', hintText: 'z. B. 1.234,56', ), ), const SizedBox(height: 8), ], ), ), const SizedBox(height: 16), SizedBox( width: double.infinity, height: 52, child: FilledButton( onPressed: _saving ? null : _pdfErstellenUndSenden, child: const Text('PDF erstellen & senden'), ), ), const SizedBox(height: 10), SizedBox( width: double.infinity, height: 52, child: OutlinedButton.icon( onPressed: _saving ? null : _emailMitPdf, icon: const Icon(Icons.email_outlined, size: 20), label: const Text('PDF per E-Mail senden'), style: OutlinedButton.styleFrom( foregroundColor: AppTheme.accentCyan, side: const BorderSide(color: Color(0xFF404040)), padding: const EdgeInsets.symmetric(vertical: 14), ), ), ), ], ), if (_saving) const ModalBarrier( dismissible: false, color: Color(0x66000000), ), if (_saving) const Center(child: CircularProgressIndicator()), ], ), ); } }