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,309 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/dto/discount_add_response.dart';
import 'package:hl_lieferservice/dto/discount_update_response.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/repository/delivery_repository.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
import '../../../../dto/discount_remove_response.dart';
import '../../../../model/article.dart';
import '../../../../model/delivery.dart' as model;
class DeliveryBloc extends Bloc<DeliveryEvent, DeliveryState> {
OperationBloc opBloc;
DeliveryRepository repository;
DeliveryBloc({required this.opBloc, required this.repository})
: super(DeliveryInitial()) {
on<UnscanArticleEvent>(_unscan);
on<ResetScanAmountEvent>(_resetAmount);
on<LoadDeliveryEvent>(_load);
on<AddDiscountEvent>(_addDiscount);
on<RemoveDiscountEvent>(_removeDiscount);
on<UpdateDiscountEvent>(_updateDiscount);
on<UpdateDeliveryOption>(_updateDeliveryOptions);
on<UpdateSelectedPaymentMethod>(_updatePayment);
}
void _updatePayment(
UpdateSelectedPaymentMethod event,
Emitter<DeliveryState> emit,
) {
final currentState = state;
if (currentState is DeliveryLoaded) {
emit(
DeliveryLoaded(
delivery: currentState.delivery.copyWith(payment: event.payment),
),
);
}
}
void _updateDeliveryOptions(
UpdateDeliveryOption event,
Emitter<DeliveryState> emit,
) {
final currentState = state;
if (currentState is DeliveryLoaded) {
List<model.DeliveryOption> options =
currentState.delivery.options.map((option) {
if (option.key == event.key) {
return option.copyWith(value: event.value.toString());
}
return option;
}).toList();
emit(
DeliveryLoaded(
delivery: currentState.delivery.copyWith(options: options),
),
);
}
}
void _updateDiscount(
UpdateDiscountEvent event,
Emitter<DeliveryState> emit,
) async {
opBloc.add(LoadOperation());
try {
final currentState = state;
if (currentState is DeliveryLoaded) {
DiscountUpdateResponseDTO response = await repository.updateDiscount(
event.deliveryId,
event.reason,
event.value,
);
model.Delivery delivery = currentState.delivery;
if (response.values?.receipt != null) {
delivery.totalNetValue = response.values!.receipt.net;
delivery.totalGrossValue = response.values!.receipt.gross;
}
String discountArticleNumber = delivery.discount!.article.articleNumber;
delivery.discount = model.Discount(
article:
response.values?.article != null
? Article.fromDTO(response.values!.article)
: delivery.discount!.article,
note:
response.values?.note != null
? response.values!.note.noteDescription
: delivery.discount!.note,
noteId:
response.values?.note != null
? response.values!.note.rowId
: delivery.discount!.noteId,
);
delivery.articles = [
...delivery.articles.where(
(article) => article.articleNumber != discountArticleNumber,
),
delivery.discount!.article,
];
emit(currentState.copyWith(delivery));
opBloc.add(FinishOperation());
}
} catch (e, st) {
debugPrint(
"Fehler beim Hinzufügen eins Discounts zur Lieferung: ${event.deliveryId}:",
);
debugPrint("$e");
debugPrint("$st");
opBloc.add(
FailOperation(message: "Fehler beim Hinzufügen des Discounts: $e"),
);
}
}
void _removeDiscount(
RemoveDiscountEvent event,
Emitter<DeliveryState> emit,
) async {
opBloc.add(LoadOperation());
try {
final currentState = state;
if (currentState is DeliveryLoaded) {
model.Delivery delivery = currentState.delivery;
DiscountRemoveResponseDTO response = await repository.removeDiscount(
event.deliveryId,
);
delivery.articles =
delivery.articles
.where(
(article) =>
article.internalId !=
delivery.discount?.article.internalId,
)
.toList();
delivery.discount = null;
delivery.totalGrossValue = response.receipt.gross;
delivery.totalNetValue = response.receipt.net;
emit(currentState.copyWith(delivery));
opBloc.add(FinishOperation());
}
} catch (e, st) {
debugPrint(
"Fehler beim Löschen des Discounts der Lieferung: ${event.deliveryId}:",
);
debugPrint("$e");
debugPrint("$st");
opBloc.add(
FailOperation(message: "Fehler beim Löschen des Discounts: $e"),
);
}
}
void _addDiscount(AddDiscountEvent event, Emitter<DeliveryState> emit) async {
opBloc.add(LoadOperation());
try {
final currentState = state;
if (currentState is DeliveryLoaded) {
DiscountAddResponseDTO response = await repository.addDiscount(
event.deliveryId,
event.reason,
event.value,
);
model.Delivery delivery = currentState.delivery;
delivery.totalNetValue = response.values.receipt.net;
delivery.totalGrossValue = response.values.receipt.gross;
delivery.discount = model.Discount(
article: Article.fromDTO(response.values.article),
note: response.values.note.noteDescription,
noteId: response.values.note.rowId,
);
delivery.articles = [...delivery.articles, delivery.discount!.article];
emit(currentState.copyWith(delivery));
opBloc.add(FinishOperation());
}
} catch (e, st) {
debugPrint(
"Fehler beim Hinzufügen eins Discounts zur Lieferung: ${event.deliveryId}:",
);
debugPrint("$e");
debugPrint("$st");
opBloc.add(
FailOperation(message: "Fehler beim Hinzufügen des Discounts: $e"),
);
}
}
void _load(LoadDeliveryEvent event, Emitter<DeliveryState> emit) async {
debugPrint("Discount; ${event.delivery.discount?.note}");
emit(DeliveryLoaded(delivery: event.delivery));
}
void _unscan(UnscanArticleEvent event, Emitter<DeliveryState> emit) async {
opBloc.add(LoadOperation());
try {
String? noteId = await repository.unscan(
event.articleId,
event.newAmount,
event.reason,
);
if (noteId != null) {
final currentState = state;
if (currentState is DeliveryLoaded) {
Article article = currentState.delivery.articles.firstWhere(
(article) => article.internalId == int.parse(event.articleId),
);
article.removeNoteId = noteId;
article.scannedRemovedAmount += event.newAmount;
article.scannedAmount -= event.newAmount;
List<Article> articles = [
...currentState.delivery.articles.where(
(article) => article.internalId != int.parse(event.articleId),
),
article,
];
currentState.delivery.articles = articles;
emit.call(currentState.copyWith(currentState.delivery));
}
}
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Unscan des Artikels: ${event.articleId}:");
debugPrint("$e");
debugPrint("$st");
opBloc.add(FailOperation(message: "Fehler beim Unscan des Artikels: $e"));
}
}
void _resetAmount(
ResetScanAmountEvent event,
Emitter<DeliveryState> emit,
) async {
opBloc.add(LoadOperation());
try {
final currentState = state;
await repository.resetScan(event.articleId);
if (currentState is DeliveryLoaded) {
Article article = currentState.delivery.articles.firstWhere(
(article) => article.internalId == int.parse(event.articleId),
);
article.removeNoteId = null;
article.scannedRemovedAmount = 0;
article.scannedAmount = article.amount;
List<Article> articles = [
...currentState.delivery.articles.where(
(article) => article.internalId != int.parse(event.articleId),
),
article,
];
currentState.delivery.articles = articles;
emit.call(currentState.copyWith(currentState.delivery));
}
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Unscan des Artikels: ${event.articleId}:");
debugPrint("$e");
debugPrint("$st");
opBloc.add(FailOperation(message: "Fehler beim Zurücksetzen: $e"));
}
}
}

