Initial draft
This commit is contained in:
309
lib/feature/delivery/detail/bloc/delivery_bloc.dart
Normal file
309
lib/feature/delivery/detail/bloc/delivery_bloc.dart
Normal 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
71
lib/feature/delivery/detail/bloc/delivery_event.dart
Normal file
71
lib/feature/delivery/detail/bloc/delivery_event.dart
Normal 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;
|
||||
}
|
||||
15
lib/feature/delivery/detail/bloc/delivery_state.dart
Normal file
15
lib/feature/delivery/detail/bloc/delivery_state.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
188
lib/feature/delivery/detail/bloc/note_bloc.dart
Normal file
188
lib/feature/delivery/detail/bloc/note_bloc.dart
Normal 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."),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
lib/feature/delivery/detail/bloc/note_event.dart
Normal file
44
lib/feature/delivery/detail/bloc/note_event.dart
Normal 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;
|
||||
}
|
||||
35
lib/feature/delivery/detail/bloc/note_state.dart
Normal file
35
lib/feature/delivery/detail/bloc/note_state.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
9
lib/feature/delivery/detail/model/note.dart
Normal file
9
lib/feature/delivery/detail/model/note.dart
Normal 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;
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
221
lib/feature/delivery/detail/presentation/delivery_discount.dart
Normal file
221
lib/feature/delivery/detail/presentation/delivery_discount.dart
Normal 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"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
164
lib/feature/delivery/detail/presentation/delivery_sign.dart
Normal file
164
lib/feature/delivery/detail/presentation/delivery_sign.dart
Normal 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"),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
182
lib/feature/delivery/detail/presentation/delivery_summary.dart
Normal file
182
lib/feature/delivery/detail/presentation/delivery_summary.dart
Normal 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()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
30
lib/feature/delivery/detail/presentation/note/note_list.dart
Normal file
30
lib/feature/delivery/detail/presentation/note/note_list.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
162
lib/feature/delivery/detail/presentation/note/note_overview.dart
Normal file
162
lib/feature/delivery/detail/presentation/note/note_overview.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
32
lib/feature/delivery/detail/presentation/steps/step.dart
Normal file
32
lib/feature/delivery/detail/presentation/steps/step.dart
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
199
lib/feature/delivery/detail/presentation/steps/step_info.dart
Normal file
199
lib/feature/delivery/detail/presentation/steps/step_info.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
58
lib/feature/delivery/detail/repository/note_repository.dart
Normal file
58
lib/feature/delivery/detail/repository/note_repository.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
287
lib/feature/delivery/detail/service/notes_service.dart
Normal file
287
lib/feature/delivery/detail/service/notes_service.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user