import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import '../../../domain/entities/logistic_object.dart'; import '../../../domain/entities/tour.dart'; import '../../../domain/repositories/tour_repository.dart'; import '../../../core/constants/app_constants.dart'; import '../../../core/errors/failures.dart'; part 'scan_event.dart'; part 'scan_state.dart'; class ScanBloc extends Bloc { final TourRepository repository; Tour? currentTour; String? currentPageId; String? scannedFsaId; // Container handling for VS state machine String? containerId; String? containerType; // 'a' or 'b' ScanBloc({required this.repository}) : super(ScanInitial()) { on(_onInitializeScan); on(_onProcessBarcode); on(_onValidateBarcode); on(_onUpdateObjectState); on(_onResetScan); on(_onCreateUnknownObject); } Future _onInitializeScan(InitializeScan event, Emitter emit) async { currentTour = event.tour; containerId = null; containerType = null; emit(ScanReady(tour: event.tour)); } Future _onProcessBarcode(ProcessBarcode event, Emitter emit) async { emit(ScanProcessing(barcode: event.barcode)); final barcode = event.barcode.trim(); // Prüfe auf spezielle Barcodes (Seiten-Codes) if (currentTour != null) { final pageInfo = _findPageForBarcode(barcode); if (pageInfo != null) { emit(ScanPageDetected( pageId: pageInfo['pageId']!, label: pageInfo['label']!, tour: currentTour!, )); return; } } // Suche Objekt nach Barcode final result = await repository.getObjectByBarcode(barcode); result.fold( (failure) => emit(ScanError(message: _mapFailureToMessage(failure))), (object) { if (object != null) { _processScannedObject(object, barcode, emit); } else { // Unbekanntes Objekt - prüfe auf gültiges Präfix final prefix = barcode.length >= 3 ? barcode.substring(0, 3) : ''; if (_isValidPrefix(prefix)) { emit(ScanUnknownObject( barcode: barcode, prefix: prefix, tour: currentTour, )); } else { emit(ScanError(message: 'Unbekannter Barcode: $barcode')); } } }, ); } void _processScannedObject(LogisticObject object, String barcode, Emitter emit) { if (currentTour == null) { emit(const ScanError(message: 'Keine Tour ausgewählt')); return; } final tourType = currentTour!.type; // Dispatch to appropriate state machine based on tour type switch (tourType) { case TourTypes.stockStart: _stockStartStateMachine(object, emit); break; case TourTypes.vehStart: _vehStartStateMachine(object, emit); break; case TourTypes.vehBulk: _vehBulkStateMachine(object, emit); break; case TourTypes.veh: _vehStateMachine(object, emit); break; case TourTypes.fsa: _fsaStateMachine(object, emit); break; case TourTypes.vs: _vsStateMachine(object, emit); break; case TourTypes.vehVs: _vehVsStateMachine(object, emit); break; case TourTypes.gi: _giStateMachine(object, emit); break; case TourTypes.vehEnd: _vehEndStateMachine(object, emit); break; case TourTypes.stockEnd: _stockEndStateMachine(object, emit); break; case TourTypes.stock: _stockStateMachine(object, emit); break; default: // Fallback to simple state machine for unknown tour types final nextState = _determineNextState(object); emit(ScanObjectDetected( object: object, suggestedState: nextState, tour: currentTour, )); } } // ============================================================================ // STATE MACHINE: stockStart (Lager Beladung) // ============================================================================ void _stockStartStateMachine(LogisticObject object, Emitter emit) { final currentState = object.state; if (currentState == ObjectStates.unknown) { emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.toDelivery, tour: currentTour, )); } else if (currentState == ObjectStates.finGITmp) { emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.retGI, tour: currentTour, )); } else { emit(ScanError( message: 'Fehler: Ungültiger Barcode für Lager Beladung', )); } } // ============================================================================ // STATE MACHINE: vehStart (Fahrzeug Start) // ============================================================================ void _vehStartStateMachine(LogisticObject object, Emitter emit) { final currentState = object.state; if (currentState == ObjectStates.toDelivery) { emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.delivery, tour: currentTour, )); } else if (currentState == ObjectStates.retGI) { emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.retGIFzg, tour: currentTour, )); } else { emit(const ScanError(message: 'Fehler: Ungültiger Barcode')); } } // ============================================================================ // STATE MACHINE: vehBulk (Fahrzeug Bulk) // ============================================================================ void _vehBulkStateMachine(LogisticObject object, Emitter emit) { final objectType = object.type; final currentState = object.state; // Type 9 = special bulk handling if (objectType == 9) { // Bulk update all objects with state to_delivery emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.delivery, tour: currentTour, )); } else { if (currentState == ObjectStates.toDelivery) { emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.delivery, tour: currentTour, )); } else if (currentState == ObjectStates.retGI) { emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.retGIFzg, tour: currentTour, )); } else { emit(const ScanError(message: 'Fehler: Ungültiger Barcode')); } } } // ============================================================================ // STATE MACHINE: veh (Fahrzeug - Stationen) // ============================================================================ void _vehStateMachine(LogisticObject object, Emitter emit) { final currentState = object.state; switch (currentState) { case ObjectStates.delivery: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.station, tour: currentTour, )); break; case ObjectStates.retFail: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.retFailFzg, tour: currentTour, )); break; case ObjectStates.retGI: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.retGIFzg, tour: currentTour, )); break; case ObjectStates.retDS: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.retDSFzg, tour: currentTour, )); break; case ObjectStates.station: // Reverse transition: back to delivery emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.delivery, tour: currentTour, )); break; case ObjectStates.inFA: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.hdl, tour: currentTour, )); break; default: emit(const ScanError(message: 'Fehler: Falscher Status')); } } // ============================================================================ // STATE MACHINE: fsa (Fahrscheinautomat) // ============================================================================ void _fsaStateMachine(LogisticObject object, Emitter emit) { final objectType = object.type; final currentState = object.state; final subtype = object.subtype.toLowerCase(); // GK = Geldkassette (type 1) if (_isTypeGK(objectType, subtype)) { _fsaGKStateMachine(object, currentState, emit); } // HP = Hauptkasse/Druckerpatronen (type 2) else if (_isTypeHP(objectType, subtype)) { _fsaHPStateMachine(object, currentState, emit); } // FR = Fahrkartenrolle (type 5) else if (_isTypeFR(objectType, subtype)) { _fsaFRStateMachine(object, currentState, emit); } else { emit(const ScanError(message: 'Fehler: Falscher Typ für FSA')); } } void _fsaGKStateMachine(LogisticObject object, String currentState, Emitter emit) { switch (currentState) { case ObjectStates.station: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.inFA, tour: currentTour, )); break; case ObjectStates.inFA: // Special handling: Fehlkassette logic emit(ScanFehlKassetteDetected( object: object, suggestedState: ObjectStates.retFail, tour: currentTour, )); break; case ObjectStates.unknown: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.retGI, tour: currentTour, originBarcode: object.code, )); break; default: emit(const ScanError(message: 'Fehler: Falscher Status')); } } void _fsaHPStateMachine(LogisticObject object, String currentState, Emitter emit) { switch (currentState) { case ObjectStates.station: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.inFA, tour: currentTour, )); break; case ObjectStates.inFA: // Bidirectional: back to station emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.station, tour: currentTour, )); break; case ObjectStates.unknown: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.retDS, tour: currentTour, originBarcode: object.code, )); break; default: emit(const ScanError(message: 'Fehler: Falscher Status')); } } void _fsaFRStateMachine(LogisticObject object, String currentState, Emitter emit) { switch (currentState) { case ObjectStates.station: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.inFA, tour: currentTour, )); break; case ObjectStates.inFA: // Bidirectional: back to station emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.station, tour: currentTour, )); break; default: emit(const ScanError(message: 'Fehler: Falscher Status')); } } // ============================================================================ // STATE MACHINE: vs (Versorgungsstelle) // ============================================================================ void _vsStateMachine(LogisticObject object, Emitter emit) { final objectType = object.type; final currentState = object.state; final subtype = object.subtype.toLowerCase(); // SB = Safebag if (_isTypeSB(objectType, subtype)) { _vsSBStateMachine(object, currentState, emit); } // ABS = Abfallbehälter else if (_isTypeABS(objectType, subtype)) { _vsABSStateMachine(object, currentState, emit); } // CNTR = Container else if (_isTypeCNTR(objectType, subtype)) { _vsCNTRStateMachine(object, currentState, subtype, emit); } else { emit(const ScanError(message: 'Fehler: Falscher Status')); } } void _vsSBStateMachine(LogisticObject object, String currentState, Emitter emit) { if (containerId == null) { emit(const ScanError(message: 'Fehler: Falscher Zustand - Container nicht ausgewählt')); return; } if (containerType == 'a') { if (currentState == ObjectStates.inVS) { emit(ScanContainerObjectDetected( object: object, suggestedState: ObjectStates.retGI, tour: currentTour, containerId: containerId!, containerType: containerType!, )); } else { emit(const ScanError(message: 'Fehler: Falscher Zustand')); } } else { emit(const ScanError(message: 'Fehler: Falscher Zustand')); } } void _vsABSStateMachine(LogisticObject object, String currentState, Emitter emit) { if (containerId == null) { emit(const ScanError(message: 'Fehler: Falscher Zustand - Container nicht ausgewählt')); return; } if (containerType == 'a') { emit(const ScanError(message: 'Fehler: Falscher Zustand')); } else { if (currentState == ObjectStates.inVS) { emit(ScanContainerObjectDetected( object: object, suggestedState: ObjectStates.retDS, tour: currentTour, containerId: containerId!, containerType: containerType!, )); } else { emit(const ScanError(message: 'Fehler: Falscher Zustand')); } } } void _vsCNTRStateMachine(LogisticObject object, String currentState, String subtype, Emitter emit) { containerId = object.code; if (subtype == 'cntra') { containerType = 'a'; emit(ScanContainerDetected( object: object, suggestedState: ObjectStates.retcGI, tour: currentTour, containerId: containerId!, containerType: containerType!, )); } else if (subtype == 'cntrb') { containerType = 'b'; emit(ScanContainerDetected( object: object, suggestedState: ObjectStates.retcDS, tour: currentTour, containerId: containerId!, containerType: containerType!, )); } else { emit(const ScanError(message: 'Fehler: Unbekannter Container-Typ')); } } // ============================================================================ // STATE MACHINE: vehVs (Fahrzeug VS) // ============================================================================ void _vehVsStateMachine(LogisticObject object, Emitter emit) { final objectType = object.type; final currentState = object.state; final subtype = object.subtype.toLowerCase(); // SB and ABS not allowed in vehVs if (_isTypeSB(objectType, subtype) || _isTypeABS(objectType, subtype)) { emit(const ScanError(message: 'Fehler: Falscher Status')); return; } // CNTR = Container if (_isTypeCNTR(objectType, subtype)) { _vehVsCNTRStateMachine(object, currentState, subtype, emit); } else { emit(const ScanError(message: 'Fehler: Falscher Typ')); } } void _vehVsCNTRStateMachine(LogisticObject object, String currentState, String subtype, Emitter emit) { if (subtype == 'cntra') { if (currentState == ObjectStates.retcGI) { // Update all container objects and clear container emit(ScanContainerCloseDetected( object: object, suggestedState: ObjectStates.unknown, targetStateForObjects: ObjectStates.retGIFzg, tour: currentTour, containerType: 'a', )); containerId = null; containerType = null; } else { emit(const ScanError(message: 'Fehler: Falscher Status')); } } else if (subtype == 'cntrb') { if (currentState == ObjectStates.retcDS) { // Update all container objects and clear container emit(ScanContainerCloseDetected( object: object, suggestedState: ObjectStates.unknown, targetStateForObjects: ObjectStates.retDSFzg, tour: currentTour, containerType: 'b', )); containerId = null; containerType = null; } else { emit(const ScanError(message: 'Fehler: Falscher Status')); } } } // ============================================================================ // STATE MACHINE: gi (Geldinstitut) // ============================================================================ void _giStateMachine(LogisticObject object, Emitter emit) { final objectType = object.type; final currentState = object.state; final subtype = object.subtype.toLowerCase(); // GK = Geldkassette if (_isTypeGK(objectType, subtype)) { _giGKStateMachine(object, currentState, emit); } // SB = Safebag else if (_isTypeSB(objectType, subtype)) { _giSBStateMachine(object, currentState, emit); } else { emit(const ScanError(message: 'Fehler: Falscher Typ')); } } void _giGKStateMachine(LogisticObject object, String currentState, Emitter emit) { switch (currentState) { case ObjectStates.retGIFzg: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.finGI, tour: currentTour, )); break; case ObjectStates.unknown: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.retDSEmpty, tour: currentTour, )); break; case ObjectStates.delivery: emit(const ScanError(message: 'Fehler: Falscher Status')); break; default: emit(const ScanError(message: 'Fehler: Falscher Status')); } } void _giSBStateMachine(LogisticObject object, String currentState, Emitter emit) { if (currentState == ObjectStates.retGIFzg) { emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.finGI, tour: currentTour, )); } else { emit(const ScanError(message: 'Fehler: Falscher Status')); } } // ============================================================================ // STATE MACHINE: vehEnd (Fahrzeug Ende) // ============================================================================ void _vehEndStateMachine(LogisticObject object, Emitter emit) { final currentState = object.state; switch (currentState) { case ObjectStates.retFailFzg: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.retFailStk, tour: currentTour, )); break; case ObjectStates.retDSFzg: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.retDSStk, tour: currentTour, )); break; case ObjectStates.delivery: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.retDSStk, tour: currentTour, )); break; case ObjectStates.retDSEmpty: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.retDSStk, tour: currentTour, )); break; case ObjectStates.retGIFzg: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.retGIStk, tour: currentTour, )); break; default: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.retDSErr, tour: currentTour, )); } } // ============================================================================ // STATE MACHINE: stockEnd (Lager Ende) // ============================================================================ void _stockEndStateMachine(LogisticObject object, Emitter emit) { final currentState = object.state; switch (currentState) { case ObjectStates.retFailStk: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.finDSFail, tour: currentTour, )); break; case ObjectStates.retDSStk: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.finDS, tour: currentTour, )); break; case ObjectStates.retDSErr: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.finDSErr, tour: currentTour, )); break; case ObjectStates.retGIStk: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.finGITmp, tour: currentTour, )); break; default: emit(const ScanError(message: 'Fehler: Falscher Status')); } } // ============================================================================ // STATE MACHINE: stock (Lager - HADAG) // ============================================================================ void _stockStateMachine(LogisticObject object, Emitter emit) { final objectType = object.type; final currentState = object.state; final subtype = object.subtype.toLowerCase(); // Only HP and GK allowed if (!_isTypeHP(objectType, subtype) && !_isTypeGK(objectType, subtype)) { emit(const ScanError(message: 'Fehler: Falscher Typ')); return; } switch (currentState) { case ObjectStates.unknown: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.station, tour: currentTour, )); break; case ObjectStates.stkHadag: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.station, tour: currentTour, )); break; case ObjectStates.station: emit(ScanObjectDetected( object: object, suggestedState: ObjectStates.stkHadag, tour: currentTour, )); break; default: emit(ScanUnknownObject( barcode: object.code, prefix: object.code.length >= 3 ? object.code.substring(0, 3) : '', tour: currentTour, )); } } // ============================================================================ // Helper methods for type checking // ============================================================================ bool _isTypeGK(int type, String subtype) { // GK = Geldkassette (type 1, subtypes meka, mekb, mekc, mekd, beka, bekb, bekc, bekd) return type == 1 || subtype.startsWith('mek') || subtype.startsWith('bek'); } bool _isTypeHP(int type, String subtype) { // HP = Hauptkasse/Druckerpatronen (type 2, subtypes hp1a, hp1b, etc.) return type == 2 || subtype.startsWith('hp'); } bool _isTypeFR(int type, String subtype) { // FR = Fahrkartenrolle (type 5, subtype fra) return type == 5 || subtype.startsWith('fr'); } bool _isTypeSB(int type, String subtype) { // SB = Safebag (type 6) return type == 6 || subtype == 'sb'; } bool _isTypeABS(int type, String subtype) { // ABS = Abfallbehälter (type 7) return type == 7 || subtype == 'abs'; } bool _isTypeCNTR(int type, String subtype) { // CNTR = Container (type 8) return type == 8 || subtype.startsWith('cntr'); } // ============================================================================ // Legacy simple state machine (fallback) // ============================================================================ String _determineNextState(LogisticObject object) { switch (object.state) { case ObjectStates.unknown: return ObjectStates.toDelivery; case ObjectStates.toDelivery: return ObjectStates.delivery; case ObjectStates.delivery: return ObjectStates.station; case ObjectStates.station: return ObjectStates.inFA; case ObjectStates.inFA: return ObjectStates.retGI; case ObjectStates.retGI: return ObjectStates.retGIFzg; case ObjectStates.retGIFzg: return ObjectStates.finGI; case ObjectStates.retFail: return ObjectStates.retFailFzg; case ObjectStates.retFailFzg: return ObjectStates.retFailStk; case ObjectStates.retDS: return ObjectStates.retDSFzg; case ObjectStates.retDSFzg: return ObjectStates.finDS; default: return object.state; } } // ============================================================================ // Event handlers // ============================================================================ Future _onValidateBarcode(ValidateBarcode event, Emitter emit) async { if (event.barcode.isEmpty) { emit(const ScanValidationError(message: 'Barcode darf nicht leer sein')); return; } if (event.barcode.length < 6) { emit(const ScanValidationError(message: 'Barcode zu kurz')); return; } add(ProcessBarcode(barcode: event.barcode)); } Future _onUpdateObjectState(UpdateObjectState event, Emitter emit) async { emit(ScanProcessing(barcode: event.object.code)); final result = await repository.updateObjectState( event.object.objectId, event.newState, locationId: currentTour?.locationId, refType: currentTour != null ? _getTourTypeCode(currentTour!.type) : null, refId: currentTour?.tourId, containerCode: event.containerCode, ); result.fold( (failure) => emit(ScanError(message: _mapFailureToMessage(failure))), (_) { emit(ScanObjectUpdated( object: event.object.copyWith(state: event.newState), previousState: event.object.state, newState: event.newState, tour: currentTour, )); }, ); } Future _onCreateUnknownObject(CreateUnknownObject event, Emitter emit) async { emit(ScanProcessing(barcode: event.barcode)); final result = await repository.createObject( type: event.type, code: event.barcode, isManual: true, ); result.fold( (failure) => emit(ScanError(message: _mapFailureToMessage(failure))), (_) { emit(ScanObjectCreated(barcode: event.barcode)); }, ); } void _onResetScan(ResetScan event, Emitter emit) { if (currentTour != null) { emit(ScanReady(tour: currentTour!)); } else { emit(ScanInitial()); } } // ============================================================================ // Helper methods // ============================================================================ Map? _findPageForBarcode(String barcode) { if (currentTour == null) return null; for (final page in currentTour!.pages) { if (page.code == barcode) { return { 'pageId': page.pageId, 'label': page.label ?? page.pageId, }; } if (page.pageId.toLowerCase().startsWith('fsa') && page.type.isNotEmpty) { // FSA page detected } } return null; } bool _isValidPrefix(String prefix) { final validPrefixes = ['MEK', 'BEK', 'HOP', 'H1P', 'H2P', 'H3P', 'FR', 'SB', 'ABS', 'FZG']; return validPrefixes.any((p) => prefix.toUpperCase().startsWith(p)); } int? _getTourTypeCode(String type) { switch (type) { case TourTypes.stockStart: return 1; case TourTypes.vehStart: return 2; case TourTypes.veh: return 3; case TourTypes.fsa: return 4; case TourTypes.vs: return 5; case TourTypes.gi: return 6; case TourTypes.vehEnd: return 7; case TourTypes.stockEnd: return 8; default: return null; } } String _mapFailureToMessage(Failure failure) { return switch (failure) { ServerFailure _ => 'Serverfehler: ${failure.message}', NetworkFailure _ => 'Netzwerkfehler. Bitte überprüfen Sie Ihre Internetverbindung.', NotFoundFailure _ => 'Objekt nicht gefunden', BarcodeFailure _ => 'Barcode-Fehler: ${failure.message}', _ => 'Ein Fehler ist aufgetreten: ${failure.message}', }; } }