View File

@ -0,0 +1,71 @@
import 'package:hl_lieferservice/model/delivery.dart';
import 'package:hl_lieferservice/model/tour.dart';
abstract class DeliveryEvent {}
class LoadDeliveryEvent extends DeliveryEvent {
LoadDeliveryEvent({required this.delivery});
Delivery delivery;
}
class UnscanArticleEvent extends DeliveryEvent {
UnscanArticleEvent({
required this.articleId,
required this.newAmount,
required this.reason,
});
String articleId;
String reason;
int newAmount;
}
class ResetScanAmountEvent extends DeliveryEvent {
ResetScanAmountEvent({required this.articleId});
String articleId;
}
class AddDiscountEvent extends DeliveryEvent {
AddDiscountEvent({
required this.deliveryId,
required this.value,
required this.reason,
});
String deliveryId;
String reason;
int value;
}
class RemoveDiscountEvent extends DeliveryEvent {
RemoveDiscountEvent({required this.deliveryId});
String deliveryId;
}
class UpdateDiscountEvent extends DeliveryEvent {
UpdateDiscountEvent({
required this.deliveryId,
required this.value,
required this.reason,
});
String deliveryId;
String? reason;
int? value;
}
class UpdateDeliveryOption extends DeliveryEvent {
UpdateDeliveryOption({required this.key, required this.value});
String key;
dynamic value;
}
class UpdateSelectedPaymentMethod extends DeliveryEvent {
UpdateSelectedPaymentMethod({required this.payment});
Payment payment;
}

View File

@ -0,0 +1,15 @@
import 'package:hl_lieferservice/model/delivery.dart';
abstract class DeliveryState {}
class DeliveryInitial extends DeliveryState {}
class DeliveryLoaded extends DeliveryState {
DeliveryLoaded({required this.delivery});
Delivery delivery;
DeliveryLoaded copyWith(Delivery? delivery) {
return DeliveryLoaded(delivery: delivery ?? this.delivery);
}
}

View File

