Final commit.

This commit is contained in:
Dennis Nemec
2026-06-01 17:12:28 +02:00
parent 3ecbc82885
commit a9bf8ecdd1
385 changed files with 29081 additions and 12089 deletions

View File

@ -1,172 +0,0 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart';
import 'package:hl_lieferservice/feature/authentication/exceptions.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
import 'package:rxdart/rxdart.dart';
import '../../../../model/delivery.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;
final AuthBloc authBloc;
final String deliveryId;
StreamSubscription? _combinedSubscription;
NoteBloc({
required this.repository,
required this.opBloc,
required this.authBloc,
required this.deliveryId,
}) : super(NoteInitial()) {
_combinedSubscription = CombineLatestStream.combine3(
repository.notes,
repository.images,
repository.templates,
(note, image, templates) {
if (note == null || image == null || templates == null) {
return null;
}
return {"note": note, "image": image, "templates": templates};
},
)
.where((data) => data != null)
.listen(
(data) => add(
DataUpdated(
images: data!["image"] as List<ImageNote>,
notes: data["note"] as List<Note>,
templates: data["templates"] as List<NoteTemplate>,
),
),
);
on<LoadNote>(_load);
on<AddNote>(_add);
on<EditNote>(_edit);
on<RemoveNote>(_remove);
on<AddImageNote>(_upload);
on<RemoveImageNote>(_removeImage);
on<ResetNotes>(_reset);
on<DataUpdated>(_dataUpdated);
}
@override
Future<void> close() {
_combinedSubscription?.cancel();
return super.close();
}
void _handleError(Object e, String fallbackMessage) {
if (e is UserUnauthorized) {
authBloc.add(SessionExpiredEvent());
} else {
opBloc.add(FailOperation(message: fallbackMessage));
}
}
Future<void> _dataUpdated(DataUpdated event, Emitter<NoteState> emit) async {
emit(
NoteLoaded(
notes: event.notes,
images: event.images,
templates: event.templates,
),
);
}
Future<void> _reset(ResetNotes event, Emitter<NoteState> emit) async {
emit.call(NoteInitial());
}
Future<void> _removeImage(
RemoveImageNote event,
Emitter<NoteState> emit,
) async {
opBloc.add(StartOperation());
try {
await repository.deleteImage(event.deliveryId, event.objectId);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Löschen des Bildes: $e $st");
_handleError(e, "Fehler beim Löschen des Bildes");
}
}
Future<void> _upload(AddImageNote event, Emitter<NoteState> emit) async {
opBloc.add(StartOperation());
try {
Uint8List imageBytes = await event.file.readAsBytes();
await repository.addImage(event.deliveryId, imageBytes);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Hinzufügen des Bildes: $e $st");
_handleError(e, "Fehler beim Hinzufügen des Bildes");
}
}
Future<void> _load(LoadNote event, Emitter<NoteState> emit) async {
if (state is NoteLoaded || state is NoteLoading) {
return;
}
emit.call(NoteLoading());
try {
await repository.loadNotes(event.delivery.id);
await repository.loadTemplates();
} catch (e, st) {
debugPrint("Fehler beim Herunterladen der Notizen: $e $st");
if (e is UserUnauthorized) {
authBloc.add(SessionExpiredEvent());
return;
}
opBloc.add(FailOperation(message: "Notizen konnten nicht heruntergeladen werden."));
emit.call(NoteLoadingFailed());
}
}
Future<void> _add(AddNote event, Emitter<NoteState> emit) async {
opBloc.add(StartOperation());
try {
await repository.addNote(event.deliveryId, event.note);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Hinzufügen der Notiz: $e $st");
_handleError(e, "Fehler beim Hinzufügen der Notiz");
}
}
Future<void> _edit(EditNote event, Emitter<NoteState> emit) async {
opBloc.add(StartOperation());
try {
await repository.editNote(event.noteId, event.content);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Editieren der Notiz: $e $st");
_handleError(e, "Fehler beim Editieren der Notiz");
}
}
Future<void> _remove(RemoveNote event, Emitter<NoteState> emit) async {
opBloc.add(StartOperation());
try {
await repository.deleteNote(event.noteId);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Löschen der Notiz: $e $st");
_handleError(e, "Notiz konnte nicht gelöscht werden");
}
}
}

