Files
Holzleitner-Lieferservice-App/lib/feature/delivery/detail/presentation/delivery_sign.dart
2026-05-11 17:12:05 +02:00

374 lines
11 KiB
Dart

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 }
class SignatureView extends StatefulWidget {
const SignatureView({
super.key,
required this.onSigned,
required this.delivery,
});
final Delivery delivery;
/// 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;
@override
State<StatefulWidget> createState() => _SignatureViewState();
}
class _SignatureViewState extends State<SignatureView> {
final SignatureController _customerController = SignatureController(
penStrokeWidth: 5,
penColor: Colors.black,
exportBackgroundColor: Colors.white,
);
final SignatureController _driverController = SignatureController(
penStrokeWidth: 5,
penColor: Colors.black,
exportBackgroundColor: Colors.white,
);
_SigningPhase _phase = _SigningPhase.customerAcceptance;
@override
void initState() {
super.initState();
context.read<NoteBloc>().add(LoadNote(delivery: widget.delivery));
}
@override
void dispose() {
_customerController.dispose();
_driverController.dispose();
super.dispose();
}
void _onAcceptanceDone() {
setState(() => _phase = _SigningPhase.customerSignature);
}
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()),
);
}
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"),
),
),
),
),
);
},
);
}
}
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();
}
@override
Widget build(BuildContext context) {
final formattedDate = DateFormat("dd.MM.yyyy").format(DateTime.now());
return Scaffold(
appBar: AppBar(title: Text(widget.appBarTitle)),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: ListView(
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",
),
],
),
),
),
),
],
),
),
bottomNavigationBar: SafeArea(
top: false,
child: SizedBox(
width: double.infinity,
height: 90,
child: Center(
child: FilledButton(
onPressed: _isEmpty ? null : widget.onContinue,
child: Text(widget.buttonLabel),
),
),
),
),
);
}
}