import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:signature/signature.dart'; import 'package:hl_lieferservice/domain/entity/delivery.dart'; import 'package:hl_lieferservice/domain/entity/delivery_note.dart'; import 'package:hl_lieferservice/domain/entity/tour_details.dart'; /// Daten, die der Abschluss-Flow an den Aufrufer zurückgibt: beide /// Unterschriften als PNG plus die dokumentierten Bestätigungen des Kunden. class SignatureResult { const SignatureResult({ required this.customerSignaturePng, required this.driverSignaturePng, required this.receiptConfirmed, required this.notesAcknowledged, required this.acknowledgedNoteIds, required this.paymentCollected, }); final Uint8List customerSignaturePng; final Uint8List driverSignaturePng; final bool receiptConfirmed; final bool notesAcknowledged; final List acknowledgedNoteIds; /// Fahrer hat das Inkasso (Bar/EC) des offenen Betrags bestätigt. `false`, /// wenn kein Inkasso anfiel (offen == 0 oder „Auf Rechnung"). final bool paymentCollected; } /// Mehrstufiger Unterschrift-Flow zum Abschließen einer Lieferung. /// /// Stufe 0 (Fahrer, optional): nur wenn beim Abschluss ein offener Betrag /// per Vor-Ort-Inkasso (Bar/EC) zu kassieren ist ([requiresCollection]). /// Der Fahrer bestätigt, dass der Betrag erhalten/abgerechnet wurde — VOR /// beiden Unterschriften. /// Stufe 1 (Kunde): sieht die Anmerkungen zur Lieferung, hakt zwei /// Bestätigungen ab (Anmerkungen-Kenntnisnahme — nur Pflicht, wenn Notizen /// vorhanden; Empfangsbestätigung — immer Pflicht) und unterschreibt. /// Stufe 2 (Fahrer): unterschreibt. /// /// Erst nach beiden Unterschriften ruft die View [onSigned] mit dem /// vollständigen [SignatureResult] auf — der Aufrufer triggert dann den /// Backend-Abschluss und schließt die Seite. class SignatureView extends StatefulWidget { const SignatureView({ super.key, required this.delivery, required this.details, required this.onSigned, this.requiresCollection = false, this.openAmount = 0, this.paymentMethodLabel = '', }); final Delivery delivery; final TourDetails details; final void Function(SignatureResult result) onSigned; /// Offener Betrag muss vor Ort kassiert werden (offen > 0 UND Bar/EC). /// Schaltet Stufe 0 (Inkasso-Bestätigung) frei. final bool requiresCollection; /// Offener Betrag in Euro (nur für die Anzeige in Stufe 0). final double openAmount; /// Anzeigename der Zahlungsmethode (z. B. „Bar", „EC-Karte"). final String paymentMethodLabel; @override State createState() => _SignatureViewState(); } /// Stufen des Abschluss-Flows. enum _SignStage { payment, customer, driver } class _SignatureViewState extends State { static const String _receiptText = 'Ich bestätige, dass ich die Ware im ordnungsgemäßen Zustand erhalten ' 'habe und, dass die Aufstell- und Einbauarbeiten korrekt durchgeführt ' 'wurden.'; static const String _notesText = 'Ich nehme die oben genannten Anmerkungen zur Lieferung zur Kenntnis.'; final SignatureController _customerController = SignatureController( penStrokeWidth: 3, penColor: Colors.black, exportBackgroundColor: Colors.white, ); final SignatureController _driverController = SignatureController( penStrokeWidth: 3, penColor: Colors.black, exportBackgroundColor: Colors.white, ); late final List _notes; late _SignStage _stage; bool _paymentConfirmed = false; bool _receiptAccepted = false; bool _notesAccepted = false; bool _customerEmpty = true; bool _driverEmpty = true; bool get _notesEmpty => _notes.isEmpty; @override void initState() { super.initState(); // Inkasso-Bestätigung (Stufe 0) nur wenn gefordert, sonst direkt zum Kunden. _stage = widget.requiresCollection ? _SignStage.payment : _SignStage.customer; _notes = widget.details.notesByDeliveryId[widget.delivery.id] ?? const []; _customerController.addListener(() { if (_customerEmpty != _customerController.isEmpty) { setState(() => _customerEmpty = _customerController.isEmpty); } }); _driverController.addListener(() { if (_driverEmpty != _driverController.isEmpty) { setState(() => _driverEmpty = _driverController.isEmpty); } }); } @override void dispose() { _customerController.dispose(); _driverController.dispose(); super.dispose(); } bool get _customerStepValid => _receiptAccepted && (_notesAccepted || _notesEmpty) && !_customerEmpty; /// Ist der Primär-Button auf der aktuellen Stufe aktiv? bool get _stageValid { switch (_stage) { case _SignStage.payment: return _paymentConfirmed; case _SignStage.customer: return _customerStepValid; case _SignStage.driver: return !_driverEmpty; } } Future _onPrimaryPressed() async { switch (_stage) { case _SignStage.payment: setState(() => _stage = _SignStage.customer); return; case _SignStage.customer: setState(() => _stage = _SignStage.driver); return; case _SignStage.driver: final customerPng = await _customerController.toPngBytes(); final driverPng = await _driverController.toPngBytes(); if (customerPng == null || driverPng == null) return; widget.onSigned( SignatureResult( customerSignaturePng: customerPng, driverSignaturePng: driverPng, receiptConfirmed: _receiptAccepted, notesAcknowledged: _notesEmpty ? false : _notesAccepted, acknowledgedNoteIds: _notesEmpty ? const [] : _notes.map((n) => n.id).toList(), paymentCollected: widget.requiresCollection && _paymentConfirmed, ), ); } } String get _paymentText { final amount = widget.openAmount.toStringAsFixed(2).replaceAll('.', ','); final via = widget.paymentMethodLabel.isEmpty ? '' : ' per ${widget.paymentMethodLabel}'; return 'Ich bestätige, dass der offene Betrag von $amount €$via ' 'erhalten bzw. abgerechnet wurde.'; } @override Widget build(BuildContext context) { final theme = Theme.of(context); final customer = widget.details.customerOf(widget.delivery); final date = DateFormat('dd.MM.yyyy').format(DateTime.now()); final isPayment = _stage == _SignStage.payment; final isDriver = _stage == _SignStage.driver; final String title; switch (_stage) { case _SignStage.payment: title = 'Zahlung bestätigen'; case _SignStage.customer: title = 'Unterschrift des Kunden'; case _SignStage.driver: title = 'Unterschrift des Fahrers'; } return Scaffold( appBar: AppBar(title: Text(title)), body: SafeArea( child: ListView( padding: const EdgeInsets.all(16), children: [ if (isPayment) ...[ // Stufe 0 — Fahrer kassiert (Bar/EC) und bestätigt VOR den // Unterschriften. _PaymentDueCard( openAmount: widget.openAmount, methodLabel: widget.paymentMethodLabel, ), const SizedBox(height: 16), _ConfirmTile( value: _paymentConfirmed, enabled: true, label: _paymentText, onChanged: (v) => setState(() => _paymentConfirmed = v), ), ] else ...[ if (!isDriver) ...[ _NotesSection(notes: _notes), const SizedBox(height: 16), _ConfirmTile( value: _notesAccepted, enabled: !_notesEmpty, label: _notesText, onChanged: (v) => setState(() => _notesAccepted = v), ), _ConfirmTile( value: _receiptAccepted, enabled: true, label: _receiptText, onChanged: (v) => setState(() => _receiptAccepted = v), ), const SizedBox(height: 16), ], Text( 'Lieferung an: ${customer?.name ?? '⟨Unbekannter Kunde⟩'}', style: theme.textTheme.titleSmall ?.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 8), _SignaturePad( controller: isDriver ? _driverController : _customerController, onClear: () => (isDriver ? _driverController : _customerController) .clear(), ), const SizedBox(height: 4), Text( '${customer?.address.city ?? ''}, den $date', style: theme.textTheme.bodySmall, ), ], const SizedBox(height: 24), FilledButton.icon( onPressed: _stageValid ? _onPrimaryPressed : null, icon: Icon(isDriver ? Icons.check : Icons.arrow_forward), label: Text(isDriver ? 'Abschließen' : 'Weiter'), ), ], ), ), ); } } // ─── Inkasso-Hinweis (Stufe 0) ────────────────────────────────────────────── class _PaymentDueCard extends StatelessWidget { const _PaymentDueCard({required this.openAmount, required this.methodLabel}); final double openAmount; final String methodLabel; @override Widget build(BuildContext context) { final theme = Theme.of(context); final amount = openAmount.toStringAsFixed(2).replaceAll('.', ','); return Card( margin: EdgeInsets.zero, color: theme.colorScheme.primary.withValues(alpha: 0.07), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Offener Betrag', style: theme.textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 8), Row( children: [ Icon(Icons.payments_outlined, color: theme.colorScheme.primary), const SizedBox(width: 10), Text( '$amount €', style: theme.textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w800, color: theme.colorScheme.primary, ), ), const Spacer(), if (methodLabel.isNotEmpty) Chip( label: Text(methodLabel), visualDensity: VisualDensity.compact, ), ], ), const SizedBox(height: 8), Text( 'Bitte den Betrag bar entgegennehmen oder über das EC-Gerät ' 'abrechnen und anschließend bestätigen.', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ), ); } } // ─── Notizen-Block ──────────────────────────────────────────────────────── class _NotesSection extends StatelessWidget { const _NotesSection({required this.notes}); final List notes; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Anmerkungen zur Lieferung', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 8), if (notes.isEmpty) Card( margin: EdgeInsets.zero, child: Padding( padding: const EdgeInsets.all(16), child: Text( 'Keine Anmerkungen vorhanden.', style: TextStyle( fontStyle: FontStyle.italic, color: theme.colorScheme.onSurfaceVariant, ), ), ), ) else Card( margin: EdgeInsets.zero, child: Column( children: [ for (int i = 0; i < notes.length; i++) ...[ ListTile( leading: Icon( notes[i].imageAttachment != null ? (notes[i].imageAttachmentDeleted ? Icons.picture_as_pdf_outlined : Icons.photo_outlined) : Icons.event_note_outlined, color: theme.colorScheme.primary, ), title: Text( notes[i].text?.trim().isNotEmpty == true ? notes[i].text!.trim() : (notes[i].imageAttachmentDeleted ? 'Bild im Lieferbericht enthalten' : 'Foto-Anhang'), ), ), if (i < notes.length - 1) const Divider(height: 1, indent: 16, endIndent: 16), ], ], ), ), ], ); } } // ─── Bestätigungs-Checkbox ────────────────────────────────────────────────── class _ConfirmTile extends StatelessWidget { const _ConfirmTile({ required this.value, required this.enabled, required this.label, required this.onChanged, }); final bool value; final bool enabled; final String label; final ValueChanged onChanged; @override Widget build(BuildContext context) { return CheckboxListTile( value: value, onChanged: enabled ? (v) => onChanged(v ?? false) : null, controlAffinity: ListTileControlAffinity.leading, contentPadding: EdgeInsets.zero, title: Text(label, style: const TextStyle(fontSize: 14)), ); } } // ─── Signatur-Pad ─────────────────────────────────────────────────────────── class _SignaturePad extends StatelessWidget { const _SignaturePad({required this.controller, required this.onClear}); final SignatureController controller; final VoidCallback onClear; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( height: 220, decoration: BoxDecoration( color: Colors.white, border: Border.all(color: theme.colorScheme.outline), borderRadius: BorderRadius.circular(8), ), clipBehavior: Clip.antiAlias, child: Signature( controller: controller, backgroundColor: Colors.white, ), ), Align( alignment: Alignment.centerRight, child: TextButton.icon( onPressed: onClear, icon: const Icon(Icons.undo), label: const Text('Löschen'), ), ), ], ); } }