View File

@ -1,75 +0,0 @@
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 ResetNotes extends NoteEvent {}
class AddNote extends NoteEvent {
AddNote({required this.note, required this.deliveryId});
final String note;
final String deliveryId;
}
class AddNoteOffline extends NoteEvent {
AddNoteOffline({required this.note, required this.deliveryId, required this.noteId});
final String note;
final String noteId;
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;
}
class NotesUpdated extends NoteEvent {
final List<Note> notes;
NotesUpdated({required this.notes});
}
class ImageUpdated extends NoteEvent {
final List<ImageNote> images;
ImageUpdated({required this.images});
}
class DataUpdated extends NoteEvent {
final List<ImageNote> images;
final List<Note> notes;
final List<NoteTemplate> templates;
DataUpdated({required this.images, required this.notes, required this.templates});
}

View File

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

View File

@ -0,0 +1,46 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'workflow_event.dart';
import 'workflow_state.dart';
/// Bloc, der den Detail-Workflow einer einzelnen Lieferung trägt.
///
/// Pro `DeliveryDetail`-Page-Push neu instanziert (per `BlocProvider` direkt
/// in der Page), Lifetime endet beim Pop. Strukturelle Persistenzen
/// (Notizen speichern, Artikel entfernen, Complete) gehen NICHT durch
/// diesen Bloc, sondern direkt am globalen `TourBloc` — der Workflow-Bloc
/// trägt nur Step-State + lokale Drafts.
class DeliveryWorkflowBloc
extends Bloc<DeliveryWorkflowEvent, DeliveryWorkflowState> {
DeliveryWorkflowBloc({required String deliveryId})
: super(DeliveryWorkflowState.initial(deliveryId)) {
on<WorkflowGoToStep>((e, emit) => emit(state.copyWith(step: e.step)));
on<WorkflowNextStep>((e, emit) {
final next = state.step.index + 1;
if (next < WorkflowStep.values.length) {
emit(state.copyWith(step: WorkflowStep.values[next]));
}
});
on<WorkflowPreviousStep>((e, emit) {
final prev = state.step.index - 1;
if (prev >= 0) {
emit(state.copyWith(step: WorkflowStep.values[prev]));
}
});
on<WorkflowAddPendingImage>((e, emit) {
final next = [
...state.pendingImageNotes,
PendingImageNote(file: e.file, pickedAt: DateTime.now()),
];
emit(state.copyWith(pendingImageNotes: next));
});
on<WorkflowRemovePendingImage>((e, emit) {
if (e.index < 0 || e.index >= state.pendingImageNotes.length) return;
final next = [...state.pendingImageNotes]..removeAt(e.index);
emit(state.copyWith(pendingImageNotes: next));
});
on<WorkflowOverridePaymentMethod>((e, emit) => emit(
state.copyWith(paymentMethodOverrideId: e.paymentMethodId),
));
}
}

View File

