import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_email_sender/flutter_email_sender.dart'; import 'package:image_picker/image_picker.dart'; import 'package:signature/signature.dart'; import '../../models/auftrag.dart'; import '../../services/auftrag_repository.dart'; import '../../services/auftrag_storage_service.dart'; import '../../services/pdf_export_service.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 _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; @override void initState() { super.initState(); _signatur = SignatureController( penStrokeWidth: 2, penColor: Colors.black, exportBackgroundColor: Colors.white, ); _docId = widget.auftragId ?? _repo.neueId(); if (widget.auftragId != null) { _laden(); } else { _loading = false; } } Future _laden() async { final a = await _repo.get(widget.auftragId!); if (!mounted) return; if (a == null) { setState(() => _loading = false); _snack('Rechnung nicht gefunden.'); return; } _isNeu = false; _rechnungsnummer.text = a.rechnungsnummer; _titel.text = a.titel; _beschreibung.text = a.beschreibung; _kunde.text = a.kundenName; _kundenEmail.text = a.kundenEmail; _betrag.text = a.betragText; _fotoUrls = List.from(a.fotoUrls); _unterschriftUrl = a.unterschriftUrl; _signaturVorhanden = a.hasUnterschrift; setState(() => _loading = false); } @override void dispose() { _rechnungsnummer.dispose(); _titel.dispose(); _beschreibung.dispose(); _kunde.dispose(); _kundenEmail.dispose(); _betrag.dispose(); _signatur.dispose(); super.dispose(); } void _snack(String t) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t))); } 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(), kundenEmail: _kundenEmail.text.trim(), rechnungsnummer: rn, betragText: _betrag.text.trim(), fotoUrls: _fotoUrls, unterschriftUrl: sigUrl, createdAt: null, ); } 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 url = await _storage.hochladenFoto(_docId, bytes, name); _fotoUrls.add(url); i++; } setState(() {}); } catch (e) { _snack('Foto-Upload: $e'); } finally { if (mounted) setState(() => _saving = false); } } void _fotoEntfernen(int index) { setState(() { _fotoUrls.removeAt(index); }); } Future _speichern() async { final titel = _titel.text.trim(); if (titel.isEmpty) { _snack('Bitte einen Titel / Leistung eintragen.'); return; } 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'; } setState(() => _saving = true); 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); _isNeu = false; if (mounted) setState(() {}); _snack('Rechnung gespeichert.'); } catch (e) { _snack('Speichern fehlgeschlagen: $e'); } finally { if (mounted) setState(() => _saving = false); } } Future _pdfDateiErzeugen() async { final titel = _titel.text.trim(); if (titel.isEmpty) { _snack('Titel eintragen (für die Rechnung).'); 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 PdfExportService.ladeUrl(sigUrl); } final fotoBytes = []; for (final url in _fotoUrls) { fotoBytes.add(await PdfExportService.ladeUrl(url)); } final auftrag = _aktuellerAuftrag(sigUrl); return PdfExportService.buildPdf( auftrag: auftrag, fotoBytes: fotoBytes, unterschriftBytes: sigBytes, ); } Future _pdfTeilen() async { setState(() => _saving = true); try { final file = await _pdfDateiErzeugen(); if (file == null) return; final rn = _rechnungsnummer.text.trim(); await PdfExportService.teilen(file, rechnungsnummer: rn); } catch (e) { _snack('PDF / Teilen: $e'); } finally { if (mounted) setState(() => _saving = false); } } Future _emailMitPdf() async { setState(() => _saving = true); try { 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], ), ); } catch (e) { _snack('E-Mail: $e (Mail-App / Konto prüfen)'); } finally { if (mounted) setState(() => _saving = false); } } @override Widget build(BuildContext context) { if (_loading) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } return Scaffold( appBar: AppBar( title: Text(_isNeu ? 'Neue Rechnung' : 'Rechnung bearbeiten'), ), body: Stack( children: [ ListView( padding: const EdgeInsets.all(20), children: [ TextField( controller: _rechnungsnummer, decoration: const InputDecoration( labelText: 'Rechnungsnummer', hintText: 'Wird beim Speichern automatisch gesetzt, wenn leer', ), ), const SizedBox(height: 12), TextField( controller: _titel, decoration: const InputDecoration( labelText: 'Leistung / Titel', hintText: 'z. B. Elektroinstallation Küche', ), ), const SizedBox(height: 12), TextField( controller: _kunde, decoration: const InputDecoration( labelText: 'Kunde (Name)', ), ), 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: 12), TextField( controller: _beschreibung, minLines: 3, maxLines: 8, decoration: const InputDecoration( labelText: 'Beschreibung / Positionen', alignLabelWithHint: true, ), ), const SizedBox(height: 24), Row( children: [ Text( 'Fotos', style: Theme.of(context).textTheme.titleMedium, ), const Spacer(), FilledButton.tonalIcon( onPressed: _saving ? null : _fotosHinzufuegen, icon: const Icon(Icons.add_photo_alternate_outlined), label: const Text('Hinzufügen'), ), ], ), const SizedBox(height: 8), if (_fotoUrls.isEmpty) Text( 'Noch keine Fotos.', style: Theme.of(context).textTheme.bodySmall, ) else SizedBox( height: 100, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: _fotoUrls.length, separatorBuilder: (context, i) => const SizedBox(width: 8), itemBuilder: (context, i) { return Stack( clipBehavior: Clip.none, children: [ ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network( _fotoUrls[i], width: 100, height: 100, fit: BoxFit.cover, errorBuilder: (context, error, stack) => Container( width: 100, height: 100, color: Colors.grey.shade300, child: const Icon(Icons.broken_image), ), ), ), Positioned( top: -4, right: -4, child: IconButton.filled( style: IconButton.styleFrom( padding: const EdgeInsets.all(4), minimumSize: const Size(28, 28), ), onPressed: _saving ? null : () => _fotoEntfernen(i), icon: const Icon(Icons.close, size: 16), ), ), ], ); }, ), ), const SizedBox(height: 24), Text( 'Unterschrift Kunde', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 4), 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: Theme.of(context).textTheme.bodySmall, ), ), ClipRRect( borderRadius: BorderRadius.circular(12), child: SizedBox( height: 180, child: Signature( key: const Key('signatur'), controller: _signatur, backgroundColor: Colors.grey.shade200, ), ), ), TextButton.icon( onPressed: _saving ? null : () => _signatur.clear(), icon: const Icon(Icons.clear), label: const Text('Unterschrift leeren'), ), const SizedBox(height: 24), FilledButton.icon( onPressed: _saving ? null : _speichern, icon: const Icon(Icons.save_outlined), label: const Text('Rechnung speichern'), ), const SizedBox(height: 8), OutlinedButton.icon( onPressed: _saving ? null : _pdfTeilen, icon: const Icon(Icons.share_outlined), label: const Text('PDF erzeugen & teilen'), ), const SizedBox(height: 8), OutlinedButton.icon( onPressed: _saving ? null : _emailMitPdf, icon: const Icon(Icons.email_outlined), label: const Text('PDF per E-Mail senden'), ), const SizedBox(height: 8), Text( 'E-Mail: Öffnet die Mail-App mit Anhang. Auf dem Simulator ' 'oft nicht verfügbar – echtes Gerät nutzen. ' 'Kunden-E-Mail oben ausfüllen oder Empfänger in der Mail-App wählen.', style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 32), ], ), if (_saving) const ModalBarrier( dismissible: false, color: Color(0x33000000), ), if (_saving) const Center(child: CircularProgressIndicator()), ], ), ); } }