Initial draft

This commit is contained in:
Dennis Nemec
2025-09-20 16:14:06 +02:00
commit b19a6e1cd4
219 changed files with 10317 additions and 0 deletions

View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/article/article_list_item.dart';
import 'package:hl_lieferservice/model/article.dart';
class ArticleList extends StatefulWidget {
const ArticleList({
super.key,
required this.articles,
required this.deliveryId,
});
final List<Article> articles;
final String deliveryId;
@override
State<StatefulWidget> createState() => _ArticleListState();
}
class _ArticleListState extends State<ArticleList> {
@override
Widget build(BuildContext context) {
return ListView.separated(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder:
(context, index) => ArticleListItem(
article: widget.articles[index],
deliveryId: widget.deliveryId,
),
separatorBuilder: (context, index) => const Divider(height: 0),
itemCount: widget.articles.length,
);
}
}

View File

@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/article/article_reset_scan_dialog.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/article/article_unscan_dialog.dart';
import 'package:hl_lieferservice/model/article.dart';
class ArticleListItem extends StatefulWidget {
const ArticleListItem({
super.key,
required this.article,
required this.deliveryId,
});
final Article article;
final String deliveryId;
@override
State<StatefulWidget> createState() => _ArticleListItem();
}
class _ArticleListItem extends State<ArticleListItem> {
Widget _leading() {
int amount = widget.article.getScannedAmount();
Color? color;
Color? textColor;
if (amount == 0) {
color = Colors.redAccent;
textColor = Theme.of(context).colorScheme.onSecondary;
}
return CircleAvatar(
backgroundColor: color,
child: Text("${amount}x", style: TextStyle(color: textColor)),
);
}
@override
Widget build(BuildContext context) {
Widget actionButton = IconButton.outlined(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(Colors.redAccent),
),
onPressed: () {
showDialog(
context: context,
builder: (context) => ArticleUnscanDialog(article: widget.article),
);
},
icon: Icon(
Icons.delete,
color: Theme.of(context).colorScheme.onSecondary,
),
);
if (widget.article.unscanned()) {
actionButton = IconButton.outlined(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(Colors.blueAccent),
),
onPressed: () {
showDialog(
context: context,
builder:
(context) => ResetArticleAmountDialog(article: widget.article),
);
},
icon: Icon(
Icons.refresh,
color: Theme.of(context).colorScheme.onSecondary,
),
);
}
return Padding(
padding: const EdgeInsets.all(0),
child: ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerLowest,
title: Text(widget.article.name),
leading: _leading(),
subtitle: Text("Artikelnr. ${widget.article.articleNumber}"),
trailing: actionButton,
),
);
}
}

View File

@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/detail/bloc/delivery_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/detail/bloc/delivery_event.dart';
import '../../../../../model/article.dart';
class ResetArticleAmountDialog extends StatefulWidget {
const ResetArticleAmountDialog({super.key, required this.article});
final Article article;
@override
State<StatefulWidget> createState() => _ResetArticleAmountDialogState();
}
class _ResetArticleAmountDialogState extends State<ResetArticleAmountDialog> {
void _reset() {
context.read<DeliveryBloc>().add(
ResetScanAmountEvent(articleId: widget.article.internalId.toString()),
);
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text("Anzahl Artikel zurücksetzen?"),
content: SizedBox(
height: 120,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Wollen Sie die entfernten Artikel wieder hinzufügen?"),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FilledButton(
onPressed: _reset,
child: const Text("Zurücksetzen"),
),
OutlinedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("Abbrechen"),
),
],
),
],
),
),
);
}
}

View File

@ -0,0 +1,153 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/detail/bloc/delivery_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/detail/bloc/delivery_event.dart';
import '../../../../../model/article.dart';
class ArticleUnscanDialog extends StatefulWidget {
const ArticleUnscanDialog({super.key, required this.article});
final Article article;
@override
State<StatefulWidget> createState() => _ArticleUnscanDialogState();
}
class _ArticleUnscanDialogState extends State<ArticleUnscanDialog> {
late TextEditingController unscanAmountController;
late TextEditingController unscanNoteController;
bool isValidText = false;
final _formKey = GlobalKey<FormState>();
void _unscan() {
context.read<DeliveryBloc>().add(
UnscanArticleEvent(
articleId: widget.article.internalId.toString(),
newAmount: int.parse(unscanAmountController.text),
reason: unscanNoteController.text,
),
);
Navigator.pop(context);
}
@override
void initState() {
super.initState();
unscanAmountController = TextEditingController(text: "1");
unscanNoteController = TextEditingController(text: "");
unscanNoteController.addListener(() {
setState(() {
isValidText = _isValid();
});
});
unscanAmountController.addListener(() {
setState(() {
isValidText = _isValid();
});
});
}
@override
void dispose() {
unscanAmountController.dispose();
unscanNoteController.dispose();
super.dispose();
}
bool _isValid() {
return _isAmountValid() && unscanNoteController.text.isNotEmpty;
}
bool _isAmountValid() {
final amount = int.tryParse(unscanAmountController.text);
return amount != null && amount > 0 && amount <= widget.article.amount;
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text("Scan rückgängig machen"),
content: SizedBox(
width: double.infinity,
height: 350,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
"Wollen Sie den Scanvorgang des Artikel '${widget.article.name}' rückgängig machen und den Artikel aus der Bestellung entfernen?",
),
Form(
key: _formKey,
autovalidateMode: AutovalidateMode.always,
child: Column(
children: [
TextFormField(
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly,
],
validator: (text) {
if (text == null || text.isEmpty) {
return "Geben Sie eine Zahl ein";
}
final amount = int.tryParse(text);
if (amount == null || amount <= 0) {
return "Geben Sie eine gültige Zahl ein";
}
if (amount > widget.article.amount) {
return "Maximal ${widget.article.amount} möglich.";
}
return null;
},
controller: unscanAmountController,
decoration: const InputDecoration(
labelText: "Menge zu löschender Artikel",
),
),
TextFormField(
controller: unscanNoteController,
keyboardType: TextInputType.text,
decoration: const InputDecoration(
labelText: "Grund für die Entfernung",
),
validator: (text) {
if (text == null || text.isEmpty) {
return "Geben Sie einen Grund an.";
}
return null;
},
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
FilledButton(
onPressed: isValidText ? _unscan : null,
child: const Text("Entfernen"),
),
OutlinedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text("Abbrechen"),
),
],
),
],
),
),
);
}
}