@ -0,0 +1,43 @@
import 'package:image_picker/image_picker.dart';
import 'workflow_state.dart';
/// Events des Detail-Workflow-Blocs. Strukturelle Aktionen (Notiz speichern,
/// Artikel entfernen, Lieferung abschließen) dispatchen NICHT hier, sondern
/// am `TourBloc` — der Workflow-Bloc kümmert sich nur um Step-Navigation
/// und Drafts.
sealed class DeliveryWorkflowEvent {
const DeliveryWorkflowEvent();
}
class WorkflowGoToStep extends DeliveryWorkflowEvent {
const WorkflowGoToStep(this.step);
final WorkflowStep step;
}
class WorkflowNextStep extends DeliveryWorkflowEvent {
const WorkflowNextStep();
}
class WorkflowPreviousStep extends DeliveryWorkflowEvent {
const WorkflowPreviousStep();
}
// ─── Bild-Notiz-Drafts ──────────────────────────────────────────────────
class WorkflowAddPendingImage extends DeliveryWorkflowEvent {
const WorkflowAddPendingImage(this.file);
final XFile file;
}
class WorkflowRemovePendingImage extends DeliveryWorkflowEvent {
const WorkflowRemovePendingImage(this.index);
final int index;
}
// ─── Payment-Auswahl ────────────────────────────────────────────────────
class WorkflowOverridePaymentMethod extends DeliveryWorkflowEvent {
const WorkflowOverridePaymentMethod({required this.paymentMethodId});
final String? paymentMethodId;
}

View File

@ -0,0 +1,87 @@
import 'package:image_picker/image_picker.dart';
/// Die 5 Steps der Auslieferungs-Detail-Page. Reihenfolge ≙ Index.
enum WorkflowStep {
info,
notes,
articles,
services,
summary,
}
extension WorkflowStepX on WorkflowStep {
String get displayName => switch (this) {
WorkflowStep.info => 'Info',
WorkflowStep.notes => 'Notizen',
WorkflowStep.articles => 'Artikel & Gutschriften',
WorkflowStep.services => 'Services',
WorkflowStep.summary => 'Übersicht',
};
/// Kurze Bezeichnung für den Header-Step (Platz ist eng auf Mobilgeräten).
String get shortName => switch (this) {
WorkflowStep.info => 'Info',
WorkflowStep.notes => 'Notizen',
WorkflowStep.articles => 'Artikel',
WorkflowStep.services => 'Services',
WorkflowStep.summary => 'Übersicht',
};
}
/// Eine im Workflow geparkte Bild-Notiz, die noch nicht hochgeladen werden
/// kann — wartet auf den Foto-Upload-Endpoint.
class PendingImageNote {
const PendingImageNote({required this.file, required this.pickedAt});
/// Das vom `image_picker` zurückgegebene File-Handle.
final XFile file;
final DateTime pickedAt;
}
/// State des Detail-Workflows. Ein State, ein Bloc — der Step-Wechsel,
/// die Drafts und die Payment-Auswahl liegen alle hier. So sieht jede
/// Step-Page denselben kohärenten Zustand und ein Step kann Daten aus
/// einem anderen lesen (z. B. Summary liest Article-Drafts).
class DeliveryWorkflowState {
const DeliveryWorkflowState({
required this.deliveryId,
required this.step,
required this.pendingImageNotes,
required this.paymentMethodOverrideId,
});
factory DeliveryWorkflowState.initial(String deliveryId) =>
DeliveryWorkflowState(
deliveryId: deliveryId,
step: WorkflowStep.info,
pendingImageNotes: const [],
paymentMethodOverrideId: null,
);
final String deliveryId;
final WorkflowStep step;
/// Lokal gehaltene Bild-Notizen — solange kein Upload-Endpoint da ist.
final List<PendingImageNote> pendingImageNotes;
/// Wenn der Fahrer im Summary die Zahlungsmethode überschreibt, landet
/// die neue Id hier. `null` = Methode der Lieferung bleibt.
final String? paymentMethodOverrideId;
DeliveryWorkflowState copyWith({
WorkflowStep? step,
List<PendingImageNote>? pendingImageNotes,
Object? paymentMethodOverrideId = _sentinel,
}) {
return DeliveryWorkflowState(
deliveryId: deliveryId,
step: step ?? this.step,
pendingImageNotes: pendingImageNotes ?? this.pendingImageNotes,
paymentMethodOverrideId: identical(paymentMethodOverrideId, _sentinel)
? this.paymentMethodOverrideId
: paymentMethodOverrideId as String?,
);
}
}
const Object _sentinel = Object();