Final commit.
This commit is contained in:
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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});
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
46
lib/feature/delivery/detail/bloc/workflow_bloc.dart
Normal file
46
lib/feature/delivery/detail/bloc/workflow_bloc.dart
Normal 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),
|
||||
));
|
||||
}
|
||||
}
|
||||
43
lib/feature/delivery/detail/bloc/workflow_event.dart
Normal file
43
lib/feature/delivery/detail/bloc/workflow_event.dart
Normal 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;
|
||||
}
|
||||
87
lib/feature/delivery/detail/bloc/workflow_state.dart
Normal file
87
lib/feature/delivery/detail/bloc/workflow_state.dart
Normal 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();
|
||||
Reference in New Issue
Block a user