View File

@ -0,0 +1,186 @@
import 'dart:typed_data';
import 'package:easy_stepper/easy_stepper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/detail/bloc/delivery_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/detail/bloc/delivery_event.dart';
import 'package:hl_lieferservice/feature/delivery/detail/bloc/delivery_state.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/delivery_sign.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step.dart';
import 'package:hl_lieferservice/model/delivery.dart' as model;
class DeliveryDetail extends StatefulWidget {
final model.Delivery delivery;
const DeliveryDetail({super.key, required this.delivery});
@override
State<StatefulWidget> createState() => _DeliveryDetailState();
}
class _DeliveryDetailState extends State<DeliveryDetail> {
late int _step;
late List<EasyStep> _steps;
@override
void initState() {
super.initState();
// Initialize BLOC
context.read<DeliveryBloc>().add(
LoadDeliveryEvent(delivery: widget.delivery),
);
// Initialize steps
_step = 0;
_steps = [
EasyStep(
icon: const Icon(Icons.info),
customTitle: Text("Info", textAlign: TextAlign.center),
),
EasyStep(
icon: const Icon(Icons.book),
customTitle: Text("Notizen", textAlign: TextAlign.center),
),
EasyStep(
icon: const Icon(Icons.shopping_cart),
customTitle: Text("Artikel/Gutschriften", textAlign: TextAlign.center),
),
EasyStep(
icon: const Icon(Icons.settings),
customTitle: Text("Optionen", textAlign: TextAlign.center),
),
EasyStep(
icon: const Icon(Icons.check_box),
customTitle: Text(
"Überprüfen",
textAlign: TextAlign.center,
overflow: TextOverflow.clip,
),
),
];
}
Widget _stepInfo() {
return DecoratedBox(
decoration: const BoxDecoration(),
child: SizedBox(
height: 115,
child: EasyStepper(
activeStep: _step,
showLoadingAnimation: false,
activeStepTextColor: Theme.of(context).primaryColor,
activeStepBorderType: BorderType.dotted,
finishedStepBorderType: BorderType.normal,
unreachedStepBorderType: BorderType.normal,
activeStepBackgroundColor: Colors.white,
borderThickness: 2,
internalPadding: 0.0,
enableStepTapping: true,
stepRadius: 25.0,
onStepReached:
(index) => {
setState(() {
_step = index;
}),
},
steps: _steps,
),
),
);
}
Widget _stepMissingWarning() {
return Center(
child: Text("Kein Inhalt für den aktuellen Step $_step gefunden."),
);
}
void _clickForward() {
if (_step < _steps.length) {
setState(() {
_step += 1;
});
}
}
void _clickBack() {
if (_step > 0) {
setState(() {
_step -= 1;
});
}
}
void _openSignatureView() {
Navigator.of(context).push(
MaterialPageRoute(
builder:
(context) => SignatureView(
onSigned: _onSign,
customer: widget.delivery.customer,
),
),
);
}
void _onSign(Uint8List customer, Uint8List driver) async {
}
Widget _stepsNavigation() {
return SizedBox(
width: double.infinity,
height: 90,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
OutlinedButton(
onPressed: _step == 0 ? null : _clickBack,
child: const Text("zurück"),
),
Padding(
padding: const EdgeInsets.only(left: 20),
child: FilledButton(
onPressed: _step == _steps.length - 1 ? _openSignatureView : _clickForward,
child:
_step == _steps.length - 1
? const Text("Unterschreiben")
: const Text("weiter"),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Auslieferungsdetails")),
body: BlocBuilder<DeliveryBloc, DeliveryState>(
builder: (context, state) {
final currentState = state;
if (currentState is DeliveryLoaded) {
return Column(
children: [
_stepInfo(),
const Divider(),
Expanded(
child:
StepFactory().make(_step, currentState.delivery) ??
_stepMissingWarning(),
),
_stepsNavigation(),
],
);
}
return Container();
},
),
);
}
}

View File

@ -0,0 +1,221 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/detail/bloc/delivery_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/detail/bloc/delivery_event.dart';
import 'package:hl_lieferservice/model/delivery.dart';
class DeliveryDiscount extends StatefulWidget {
const DeliveryDiscount({
super.key,
this.discount,
required this.disabled,
required this.deliveryId,
});
final bool disabled;
final Discount? discount;
final String deliveryId;
@override
State<StatefulWidget> createState() => _DeliveryDiscountState();
}
class _DeliveryDiscountState extends State<DeliveryDiscount> {
final int stepSize = 10;
late TextEditingController _reasonController;
late bool _isReasonEmpty;
late bool _isUpdated;
late int _discountValue;
@override
void initState() {
super.initState();
_reasonController = TextEditingController(text: widget.discount?.note);
_isReasonEmpty = _reasonController.text.isEmpty;
_reasonController.addListener(() {
setState(() {
_isReasonEmpty = _reasonController.text.isEmpty;
});
});
_discountValue =
widget.discount?.article.getGrossPrice().floor().abs() ?? 0;
_isUpdated = _discountValue > 0 && _reasonController.text.isNotEmpty;
}
@override
void dispose() {
super.dispose();
_reasonController.dispose();
}
bool _maximumReached() {
return _discountValue >= 150;
}
bool _minimumReached() {
return _discountValue <= 0;
}
Widget _incrementDiscount() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton.filled(
onPressed:
_minimumReached() || widget.disabled
? null
: () {
setState(() {
if (_discountValue - stepSize >= 0) {
_discountValue -= stepSize;
}
});
},
icon: const Icon(Icons.remove),
style: ButtonStyle(
backgroundColor:
_minimumReached() || widget.disabled
? WidgetStateProperty.all(Colors.grey)
: WidgetStateProperty.all(Colors.red),
),
),
Padding(
padding: const EdgeInsets.all(5),
child: Column(
children: [
Text(
"${_discountValue.abs()}",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18.0,
),
),
const Text("max. 150€", style: TextStyle(fontSize: 10.0)),
],
),
),
IconButton.filled(
onPressed:
_maximumReached() || widget.disabled
? null
: () {
setState(() {
_discountValue += stepSize;
});
},
icon: const Icon(Icons.add),
style: ButtonStyle(
backgroundColor:
_maximumReached() || widget.disabled
? WidgetStateProperty.all(Colors.grey)
: WidgetStateProperty.all(Colors.green),
),
),
],
);
}
void _resetValues() async {
setState(() {
_discountValue = 0;
_reasonController.clear();
_isUpdated = false;
});
context.read<DeliveryBloc>().add(
RemoveDiscountEvent(deliveryId: widget.deliveryId),
);
}
void _updateValues() async {
if (_isUpdated) {
context.read<DeliveryBloc>().add(
UpdateDiscountEvent(
deliveryId: widget.deliveryId,
value: _discountValue,
reason: _reasonController.text,
),
);
} else {
context.read<DeliveryBloc>().add(
AddDiscountEvent(
deliveryId: widget.deliveryId,
value: _discountValue,
reason: _reasonController.text,
),
);
setState(() {
setState(() {
_isUpdated = true;
});
});
}
}
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSecondary,
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Form(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Betrag:",
style: TextStyle(fontWeight: FontWeight.bold),
),
_incrementDiscount(),
const Padding(
padding: const EdgeInsets.only(top: 10),
child: const Text(
"Begründung:",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: TextFormField(
controller: _reasonController,
validator: (text) {
if (text == null || text.isEmpty) {
return "Begründung für Gutschrift notwendig.";
}
return null;
},
),
),
Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 10),
child: FilledButton(
onPressed:
!_isReasonEmpty && _discountValue > 0
? _updateValues
: null,
child: const Text("Speichern"),
),
),
OutlinedButton(
onPressed: _discountValue > 0 ? _resetValues : null,
child: const Text("Gutschrift entfernen"),
),
],
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/detail/bloc/delivery_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/detail/bloc/delivery_event.dart';
import 'package:hl_lieferservice/model/delivery.dart' as model;
class DeliveryOptionsView extends StatefulWidget {
const DeliveryOptionsView({super.key, required this.options});
final List<model.DeliveryOption> options;
@override
State<StatefulWidget> createState() => _DeliveryOptionsViewState();
}
class _DeliveryOptionsViewState extends State<DeliveryOptionsView> {
@override
void initState() {
super.initState();
}
@override
void didUpdateWidget(covariant DeliveryOptionsView oldWidget) {
super.didUpdateWidget(oldWidget);
}
void _update(model.DeliveryOption option, dynamic value) {
context.read<DeliveryBloc>().add(
UpdateDeliveryOption(key: option.key, value: value),
);
}
List<Widget> _options() {
List<Widget> boolOptions =
widget.options.where((option) => !option.numerical).map((option) {
return CheckboxListTile(
value: option.getValue() as bool,
onChanged: (value) => _update(option, value),
title: Text(option.display),
);
}).toList();
List<Widget> numericalOptions =
widget.options.where((option) => option.numerical).map((option) {
return Padding(
padding: const EdgeInsets.all(15),
child: TextFormField(
decoration: InputDecoration(labelText: option.display),
initialValue: option.getValue().toString(),
keyboardType: TextInputType.number,
onTapOutside: (event) => FocusScope.of(context).unfocus(),
onChanged: (value) => _update(option, value),
),
);
}).toList();
return [
Padding(
padding: const EdgeInsets.only(bottom: 5),
child: Text(
"Auswählbare Optionen",
style: Theme.of(context).textTheme.headlineSmall,
),
),
...boolOptions,
Padding(
padding: const EdgeInsets.only(top: 10),
child: Text(
"Zahlenwerte",
style: Theme.of(context).textTheme.headlineSmall,
),
),
...numericalOptions,
];
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(10),
child: ListView(children: _options()),
);
}
}