@ -0,0 +1,188 @@
import 'dart:typed_data';
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.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 'note_event.dart';
import 'note_state.dart';
import 'package:hl_lieferservice/feature/delivery/detail/repository/note_repository.dart';
class NoteBloc extends Bloc<NoteEvent, NoteState> {
final NoteRepository repository;
final OperationBloc opBloc;
NoteBloc({required this.repository, required this.opBloc})
: super(NoteInitial()) {
on<LoadNote>(_load);
on<AddNote>(_add);
on<EditNote>(_edit);
on<RemoveNote>(_remove);
on<AddImageNote>(_upload);
on<RemoveImageNote>(_removeImage);
}
Future<void> _removeImage(
RemoveImageNote event,
Emitter<NoteState> emit,
) async {
opBloc.add(LoadOperation());
try {
final currentState = state;
await repository.deleteImage(event.deliveryId, event.objectId);
if (currentState is NoteLoaded) {
emit.call(
currentState.copyWith(
images:
currentState.images
.where((image) => image.$1.objectId != event.objectId)
.toList(),
),
);
}
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Löschen des Bildes: $e");
debugPrint(st.toString());
opBloc.add(FailOperation(message: e.toString()));
}
}
Future<void> _upload(AddImageNote event, Emitter<NoteState> emit) async {
opBloc.add(LoadOperation());
try {
final currentState = state;
Uint8List imageBytes = await event.file.readAsBytes();
ImageNote note = await repository.addImage(event.deliveryId, imageBytes);
if (currentState is NoteLoaded) {
emit.call(
currentState.copyWith(
images: [...currentState.images, (note, imageBytes)],
),
);
}
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Hinzufügen des Bildes: $e");
debugPrint(st.toString());
opBloc.add(FailOperation(message: e.toString()));
}
}
Future<void> _load(LoadNote event, Emitter<NoteState> emit) async {
emit.call(NoteLoading());
try {
List<String> urls =
event.delivery.images.map((image) => image.url).toList();
List<Note> notes = await repository.loadNotes(event.delivery.id);
List<NoteTemplate> templates = await repository.loadTemplates();
List<Uint8List> images = await repository.loadImages(urls);
emit.call(
NoteLoaded(
notes: notes,
templates: templates,
images: List.generate(
images.length,
(index) => (event.delivery.images[index], images[index]),
),
),
);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Herunterladen der Notizen: $e");
debugPrint(st.toString());
opBloc.add(
FailOperation(message: "Notizen konnten nicht heruntergeladen werden."),
);
emit.call(NoteLoadingFailed());
}
}
Future<void> _add(AddNote event, Emitter<NoteState> emit) async {
opBloc.add(LoadOperation());
try {
final currentState = state;
Note note = await repository.addNote(event.deliveryId, event.note);
if (currentState is NoteLoaded) {
List<Note> refreshedNotes = [...currentState.notes, note];
emit.call(currentState.copyWith(notes: refreshedNotes));
}
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Hinzufügen der Notiz: $e");
debugPrint(st.toString());
opBloc.add(FailOperation(message: e.toString()));
}
}
Future<void> _edit(EditNote event, Emitter<NoteState> emit) async {
opBloc.add(LoadOperation());
try {
final currentState = state;
await repository.editNote(event.noteId, event.content);
if (currentState is NoteLoaded) {
List<Note> refreshedNotes = [
...currentState.notes.where(
(note) => note.id != int.parse(event.noteId),
),
Note(content: event.content, id: int.parse(event.noteId)),
];
emit.call(currentState.copyWith(notes: refreshedNotes));
}
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Hinzufügen der Notiz: $e");
debugPrint(st.toString());
opBloc.add(FailOperation(message: e.toString()));
}
}
Future<void> _remove(RemoveNote event, Emitter<NoteState> emit) async {
opBloc.add(LoadOperation());
try {
final currentState = state;
await repository.deleteNote(event.noteId);
if (currentState is NoteLoaded) {
List<Note> refreshedNotes =
currentState.notes
.where((note) => note.id != int.parse(event.noteId))
.toList();
emit.call(currentState.copyWith(notes: refreshedNotes));
}
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Hinzufügen der Notiz: $e");
debugPrint(st.toString());
opBloc.add(
FailOperation(message: "Notizen konnte nicht gelöscht werden."),
);
}
}
}

View File

@ -0,0 +1,44 @@
import 'package:hl_lieferservice/model/delivery.dart';
import 'package:image_picker/image_picker.dart';
abstract class NoteEvent {}
class LoadNote extends NoteEvent {
LoadNote({required this.delivery});
final Delivery delivery;
}
class AddNote extends NoteEvent {
AddNote({required this.note, required this.deliveryId});
final String note;
final String deliveryId;
}
class RemoveNote extends NoteEvent {
RemoveNote({required this.noteId});
final String noteId;
}
class EditNote extends NoteEvent {
EditNote({required this.content, required this.noteId});
final String noteId;
final String content;
}
class AddImageNote extends NoteEvent {
AddImageNote({required this.file, required this.deliveryId});
final XFile file;
final String deliveryId;
}
class RemoveImageNote extends NoteEvent {
RemoveImageNote({required this.objectId, required this.deliveryId});
final String objectId;
final String deliveryId;
}

View File

