Final commit.
This commit is contained in:
@ -1,56 +1,129 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/detail/bloc/note_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/detail/bloc/note_event.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/detail/bloc/note_state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hl_lieferservice/model/delivery.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:signature/signature.dart';
|
||||
|
||||
enum _SigningPhase { customerAcceptance, customerSignature, driverSignature }
|
||||
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<String> 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.onSigned,
|
||||
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;
|
||||
|
||||
/// Callback that is called when the user has signed.
|
||||
/// The parameter stores the path to the image file of the signature.
|
||||
final void Function(
|
||||
Uint8List customerSignaturePng,
|
||||
Uint8List driverSignaturePng,
|
||||
)
|
||||
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<StatefulWidget> createState() => _SignatureViewState();
|
||||
State<SignatureView> createState() => _SignatureViewState();
|
||||
}
|
||||
|
||||
/// Stufen des Abschluss-Flows.
|
||||
enum _SignStage { payment, customer, driver }
|
||||
|
||||
class _SignatureViewState extends State<SignatureView> {
|
||||
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: 5,
|
||||
penStrokeWidth: 3,
|
||||
penColor: Colors.black,
|
||||
exportBackgroundColor: Colors.white,
|
||||
);
|
||||
|
||||
final SignatureController _driverController = SignatureController(
|
||||
penStrokeWidth: 5,
|
||||
penStrokeWidth: 3,
|
||||
penColor: Colors.black,
|
||||
exportBackgroundColor: Colors.white,
|
||||
);
|
||||
|
||||
_SigningPhase _phase = _SigningPhase.customerAcceptance;
|
||||
late final List<DeliveryNote> _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();
|
||||
context.read<NoteBloc>().add(LoadNote(delivery: widget.delivery));
|
||||
// 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 <DeliveryNote>[];
|
||||
_customerController.addListener(() {
|
||||
if (_customerEmpty != _customerController.isEmpty) {
|
||||
setState(() => _customerEmpty = _customerController.isEmpty);
|
||||
}
|
||||
});
|
||||
_driverController.addListener(() {
|
||||
if (_driverEmpty != _driverController.isEmpty) {
|
||||
setState(() => _driverEmpty = _driverController.isEmpty);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -60,314 +133,333 @@ class _SignatureViewState extends State<SignatureView> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onAcceptanceDone() {
|
||||
setState(() => _phase = _SigningPhase.customerSignature);
|
||||
}
|
||||
bool get _customerStepValid =>
|
||||
_receiptAccepted && (_notesAccepted || _notesEmpty) && !_customerEmpty;
|
||||
|
||||
void _onCustomerSigned() {
|
||||
setState(() => _phase = _SigningPhase.driverSignature);
|
||||
}
|
||||
|
||||
Future<void> _onDriverSigned() async {
|
||||
widget.onSigned(
|
||||
(await _customerController.toPngBytes())!,
|
||||
(await _driverController.toPngBytes())!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return switch (_phase) {
|
||||
_SigningPhase.customerAcceptance => _AcceptanceStep(
|
||||
onContinue: _onAcceptanceDone,
|
||||
),
|
||||
_SigningPhase.customerSignature => _SignaturePadStep(
|
||||
controller: _customerController,
|
||||
delivery: widget.delivery,
|
||||
appBarTitle: "Unterschrift des Kunden",
|
||||
buttonLabel: "Weiter",
|
||||
onContinue: _onCustomerSigned,
|
||||
),
|
||||
_SigningPhase.driverSignature => _SignaturePadStep(
|
||||
controller: _driverController,
|
||||
delivery: widget.delivery,
|
||||
appBarTitle: "Unterschrift des Fahrers",
|
||||
buttonLabel: "Absenden",
|
||||
onContinue: _onDriverSigned,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _AcceptanceStep extends StatefulWidget {
|
||||
const _AcceptanceStep({required this.onContinue});
|
||||
|
||||
final VoidCallback onContinue;
|
||||
|
||||
@override
|
||||
State<_AcceptanceStep> createState() => _AcceptanceStepState();
|
||||
}
|
||||
|
||||
class _AcceptanceStepState extends State<_AcceptanceStep> {
|
||||
bool _customerAccepted = false;
|
||||
bool _noteAccepted = false;
|
||||
|
||||
Widget _notesContent(NoteState noteState) {
|
||||
if (noteState is! NoteLoaded) {
|
||||
return const SizedBox(
|
||||
width: double.infinity,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
/// 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;
|
||||
}
|
||||
if (noteState.notes.isEmpty) {
|
||||
return const SizedBox(
|
||||
width: double.infinity,
|
||||
child: Center(child: Text("Keine Notizen vorhanden")),
|
||||
);
|
||||
}
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.event_note_outlined),
|
||||
title: Text(noteState.notes[index].content),
|
||||
contentPadding: const EdgeInsets.all(20),
|
||||
tileColor: Theme.of(context).colorScheme.onSecondary,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const Divider(height: 0),
|
||||
itemCount: noteState.notes.length,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _notes(NoteState noteState) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 15),
|
||||
child: Text(
|
||||
"Notizen",
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
_notesContent(noteState),
|
||||
const Padding(padding: EdgeInsets.only(top: 25), child: Divider()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<NoteBloc, NoteState>(
|
||||
builder: (context, noteState) {
|
||||
final notesEmpty = switch (noteState) {
|
||||
NoteLoadedBase(notes: final ns) => ns.isEmpty,
|
||||
_ => true,
|
||||
};
|
||||
final isButtonEnabled =
|
||||
_customerAccepted && (_noteAccepted || notesEmpty);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Unterschrift des Kunden")),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 25, bottom: 0),
|
||||
child: _notes(noteState),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 25.0, bottom: 0),
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _noteAccepted,
|
||||
onChanged: notesEmpty
|
||||
? null
|
||||
: (value) {
|
||||
setState(() {
|
||||
_noteAccepted = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
Flexible(
|
||||
child: InkWell(
|
||||
onTap: notesEmpty
|
||||
? null
|
||||
: () {
|
||||
setState(() {
|
||||
_noteAccepted = !_noteAccepted;
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
"Ich nehme die oben genannten Anmerkungen zur Lieferung zur Kenntnis.",
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 25.0, bottom: 10.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _customerAccepted,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_customerAccepted = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
Flexible(
|
||||
child: InkWell(
|
||||
child: Text(
|
||||
"Ware in ordnungsgemäßem Zustand erhalten. Aufstell- und Einbauarbeiten wurden korrekt durchgeführt",
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_customerAccepted = !_customerAccepted;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
top: false,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 90,
|
||||
child: Center(
|
||||
child: FilledButton(
|
||||
onPressed: isButtonEnabled ? widget.onContinue : null,
|
||||
child: const Text("Unterschreiben"),
|
||||
),
|
||||
),
|
||||
),
|
||||
Future<void> _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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SignaturePadStep extends StatefulWidget {
|
||||
const _SignaturePadStep({
|
||||
required this.controller,
|
||||
required this.delivery,
|
||||
required this.appBarTitle,
|
||||
required this.buttonLabel,
|
||||
required this.onContinue,
|
||||
});
|
||||
|
||||
final SignatureController controller;
|
||||
final Delivery delivery;
|
||||
final String appBarTitle;
|
||||
final String buttonLabel;
|
||||
final VoidCallback onContinue;
|
||||
|
||||
@override
|
||||
State<_SignaturePadStep> createState() => _SignaturePadStepState();
|
||||
}
|
||||
|
||||
class _SignaturePadStepState extends State<_SignaturePadStep> {
|
||||
bool _isEmpty = true;
|
||||
late final VoidCallback _listener;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isEmpty = widget.controller.isEmpty;
|
||||
_listener = () {
|
||||
if (_isEmpty != widget.controller.isEmpty) {
|
||||
setState(() {
|
||||
_isEmpty = widget.controller.isEmpty;
|
||||
});
|
||||
}
|
||||
};
|
||||
widget.controller.addListener(_listener);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_listener);
|
||||
super.dispose();
|
||||
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 formattedDate = DateFormat("dd.MM.yyyy").format(DateTime.now());
|
||||
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(widget.appBarTitle)),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
appBar: AppBar(title: Text(title)),
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: MediaQuery.of(context).size.height * 0.75,
|
||||
child: DecoratedBox(
|
||||
decoration: const BoxDecoration(color: Colors.white),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Lieferung an: ${widget.delivery.customer.name}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Signature(
|
||||
controller: widget.controller,
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
Text(
|
||||
"${widget.delivery.customer.address.city}, den $formattedDate",
|
||||
),
|
||||
],
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
top: false,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 90,
|
||||
child: Center(
|
||||
child: FilledButton(
|
||||
onPressed: _isEmpty ? null : widget.onContinue,
|
||||
child: Text(widget.buttonLabel),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Notizen-Block ────────────────────────────────────────────────────────
|
||||
|
||||
class _NotesSection extends StatelessWidget {
|
||||
const _NotesSection({required this.notes});
|
||||
|
||||
final List<DeliveryNote> 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<bool> 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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user