View File

@ -0,0 +1,164 @@
import 'dart:typed_data';
import 'package:hl_lieferservice/model/customer.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:signature/signature.dart';
class SignatureView extends StatefulWidget {
const SignatureView({
super.key,
required this.onSigned,
required this.customer,
});
final Customer customer;
/// 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,
);
bool _isDriverSigning = false;
bool _customerAccepted = false;
@override
void initState() {
super.initState();
}
@override
void dispose() {
_customerController.dispose();
super.dispose();
}
Widget _signatureField() {
return Signature(
controller: _isDriverSigning ? _driverController : _customerController,
backgroundColor: Colors.white,
);
}
@override
Widget build(BuildContext context) {
String formattedDate = DateFormat("dd.MM.yyyy").format(DateTime.now());
return Scaffold(
appBar: AppBar(
title:
!_isDriverSigning
? const Text("Unterschrift des Kunden")
: const Text("Unterschrift des Fahrers"),
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: SizedBox(
width: double.infinity,
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.customer.name}",
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
Expanded(child: _signatureField()),
],
),
),
const Divider(),
Text(
"${widget.customer.address.city}, den $formattedDate",
),
],
),
),
),
),
),
!_isDriverSigning
? Padding(
padding: const EdgeInsets.only(top: 25.0, bottom: 25.0),
child: Row(
children: [
Checkbox(
value: _customerAccepted,
onChanged: (value) {
setState(() {
_customerAccepted = value!;
});
},
),
const Flexible(
child: Text(
"Ich bestätige, dass ich die Ware im ordnungsgemäßen Zustand erhalten habe und, dass die Aufstell- und Einbauarbeiten korrekt durchgeführt wurden.",
overflow: TextOverflow.fade,
),
),
],
),
)
: Container(),
Padding(
padding: const EdgeInsets.only(top: 25.0, bottom: 25.0),
child: Center(
child: FilledButton(
onPressed:
!_customerAccepted
? null
: () async {
if (!_isDriverSigning) {
setState(() {
_isDriverSigning = true;
});
} else {
widget.onSigned(
(await _customerController.toPngBytes())!,
(await _driverController.toPngBytes())!,
);
}
},
child:
!_isDriverSigning
? const Text("Weiter")
: const Text("Absenden"),
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,182 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/detail/bloc/delivery_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/detail/bloc/delivery_event.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_state.dart';
import 'package:hl_lieferservice/model/delivery.dart';
import '../../../../model/tour.dart';
class DeliverySummary extends StatefulWidget {
const DeliverySummary({super.key, required this.delivery});
final Delivery delivery;
@override
State<StatefulWidget> createState() => _DeliverySummaryState();
}
class _DeliverySummaryState extends State<DeliverySummary> {
late List<Payment> _paymentMethods;
@override
void initState() {
super.initState();
final tourState = context.read<TourBloc>().state as TourLoaded;
_paymentMethods = [
widget.delivery.payment,
...tourState.tour.paymentMethods,
];
}
Widget _deliveredArticles() {
List<Widget> items =
widget.delivery
.getDeliveredArticles()
.map(
(article) => DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSecondary,
),
child: ListTile(
title: Text(article.name),
subtitle: Text("Artikelnr. ${article.articleNumber}"),
trailing: Text(
"${article.scannable ? article.getGrossPriceScanned().toStringAsFixed(2) : article.getGrossPrice().toStringAsFixed(2)}",
),
leading: CircleAvatar(
child: Text(
"${article.scannable ? article.scannedAmount : article.amount}x",
),
),
),
),
)
.toList();
items.add(
DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSecondary,
),
child: ListTile(
title: const Text(
"Gesamtsumme:",
style: TextStyle(fontWeight: FontWeight.bold),
),
trailing: Text(
"${widget.delivery.getGrossPrice().toStringAsFixed(2)}",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
);
return ListView(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
children: items,
);
}
Widget _paymentOptions() {
List<DropdownMenuEntry> entries =
_paymentMethods
.map(
(payment) => DropdownMenuEntry(
value: payment.id,
label: "${payment.description} (${payment.shortcode})",
),
)
.toList();
debugPrint(widget.delivery.payment.id);
return DropdownMenu(
dropdownMenuEntries: entries,
initialSelection: widget.delivery.payment.id,
onSelected: (id) {
context.read<DeliveryBloc>().add(
UpdateSelectedPaymentMethod(
payment: _paymentMethods.firstWhere(
(payment) => payment.id == id,
),
),
);
},
);
}
Widget _payment() {
return _paymentOptions();
}
Widget _paymentDone() {
return DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSecondary,
),
child: Column(
children: [
ListTile(
title: const Text(
"Bei Bestellung bezahlt:",
style: TextStyle(fontWeight: FontWeight.bold),
),
trailing: Text("${widget.delivery.prepayment.toStringAsFixed(2)}"),
),
ListTile(
title: const Text(
"Offener Betrag:",
style: TextStyle(fontWeight: FontWeight.bold),
),
trailing: Text(
"${widget.delivery.getOpenPrice().toStringAsFixed(2)}",
style: TextStyle(fontWeight: FontWeight.w900, color: Colors.red),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final insets = EdgeInsets.all(10);
return Padding(
padding: const EdgeInsets.all(10),
child: ListView(
children: [
Text(
"Ausgelieferte Artikel",
style: Theme.of(context).textTheme.headlineSmall,
),
Padding(padding: insets, child: _deliveredArticles()),
Padding(
padding: const EdgeInsets.only(top: 10),
child: Text(
"Geleistete Zahlung",
style: Theme.of(context).textTheme.headlineSmall,
),
),
Padding(padding: insets, child: _paymentDone()),
Padding(
padding: const EdgeInsets.only(top: 10),
child: Text(
"Zahlungsmethode",
style: Theme.of(context).textTheme.headlineSmall,
),
),
Padding(padding: insets, child: _payment()),
],
),
);
}
}

View File

@ -0,0 +1,151 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
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/model/delivery.dart';
class NoteAddDialog extends StatefulWidget {
final String delivery;
final List<NoteTemplate> templates;
const NoteAddDialog({
super.key,
required this.delivery,
required this.templates,
});
@override
State<StatefulWidget> createState() => _NoteAddDialogState();
}
class _NoteAddDialogState extends State<NoteAddDialog> {
final _noteController = TextEditingController();
final _noteSelectionController = TextEditingController();
late FocusNode _noteFieldFocusNode;
bool _isCustomNotesEmpty = true;
@override
void initState() {
super.initState();
_noteFieldFocusNode = FocusNode();
_noteController.addListener(() {
setState(() {
_isCustomNotesEmpty = _noteController.text.isEmpty;
});
});
}
void _onSave() {
String content = _noteController.text;
if (_noteSelectionController.text.isNotEmpty) {
NoteTemplate template = widget.templates.firstWhere(
(note) => note.title == _noteSelectionController.text,
);
content = template.content;
}
context.read<NoteBloc>().add(
AddNote(note: content, deliveryId: widget.delivery),
);
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
return Dialog(
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.75,
height: MediaQuery.of(context).size.height * 0.45,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Notiz hinzufügen",
style: Theme.of(context).textTheme.headlineSmall,
),
IconButton(
onPressed: () {
Navigator.of(context).pop();
},
icon: Icon(Icons.close),
),
],
),
Padding(
padding: const EdgeInsets.only(bottom: 10.0, top: 20),
child: DropdownMenu(
controller: _noteSelectionController,
onSelected: (int? value) {
setState(() {
_noteSelectionController.text =
widget.templates[value!].title;
});
},
enabled: _isCustomNotesEmpty,
width: double.infinity,
label: const Text("Notiz auswählen"),
dropdownMenuEntries:
widget.templates
.mapIndexed(
(i, note) =>
DropdownMenuEntry(value: i, label: note.title),
)
.toList(),
),
),
const Padding(
padding: EdgeInsets.only(top: 0.0, bottom: 0.0),
child: Text("oder"),
),
Padding(
padding: const EdgeInsets.only(top: 10.0, bottom: 20.0),
child: TextFormField(
controller: _noteController,
enabled: _noteSelectionController.text.isEmpty,
focusNode: _noteFieldFocusNode,
decoration: const InputDecoration(
labelText: "Eigene Notiz",
border: OutlineInputBorder(),
),
minLines: 3,
maxLines: 5,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
FilledButton(
onPressed:
_noteSelectionController.text.isNotEmpty ||
_noteController.text.isNotEmpty
? _onSave
: null,
child: const Text("Hinzufügen"),
),
Padding(
padding: const EdgeInsets.only(left: 10.0),
child: OutlinedButton(
onPressed: null,
child: const Text("Zurücksetzen"),
),
),
],
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,104 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
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/model/delivery.dart';
class NoteEditDialog extends StatefulWidget {
final Note note;
const NoteEditDialog({super.key, required this.note});
@override
State<StatefulWidget> createState() => _NoteEditDialogState();
}
class _NoteEditDialogState extends State<NoteEditDialog> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _editController;
@override
void initState() {
super.initState();
_editController = TextEditingController(text: widget.note.content);
}
void _onEdit(BuildContext context) {
context.read<NoteBloc>().add(
EditNote(
content: _editController.text,
noteId: widget.note.id.toString(),
),
);
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
return Dialog(
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.75,
height: MediaQuery.of(context).size.height * 0.32,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Text(
"Notiz bearbeiten",
style: Theme.of(context).textTheme.headlineMedium,
),
Padding(
padding: const EdgeInsets.only(top: 15),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
onTapOutside: (event) {
FocusScope.of(context).unfocus();
},
decoration: InputDecoration(label: const Text("Notiz")),
controller: _editController,
minLines: 4,
maxLines: 8,
),
Padding(
padding: const EdgeInsets.only(top: 20),
child: Row(
children: [
FilledButton(
onPressed: () {
_onEdit(context);
},
child: const Text("Bearbeiten"),
),
Padding(
padding: const EdgeInsets.only(left: 10),
child: OutlinedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("Abbrechen"),
),
),
],
),
),
],
),
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,109 @@
import 'dart:typed_data';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
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/model/delivery.dart';
class NoteImageOverview extends StatefulWidget {
final List<(ImageNote, Uint8List)> images;
final String deliveryId;
const NoteImageOverview({
super.key,
required this.images,
required this.deliveryId,
});
@override
State<StatefulWidget> createState() => _NoteImageOverviewState();
}
class _NoteImageOverviewState extends State<NoteImageOverview> {
int? _imageDeleting;
void _onRemoveImage(int index) {
ImageNote note = widget.images[index].$1;
context.read<NoteBloc>().add(
RemoveImageNote(objectId: note.objectId, deliveryId: widget.deliveryId),
);
}
Widget _buildImageCarousel() {
return CarouselSlider(
options: CarouselOptions(
height: 300.0,
aspectRatio: 2.0,
enableInfiniteScroll: false,
),
items:
widget.images.mapIndexed((index, data) {
ImageNote note = data.$1;
Uint8List bytes = data.$2;
return Builder(
builder: (BuildContext context) {
return Stack(
children: [
Padding(
padding: const EdgeInsets.all(15.0),
child: Image.memory(
bytes,
fit: BoxFit.fill,
width: 1920.0,
height: 1090.0,
),
),
_imageDeleting == index
? Stack(
children: [
Padding(
padding: const EdgeInsets.all(15.0),
child: Container(
color: Colors.black.withValues(alpha: 0.5),
),
),
Center(
child: CircularProgressIndicator(
backgroundColor:
Theme.of(context).colorScheme.onSecondary,
),
),
],
)
: Container(),
Positioned(
right: 0.0,
top: 0.0,
child: CircleAvatar(
radius: 20,
child: IconButton.filled(
onPressed:
!(_imageDeleting == index)
? () {
_onRemoveImage(index);
}
: null,
icon: const Icon(Icons.delete, color: Colors.white),
),
),
),
],
);
},
);
}).toList(),
);
}
@override
Widget build(BuildContext context) {
return widget.images.isEmpty
? const Center(child: Text("Noch keine Bilder hochgeladen"))
: _buildImageCarousel();
}
}

View File

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:hl_lieferservice/feature/delivery/detail/model/note.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/note/note_list_item.dart';
class NoteList extends StatelessWidget {
final List<NoteInformation> notes;
final String deliveryId;
const NoteList({super.key, required this.notes, required this.deliveryId});
@override
Widget build(BuildContext context) {
if (notes.isEmpty) {
return const Center(child: Text("keine Notizen vorhanden"));
}
return ListView.separated(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder:
(context, index) => NoteListItem(
note: notes[index],
deliveryId: deliveryId,
index: index,
),
separatorBuilder: (context, index) => const Divider(height: 0),
itemCount: notes.length,
);
}
}

View File

@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
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/model/note.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/note/note_edit_dialog.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_state.dart';
import '../../../../../model/delivery.dart';
class NoteListItem extends StatelessWidget {
final NoteInformation note;
final String deliveryId;
final int index;
const NoteListItem({
super.key,
required this.note,
required this.deliveryId,
required this.index,
});
void _onDelete(BuildContext context) {
context.read<NoteBloc>().add(RemoveNote(noteId: note.note.id.toString()));
}
Widget? _subtitle(BuildContext context) {
String discountArticleId =
(context.read<TourBloc>().state as TourLoaded)
.tour
.discountArticleNumber;
if (note.article != null && note.article?.articleNumber == discountArticleId) {
return const Text("Begründung der Gutschrift");
}
return note.article != null ? Text(note.article!.name) : null;
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(0),
child: ListTile(
title: Text(note.note.content),
subtitle: _subtitle(context),
tileColor: Theme.of(context).colorScheme.surfaceContainerLowest,
leading: CircleAvatar(child: Text("${index + 1}")),
trailing: PopupMenuButton(
itemBuilder: (context) {
return [
PopupMenuItem(
onTap: () {
showDialog(
context: context,
builder: (context) => NoteEditDialog(note: note.note),
);
},
child: Row(
children: [
Icon(Icons.edit, color: Colors.blueAccent),
Padding(
padding: const EdgeInsets.only(left: 5),
child: const Text("Editieren"),
),
],
),
),
PopupMenuItem(
onTap: () {
_onDelete(context);
},
child: Row(
children: [
Icon(Icons.delete, color: Colors.redAccent),
Padding(
padding: const EdgeInsets.only(left: 5),
child: const Text("Löschen"),
),
],
),
),
];
},
),
),
);
}
}

View File

@ -0,0 +1,162 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
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/model/note.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/note/note_add_dialog.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/note/note_image_overview.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/note/note_list.dart';
import 'package:hl_lieferservice/model/delivery.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
import 'package:image_picker/image_picker.dart';
class NoteOverview extends StatefulWidget {
final List<NoteInformation> notes;
final List<NoteTemplate> templates;
final List<(ImageNote, Uint8List)> images;
final String deliveryId;
const NoteOverview({
super.key,
required this.notes,
required this.deliveryId,
required this.templates,
required this.images,
});
@override
State<StatefulWidget> createState() => _NoteOverviewState();
}
class _NoteOverviewState extends State<NoteOverview> {
final _imagePicker = ImagePicker();
Widget _notes() {
return NoteList(notes: widget.notes, deliveryId: widget.deliveryId);
}
Widget _images() {
return NoteImageOverview(
images: widget.images,
deliveryId: widget.deliveryId,
);
}
void _onAddNote(BuildContext context) {
showDialog(
context: context,
builder: (context) {
return NoteAddDialog(
delivery: widget.deliveryId,
templates: widget.templates,
);
},
);
}
void _onAddImage(BuildContext context) async {
XFile? file = await _imagePicker.pickImage(source: ImageSource.camera);
if (file == null) {
context.read<OperationBloc>().add(
FailOperation(message: "Fehler beim Aufnehmen des Bildes"),
);
return;
}
context.read<NoteBloc>().add(
AddImageNote(file: file, deliveryId: widget.deliveryId),
);
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
Padding(
padding: const EdgeInsets.all(10),
child: ListView(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Text(
"Notizen",
style: Theme.of(context).textTheme.headlineMedium,
),
),
_notes(),
Padding(
padding: const EdgeInsets.only(bottom: 10, top: 10),
child: Text(
"Bilder",
style: Theme.of(context).textTheme.headlineMedium,
),
),
_images(),
],
),
),
Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.only(right: 25),
child: PopupMenuButton(
itemBuilder: (context) {
return [
PopupMenuItem(
child: Row(
children: [
Icon(
Icons.note_add_rounded,
color: Theme.of(context).primaryColor,
),
Padding(
padding: const EdgeInsets.only(left: 10),
child: const Text("Notiz hinzufügen"),
),
],
),
onTap: () => _onAddNote(context),
),
PopupMenuItem(
child: Row(
children: [
Icon(
Icons.image,
color: Theme.of(context).primaryColor,
),
Padding(
padding: const EdgeInsets.only(left: 10),
child: const Text("Bild hochladen"),
),
],
),
onTap: () => _onAddImage(context),
),
];
},
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
Theme.of(context).primaryColor,
),
),
child: CircleAvatar(
radius: 32,
backgroundColor: Theme.of(context).primaryColor,
child: Icon(
Icons.add,
color: Theme.of(context).colorScheme.onSecondary,
),
),
),
),
),
],
);
}
}