@ -0,0 +1,35 @@
import 'dart:typed_data';
import 'package:hl_lieferservice/model/delivery.dart';
abstract class NoteState {}
class NoteInitial extends NoteState {}
class NoteLoading extends NoteState {}
class NoteLoadingFailed extends NoteState {}
class NoteLoaded extends NoteState {
NoteLoaded({
required this.notes,
required this.templates,
required this.images,
});
List<Note> notes;
List<NoteTemplate> templates;
List<(ImageNote, Uint8List)> images;
NoteLoaded copyWith({
List<Note>? notes,
List<NoteTemplate>? templates,
List<(ImageNote, Uint8List)>? images,
}) {
return NoteLoaded(
notes: notes ?? this.notes,
templates: templates ?? this.templates,
images: images ?? this.images,
);
}
}

View File

@ -0,0 +1,9 @@
import 'package:hl_lieferservice/model/article.dart';
import 'package:hl_lieferservice/model/delivery.dart';
class NoteInformation {
NoteInformation({required this.note, this.article});
Note note;
Article? article;
}

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);
}
}

View File

@ -0,0 +1,38 @@
import 'package:hl_lieferservice/dto/discount_add_response.dart';
import 'package:hl_lieferservice/dto/discount_remove_response.dart';
import 'package:hl_lieferservice/dto/discount_update_response.dart';
import 'package:hl_lieferservice/feature/delivery/overview/service/delivery_info_service.dart';
class DeliveryRepository {
DeliveryRepository({required this.service});
DeliveryInfoService service;
Future<String?> unscan(String articleId, int newAmount, String reason) async {
return await service.unscanArticle(articleId, newAmount, reason);
}
Future<void> resetScan(String articleId) async {
return await service.resetScannedArticleAmount(articleId);
}
Future<DiscountAddResponseDTO> addDiscount(
String deliveryId,
String reason,
int value,
) {
return service.addDiscount(deliveryId, value, reason);
}
Future<DiscountRemoveResponseDTO> removeDiscount(String deliveryId) {
return service.removeDiscount(deliveryId);
}
Future<DiscountUpdateResponseDTO> updateDiscount(
String deliveryId,
String? reason,
int? value,
) {
return service.updateDiscount(deliveryId, reason, value);
}
}

View File

@ -0,0 +1,58 @@
import 'dart:typed_data';
import 'package:hl_lieferservice/model/delivery.dart';
import 'package:hl_lieferservice/feature/delivery/detail/service/notes_service.dart';
class NoteRepository {
final NoteService service;
NoteRepository({required this.service});
Future<Note> addNote(String deliveryId, String content) async {
return service.addNote(content, int.parse(deliveryId));
}
Future<void> editNote(String noteId, String content) async {
return service.editNote(Note(content: content, id: int.parse(noteId)));
}
Future<void> deleteNote(String noteId) async {
return service.deleteNote(int.parse(noteId));
}
Future<List<Note>> loadNotes(String deliveryId) async {
return service.getNotes(deliveryId);
}
Future<List<Uint8List>> loadImages(List<String> urls) async {
List<Uint8List> images = [];
for (final image in await service.downloadImages(urls)) {
images.add(await image);
}
return images;
}
Future<List<NoteTemplate>> loadTemplates() async {
return service.getNoteTemplates();
}
Future<ImageNote> addImage(String deliveryId, Uint8List bytes) async {
final fileName =
"delivery_note_${deliveryId}_${DateTime.timestamp().microsecondsSinceEpoch}.jpg";
String objectId = await service.uploadImage(
deliveryId,
fileName,
bytes,
"image/png",
);
return ImageNote.make(objectId, fileName);
}
Future<void> deleteImage(String deliveryId, String objectId) async {
await service.removeImage(objectId);
}
}

View File