View File

@ -0,0 +1,32 @@
import 'package:flutter/cupertino.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step_article_management.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step_delivery_options.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step_info.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step_note.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step_summary.dart';
import 'package:hl_lieferservice/model/delivery.dart';
abstract class IStepFactory {
Widget? make(int step, Delivery delivery);
}
class StepFactory extends IStepFactory {
@override
Widget? make(int step, Delivery delivery) {
switch(step) {
case 0:
return DeliveryStepInfo(delivery: delivery);
case 1:
return DeliveryStepNote(delivery: delivery);
case 2:
return DeliveryStepArticleManagement(delivery: delivery);
case 3:
return DeliveryStepOptions(delivery: delivery);
case 4:
return DeliveryStepSummary(delivery: delivery);
}
return null;
}
}

View File

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/article/article_list.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/delivery_discount.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_state.dart';
import 'package:hl_lieferservice/model/delivery.dart';
class DeliveryStepArticleManagement extends StatefulWidget {
final Delivery delivery;
const DeliveryStepArticleManagement({required this.delivery, super.key});
@override
State<StatefulWidget> createState() => _DeliveryStepInfo();
}
class _DeliveryStepInfo extends State<DeliveryStepArticleManagement> {
Widget _articleOverview() {
TourLoaded tour = context.read<TourBloc>().state as TourLoaded;
return ArticleList(
articles:
widget.delivery.articles
.where(
(article) =>
article.articleNumber != tour.tour.discountArticleNumber,
)
.toList(),
deliveryId: widget.delivery.id,
);
}
Widget _discountView() {
return DeliveryDiscount(
disabled: false,
discount: widget.delivery.discount,
deliveryId: widget.delivery.id,
);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(10),
child: Container(
width: double.infinity,
alignment: Alignment.centerLeft,
child: ListView(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Text(
"Artikel",
style: Theme.of(context).textTheme.headlineMedium,
),
),
_articleOverview(),
Padding(
padding: const EdgeInsets.only(top: 20, bottom: 10),
child: Text(
"Gutschriften",
style: Theme.of(context).textTheme.headlineMedium,
),
),
_discountView(),
],
),
),
);
}
}

View File

@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/delivery_options.dart';
import 'package:hl_lieferservice/model/delivery.dart' as model;
class DeliveryStepOptions extends StatefulWidget {
final model.Delivery delivery;
const DeliveryStepOptions({required this.delivery, super.key});
@override
State<StatefulWidget> createState() => _DeliveryStepInfo();
}
class _DeliveryStepInfo extends State<DeliveryStepOptions> {
@override
Widget build(BuildContext context) {
debugPrint(
"${widget.delivery.options.map((option) => "${option.display}, ${option.value}")}",
);
return DeliveryOptionsView(options: widget.delivery.options);
}
}

View File

@ -0,0 +1,199 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/model/article.dart';
import 'package:hl_lieferservice/model/delivery.dart';
import '../../../overview/bloc/tour_bloc.dart';
import '../../../overview/bloc/tour_state.dart';
class DeliveryStepInfo extends StatefulWidget {
final Delivery delivery;
const DeliveryStepInfo({required this.delivery, super.key});
@override
State<StatefulWidget> createState() => _DeliveryStepInfo();
}
class _DeliveryStepInfo extends State<DeliveryStepInfo> {
Widget _fastActions() {
return SizedBox(
width: double.infinity,
child: Card(
color: Theme.of(context).colorScheme.onSecondary,
child: Padding(
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Column(
children: [
IconButton.filled(onPressed: () {}, icon: Icon(Icons.phone)),
Text("Anrufen"),
],
),
Column(
children: [
IconButton.filled(
onPressed: () {},
icon: Icon(Icons.map_outlined),
),
Text("Navigation starten"),
],
),
],
),
),
),
);
}
Widget _customerInformation() {
return SizedBox(
width: double.infinity,
child: Card(
color: Theme.of(context).colorScheme.onSecondary,
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
children: [
Row(
children: [
Icon(Icons.person, color: Theme.of(context).primaryColor),
Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
widget.delivery.customer.name,
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
Padding(
padding: const EdgeInsets.only(top: 15),
child: Row(
children: [
Icon(
Icons.other_houses,
color: Theme.of(context).primaryColor,
),
Padding(
padding: const EdgeInsets.only(left: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.delivery.customer.address.street),
Text(
"${widget.delivery.customer.address.postalCode} ${widget.delivery.customer.address.city}",
),
],
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(top: 15),
child: Row(
children: [
Icon(Icons.phone, color: Theme.of(context).primaryColor),
Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
widget.delivery.contactPerson?.phoneNumber.toString() ??
"",
),
),
],
),
),
],
),
),
),
);
}
Widget _articleList() {
TourLoaded tour = context.read<TourBloc>().state as TourLoaded;
List<Article> filteredArticles =
widget.delivery.articles
.where(
(article) =>
article.articleNumber != tour.tour.discountArticleNumber,
)
.toList();
return ListView.separated(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
Article article = filteredArticles[index];
return DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSecondary,
),
child: ListTile(
title: Text(article.name),
subtitle: Text("Artikelnr. ${article.articleNumber}"),
leading: Chip(label: Text("${article.amount.toString()}x")),
),
);
},
separatorBuilder: (context, index) => const Divider(height: 0),
itemCount: filteredArticles.length,
);
}
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.all(10),
child: ListView(
children: [
Text(
"Schnellaktionen",
style: Theme.of(context).textTheme.headlineSmall,
),
Padding(
padding: const EdgeInsets.only(top: 10),
child: _fastActions(),
),
Padding(
padding: const EdgeInsets.only(top: 20),
child: Text(
"Kundeninformationen",
style: Theme.of(context).textTheme.headlineSmall,
),
),
Padding(
padding: const EdgeInsets.only(top: 10),
child: _customerInformation(),
),
Padding(
padding: const EdgeInsets.only(top: 20),
child: Text(
"Zu liefernde Artikel",
style: Theme.of(context).textTheme.headlineSmall,
),
),
Padding(
padding: const EdgeInsets.only(top: 20),
child: _articleList(),
),
],
),
),
);
}
}