@ -0,0 +1,287 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:hl_lieferservice/dto/note_get_response.dart';
import 'package:hl_lieferservice/services/erpframe.dart';
import 'package:docuframe/docuframe.dart' as df;
import 'package:flutter/cupertino.dart';
import 'package:http/http.dart' as http;
import '../../../../dto/basic_response.dart';
import '../../../../dto/note_add_response.dart';
import '../../../../dto/note_template_response.dart';
import '../../../../model/delivery.dart';
class NoteService extends ErpFrameService {
NoteService({required super.config});
Future<void> deleteNote(int noteId) async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response = await df.Macro(
config: dfConfig,
session: session,
).execute("_web_deleteNote", parameter: {"id": noteId});
Map<String, dynamic> responseJson = jsonDecode(response.body!);
BasicResponseDTO responseDto = BasicResponseDTO.fromJson(responseJson);
if (responseDto.succeeded == true) {
return;
} else {
throw responseDto.message;
}
} catch (e, st) {
debugPrint("ERROR WHILE DELETING NOTE $noteId");
debugPrint("$e");
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
Future<void> editNote(Note newNote) async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response = await df.Macro(
config: dfConfig,
session: session,
).execute(
"_web_editNote",
parameter: {"id": newNote.id, "note": newNote.content},
);
Map<String, dynamic> responseJson = jsonDecode(response.body!);
BasicResponseDTO responseDto = BasicResponseDTO.fromJson(responseJson);
if (responseDto.succeeded == true) {
return;
} else {
throw responseDto.message;
}
} catch (e, st) {
debugPrint("ERROR WHILE EDITING NOTE ${newNote.id}");
debugPrint("$e");
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
Future<List<NoteTemplate>> getNoteTemplates() async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response = await df.Macro(
config: dfConfig,
session: session,
).execute("_web_getNoteTemplates");
Map<String, dynamic> responseJson = jsonDecode(response.body!);
NoteTemplateResponseDTO responseDto = NoteTemplateResponseDTO.fromJson(
responseJson,
);
if (responseDto.succeeded == true) {
return responseDto.notes.map(NoteTemplate.fromDTO).toList();
} else {
throw responseDto.message;
}
} catch (e, st) {
debugPrint("ERROR WHILE GETTING NOTE TEMPLATES");
debugPrint("$e");
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
Future<List<Note>> getNotes(String deliveryId) async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response = await df.Macro(
config: dfConfig,
session: session,
).execute("_web_getNotes", parameter: {"delivery_id": deliveryId});
debugPrint(deliveryId);
Map<String, dynamic> responseJson = jsonDecode(response.body!);
debugPrint(responseJson.toString());
NoteGetResponseDTO responseDto = NoteGetResponseDTO.fromJson(
responseJson,
);
if (responseDto.succeeded == true) {
return responseDto.notes
.map((noteDto) => Note.fromDto(noteDto))
.toList();
} else {
throw responseDto.message;
}
} catch (e, st) {
debugPrint("ERROR WHILE GETTING NOTES");
debugPrint("$e");
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
Future<Note> addNote(String note, int deliveryId) async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response = await df.Macro(
config: dfConfig,
session: session,
).execute(
"_web_addNote",
parameter: {"receipt_id": deliveryId, "note": note},
);
debugPrint(deliveryId.toString());
Map<String, dynamic> responseJson = jsonDecode(response.body!);
debugPrint(responseJson.toString());
NoteAddResponseDTO responseDto = NoteAddResponseDTO.fromJson(
responseJson,
);
if (responseDto.succeeded == true) {
return Note.fromDto(responseDto.note!);
} else {
debugPrint("ERROR: ${responseDto.message}");
throw responseDto.message;
}
} catch (e) {
rethrow;
} finally {
await logout(session);
}
}
Future<String> uploadImage(
String deliveryId,
String filename,
Uint8List bytes,
String? mimeType,
) async {
df.LoginSession? session;
try {
session = await getSession();
// First get UPLOAD ID
df.UploadFile uploadHandler = df.UploadFile(
config: dfConfig,
session: session,
);
df.GetUploadIdResponse uploadIdResponse =
await uploadHandler.getUploadId();
// Upload binary data to DOCUframe
debugPrint(filename);
df.FileInformationResponse response = await uploadHandler.uploadFile(
uploadIdResponse.uploadId,
bytes,
filename,
mimeType ?? "image/jpeg",
);
debugPrint(response.body);
// Commit file upload
df.CommitFileUploadResponse commitResponse = await uploadHandler
.commitUpload(uploadIdResponse.uploadId);
debugPrint(commitResponse.body);
return commitResponse.objectId;
} catch (e, st) {
debugPrint("An error occured:");
debugPrint("$e");
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
Future<List<Future<Uint8List>>> downloadImages(List<String> urls) async {
df.LoginSession? session;
debugPrint(urls.toString());
try {
session = await getSession();
final header = {
"sessionId": session.getAuthorizationHeader().$2,
"appKey": config.appNames[0],
};
return urls.map((url) async {
return (await http.get(
Uri.parse("${config.host}$url"),
headers: header,
)).bodyBytes;
}).toList();
} catch (e, st) {
debugPrint("An error occured:");
debugPrint("$e");
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
Future<void> removeImage(String oid) async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response = await df.Macro(
config: dfConfig,
session: session,
).execute("_web_removeImage", parameter: {"oid": oid});
Map<String, dynamic> responseJson = jsonDecode(response.body!);
debugPrint(oid);
debugPrint(responseJson.toString());
BasicResponseDTO responseDto = BasicResponseDTO.fromJson(responseJson);
if (responseDto.succeeded == true) {
return;
} else {
debugPrint("ERROR: ${responseDto.message}");
throw responseDto.message;
}
} on df.DocuFrameException catch (e, st) {
debugPrint("${e.errorMessage}");
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
}

View File

@ -0,0 +1,33 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_event.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_state.dart';
import 'package:hl_lieferservice/feature/delivery/overview/repository/tour_repository.dart';
import 'package:hl_lieferservice/model/tour.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
class TourBloc extends Bloc<TourEvent, TourState> {
OperationBloc opBloc;
TourRepository deliveryRepository;
TourBloc({required this.opBloc, required this.deliveryRepository})
: super(TourInitial()) {
on<LoadTour>(_load);
}
Future<void> _load(LoadTour event, Emitter<TourState> emit) async {
opBloc.add(LoadOperation());
try {
Tour tour = await deliveryRepository.loadAll(event.teamId);
List<Payment> payments = await deliveryRepository.loadPaymentOptions();
tour.paymentMethods = payments;
emit(TourLoaded(tour: tour));
opBloc.add(FinishOperation());
} catch (e) {
opBloc.add(
FailOperation(message: "Fehler beim Laden der heutigen Fahrten"),
);
}
}
}

View File

@ -0,0 +1,7 @@
abstract class TourEvent {}
class LoadTour extends TourEvent {
String teamId;
LoadTour({required this.teamId});
}

View File

@ -0,0 +1,13 @@
import '../../../../model/tour.dart';
abstract class TourState {}
class TourInitial extends TourState {}
class TourLoading extends TourState {}
class TourLoaded extends TourState {
Tour tour;
TourLoaded({required this.tour});
}

View File

@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import 'package:hl_lieferservice/model/tour.dart';
import 'package:intl/intl.dart';
class DeliveryInfo extends StatelessWidget {
final Tour tour;
const DeliveryInfo({super.key, required this.tour});
@override
Widget build(BuildContext context) {
String date = DateFormat("dd.MM.yyyy").format(tour.date);
String amountDeliveries = tour.deliveries.length.toString();
return Padding(
padding: const EdgeInsets.all(10),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Text(
"Informationen",
style: Theme.of(context).textTheme.headlineSmall,
),
),
SizedBox(
width: double.infinity,
child: Card(
color: Theme.of(context).colorScheme.onSecondary,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.calendar_month),
Padding(
padding: const EdgeInsets.only(left: 5),
child: Text("Datum"),
),
],
),
Text(date),
],
),
Padding(
padding: const EdgeInsets.only(top: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.local_shipping_outlined),
Padding(
padding: const EdgeInsets.only(left: 5),
child: Text("Lieferungen"),
),
],
),
Text(amountDeliveries),
],
),
),
],
),
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:hl_lieferservice/model/delivery.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/delivery_detail_page.dart';
class DeliveryListItem extends StatelessWidget {
final Delivery delivery;
const DeliveryListItem({super.key, required this.delivery});
Widget _leading(BuildContext context) {
if (delivery.state == DeliveryState.finished) {
return Icon(Icons.check_circle, color: Colors.green);
}
if (delivery.state == DeliveryState.canceled) {
return Icon(Icons.cancel_rounded, color: Colors.red);
}
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Icon(Icons.location_on, color: Theme.of(context).primaryColor),
Text("5min"),
],
);
}
void _goToDelivery(BuildContext context) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DeliveryDetail(delivery: delivery),
),
);
}
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(
delivery.customer.name,
style: Theme.of(context).textTheme.titleMedium,
),
leading: _leading(context),
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
subtitle: Text(delivery.customer.address.toString()),
trailing: Icon(Icons.arrow_forward_ios),
onTap: () => _goToDelivery(context),
);
}
}

View File

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:hl_lieferservice/model/delivery.dart';
import 'delivery_item.dart';
class DeliveryList extends StatefulWidget {
final List<Delivery> deliveries;
const DeliveryList({super.key, required this.deliveries});
@override
State<StatefulWidget> createState() => _DeliveryListState();
}
class _DeliveryListState extends State<DeliveryList> {
@override
Widget build(BuildContext context) {
if (widget.deliveries.isEmpty) {
return ListView(
children: [Center(child: const Text("Keine Auslieferungen gefunden"))],
);
}
return ListView.separated(
separatorBuilder: (context, index) => const Divider(height: 0),
itemBuilder:
(context, index) =>
DeliveryListItem(delivery: widget.deliveries[index]),
itemCount: widget.deliveries.length,
);
}
}

View File

@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_event.dart';
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_info.dart';
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_list.dart';
import 'package:hl_lieferservice/model/tour.dart';
import '../../../authentication/bloc/auth_bloc.dart';
import '../../../authentication/bloc/auth_state.dart';
class DeliveryOverview extends StatefulWidget {
const DeliveryOverview({super.key, required this.tour});
final Tour tour;
@override
State<StatefulWidget> createState() => _DeliveryOverviewState();
}
class _DeliveryOverviewState extends State<DeliveryOverview> {
String? _selectedCarPlate;
@override
void initState() {
super.initState();
// Select the first car for initialization
_selectedCarPlate = widget.tour.driver.cars.firstOrNull?.plate;
}
Future<void> _loadTour() async {
Authenticated state = context.read<AuthBloc>().state as Authenticated;
context.read<TourBloc>().add(LoadTour(teamId: state.teamId));
}
Widget _carSelection() {
return SizedBox(
width: double.infinity,
height: 50,
child: ListView(
scrollDirection: Axis.horizontal,
children:
widget.tour.driver.cars.map((car) {
Color? backgroundColor;
Color? iconColor = Theme.of(context).primaryColor;
Color? textColor;
if (_selectedCarPlate == car.plate) {
backgroundColor = Theme.of(context).primaryColor;
textColor = Theme.of(context).colorScheme.onSecondary;
iconColor = Theme.of(context).colorScheme.onSecondary;
}
return Padding(
padding: const EdgeInsets.only(right: 8),
child: GestureDetector(
onTap: () {
setState(() {
_selectedCarPlate = car.plate;
});
},
child: Chip(
backgroundColor: backgroundColor,
label: Row(
children: [
Icon(Icons.local_shipping, color: iconColor, size: 20),
Padding(
padding: const EdgeInsets.only(left: 5),
child: Text(
car.plate,
style: TextStyle(color: textColor, fontSize: 12),
),
),
],
),
),
),
);
}).toList(),
),
);
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: _loadTour,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DeliveryInfo(tour: widget.tour),
Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
"Fahrten",
style: Theme.of(context).textTheme.headlineSmall,
),
],
),
IconButton(icon: Icon(Icons.filter_list), onPressed: () {}),
],
),
),
Padding(
padding: const EdgeInsets.only(left: 10, right: 10, bottom: 20),
child: _carSelection(),
),
Expanded(child: DeliveryList(deliveries: widget.tour.deliveries)),
],
),
);
}
}