View File

@ -0,0 +1,91 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
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:hl_lieferservice/feature/delivery/detail/model/note.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/note/note_overview.dart';
import 'package:hl_lieferservice/model/delivery.dart';
class DeliveryStepNote extends StatefulWidget {
final Delivery delivery;
const DeliveryStepNote({required this.delivery, super.key});
@override
State<StatefulWidget> createState() => _DeliveryStepInfo();
}
class _DeliveryStepInfo extends State<DeliveryStepNote> {
@override
void initState() {
super.initState();
context.read<NoteBloc>().add(LoadNote(delivery: widget.delivery));
}
Widget _notesLoadingFailed() {
return Center(child: Text("Notizen können nicht heruntergeladen werden.."));
}
Widget _notesLoading() {
return Center(child: CircularProgressIndicator());
}
Widget _blocUndefinedState() {
return Center(child: const Text("NoteBloc in einem Fehlerhaften Zustand"));
}
Widget _notesOverview(
BuildContext context,
List<Note> notes,
List<NoteTemplate> templates,
List<(ImageNote, Uint8List)> images,
) {
List<NoteInformation> hydratedNotes =
notes
.map(
(note) => NoteInformation(
note: note,
article: widget.delivery.findArticleWithNoteId(
note.id.toString(),
),
),
)
.toList();
return NoteOverview(
notes: hydratedNotes,
deliveryId: widget.delivery.id,
templates: templates,
images: images,
);
}
@override
Widget build(BuildContext context) {
return BlocBuilder<NoteBloc, NoteState>(
builder: (context, state) {
if (state is NoteLoading) {
return _notesLoading();
}
if (state is NoteLoaded) {
return _notesOverview(
context,
state.notes,
state.templates,
state.images,
);
}
if (state is NoteLoadingFailed) {
return _notesLoadingFailed();
}
return _blocUndefinedState();
},
);
}
}

View File

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/delivery_summary.dart';
import 'package:hl_lieferservice/model/delivery.dart';
class DeliveryStepSummary extends StatefulWidget {
final Delivery delivery;
const DeliveryStepSummary({required this.delivery, super.key});
@override
State<StatefulWidget> createState() => _DeliveryStepInfo();
}
class _DeliveryStepInfo extends State<DeliveryStepSummary> {
@override
Widget build(BuildContext context) {
return DeliverySummary(delivery: widget.delivery);
}
}