View File

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_overview.dart';
import '../bloc/tour_bloc.dart';
import '../bloc/tour_state.dart';
class DeliveryOverviewPage extends StatefulWidget {
const DeliveryOverviewPage({super.key});
@override
State<StatefulWidget> createState() => _DeliveryOverviewPageState();
}
class _DeliveryOverviewPageState extends State<DeliveryOverviewPage> {
@override
Widget build(BuildContext context) {
return BlocBuilder<TourBloc, TourState>(
builder: (context, state) {
if (state is TourLoaded) {
final currentState = state;
return Center(child: DeliveryOverview(tour: currentState.tour));
}
return Container();
},
);
}
}

View File

@ -0,0 +1,19 @@
import 'package:hl_lieferservice/feature/delivery/overview/service/delivery_info_service.dart';
import 'package:hl_lieferservice/model/tour.dart';
class TourRepository {
DeliveryInfoService service;
TourRepository({required this.service});
Future<Tour> loadAll(String userId) async {
Tour? tour = await service.getTourOfToday(userId);
return tour!;
}
Future<List<Payment>> loadPaymentOptions() async {
return (await service.getPaymentMethods())
.map((option) => Payment.fromDTO(option))
.toList();
}
}

View File

@ -0,0 +1,278 @@
import 'dart:convert';
import 'package:docuframe/docuframe.dart' as df;
import 'package:flutter/material.dart';
import 'package:hl_lieferservice/dto/delivery_response.dart';
import 'package:hl_lieferservice/dto/delivery_update.dart';
import 'package:hl_lieferservice/dto/delivery_update_response.dart';
import 'package:hl_lieferservice/dto/payment.dart';
import 'package:hl_lieferservice/dto/payments.dart';
import 'package:hl_lieferservice/model/car.dart';
import 'package:hl_lieferservice/model/delivery.dart';
import 'package:hl_lieferservice/model/tour.dart';
import 'package:hl_lieferservice/util.dart';
import 'package:hl_lieferservice/services/erpframe.dart';
import '../../../../dto/basic_response.dart';
import '../../../../dto/discount_add_response.dart';
import '../../../../dto/discount_remove_response.dart';
import '../../../../dto/discount_update_response.dart';
import '../../../../dto/scan_response.dart';
class DeliveryInfoService extends ErpFrameService {
DeliveryInfoService({required super.config});
Future<void> updateDelivery(Delivery delivery) async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response =
await df.Macro(config: dfConfig, session: session).execute(
"_web_updateDelivery",
parameter: DeliveryUpdateDTO.fromEntity(delivery).toJson()
as Map<String, dynamic>);
df.Logout(config: dfConfig, session: session).logout();
Map<String, dynamic> responseJson = jsonDecode(response.body!);
DeliveryUpdateResponseDTO responseDto =
DeliveryUpdateResponseDTO.fromJson(responseJson);
if (responseDto.code == "200") {
return;
}
throw responseDto.message;
} on df.DocuFrameException catch (e, st) {
debugPrint("ERROR WHILE UPDATING DELIVERY");
debugPrint(e.errorMessage);
debugPrint(e.errorCode);
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
/// List all available deliveries for today.
Future<Tour?> getTourOfToday(String userId) async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response =
await df.Macro(config: dfConfig, session: session).execute(
"_web_getDeliveries",
parameter: {"driver_id": userId, "date": getTodayDate()});
Map<String, dynamic> responseJson = jsonDecode(response.body!);
DeliveryResponseDTO responseDto =
DeliveryResponseDTO.fromJson(responseJson);
return Tour(
discountArticleNumber: responseDto.discountArticleNumber,
date: DateTime.now(),
deliveries: responseDto.deliveries.map(Delivery.fromDTO).toList(),
paymentMethods: [],
driver: Driver(
cars: responseDto.driver.cars
.map((carDto) =>
Car(id: int.parse(carDto.id), plate: carDto.plate))
.toList(),
teamNumber: int.parse(responseDto.driver.id),
name: responseDto.driver.name,
salutation: responseDto.driver.salutation));
} catch (e, stacktrace) {
debugPrint(e.toString());
debugPrint(stacktrace.toString());
debugPrint("RANDOM EXCEPTION!");
rethrow;
} finally {
await logout(session);
}
}
Future<List<PaymentMethodDTO>> getPaymentMethods() async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response =
await df.Macro(config: dfConfig, session: session)
.execute("_web_getPaymentMethods", parameter: {});
Map<String, dynamic> responseJson = jsonDecode(response.body!);
PaymentMethodListDTO responseDto =
PaymentMethodListDTO.fromJson(responseJson);
return responseDto.paymentMethods;
} catch (e, st) {
debugPrint("ERROR while retrieving allowed payment methods");
debugPrint(e.toString());
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
Future<String?> unscanArticle(
String internalId, int amount, String reason) async {
df.LoginSession? session;
debugPrint("AMOUNT: $amount");
debugPrint("ID: $internalId");
try {
session = await getSession();
df.DocuFrameMacroResponse response =
await df.Macro(config: dfConfig, session: session)
.execute("_web_unscanArticle", parameter: {
"article_id": internalId,
"amount": amount.toString(),
"reason": reason
});
Map<String, dynamic> responseJson = jsonDecode(response.body!);
debugPrint(responseJson.toString());
ScanResponseDTO responseDto = ScanResponseDTO.fromJson(responseJson);
if (responseDto.succeeded == true) {
return responseDto.noteId;
} else {
throw responseDto.message;
}
} catch (e, st) {
debugPrint("ERROR WHILE REVERTING THE SCAN OF ARTICLE $internalId");
debugPrint(e.toString());
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
Future<void> resetScannedArticleAmount(String receiptRowId) async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response =
await df.Macro(config: dfConfig, session: session).execute(
"_web_unscanArticleReset",
parameter: {"receipt_row_id": receiptRowId});
Map<String, dynamic> responseJson = jsonDecode(response.body!);
BasicResponseDTO responseDto = BasicResponseDTO.fromJson(responseJson);
if (responseDto.succeeded == true) {
return;
} else {
throw responseDto.message;
}
} catch (e, st) {
debugPrint("ERROR WHILE REVERTING THE UNSCAN OF ARTICLE $receiptRowId");
debugPrint(e.toString());
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
Future<DiscountAddResponseDTO> addDiscount(
String deliveryId, int discount, String note) async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response =
await df.Macro(config: dfConfig, session: session)
.execute("_web_addDiscount", parameter: {
"delivery_id": deliveryId,
"discount": discount.toString(),
"note": note
});
debugPrint("BODY: ${response.body!}");
Map<String, dynamic> responseJson = jsonDecode(response.body!);
// let it throw, if the values are invalid
return DiscountAddResponseDTO.fromJson(responseJson);
} catch (e, st) {
debugPrint("ERROR while adding discount");
debugPrint(e.toString());
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
Future<DiscountRemoveResponseDTO> removeDiscount(String deliveryId) async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response =
await df.Macro(config: dfConfig, session: session)
.execute("_web_removeDiscount", parameter: {
"delivery_id": deliveryId,
});
debugPrint("${response.body!}");
Map<String, dynamic> responseJson = jsonDecode(response.body!);
// let it throw, if the values are invalid
return DiscountRemoveResponseDTO.fromJson(responseJson);
} catch (e, st) {
debugPrint("ERROR while removing discount");
debugPrint(e.toString());
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
Future<DiscountUpdateResponseDTO> updateDiscount(
String deliveryId, String? note, int? discount) async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response =
await df.Macro(config: dfConfig, session: session)
.execute("_web_updateDiscount", parameter: {
"delivery_id": deliveryId,
"discount": discount,
"note": note
});
Map<String, dynamic> responseJson = jsonDecode(response.body!);
// let it throw, if the values are invalid
return DiscountUpdateResponseDTO.fromJson(responseJson);
} catch (e, st) {
debugPrint("ERROR while retrieving allowed payment methods");
debugPrint(e.toString());
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
}

View File

@ -0,0 +1,14 @@
import 'dart:convert';
import '../../../../services/erpframe.dart';
import 'package:docuframe/docuframe.dart' as df;
import 'package:flutter/cupertino.dart';
import '../../../../dto/discount_add_response.dart';
import '../../../../dto/discount_remove_response.dart';
import '../../../../dto/discount_update_response.dart';
class DiscountService extends ErpFrameService {
DiscountService({required super.config});
}