first commit
This commit is contained in:
132
app/lib/core/constants/app_constants.dart
Normal file
132
app/lib/core/constants/app_constants.dart
Normal file
@@ -0,0 +1,132 @@
|
||||
class AppConstants {
|
||||
// API
|
||||
static const String baseUrl = 'https://hha-app1.assecutor.de/hha';
|
||||
static const int connectionTimeout = 30000;
|
||||
static const int receiveTimeout = 30000;
|
||||
|
||||
// Database
|
||||
static const String databaseName = 'hha_logistics.db';
|
||||
static const int databaseVersion = 1;
|
||||
|
||||
// App Info
|
||||
static const String appName = 'HHA Logistics';
|
||||
static const String appVersion = '2.0.0';
|
||||
|
||||
// Sync
|
||||
static const Duration syncInterval = Duration(minutes: 1);
|
||||
static const Duration locationUpdateInterval = Duration(seconds: 30);
|
||||
}
|
||||
|
||||
class ObjectStates {
|
||||
static const String unknown = 'unknown';
|
||||
static const String toDelivery = 'to_delivery';
|
||||
static const String delivery = 'delivery';
|
||||
static const String station = 'station';
|
||||
static const String inFA = 'in_fa';
|
||||
static const String inVS = 'in_vs';
|
||||
static const String retFail = 'ret_fail';
|
||||
static const String retFailFzg = 'ret_fail_fzg';
|
||||
static const String retFailStk = 'ret_fail_stk';
|
||||
static const String retGI = 'ret_gi';
|
||||
static const String retGIFzg = 'ret_gi_fzg';
|
||||
static const String retGIStk = 'ret_gi_stk';
|
||||
static const String retDS = 'ret_ds';
|
||||
static const String retDSFzg = 'ret_ds_fzg';
|
||||
static const String retDSStk = 'ret_ds_stk';
|
||||
static const String retDSErr = 'ret_ds_err';
|
||||
static const String retDSEmpty = 'ret_ds_empty';
|
||||
static const String finDS = 'fin_ds';
|
||||
static const String finGI = 'fin_gi';
|
||||
static const String finDSFail = 'fin_ds_fail';
|
||||
static const String finDSErr = 'fin_ds_err';
|
||||
static const String hdl = 'hdl';
|
||||
static const String stkHadag = 'stk_hadag';
|
||||
static const String finGITmp = 'fin_gi_tmp';
|
||||
static const String retcGI = 'retc_gi';
|
||||
static const String retcDS = 'retc_ds';
|
||||
static const String retDSFix = 'ret_ds_fix';
|
||||
static const String retFixStk = 'ret_fix_stk';
|
||||
static const String finFix = 'fin_fix';
|
||||
static const String trig = 'trig';
|
||||
}
|
||||
|
||||
class TourTypes {
|
||||
static const String stockStart = 'stock_start';
|
||||
static const String stock = 'stock';
|
||||
static const String start = 'start';
|
||||
static const String station = 'st';
|
||||
static const String hls = 'hls';
|
||||
static const String vs = 'vs';
|
||||
static const String stockEnd = 'stock_end';
|
||||
static const String end = 'end';
|
||||
static const String fsa = 'fsa';
|
||||
static const String gi = 'gi';
|
||||
static const String veh = 'veh';
|
||||
static const String vehStart = 'veh_start';
|
||||
static const String vehBulk = 'veh_bulk';
|
||||
static const String vehVs = 'veh_vs';
|
||||
static const String vehEnd = 'veh_end';
|
||||
static const String menu = 'me';
|
||||
}
|
||||
|
||||
class ObjectTypes {
|
||||
static const String gk = 'gk'; // Geldkassette
|
||||
static const String hp = 'hp'; // HP Patronen
|
||||
static const String fr = 'fr'; // Fahrkartenrolle
|
||||
static const String sb = 'sb'; // Safebag
|
||||
static const String abs = 'abs'; // Abfallbehälter
|
||||
static const String cntr = 'cntr'; // Container
|
||||
}
|
||||
|
||||
class ObjectSubtypes {
|
||||
// Geldkassetten
|
||||
static const String meka = 'meka';
|
||||
static const String mekb = 'mekb';
|
||||
static const String mekc = 'mekc';
|
||||
static const String mekd = 'mekd';
|
||||
static const String beka = 'beka';
|
||||
static const String bekb = 'bekb';
|
||||
static const String bekc = 'bekc';
|
||||
static const String bekd = 'bekd';
|
||||
|
||||
// HP Patronen
|
||||
static const String hp1a = 'hp1a';
|
||||
static const String hp1b = 'hp1b';
|
||||
static const String hp1c = 'hp1c';
|
||||
static const String hp2a = 'hp2a';
|
||||
static const String hp2b = 'hp2b';
|
||||
static const String hp2c = 'hp2c';
|
||||
static const String hp3a = 'hp3a';
|
||||
static const String hp3b = 'hp3b';
|
||||
static const String hp3c = 'hp3c';
|
||||
|
||||
// Fahrkartenrollen
|
||||
static const String fra = 'fra';
|
||||
|
||||
// Container
|
||||
static const String cntra = 'cntra';
|
||||
static const String cntrb = 'cntrb';
|
||||
}
|
||||
|
||||
class CounterLabels {
|
||||
static const Map<String, String> labels = {
|
||||
'meka': 'MEK',
|
||||
'beka': 'BEK',
|
||||
'hp1a': 'H1',
|
||||
'hp2a': 'H2',
|
||||
'hp3a': 'H3',
|
||||
'fra': 'P',
|
||||
'sb': 'SB',
|
||||
'abs': 'ABS',
|
||||
'mekb': 'MEK-B',
|
||||
'bekb': 'BEK-B',
|
||||
'hp1b': 'H1-B',
|
||||
'hp2b': 'H2-B',
|
||||
'mekc': 'MEK-SST',
|
||||
'bekc': 'BEK-SST',
|
||||
'hp1c': 'H1-SST',
|
||||
'hp2c': 'H2-SST',
|
||||
'mekd': 'MEK-CR',
|
||||
'bekd': 'BEK-CR',
|
||||
};
|
||||
}
|
||||
70
app/lib/core/errors/failures.dart
Normal file
70
app/lib/core/errors/failures.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class Failure extends Equatable {
|
||||
final String message;
|
||||
final String? code;
|
||||
|
||||
const Failure({
|
||||
required this.message,
|
||||
this.code,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message, code];
|
||||
}
|
||||
|
||||
class ServerFailure extends Failure {
|
||||
const ServerFailure({
|
||||
required super.message,
|
||||
super.code,
|
||||
});
|
||||
}
|
||||
|
||||
class CacheFailure extends Failure {
|
||||
const CacheFailure({
|
||||
required super.message,
|
||||
super.code,
|
||||
});
|
||||
}
|
||||
|
||||
class NetworkFailure extends Failure {
|
||||
const NetworkFailure({
|
||||
required super.message,
|
||||
super.code,
|
||||
});
|
||||
}
|
||||
|
||||
class ValidationFailure extends Failure {
|
||||
const ValidationFailure({
|
||||
required super.message,
|
||||
super.code,
|
||||
});
|
||||
}
|
||||
|
||||
class NotFoundFailure extends Failure {
|
||||
const NotFoundFailure({
|
||||
required super.message,
|
||||
super.code,
|
||||
});
|
||||
}
|
||||
|
||||
class UnauthorizedFailure extends Failure {
|
||||
const UnauthorizedFailure({
|
||||
required super.message,
|
||||
super.code,
|
||||
});
|
||||
}
|
||||
|
||||
class BarcodeFailure extends Failure {
|
||||
const BarcodeFailure({
|
||||
required super.message,
|
||||
super.code,
|
||||
});
|
||||
}
|
||||
|
||||
class SyncFailure extends Failure {
|
||||
const SyncFailure({
|
||||
required super.message,
|
||||
super.code,
|
||||
});
|
||||
}
|
||||
273
app/lib/core/theme/app_theme.dart
Normal file
273
app/lib/core/theme/app_theme.dart
Normal file
@@ -0,0 +1,273 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class AppTheme {
|
||||
// Brand Colors - HHA Corporate Colors
|
||||
static const Color hhaRed = Color(0xFFE3001B);
|
||||
static const Color hhaDarkRed = Color(0xFFB30015);
|
||||
static const Color hhaLightRed = Color(0xFFFF4D5E);
|
||||
|
||||
// State Colors
|
||||
static const Color stateDelivery = Color(0xFFB3B3B3);
|
||||
static const Color stateStation = Color(0xFFFFDD00);
|
||||
static const Color stateInFA = Color(0xFF9CDA7A);
|
||||
static const Color stateRetFail = Color(0xFFFF9081);
|
||||
static const Color stateRetDS = Color(0xFFAFE0ED);
|
||||
static const Color stateRetGI = Color(0xFFAFE0ED);
|
||||
static const Color stateFinDS = Color(0xFF29B7FB);
|
||||
static const Color stateFinGI = Color(0xFF25BAFC);
|
||||
static const Color stateInVS = Color(0xFFFAE14B);
|
||||
|
||||
// Functional Colors
|
||||
static const Color success = Color(0xFF4CAF50);
|
||||
static const Color warning = Color(0xFFFF9800);
|
||||
static const Color error = Color(0xFFE3001B);
|
||||
static const Color info = Color(0xFF2196F3);
|
||||
|
||||
// Neutral Colors
|
||||
static const Color white = Color(0xFFFFFFFF);
|
||||
static const Color background = Color(0xFFF5F5F5);
|
||||
static const Color surface = Color(0xFFFFFFFF);
|
||||
static const Color cardBackground = Color(0xFFFAFAFA);
|
||||
static const Color divider = Color(0xFFE0E0E0);
|
||||
|
||||
// Text Colors
|
||||
static const Color textPrimary = Color(0xFF212121);
|
||||
static const Color textSecondary = Color(0xFF757575);
|
||||
static const Color textTertiary = Color(0xFF9E9E9E);
|
||||
static const Color textOnDark = Color(0xFFFFFFFF);
|
||||
|
||||
static ThemeData get lightTheme {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: hhaRed,
|
||||
primaryContainer: hhaLightRed,
|
||||
onPrimaryContainer: white,
|
||||
secondary: Color(0xFF2196F3),
|
||||
onSurface: textPrimary,
|
||||
surfaceContainerHighest: background,
|
||||
error: error,
|
||||
),
|
||||
scaffoldBackgroundColor: background,
|
||||
appBarTheme: AppBarTheme(
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
backgroundColor: hhaRed,
|
||||
foregroundColor: white,
|
||||
titleTextStyle: GoogleFonts.inter(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: white,
|
||||
),
|
||||
systemOverlayStyle: const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
),
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black.withValues(alpha: 26),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
color: surface,
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
elevation: 0,
|
||||
backgroundColor: hhaRed,
|
||||
foregroundColor: white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
textStyle: GoogleFonts.inter(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: hhaRed,
|
||||
side: const BorderSide(color: hhaRed, width: 2),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
textStyle: GoogleFonts.inter(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: hhaRed,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
textStyle: GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||
backgroundColor: hhaRed,
|
||||
foregroundColor: white,
|
||||
elevation: 4,
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: surface,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: divider),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: divider),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: hhaRed, width: 2),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: error, width: 2),
|
||||
),
|
||||
labelStyle: GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
color: textSecondary,
|
||||
),
|
||||
hintStyle: GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
color: textTertiary,
|
||||
),
|
||||
),
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: background,
|
||||
selectedColor: hhaRed.withValues(alpha: 26),
|
||||
labelStyle: GoogleFonts.inter(fontSize: 12),
|
||||
secondaryLabelStyle: GoogleFonts.inter(
|
||||
fontSize: 12,
|
||||
color: hhaRed,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
listTileTheme: ListTileThemeData(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
dividerTheme: const DividerThemeData(
|
||||
color: divider,
|
||||
thickness: 1,
|
||||
space: 1,
|
||||
),
|
||||
textTheme: _textTheme,
|
||||
fontFamily: GoogleFonts.inter().fontFamily,
|
||||
);
|
||||
}
|
||||
|
||||
static TextTheme get _textTheme {
|
||||
return TextTheme(
|
||||
displayLarge: GoogleFonts.inter(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textPrimary,
|
||||
),
|
||||
displayMedium: GoogleFonts.inter(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textPrimary,
|
||||
),
|
||||
displaySmall: GoogleFonts.inter(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textPrimary,
|
||||
),
|
||||
headlineLarge: GoogleFonts.inter(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textPrimary,
|
||||
),
|
||||
headlineMedium: GoogleFonts.inter(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textPrimary,
|
||||
),
|
||||
headlineSmall: GoogleFonts.inter(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textPrimary,
|
||||
),
|
||||
titleLarge: GoogleFonts.inter(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textPrimary,
|
||||
),
|
||||
titleMedium: GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textPrimary,
|
||||
),
|
||||
titleSmall: GoogleFonts.inter(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textSecondary,
|
||||
),
|
||||
bodyLarge: GoogleFonts.inter(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: textPrimary,
|
||||
),
|
||||
bodyMedium: GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: textPrimary,
|
||||
),
|
||||
bodySmall: GoogleFonts.inter(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: textSecondary,
|
||||
),
|
||||
labelLarge: GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: textPrimary,
|
||||
),
|
||||
labelMedium: GoogleFonts.inter(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: textSecondary,
|
||||
),
|
||||
labelSmall: GoogleFonts.inter(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: textTertiary,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension ColorExtension on Color {
|
||||
Color darken([double amount = .1]) {
|
||||
assert(amount >= 0 && amount <= 1);
|
||||
final hsl = HSLColor.fromColor(this);
|
||||
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
|
||||
return hslDark.toColor();
|
||||
}
|
||||
|
||||
Color lighten([double amount = .1]) {
|
||||
assert(amount >= 0 && amount <= 1);
|
||||
final hsl = HSLColor.fromColor(this);
|
||||
final hslLight = hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0));
|
||||
return hslLight.toColor();
|
||||
}
|
||||
}
|
||||
163
app/lib/data/models/logistic_object_model.dart
Normal file
163
app/lib/data/models/logistic_object_model.dart
Normal file
@@ -0,0 +1,163 @@
|
||||
import '../../domain/entities/logistic_object.dart';
|
||||
|
||||
class LogisticObjectModel extends LogisticObject {
|
||||
const LogisticObjectModel({
|
||||
required super.id,
|
||||
required super.objectId,
|
||||
required super.type,
|
||||
required super.version,
|
||||
super.locationId,
|
||||
required super.code,
|
||||
super.remark,
|
||||
required super.state,
|
||||
required super.subtype,
|
||||
super.origin,
|
||||
super.isManual,
|
||||
super.lastModified,
|
||||
super.typeName,
|
||||
super.typeMnemonic,
|
||||
});
|
||||
|
||||
factory LogisticObjectModel.fromJson(Map<String, dynamic> json) {
|
||||
return LogisticObjectModel(
|
||||
id: json['id'] ?? 0,
|
||||
objectId: json['object_id'] ?? json['id'] ?? 0,
|
||||
type: json['type'] ?? 0,
|
||||
version: json['version'] ?? json['ver'] ?? 0,
|
||||
locationId: json['loc_id'],
|
||||
code: json['code'] ?? '',
|
||||
remark: json['rem'],
|
||||
state: json['state'] ?? 'unknown',
|
||||
subtype: json['subtype'] ?? json['type']?.toString() ?? '',
|
||||
origin: json['origin'],
|
||||
isManual: json['manual'] == '1' || json['manual'] == true,
|
||||
lastModified: json['last_modified'] != null
|
||||
? DateTime.parse(json['last_modified'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
factory LogisticObjectModel.fromMap(Map<String, dynamic> map) {
|
||||
return LogisticObjectModel(
|
||||
id: map['id'] ?? 0,
|
||||
objectId: map['object_id'] ?? 0,
|
||||
type: map['type'] ?? 0,
|
||||
version: map['version'] ?? 0,
|
||||
locationId: map['loc_id'],
|
||||
code: map['code'] ?? '',
|
||||
remark: map['rem'],
|
||||
state: map['state'] ?? 'unknown',
|
||||
subtype: map['subtype'] ?? '',
|
||||
origin: map['origin'],
|
||||
isManual: map['manual'] == '1' || map['manual'] == 1,
|
||||
lastModified: map['last_modified'] != null
|
||||
? DateTime.tryParse(map['last_modified'])
|
||||
: null,
|
||||
typeName: map['type_name'],
|
||||
typeMnemonic: map['type_mnemonic'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'object_id': objectId,
|
||||
'type': type,
|
||||
'version': version,
|
||||
'loc_id': locationId,
|
||||
'code': code,
|
||||
'rem': remark,
|
||||
'state': state,
|
||||
'subtype': subtype,
|
||||
'origin': origin,
|
||||
'manual': isManual ? 1 : 0,
|
||||
'last_modified': lastModified?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
LogisticObjectModel copyWithModel({
|
||||
int? id,
|
||||
int? objectId,
|
||||
int? type,
|
||||
int? version,
|
||||
int? locationId,
|
||||
String? code,
|
||||
String? remark,
|
||||
String? state,
|
||||
String? subtype,
|
||||
String? origin,
|
||||
bool? isManual,
|
||||
DateTime? lastModified,
|
||||
String? typeName,
|
||||
String? typeMnemonic,
|
||||
}) {
|
||||
return LogisticObjectModel(
|
||||
id: id ?? this.id,
|
||||
objectId: objectId ?? this.objectId,
|
||||
type: type ?? this.type,
|
||||
version: version ?? this.version,
|
||||
locationId: locationId ?? this.locationId,
|
||||
code: code ?? this.code,
|
||||
remark: remark ?? this.remark,
|
||||
state: state ?? this.state,
|
||||
subtype: subtype ?? this.subtype,
|
||||
origin: origin ?? this.origin,
|
||||
isManual: isManual ?? this.isManual,
|
||||
lastModified: lastModified ?? this.lastModified,
|
||||
typeName: typeName ?? this.typeName,
|
||||
typeMnemonic: typeMnemonic ?? this.typeMnemonic,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ObjectMetadataModel extends ObjectMetadata {
|
||||
const ObjectMetadataModel({
|
||||
required super.id,
|
||||
required super.type,
|
||||
required super.version,
|
||||
required super.mnemonic,
|
||||
required super.name,
|
||||
required super.prefix,
|
||||
required super.subtype,
|
||||
required super.counterText,
|
||||
});
|
||||
|
||||
factory ObjectMetadataModel.fromJson(Map<String, dynamic> json) {
|
||||
return ObjectMetadataModel(
|
||||
id: json['id'] ?? 0,
|
||||
type: json['type'] ?? 0,
|
||||
version: json['version'] ?? json['ver'] ?? 0,
|
||||
mnemonic: json['mnemonic'] ?? '',
|
||||
name: json['name'] ?? '',
|
||||
prefix: json['pre'] ?? '',
|
||||
subtype: json['subtype'] ?? '',
|
||||
counterText: json['counter_text'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
factory ObjectMetadataModel.fromMap(Map<String, dynamic> map) {
|
||||
return ObjectMetadataModel(
|
||||
id: map['id'] ?? 0,
|
||||
type: map['type'] ?? 0,
|
||||
version: map['version'] ?? 0,
|
||||
mnemonic: map['mnemonic'] ?? '',
|
||||
name: map['name'] ?? '',
|
||||
prefix: map['prefix'] ?? '',
|
||||
subtype: map['subtype'] ?? '',
|
||||
counterText: map['counter_text'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'type': type,
|
||||
'version': version,
|
||||
'mnemonic': mnemonic,
|
||||
'name': name,
|
||||
'prefix': prefix,
|
||||
'subtype': subtype,
|
||||
'counter_text': counterText,
|
||||
};
|
||||
}
|
||||
}
|
||||
194
app/lib/data/models/tour_model.dart
Normal file
194
app/lib/data/models/tour_model.dart
Normal file
@@ -0,0 +1,194 @@
|
||||
import '../../domain/entities/tour.dart';
|
||||
|
||||
class TourModel extends Tour {
|
||||
const TourModel({
|
||||
required super.id,
|
||||
required super.jobId,
|
||||
required super.tourId,
|
||||
required super.version,
|
||||
required super.state,
|
||||
required super.type,
|
||||
required super.sort,
|
||||
required super.locationId,
|
||||
required super.locationCode,
|
||||
super.locationCode2,
|
||||
super.remark,
|
||||
super.menuText,
|
||||
required super.modified,
|
||||
super.deliveryCode,
|
||||
super.locationName,
|
||||
super.isCompleted,
|
||||
super.pages,
|
||||
});
|
||||
|
||||
factory TourModel.fromJson(Map<String, dynamic> json) {
|
||||
return TourModel(
|
||||
id: json['id'] ?? 0,
|
||||
jobId: json['job_id'] ?? 0,
|
||||
tourId: json['tour_id'] ?? 0,
|
||||
version: json['version'] ?? 0,
|
||||
state: json['state'] ?? 0,
|
||||
type: json['type'] ?? '',
|
||||
sort: json['sort'] ?? 0,
|
||||
locationId: json['loc_id'] ?? 0,
|
||||
locationCode: json['loc_code'] ?? '',
|
||||
locationCode2: json['loc_code_2'],
|
||||
remark: json['rem'],
|
||||
menuText: json['menu'],
|
||||
modified: json['modified'] ?? 0,
|
||||
deliveryCode: json['del_code'],
|
||||
locationName: json['location_name'],
|
||||
isCompleted: json['state'] == 1,
|
||||
pages: (json['pages'] as List?)
|
||||
?.map((p) => TourPageModel.fromJson(p))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
factory TourModel.fromMap(Map<String, dynamic> map) {
|
||||
return TourModel(
|
||||
id: map['id'] ?? 0,
|
||||
jobId: map['job_id'] ?? 0,
|
||||
tourId: map['tour_id'] ?? 0,
|
||||
version: map['version'] ?? 0,
|
||||
state: map['state'] ?? 0,
|
||||
type: map['type'] ?? '',
|
||||
sort: map['sort'] ?? 0,
|
||||
locationId: map['loc_id'] ?? 0,
|
||||
locationCode: map['loc_code'] ?? '',
|
||||
locationCode2: map['loc_code2'],
|
||||
remark: map['rem'],
|
||||
menuText: map['menu'],
|
||||
modified: map['modified'] ?? 0,
|
||||
deliveryCode: map['del_code'],
|
||||
locationName: map['location_name'],
|
||||
isCompleted: map['state'] == 1,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'job_id': jobId,
|
||||
'tour_id': tourId,
|
||||
'version': version,
|
||||
'state': state,
|
||||
'type': type,
|
||||
'sort': sort,
|
||||
'loc_id': locationId,
|
||||
'loc_code': locationCode,
|
||||
'loc_code2': locationCode2,
|
||||
'rem': remark,
|
||||
'menu': menuText,
|
||||
'modified': modified,
|
||||
'del_code': deliveryCode,
|
||||
};
|
||||
}
|
||||
|
||||
TourModel copyWithModel({
|
||||
int? id,
|
||||
int? jobId,
|
||||
int? tourId,
|
||||
int? version,
|
||||
int? state,
|
||||
String? type,
|
||||
int? sort,
|
||||
int? locationId,
|
||||
String? locationCode,
|
||||
String? locationCode2,
|
||||
String? remark,
|
||||
String? menuText,
|
||||
int? modified,
|
||||
String? deliveryCode,
|
||||
String? locationName,
|
||||
bool? isCompleted,
|
||||
List<TourPage>? pages,
|
||||
}) {
|
||||
return TourModel(
|
||||
id: id ?? this.id,
|
||||
jobId: jobId ?? this.jobId,
|
||||
tourId: tourId ?? this.tourId,
|
||||
version: version ?? this.version,
|
||||
state: state ?? this.state,
|
||||
type: type ?? this.type,
|
||||
sort: sort ?? this.sort,
|
||||
locationId: locationId ?? this.locationId,
|
||||
locationCode: locationCode ?? this.locationCode,
|
||||
locationCode2: locationCode2 ?? this.locationCode2,
|
||||
remark: remark ?? this.remark,
|
||||
menuText: menuText ?? this.menuText,
|
||||
modified: modified ?? this.modified,
|
||||
deliveryCode: deliveryCode ?? this.deliveryCode,
|
||||
locationName: locationName ?? this.locationName,
|
||||
isCompleted: isCompleted ?? this.isCompleted,
|
||||
pages: pages ?? this.pages,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TourPageModel extends TourPage {
|
||||
const TourPageModel({
|
||||
required super.id,
|
||||
required super.tourId,
|
||||
required super.pageNumber,
|
||||
required super.pageId,
|
||||
required super.type,
|
||||
super.code,
|
||||
super.label,
|
||||
super.pickupCounts,
|
||||
super.swapCounts,
|
||||
});
|
||||
|
||||
factory TourPageModel.fromJson(Map<String, dynamic> json) {
|
||||
Map<String, int> pickupCounts = {};
|
||||
Map<String, int> swapCounts = {};
|
||||
|
||||
if (json['pickup'] != null && json['pickup']['cnt'] != null) {
|
||||
final cnt = json['pickup']['cnt'] as Map<String, dynamic>;
|
||||
pickupCounts = cnt.map((key, value) => MapEntry(key, value as int));
|
||||
}
|
||||
|
||||
if (json['swap'] != null && json['swap']['cnt'] != null) {
|
||||
final cnt = json['swap']['cnt'] as Map<String, dynamic>;
|
||||
swapCounts = cnt.map((key, value) => MapEntry(key, value as int));
|
||||
}
|
||||
|
||||
return TourPageModel(
|
||||
id: json['id'] ?? 0,
|
||||
tourId: json['tour_id'] ?? 0,
|
||||
pageNumber: json['page_number'] ?? 0,
|
||||
pageId: json['page_id'] ?? '',
|
||||
type: json['type'] ?? '',
|
||||
code: json['code'],
|
||||
label: json['lbl'],
|
||||
pickupCounts: pickupCounts,
|
||||
swapCounts: swapCounts,
|
||||
);
|
||||
}
|
||||
|
||||
factory TourPageModel.fromMap(Map<String, dynamic> map) {
|
||||
return TourPageModel(
|
||||
id: map['id'] ?? 0,
|
||||
tourId: map['tour_id'] ?? 0,
|
||||
pageNumber: map['page_number'] ?? 0,
|
||||
pageId: map['page_id'] ?? '',
|
||||
type: map['type'] ?? '',
|
||||
code: map['code'],
|
||||
label: map['label'],
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'tour_id': tourId,
|
||||
'page_number': pageNumber,
|
||||
'page_id': pageId,
|
||||
'type': type,
|
||||
'code': code,
|
||||
'label': label,
|
||||
};
|
||||
}
|
||||
}
|
||||
199
app/lib/domain/entities/counter.dart
Normal file
199
app/lib/domain/entities/counter.dart
Normal file
@@ -0,0 +1,199 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Repräsentiert einen Zählerstand für einen Objekttyp
|
||||
/// Entspricht der Lua-Logik in CreateLoadingStockStartView etc.
|
||||
class ObjectCounter extends Equatable {
|
||||
final String objectType; // z.B. 'meka', 'beka', 'hp1a', etc.
|
||||
final String label; // z.B. 'MEK', 'BEK', 'H1'
|
||||
final int currentCount; // Aktueller Bestand (z.B. im Fahrzeug)
|
||||
final int targetCount; // Soll-Zahl (z.B. Beladezähler)
|
||||
final int? alternateCount; // Alternative Zählung (z.B. HADAG, CR, SST)
|
||||
|
||||
const ObjectCounter({
|
||||
required this.objectType,
|
||||
required this.label,
|
||||
required this.currentCount,
|
||||
required this.targetCount,
|
||||
this.alternateCount,
|
||||
});
|
||||
|
||||
bool get isComplete => currentCount >= targetCount;
|
||||
bool get isOver => currentCount > targetCount;
|
||||
int get difference => targetCount - currentCount;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [objectType, label, currentCount, targetCount, alternateCount];
|
||||
}
|
||||
|
||||
/// Gruppen von Zählern für verschiedene Ansichten
|
||||
class CounterGroup extends Equatable {
|
||||
final String title;
|
||||
final List<ObjectCounter> counters;
|
||||
|
||||
const CounterGroup({
|
||||
required this.title,
|
||||
required this.counters,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [title, counters];
|
||||
}
|
||||
|
||||
/// Zähler-Übersicht für eine komplette Tour/Page
|
||||
class CounterOverview extends Equatable {
|
||||
final int tourId;
|
||||
final String? pageId;
|
||||
final List<CounterGroup> groups;
|
||||
final DateTime? lastUpdated;
|
||||
|
||||
const CounterOverview({
|
||||
required this.tourId,
|
||||
this.pageId,
|
||||
required this.groups,
|
||||
this.lastUpdated,
|
||||
});
|
||||
|
||||
/// Standard-Gruppen für StockStart (Lager Beladung)
|
||||
/// Entspricht Lua: CreateLoadingStockStartView
|
||||
factory CounterOverview.stockStart({
|
||||
required int tourId,
|
||||
required List<ObjectCounter> vehicleStock, // Bestand Fzg
|
||||
required List<ObjectCounter> loadingCounters, // Beladezähler
|
||||
required List<ObjectCounter> hadagCounters, // HADAG
|
||||
required List<ObjectCounter> sstCounters, // SST
|
||||
required List<ObjectCounter> crCounters, // CR
|
||||
}) {
|
||||
return CounterOverview(
|
||||
tourId: tourId,
|
||||
groups: [
|
||||
CounterGroup(title: 'Bestand Fzg', counters: vehicleStock),
|
||||
CounterGroup(title: 'Beladezähler', counters: loadingCounters),
|
||||
CounterGroup(title: 'HADAG', counters: hadagCounters),
|
||||
CounterGroup(title: 'SST', counters: sstCounters),
|
||||
CounterGroup(title: 'CR', counters: crCounters),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Standard-Gruppen für VehStart
|
||||
/// Entspricht Lua: CreateLoadingVehStartView
|
||||
factory CounterOverview.vehStart({
|
||||
required int tourId,
|
||||
required List<ObjectCounter> loadingCounters,
|
||||
}) {
|
||||
return CounterOverview(
|
||||
tourId: tourId,
|
||||
groups: [
|
||||
CounterGroup(title: 'Beladezähler', counters: loadingCounters),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Standard-Gruppen für Veh (Station)
|
||||
/// Entspricht Lua: CreateLoadingVehView
|
||||
factory CounterOverview.veh({
|
||||
required int tourId,
|
||||
String? pageId,
|
||||
required List<ObjectCounter> swapCounters, // Wechselzähler
|
||||
required List<ObjectCounter> pickupCounters, // Abholzähler
|
||||
}) {
|
||||
return CounterOverview(
|
||||
tourId: tourId,
|
||||
pageId: pageId,
|
||||
groups: [
|
||||
CounterGroup(title: 'Wechsel', counters: swapCounters),
|
||||
CounterGroup(title: 'Abholung', counters: pickupCounters),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [tourId, pageId, groups, lastUpdated];
|
||||
}
|
||||
|
||||
/// Pickup/Abhol-Zähler aus der Datenbank
|
||||
/// Entspricht Lua: page_pickup_count Tabelle
|
||||
class PickupCount extends Equatable {
|
||||
final int tourId;
|
||||
final String pageId;
|
||||
final String objectType;
|
||||
final int count;
|
||||
|
||||
const PickupCount({
|
||||
required this.tourId,
|
||||
required this.pageId,
|
||||
required this.objectType,
|
||||
required this.count,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [tourId, pageId, objectType, count];
|
||||
}
|
||||
|
||||
/// Swap/Wechsel-Zähler aus der Datenbank
|
||||
/// Entspricht Lua: page_swap_count Tabelle
|
||||
class SwapCount extends Equatable {
|
||||
final int tourId;
|
||||
final String pageId;
|
||||
final String objectType;
|
||||
final int count;
|
||||
|
||||
const SwapCount({
|
||||
required this.tourId,
|
||||
required this.pageId,
|
||||
required this.objectType,
|
||||
required this.count,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [tourId, pageId, objectType, count];
|
||||
}
|
||||
|
||||
/// Container-Information für VS/Verwahrungsstelle
|
||||
/// Entspricht Lua: ContainerId, ContainerType in vsStateMachine
|
||||
class ContainerInfo extends Equatable {
|
||||
final String containerId;
|
||||
final String containerType; // 'a' = Geldinstitut, 'b' = Dienststelle
|
||||
final String? subtype; // 'cntra' oder 'cntrb'
|
||||
final int? objectCount; // Anzahl Objekte im Container
|
||||
|
||||
const ContainerInfo({
|
||||
required this.containerId,
|
||||
required this.containerType,
|
||||
this.subtype,
|
||||
this.objectCount,
|
||||
});
|
||||
|
||||
bool get isForGI => containerType == 'a';
|
||||
bool get isForDS => containerType == 'b';
|
||||
|
||||
String get displayName {
|
||||
if (isForGI) return 'Container Geldinstitut';
|
||||
if (isForDS) return 'Container Dienststelle';
|
||||
return 'Container $containerId';
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [containerId, containerType, subtype, objectCount];
|
||||
}
|
||||
|
||||
/// Letzte gescannte Objekte für die Anzeige
|
||||
/// Entspricht Lua: Die Liste in ShowStockStartScreen etc.
|
||||
class RecentScan extends Equatable {
|
||||
final String objectCode;
|
||||
final String objectName;
|
||||
final String state;
|
||||
final DateTime scanTime;
|
||||
final String? imageName;
|
||||
|
||||
const RecentScan({
|
||||
required this.objectCode,
|
||||
required this.objectName,
|
||||
required this.state,
|
||||
required this.scanTime,
|
||||
this.imageName,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [objectCode, objectName, state, scanTime, imageName];
|
||||
}
|
||||
89
app/lib/domain/entities/location.dart
Normal file
89
app/lib/domain/entities/location.dart
Normal file
@@ -0,0 +1,89 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class Location extends Equatable {
|
||||
final int id;
|
||||
final int locationId;
|
||||
final int version;
|
||||
final String name;
|
||||
final String? street;
|
||||
final String? number;
|
||||
final String? zip;
|
||||
final String? city;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final String? remark;
|
||||
|
||||
const Location({
|
||||
required this.id,
|
||||
required this.locationId,
|
||||
required this.version,
|
||||
required this.name,
|
||||
this.street,
|
||||
this.number,
|
||||
this.zip,
|
||||
this.city,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.remark,
|
||||
});
|
||||
|
||||
Location copyWith({
|
||||
int? id,
|
||||
int? locationId,
|
||||
int? version,
|
||||
String? name,
|
||||
String? street,
|
||||
String? number,
|
||||
String? zip,
|
||||
String? city,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
String? remark,
|
||||
}) {
|
||||
return Location(
|
||||
id: id ?? this.id,
|
||||
locationId: locationId ?? this.locationId,
|
||||
version: version ?? this.version,
|
||||
name: name ?? this.name,
|
||||
street: street ?? this.street,
|
||||
number: number ?? this.number,
|
||||
zip: zip ?? this.zip,
|
||||
city: city ?? this.city,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
remark: remark ?? this.remark,
|
||||
);
|
||||
}
|
||||
|
||||
String get fullAddress {
|
||||
final parts = <String>[];
|
||||
if (street != null && street!.isNotEmpty) {
|
||||
parts.add(street!);
|
||||
if (number != null && number!.isNotEmpty) {
|
||||
parts.add(number!);
|
||||
}
|
||||
}
|
||||
if (zip != null && zip!.isNotEmpty) {
|
||||
parts.add(zip!);
|
||||
}
|
||||
if (city != null && city!.isNotEmpty) {
|
||||
parts.add(city!);
|
||||
}
|
||||
return parts.join(', ');
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
locationId,
|
||||
version,
|
||||
name,
|
||||
street,
|
||||
number,
|
||||
zip,
|
||||
city,
|
||||
latitude,
|
||||
longitude,
|
||||
remark,
|
||||
];
|
||||
}
|
||||
250
app/lib/domain/entities/logistic_object.dart
Normal file
250
app/lib/domain/entities/logistic_object.dart
Normal file
@@ -0,0 +1,250 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LogisticObject extends Equatable {
|
||||
final int id;
|
||||
final int objectId;
|
||||
final int type;
|
||||
final int version;
|
||||
final int? locationId;
|
||||
final String code;
|
||||
final String? remark;
|
||||
final String state;
|
||||
final String subtype;
|
||||
final String? origin;
|
||||
final bool isManual;
|
||||
final DateTime? lastModified;
|
||||
final String? typeName;
|
||||
final String? typeMnemonic;
|
||||
|
||||
const LogisticObject({
|
||||
required this.id,
|
||||
required this.objectId,
|
||||
required this.type,
|
||||
required this.version,
|
||||
this.locationId,
|
||||
required this.code,
|
||||
this.remark,
|
||||
required this.state,
|
||||
required this.subtype,
|
||||
this.origin,
|
||||
this.isManual = false,
|
||||
this.lastModified,
|
||||
this.typeName,
|
||||
this.typeMnemonic,
|
||||
});
|
||||
|
||||
LogisticObject copyWith({
|
||||
int? id,
|
||||
int? objectId,
|
||||
int? type,
|
||||
int? version,
|
||||
int? locationId,
|
||||
String? code,
|
||||
String? remark,
|
||||
String? state,
|
||||
String? subtype,
|
||||
String? origin,
|
||||
bool? isManual,
|
||||
DateTime? lastModified,
|
||||
String? typeName,
|
||||
String? typeMnemonic,
|
||||
}) {
|
||||
return LogisticObject(
|
||||
id: id ?? this.id,
|
||||
objectId: objectId ?? this.objectId,
|
||||
type: type ?? this.type,
|
||||
version: version ?? this.version,
|
||||
locationId: locationId ?? this.locationId,
|
||||
code: code ?? this.code,
|
||||
remark: remark ?? this.remark,
|
||||
state: state ?? this.state,
|
||||
subtype: subtype ?? this.subtype,
|
||||
origin: origin ?? this.origin,
|
||||
isManual: isManual ?? this.isManual,
|
||||
lastModified: lastModified ?? this.lastModified,
|
||||
typeName: typeName ?? this.typeName,
|
||||
typeMnemonic: typeMnemonic ?? this.typeMnemonic,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
objectId,
|
||||
type,
|
||||
version,
|
||||
locationId,
|
||||
code,
|
||||
remark,
|
||||
state,
|
||||
subtype,
|
||||
origin,
|
||||
isManual,
|
||||
lastModified,
|
||||
typeName,
|
||||
typeMnemonic,
|
||||
];
|
||||
}
|
||||
|
||||
class ObjectMetadata extends Equatable {
|
||||
final int id;
|
||||
final int type;
|
||||
final int version;
|
||||
final String mnemonic;
|
||||
final String name;
|
||||
final String prefix;
|
||||
final String subtype;
|
||||
final String counterText;
|
||||
|
||||
const ObjectMetadata({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.version,
|
||||
required this.mnemonic,
|
||||
required this.name,
|
||||
required this.prefix,
|
||||
required this.subtype,
|
||||
required this.counterText,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
type,
|
||||
version,
|
||||
mnemonic,
|
||||
name,
|
||||
prefix,
|
||||
subtype,
|
||||
counterText,
|
||||
];
|
||||
}
|
||||
|
||||
class ObjectStateInfo {
|
||||
final String state;
|
||||
final String displayName;
|
||||
final int colorValue;
|
||||
final String iconName;
|
||||
|
||||
const ObjectStateInfo({
|
||||
required this.state,
|
||||
required this.displayName,
|
||||
required this.colorValue,
|
||||
required this.iconName,
|
||||
});
|
||||
|
||||
static const Map<String, ObjectStateInfo> stateInfos = {
|
||||
'unknown': ObjectStateInfo(
|
||||
state: 'unknown',
|
||||
displayName: 'Unbekannt',
|
||||
colorValue: 0xFFFFFFFF,
|
||||
iconName: 'help',
|
||||
),
|
||||
'delivery': ObjectStateInfo(
|
||||
state: 'delivery',
|
||||
displayName: 'Im Fahrzeug',
|
||||
colorValue: 0xFFB3B3B3,
|
||||
iconName: 'local_shipping',
|
||||
),
|
||||
'to_delivery': ObjectStateInfo(
|
||||
state: 'to_delivery',
|
||||
displayName: 'Zum Fahrzeug',
|
||||
colorValue: 0xFFB3B3B3,
|
||||
iconName: 'local_shipping_outlined',
|
||||
),
|
||||
'station': ObjectStateInfo(
|
||||
state: 'station',
|
||||
displayName: 'An Station',
|
||||
colorValue: 0xFFFFDD00,
|
||||
iconName: 'location_on',
|
||||
),
|
||||
'in_fa': ObjectStateInfo(
|
||||
state: 'in_fa',
|
||||
displayName: 'Im Fahrscheinautomat',
|
||||
colorValue: 0xFF9CDA7A,
|
||||
iconName: 'confirmation_number',
|
||||
),
|
||||
'in_vs': ObjectStateInfo(
|
||||
state: 'in_vs',
|
||||
displayName: 'In Versorgungsstelle',
|
||||
colorValue: 0xFFFAE14B,
|
||||
iconName: 'inventory',
|
||||
),
|
||||
'ret_fail': ObjectStateInfo(
|
||||
state: 'ret_fail',
|
||||
displayName: 'Fehler - zur Dienststelle',
|
||||
colorValue: 0xFFFF9081,
|
||||
iconName: 'error',
|
||||
),
|
||||
'ret_fail_fzg': ObjectStateInfo(
|
||||
state: 'ret_fail_fzg',
|
||||
displayName: 'Fehler - im Fahrzeug',
|
||||
colorValue: 0xFFFF9081,
|
||||
iconName: 'error_outline',
|
||||
),
|
||||
'ret_ds': ObjectStateInfo(
|
||||
state: 'ret_ds',
|
||||
displayName: 'Zur Dienststelle',
|
||||
colorValue: 0xFFAFE0ED,
|
||||
iconName: 'account_balance',
|
||||
),
|
||||
'ret_ds_fzg': ObjectStateInfo(
|
||||
state: 'ret_ds_fzg',
|
||||
displayName: 'Zur Dienststelle (Fzg)',
|
||||
colorValue: 0xFFAFE0ED,
|
||||
iconName: 'account_balance_outlined',
|
||||
),
|
||||
'ret_gi': ObjectStateInfo(
|
||||
state: 'ret_gi',
|
||||
displayName: 'Zum Geldinstitut',
|
||||
colorValue: 0xFFAFE0ED,
|
||||
iconName: 'account_balance_wallet',
|
||||
),
|
||||
'ret_gi_fzg': ObjectStateInfo(
|
||||
state: 'ret_gi_fzg',
|
||||
displayName: 'Zum Geldinstitut (Fzg)',
|
||||
colorValue: 0xFFAFE0ED,
|
||||
iconName: 'account_balance_wallet_outlined',
|
||||
),
|
||||
'fin_ds': ObjectStateInfo(
|
||||
state: 'fin_ds',
|
||||
displayName: 'In Dienststelle',
|
||||
colorValue: 0xFF29B7FB,
|
||||
iconName: 'check_circle',
|
||||
),
|
||||
'fin_gi': ObjectStateInfo(
|
||||
state: 'fin_gi',
|
||||
displayName: 'In Geldinstitut',
|
||||
colorValue: 0xFF25BAFC,
|
||||
iconName: 'check_circle_outline',
|
||||
),
|
||||
'hdl': ObjectStateInfo(
|
||||
state: 'hdl',
|
||||
displayName: 'Handel',
|
||||
colorValue: 0xFF9E9E9E,
|
||||
iconName: 'shopping_cart',
|
||||
),
|
||||
'ret_ds_empty': ObjectStateInfo(
|
||||
state: 'ret_ds_empty',
|
||||
displayName: 'Leer - zur Dienststelle',
|
||||
colorValue: 0xFFAFE0ED,
|
||||
iconName: 'remove_circle_outline',
|
||||
),
|
||||
};
|
||||
|
||||
static Color getColorForState(String state) {
|
||||
final info = stateInfos[state];
|
||||
return info != null ? Color(info.colorValue) : const Color(0xFFFFFFFF);
|
||||
}
|
||||
|
||||
static String getDisplayName(String state) {
|
||||
final info = stateInfos[state];
|
||||
return info?.displayName ?? 'Unbekannt';
|
||||
}
|
||||
|
||||
static String getIconName(String state) {
|
||||
final info = stateInfos[state];
|
||||
return info?.iconName ?? 'help';
|
||||
}
|
||||
}
|
||||
163
app/lib/domain/entities/tour.dart
Normal file
163
app/lib/domain/entities/tour.dart
Normal file
@@ -0,0 +1,163 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class Tour extends Equatable {
|
||||
final int id;
|
||||
final int jobId;
|
||||
final int tourId;
|
||||
final int version;
|
||||
final int state; // 0 = offen, 1 = erledigt, 2 = abgeschlossen
|
||||
final String type;
|
||||
final int sort;
|
||||
final int locationId;
|
||||
final String locationCode;
|
||||
final String? locationCode2;
|
||||
final String? remark;
|
||||
final String? menuText;
|
||||
final int modified;
|
||||
final String? deliveryCode;
|
||||
final String? locationName;
|
||||
final bool isCompleted;
|
||||
final List<TourPage> pages;
|
||||
|
||||
const Tour({
|
||||
required this.id,
|
||||
required this.jobId,
|
||||
required this.tourId,
|
||||
required this.version,
|
||||
required this.state,
|
||||
required this.type,
|
||||
required this.sort,
|
||||
required this.locationId,
|
||||
required this.locationCode,
|
||||
this.locationCode2,
|
||||
this.remark,
|
||||
this.menuText,
|
||||
required this.modified,
|
||||
this.deliveryCode,
|
||||
this.locationName,
|
||||
this.isCompleted = false,
|
||||
this.pages = const [],
|
||||
});
|
||||
|
||||
Tour copyWith({
|
||||
int? id,
|
||||
int? jobId,
|
||||
int? tourId,
|
||||
int? version,
|
||||
int? state,
|
||||
String? type,
|
||||
int? sort,
|
||||
int? locationId,
|
||||
String? locationCode,
|
||||
String? locationCode2,
|
||||
String? remark,
|
||||
String? menuText,
|
||||
int? modified,
|
||||
String? deliveryCode,
|
||||
String? locationName,
|
||||
bool? isCompleted,
|
||||
List<TourPage>? pages,
|
||||
}) {
|
||||
return Tour(
|
||||
id: id ?? this.id,
|
||||
jobId: jobId ?? this.jobId,
|
||||
tourId: tourId ?? this.tourId,
|
||||
version: version ?? this.version,
|
||||
state: state ?? this.state,
|
||||
type: type ?? this.type,
|
||||
sort: sort ?? this.sort,
|
||||
locationId: locationId ?? this.locationId,
|
||||
locationCode: locationCode ?? this.locationCode,
|
||||
locationCode2: locationCode2 ?? this.locationCode2,
|
||||
remark: remark ?? this.remark,
|
||||
menuText: menuText ?? this.menuText,
|
||||
modified: modified ?? this.modified,
|
||||
deliveryCode: deliveryCode ?? this.deliveryCode,
|
||||
locationName: locationName ?? this.locationName,
|
||||
isCompleted: isCompleted ?? this.isCompleted,
|
||||
pages: pages ?? this.pages,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
jobId,
|
||||
tourId,
|
||||
version,
|
||||
state,
|
||||
type,
|
||||
sort,
|
||||
locationId,
|
||||
locationCode,
|
||||
locationCode2,
|
||||
remark,
|
||||
menuText,
|
||||
modified,
|
||||
deliveryCode,
|
||||
locationName,
|
||||
isCompleted,
|
||||
pages,
|
||||
];
|
||||
}
|
||||
|
||||
class TourPage extends Equatable {
|
||||
final int id;
|
||||
final int tourId;
|
||||
final int pageNumber;
|
||||
final String pageId;
|
||||
final String type;
|
||||
final String? code;
|
||||
final String? label;
|
||||
final Map<String, int> pickupCounts;
|
||||
final Map<String, int> swapCounts;
|
||||
|
||||
const TourPage({
|
||||
required this.id,
|
||||
required this.tourId,
|
||||
required this.pageNumber,
|
||||
required this.pageId,
|
||||
required this.type,
|
||||
this.code,
|
||||
this.label,
|
||||
this.pickupCounts = const {},
|
||||
this.swapCounts = const {},
|
||||
});
|
||||
|
||||
TourPage copyWith({
|
||||
int? id,
|
||||
int? tourId,
|
||||
int? pageNumber,
|
||||
String? pageId,
|
||||
String? type,
|
||||
String? code,
|
||||
String? label,
|
||||
Map<String, int>? pickupCounts,
|
||||
Map<String, int>? swapCounts,
|
||||
}) {
|
||||
return TourPage(
|
||||
id: id ?? this.id,
|
||||
tourId: tourId ?? this.tourId,
|
||||
pageNumber: pageNumber ?? this.pageNumber,
|
||||
pageId: pageId ?? this.pageId,
|
||||
type: type ?? this.type,
|
||||
code: code ?? this.code,
|
||||
label: label ?? this.label,
|
||||
pickupCounts: pickupCounts ?? this.pickupCounts,
|
||||
swapCounts: swapCounts ?? this.swapCounts,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
tourId,
|
||||
pageNumber,
|
||||
pageId,
|
||||
type,
|
||||
code,
|
||||
label,
|
||||
pickupCounts,
|
||||
swapCounts,
|
||||
];
|
||||
}
|
||||
109
app/lib/domain/repositories/tour_repository.dart
Normal file
109
app/lib/domain/repositories/tour_repository.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../entities/tour.dart';
|
||||
import '../entities/logistic_object.dart';
|
||||
import '../entities/location.dart';
|
||||
import '../entities/counter.dart';
|
||||
import '../../core/errors/failures.dart';
|
||||
|
||||
abstract class TourRepository {
|
||||
// Touren
|
||||
Future<Either<Failure, List<Tour>>> getTours();
|
||||
Future<Either<Failure, Tour>> getTourById(int tourId);
|
||||
Future<Either<Failure, void>> updateTourState(int tourId, int state);
|
||||
Future<Either<Failure, void>> completeTour(int tourId);
|
||||
|
||||
// Sync
|
||||
Future<Either<Failure, void>> syncData();
|
||||
Future<Either<Failure, bool>> checkForUpdates();
|
||||
|
||||
// Objects
|
||||
Future<Either<Failure, List<LogisticObject>>> getObjectsByTour(int tourId);
|
||||
Future<Either<Failure, List<LogisticObject>>> getObjectsByState(String state);
|
||||
Future<Either<Failure, LogisticObject?>> getObjectByBarcode(String barcode);
|
||||
Future<Either<Failure, void>> updateObjectState(
|
||||
int objectId,
|
||||
String newState, {
|
||||
int? locationId,
|
||||
int? refType,
|
||||
int? refId,
|
||||
String? containerCode,
|
||||
});
|
||||
Future<Either<Failure, void>> createObject({
|
||||
required int type,
|
||||
required String code,
|
||||
bool isManual = true,
|
||||
});
|
||||
|
||||
// Locations
|
||||
Future<Either<Failure, List<Location>>> getLocations();
|
||||
Future<Either<Failure, Location>> getLocationById(int locationId);
|
||||
|
||||
// Metadata
|
||||
Future<Either<Failure, List<ObjectMetadata>>> getObjectMetadata();
|
||||
|
||||
// Statistics
|
||||
Future<Either<Failure, TourStatistics>> getTourStatistics(int tourId);
|
||||
Future<Either<Failure, ObjectStatistics>> getObjectStatistics();
|
||||
|
||||
// Counter operations (spezifisch für Lua-kompatible Ansichten)
|
||||
Future<Either<Failure, CounterOverview>> getCounterOverview(
|
||||
int tourId,
|
||||
String tourType, {
|
||||
String? pageId,
|
||||
});
|
||||
|
||||
Future<Either<Failure, List<PickupCount>>> getPickupCounts(int tourId, String pageId);
|
||||
Future<Either<Failure, List<SwapCount>>> getSwapCounts(int tourId, String pageId);
|
||||
|
||||
// Container operations
|
||||
Future<Either<Failure, List<ContainerInfo>>> getOpenContainers();
|
||||
Future<Either<Failure, void>> addObjectToContainer(
|
||||
String containerId,
|
||||
String containerType,
|
||||
int objectId,
|
||||
);
|
||||
Future<Either<Failure, void>> closeContainer(
|
||||
String containerId,
|
||||
String containerType,
|
||||
);
|
||||
|
||||
// Recent scans
|
||||
Future<Either<Failure, List<RecentScan>>> getRecentScans(
|
||||
int tourId, {
|
||||
int limit = 10,
|
||||
});
|
||||
}
|
||||
|
||||
class TourStatistics {
|
||||
final int totalObjects;
|
||||
final int completedObjects;
|
||||
final int pendingObjects;
|
||||
final Map<String, int> objectsByState;
|
||||
final Map<String, int> objectsByType;
|
||||
final double completionPercentage;
|
||||
|
||||
const TourStatistics({
|
||||
required this.totalObjects,
|
||||
required this.completedObjects,
|
||||
required this.pendingObjects,
|
||||
required this.objectsByState,
|
||||
required this.objectsByType,
|
||||
required this.completionPercentage,
|
||||
});
|
||||
}
|
||||
|
||||
class ObjectStatistics {
|
||||
final Map<String, int> byState;
|
||||
final Map<String, int> byType;
|
||||
final Map<String, int> byLocation;
|
||||
final List<LogisticObject> recentObjects;
|
||||
final int totalCount;
|
||||
|
||||
const ObjectStatistics({
|
||||
required this.byState,
|
||||
required this.byType,
|
||||
required this.byLocation,
|
||||
required this.recentObjects,
|
||||
required this.totalCount,
|
||||
});
|
||||
}
|
||||
424
app/lib/main.dart
Normal file
424
app/lib/main.dart
Normal file
@@ -0,0 +1,424 @@
|
||||
import 'package:dartz/dartz.dart' hide State;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'core/theme/app_theme.dart';
|
||||
import 'domain/entities/tour.dart';
|
||||
import 'domain/entities/logistic_object.dart';
|
||||
import 'domain/entities/location.dart';
|
||||
import 'domain/entities/counter.dart';
|
||||
import 'domain/repositories/tour_repository.dart';
|
||||
import 'core/errors/failures.dart';
|
||||
import 'presentation/blocs/tour/tour_bloc.dart';
|
||||
import 'presentation/blocs/scan/scan_bloc.dart';
|
||||
import 'presentation/pages/tours/tours_page.dart';
|
||||
import 'presentation/pages/tours/dashboard_page.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Set preferred orientations
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
]);
|
||||
|
||||
// Set system UI overlay style
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
systemNavigationBarColor: Colors.white,
|
||||
systemNavigationBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
|
||||
runApp(const HHAApp());
|
||||
}
|
||||
|
||||
class HHAApp extends StatelessWidget {
|
||||
const HHAApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final repository = MockTourRepository();
|
||||
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => TourBloc(
|
||||
repository: repository,
|
||||
)..add(const LoadTours()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => ScanBloc(
|
||||
repository: repository,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: MaterialApp(
|
||||
title: 'HHA Logistics',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.lightTheme,
|
||||
home: const MainNavigationPage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MainNavigationPage extends StatefulWidget {
|
||||
const MainNavigationPage({super.key});
|
||||
|
||||
@override
|
||||
State<MainNavigationPage> createState() => _MainNavigationPageState();
|
||||
}
|
||||
|
||||
class _MainNavigationPageState extends State<MainNavigationPage> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
final List<Widget> _pages = const [
|
||||
DashboardPage(),
|
||||
ToursPage(),
|
||||
InventoryPage(),
|
||||
SettingsPage(),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: IndexedStack(
|
||||
index: _currentIndex,
|
||||
children: _pages,
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: _currentIndex,
|
||||
onDestinationSelected: (index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
},
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.dashboard_outlined),
|
||||
selectedIcon: Icon(Icons.dashboard),
|
||||
label: 'Dashboard',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.map_outlined),
|
||||
selectedIcon: Icon(Icons.map),
|
||||
label: 'Touren',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.inventory_2_outlined),
|
||||
selectedIcon: Icon(Icons.inventory_2),
|
||||
label: 'Bestand',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
selectedIcon: Icon(Icons.settings),
|
||||
label: 'Einstellungen',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Mock Repository for demonstration
|
||||
class MockTourRepository implements TourRepository {
|
||||
@override
|
||||
Future<Either<Failure, List<Tour>>> getTours() async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
return const Right([
|
||||
Tour(
|
||||
id: 1,
|
||||
jobId: 1,
|
||||
tourId: 1,
|
||||
version: 1,
|
||||
state: 0,
|
||||
type: 'stock_start',
|
||||
sort: 1,
|
||||
locationId: 1,
|
||||
locationCode: 'LAGER001',
|
||||
locationName: 'Hauptlager Wandsbek',
|
||||
modified: 0,
|
||||
),
|
||||
Tour(
|
||||
id: 2,
|
||||
jobId: 1,
|
||||
tourId: 2,
|
||||
version: 1,
|
||||
state: 0,
|
||||
type: 'veh_start',
|
||||
sort: 2,
|
||||
locationId: 2,
|
||||
locationCode: 'DST001',
|
||||
locationName: 'Dienststelle Hammerbrook',
|
||||
modified: 0,
|
||||
),
|
||||
Tour(
|
||||
id: 3,
|
||||
jobId: 1,
|
||||
tourId: 3,
|
||||
version: 1,
|
||||
state: 0,
|
||||
type: 'st',
|
||||
sort: 3,
|
||||
locationId: 3,
|
||||
locationCode: 'HALT001',
|
||||
locationName: 'Hauptbahnhof Nord',
|
||||
remark: '4 Fahrscheinautomaten',
|
||||
modified: 0,
|
||||
),
|
||||
Tour(
|
||||
id: 4,
|
||||
jobId: 1,
|
||||
tourId: 4,
|
||||
version: 1,
|
||||
state: 1,
|
||||
type: 'st',
|
||||
sort: 4,
|
||||
locationId: 4,
|
||||
locationCode: 'HALT002',
|
||||
locationName: 'Jungfernstieg',
|
||||
remark: '2 Fahrscheinautomaten',
|
||||
modified: 0,
|
||||
),
|
||||
Tour(
|
||||
id: 5,
|
||||
jobId: 1,
|
||||
tourId: 5,
|
||||
version: 1,
|
||||
state: 0,
|
||||
type: 'gi',
|
||||
sort: 5,
|
||||
locationId: 5,
|
||||
locationCode: 'BANK001',
|
||||
locationName: 'Geldinstitut Mitte',
|
||||
modified: 0,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Tour>> getTourById(int tourId) async {
|
||||
final tours = await getTours();
|
||||
return tours.fold(
|
||||
(failure) => Left(failure),
|
||||
(tourList) {
|
||||
final tour = tourList.firstWhere(
|
||||
(t) => t.tourId == tourId,
|
||||
orElse: () => throw Exception('Tour not found'),
|
||||
);
|
||||
return Right(tour);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> updateTourState(int tourId, int state) async {
|
||||
return const Right(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> completeTour(int tourId) async {
|
||||
return const Right(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> syncData() async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
return const Right(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, bool>> checkForUpdates() async => const Right(false);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<LogisticObject>>> getObjectsByTour(int tourId) async => const Right([]);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<LogisticObject>>> getObjectsByState(String state) async => const Right([]);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, LogisticObject?>> getObjectByBarcode(String barcode) async => const Right(null);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> updateObjectState(
|
||||
int objectId,
|
||||
String newState, {
|
||||
int? locationId,
|
||||
int? refType,
|
||||
int? refId,
|
||||
String? containerCode,
|
||||
}) async => const Right(null);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> createObject({
|
||||
required int type,
|
||||
required String code,
|
||||
bool isManual = true,
|
||||
}) async => const Right(null);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<Location>>> getLocations() async => const Right([]);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Location>> getLocationById(int locationId) async {
|
||||
return const Left(NotFoundFailure(message: 'Location not found'));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<ObjectMetadata>>> getObjectMetadata() async => const Right([]);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, TourStatistics>> getTourStatistics(int tourId) async {
|
||||
return const Right(TourStatistics(
|
||||
totalObjects: 10,
|
||||
completedObjects: 5,
|
||||
pendingObjects: 5,
|
||||
objectsByState: {'delivery': 3, 'station': 2},
|
||||
objectsByType: {'meka': 3, 'beka': 2},
|
||||
completionPercentage: 50.0,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, ObjectStatistics>> getObjectStatistics() async {
|
||||
return const Right(ObjectStatistics(
|
||||
byState: {},
|
||||
byType: {},
|
||||
byLocation: {},
|
||||
recentObjects: [],
|
||||
totalCount: 0,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, CounterOverview>> getCounterOverview(
|
||||
int tourId,
|
||||
String tourType, {
|
||||
String? pageId,
|
||||
}) async {
|
||||
return Right(CounterOverview(
|
||||
tourId: tourId,
|
||||
pageId: pageId,
|
||||
groups: [],
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<PickupCount>>> getPickupCounts(
|
||||
int tourId,
|
||||
String pageId,
|
||||
) async => const Right([]);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<SwapCount>>> getSwapCounts(
|
||||
int tourId,
|
||||
String pageId,
|
||||
) async => const Right([]);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<ContainerInfo>>> getOpenContainers() async => const Right([]);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> addObjectToContainer(
|
||||
String containerId,
|
||||
String containerType,
|
||||
int objectId,
|
||||
) async => const Right(null);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> closeContainer(
|
||||
String containerId,
|
||||
String containerType,
|
||||
) async => const Right(null);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<RecentScan>>> getRecentScans(
|
||||
int tourId, {
|
||||
int limit = 10,
|
||||
}) async => const Right([]);
|
||||
}
|
||||
|
||||
// Placeholder pages
|
||||
class InventoryPage extends StatelessWidget {
|
||||
const InventoryPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Bestand'),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory_2,
|
||||
size: 80,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Bestandsübersicht',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Hier wird der aktuelle Bestand angezeigt',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsPage extends StatelessWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Einstellungen'),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.sync),
|
||||
title: const Text('Daten synchronisieren'),
|
||||
subtitle: const Text('Letzte Synchronisation: Gerade eben'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
// Trigger sync
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.visibility),
|
||||
title: const Text('Erledigte Stationen anzeigen'),
|
||||
trailing: Switch(
|
||||
value: true,
|
||||
onChanged: (value) {},
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info),
|
||||
title: const Text('Über'),
|
||||
subtitle: const Text('Version 2.0.0'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
916
app/lib/presentation/blocs/scan/scan_bloc.dart
Normal file
916
app/lib/presentation/blocs/scan/scan_bloc.dart
Normal file
@@ -0,0 +1,916 @@
|
||||
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<ScanEvent, ScanState> {
|
||||
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<InitializeScan>(_onInitializeScan);
|
||||
on<ProcessBarcode>(_onProcessBarcode);
|
||||
on<ValidateBarcode>(_onValidateBarcode);
|
||||
on<UpdateObjectState>(_onUpdateObjectState);
|
||||
on<ResetScan>(_onResetScan);
|
||||
on<CreateUnknownObject>(_onCreateUnknownObject);
|
||||
}
|
||||
|
||||
Future<void> _onInitializeScan(InitializeScan event, Emitter<ScanState> emit) async {
|
||||
currentTour = event.tour;
|
||||
containerId = null;
|
||||
containerType = null;
|
||||
emit(ScanReady(tour: event.tour));
|
||||
}
|
||||
|
||||
Future<void> _onProcessBarcode(ProcessBarcode event, Emitter<ScanState> 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<ScanState> 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<ScanState> 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<ScanState> 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<ScanState> 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<ScanState> 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<ScanState> 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<ScanState> 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<ScanState> 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<ScanState> 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<ScanState> 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<ScanState> 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<ScanState> 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<ScanState> 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<ScanState> 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<ScanState> 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<ScanState> 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<ScanState> 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<ScanState> 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<ScanState> 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<ScanState> 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<ScanState> 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<void> _onValidateBarcode(ValidateBarcode event, Emitter<ScanState> 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<void> _onUpdateObjectState(UpdateObjectState event, Emitter<ScanState> 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<void> _onCreateUnknownObject(CreateUnknownObject event, Emitter<ScanState> 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<ScanState> emit) {
|
||||
if (currentTour != null) {
|
||||
emit(ScanReady(tour: currentTour!));
|
||||
} else {
|
||||
emit(ScanInitial());
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper methods
|
||||
// ============================================================================
|
||||
Map<String, String>? _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}',
|
||||
};
|
||||
}
|
||||
}
|
||||
69
app/lib/presentation/blocs/scan/scan_event.dart
Normal file
69
app/lib/presentation/blocs/scan/scan_event.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
part of 'scan_bloc.dart';
|
||||
|
||||
abstract class ScanEvent extends Equatable {
|
||||
const ScanEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class InitializeScan extends ScanEvent {
|
||||
final Tour tour;
|
||||
|
||||
const InitializeScan(this.tour);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [tour];
|
||||
}
|
||||
|
||||
class ProcessBarcode extends ScanEvent {
|
||||
final String barcode;
|
||||
|
||||
const ProcessBarcode({required this.barcode});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [barcode];
|
||||
}
|
||||
|
||||
class ValidateBarcode extends ScanEvent {
|
||||
final String barcode;
|
||||
|
||||
const ValidateBarcode({required this.barcode});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [barcode];
|
||||
}
|
||||
|
||||
class UpdateObjectState extends ScanEvent {
|
||||
final LogisticObject object;
|
||||
final String newState;
|
||||
final int? locationId;
|
||||
final String? containerCode;
|
||||
|
||||
const UpdateObjectState({
|
||||
required this.object,
|
||||
required this.newState,
|
||||
this.locationId,
|
||||
this.containerCode,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [object, newState, locationId, containerCode];
|
||||
}
|
||||
|
||||
class ResetScan extends ScanEvent {
|
||||
const ResetScan();
|
||||
}
|
||||
|
||||
class CreateUnknownObject extends ScanEvent {
|
||||
final String barcode;
|
||||
final int type;
|
||||
|
||||
const CreateUnknownObject({
|
||||
required this.barcode,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [barcode, type];
|
||||
}
|
||||
195
app/lib/presentation/blocs/scan/scan_state.dart
Normal file
195
app/lib/presentation/blocs/scan/scan_state.dart
Normal file
@@ -0,0 +1,195 @@
|
||||
part of 'scan_bloc.dart';
|
||||
|
||||
abstract class ScanState extends Equatable {
|
||||
const ScanState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class ScanInitial extends ScanState {}
|
||||
|
||||
class ScanReady extends ScanState {
|
||||
final Tour tour;
|
||||
|
||||
const ScanReady({required this.tour});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [tour];
|
||||
}
|
||||
|
||||
class ScanProcessing extends ScanState {
|
||||
final String barcode;
|
||||
|
||||
const ScanProcessing({required this.barcode});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [barcode];
|
||||
}
|
||||
|
||||
class ScanObjectDetected extends ScanState {
|
||||
final LogisticObject object;
|
||||
final String suggestedState;
|
||||
final Tour? tour;
|
||||
final String? originBarcode;
|
||||
|
||||
const ScanObjectDetected({
|
||||
required this.object,
|
||||
required this.suggestedState,
|
||||
this.tour,
|
||||
this.originBarcode,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [object, suggestedState, tour, originBarcode];
|
||||
}
|
||||
|
||||
// Special state for Fehlkassette (error cassette) detection
|
||||
class ScanFehlKassetteDetected extends ScanState {
|
||||
final LogisticObject object;
|
||||
final String suggestedState;
|
||||
final Tour? tour;
|
||||
|
||||
const ScanFehlKassetteDetected({
|
||||
required this.object,
|
||||
required this.suggestedState,
|
||||
this.tour,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [object, suggestedState, tour];
|
||||
}
|
||||
|
||||
// Special state for container object detection (VS state machine)
|
||||
class ScanContainerObjectDetected extends ScanState {
|
||||
final LogisticObject object;
|
||||
final String suggestedState;
|
||||
final Tour? tour;
|
||||
final String containerId;
|
||||
final String containerType;
|
||||
|
||||
const ScanContainerObjectDetected({
|
||||
required this.object,
|
||||
required this.suggestedState,
|
||||
this.tour,
|
||||
required this.containerId,
|
||||
required this.containerType,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [object, suggestedState, tour, containerId, containerType];
|
||||
}
|
||||
|
||||
// Special state for container detection
|
||||
class ScanContainerDetected extends ScanState {
|
||||
final LogisticObject object;
|
||||
final String suggestedState;
|
||||
final Tour? tour;
|
||||
final String containerId;
|
||||
final String containerType;
|
||||
|
||||
const ScanContainerDetected({
|
||||
required this.object,
|
||||
required this.suggestedState,
|
||||
this.tour,
|
||||
required this.containerId,
|
||||
required this.containerType,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [object, suggestedState, tour, containerId, containerType];
|
||||
}
|
||||
|
||||
// Special state for container close detection (vehVs state machine)
|
||||
class ScanContainerCloseDetected extends ScanState {
|
||||
final LogisticObject object;
|
||||
final String suggestedState;
|
||||
final String targetStateForObjects;
|
||||
final Tour? tour;
|
||||
final String containerType;
|
||||
|
||||
const ScanContainerCloseDetected({
|
||||
required this.object,
|
||||
required this.suggestedState,
|
||||
required this.targetStateForObjects,
|
||||
this.tour,
|
||||
required this.containerType,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [object, suggestedState, targetStateForObjects, tour, containerType];
|
||||
}
|
||||
|
||||
class ScanUnknownObject extends ScanState {
|
||||
final String barcode;
|
||||
final String prefix;
|
||||
final Tour? tour;
|
||||
|
||||
const ScanUnknownObject({
|
||||
required this.barcode,
|
||||
required this.prefix,
|
||||
this.tour,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [barcode, prefix, tour];
|
||||
}
|
||||
|
||||
class ScanPageDetected extends ScanState {
|
||||
final String pageId;
|
||||
final String label;
|
||||
final Tour tour;
|
||||
|
||||
const ScanPageDetected({
|
||||
required this.pageId,
|
||||
required this.label,
|
||||
required this.tour,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [pageId, label, tour];
|
||||
}
|
||||
|
||||
class ScanObjectUpdated extends ScanState {
|
||||
final LogisticObject object;
|
||||
final String previousState;
|
||||
final String newState;
|
||||
final Tour? tour;
|
||||
|
||||
const ScanObjectUpdated({
|
||||
required this.object,
|
||||
required this.previousState,
|
||||
required this.newState,
|
||||
this.tour,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [object, previousState, newState, tour];
|
||||
}
|
||||
|
||||
class ScanObjectCreated extends ScanState {
|
||||
final String barcode;
|
||||
|
||||
const ScanObjectCreated({required this.barcode});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [barcode];
|
||||
}
|
||||
|
||||
class ScanError extends ScanState {
|
||||
final String message;
|
||||
|
||||
const ScanError({required this.message});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
|
||||
class ScanValidationError extends ScanState {
|
||||
final String message;
|
||||
|
||||
const ScanValidationError({required this.message});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
124
app/lib/presentation/blocs/tour/tour_bloc.dart
Normal file
124
app/lib/presentation/blocs/tour/tour_bloc.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../../domain/entities/tour.dart';
|
||||
import '../../../domain/entities/logistic_object.dart';
|
||||
import '../../../domain/repositories/tour_repository.dart';
|
||||
import '../../../core/errors/failures.dart';
|
||||
|
||||
part 'tour_event.dart';
|
||||
part 'tour_state.dart';
|
||||
|
||||
class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
final TourRepository repository;
|
||||
|
||||
TourBloc({required this.repository}) : super(TourInitial()) {
|
||||
on<LoadTours>(_onLoadTours);
|
||||
on<SelectTour>(_onSelectTour);
|
||||
on<CompleteTour>(_onCompleteTour);
|
||||
on<SyncData>(_onSyncData);
|
||||
on<RefreshTours>(_onRefreshTours);
|
||||
on<LoadTourDetails>(_onLoadTourDetails);
|
||||
}
|
||||
|
||||
Future<void> _onLoadTours(LoadTours event, Emitter<TourState> emit) async {
|
||||
emit(TourLoading());
|
||||
|
||||
final result = await repository.getTours();
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(TourError(message: _mapFailureToMessage(failure))),
|
||||
(tours) {
|
||||
final openTours = tours.where((t) => t.state < 2).toList();
|
||||
final completedTours = tours.where((t) => t.state == 1).toList();
|
||||
|
||||
emit(ToursLoaded(
|
||||
tours: openTours,
|
||||
completedTours: completedTours,
|
||||
allTours: tours,
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSelectTour(SelectTour event, Emitter<TourState> emit) async {
|
||||
final currentState = state;
|
||||
if (currentState is ToursLoaded) {
|
||||
emit(currentState.copyWith(selectedTour: event.tour));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onCompleteTour(CompleteTour event, Emitter<TourState> emit) async {
|
||||
final currentState = state;
|
||||
if (currentState is ToursLoaded) {
|
||||
emit(TourLoading());
|
||||
|
||||
final result = await repository.completeTour(event.tourId);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(TourError(message: _mapFailureToMessage(failure))),
|
||||
(_) => add(const LoadTours()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSyncData(SyncData event, Emitter<TourState> emit) async {
|
||||
final currentState = state;
|
||||
if (currentState is ToursLoaded) {
|
||||
emit(currentState.copyWith(isSyncing: true));
|
||||
|
||||
final result = await repository.syncData();
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
emit(currentState.copyWith(isSyncing: false));
|
||||
emit(SyncError(message: _mapFailureToMessage(failure)));
|
||||
},
|
||||
(_) {
|
||||
emit(currentState.copyWith(isSyncing: false));
|
||||
add(const LoadTours());
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onRefreshTours(RefreshTours event, Emitter<TourState> emit) async {
|
||||
add(const LoadTours());
|
||||
}
|
||||
|
||||
Future<void> _onLoadTourDetails(LoadTourDetails event, Emitter<TourState> emit) async {
|
||||
final currentState = state;
|
||||
if (currentState is ToursLoaded) {
|
||||
emit(currentState.copyWith(isLoadingDetails: true));
|
||||
|
||||
final objectsResult = await repository.getObjectsByTour(event.tourId);
|
||||
final statsResult = await repository.getTourStatistics(event.tourId);
|
||||
|
||||
objectsResult.fold(
|
||||
(failure) => emit(TourError(message: _mapFailureToMessage(failure))),
|
||||
(objects) {
|
||||
statsResult.fold(
|
||||
(failure) => emit(TourError(message: _mapFailureToMessage(failure))),
|
||||
(statistics) {
|
||||
emit(currentState.copyWith(
|
||||
selectedTourObjects: objects,
|
||||
selectedTourStats: statistics,
|
||||
isLoadingDetails: false,
|
||||
));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _mapFailureToMessage(Failure failure) {
|
||||
return switch (failure) {
|
||||
ServerFailure _ => 'Serverfehler: ${failure.message}',
|
||||
NetworkFailure _ => 'Netzwerkfehler. Bitte überprüfen Sie Ihre Internetverbindung.',
|
||||
CacheFailure _ => 'Cachefehler: ${failure.message}',
|
||||
NotFoundFailure _ => 'Daten nicht gefunden',
|
||||
UnauthorizedFailure _ => 'Nicht autorisiert. Bitte melden Sie sich erneut an.',
|
||||
_ => 'Ein unerwarteter Fehler ist aufgetreten',
|
||||
};
|
||||
}
|
||||
}
|
||||
56
app/lib/presentation/blocs/tour/tour_event.dart
Normal file
56
app/lib/presentation/blocs/tour/tour_event.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
part of 'tour_bloc.dart';
|
||||
|
||||
abstract class TourEvent extends Equatable {
|
||||
const TourEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class LoadTours extends TourEvent {
|
||||
const LoadTours();
|
||||
}
|
||||
|
||||
class SelectTour extends TourEvent {
|
||||
final Tour tour;
|
||||
|
||||
const SelectTour(this.tour);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [tour];
|
||||
}
|
||||
|
||||
class CompleteTour extends TourEvent {
|
||||
final int tourId;
|
||||
|
||||
const CompleteTour(this.tourId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [tourId];
|
||||
}
|
||||
|
||||
class SyncData extends TourEvent {
|
||||
const SyncData();
|
||||
}
|
||||
|
||||
class RefreshTours extends TourEvent {
|
||||
const RefreshTours();
|
||||
}
|
||||
|
||||
class LoadTourDetails extends TourEvent {
|
||||
final int tourId;
|
||||
|
||||
const LoadTourDetails(this.tourId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [tourId];
|
||||
}
|
||||
|
||||
class UpdateShowCompleted extends TourEvent {
|
||||
final bool showCompleted;
|
||||
|
||||
const UpdateShowCompleted(this.showCompleted);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [showCompleted];
|
||||
}
|
||||
95
app/lib/presentation/blocs/tour/tour_state.dart
Normal file
95
app/lib/presentation/blocs/tour/tour_state.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
part of 'tour_bloc.dart';
|
||||
|
||||
abstract class TourState extends Equatable {
|
||||
const TourState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class TourInitial extends TourState {}
|
||||
|
||||
class TourLoading extends TourState {}
|
||||
|
||||
class ToursLoaded extends TourState {
|
||||
final List<Tour> tours;
|
||||
final List<Tour> completedTours;
|
||||
final List<Tour> allTours;
|
||||
final Tour? selectedTour;
|
||||
final List<LogisticObject>? selectedTourObjects;
|
||||
final TourStatistics? selectedTourStats;
|
||||
final bool isSyncing;
|
||||
final bool isLoadingDetails;
|
||||
final bool showCompleted;
|
||||
|
||||
const ToursLoaded({
|
||||
required this.tours,
|
||||
this.completedTours = const [],
|
||||
required this.allTours,
|
||||
this.selectedTour,
|
||||
this.selectedTourObjects,
|
||||
this.selectedTourStats,
|
||||
this.isSyncing = false,
|
||||
this.isLoadingDetails = false,
|
||||
this.showCompleted = true,
|
||||
});
|
||||
|
||||
ToursLoaded copyWith({
|
||||
List<Tour>? tours,
|
||||
List<Tour>? completedTours,
|
||||
List<Tour>? allTours,
|
||||
Tour? selectedTour,
|
||||
List<LogisticObject>? selectedTourObjects,
|
||||
TourStatistics? selectedTourStats,
|
||||
bool? isSyncing,
|
||||
bool? isLoadingDetails,
|
||||
bool? showCompleted,
|
||||
}) {
|
||||
return ToursLoaded(
|
||||
tours: tours ?? this.tours,
|
||||
completedTours: completedTours ?? this.completedTours,
|
||||
allTours: allTours ?? this.allTours,
|
||||
selectedTour: selectedTour ?? this.selectedTour,
|
||||
selectedTourObjects: selectedTourObjects ?? this.selectedTourObjects,
|
||||
selectedTourStats: selectedTourStats ?? this.selectedTourStats,
|
||||
isSyncing: isSyncing ?? this.isSyncing,
|
||||
isLoadingDetails: isLoadingDetails ?? this.isLoadingDetails,
|
||||
showCompleted: showCompleted ?? this.showCompleted,
|
||||
);
|
||||
}
|
||||
|
||||
int get completedCount => completedTours.length;
|
||||
int get totalCount => allTours.length;
|
||||
double get completionPercentage => totalCount > 0 ? (completedCount / totalCount) * 100 : 0;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
tours,
|
||||
completedTours,
|
||||
allTours,
|
||||
selectedTour,
|
||||
selectedTourObjects,
|
||||
selectedTourStats,
|
||||
isSyncing,
|
||||
isLoadingDetails,
|
||||
showCompleted,
|
||||
];
|
||||
}
|
||||
|
||||
class TourError extends TourState {
|
||||
final String message;
|
||||
|
||||
const TourError({required this.message});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
|
||||
class SyncError extends TourState {
|
||||
final String message;
|
||||
|
||||
const SyncError({required this.message});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
626
app/lib/presentation/pages/scan/scan_page.dart
Normal file
626
app/lib/presentation/pages/scan/scan_page.dart
Normal file
@@ -0,0 +1,626 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
import '../../../domain/entities/tour.dart';
|
||||
import '../../blocs/scan/scan_bloc.dart';
|
||||
import 'scan_result_sheet.dart';
|
||||
|
||||
class ScanPage extends StatefulWidget {
|
||||
final Tour tour;
|
||||
|
||||
const ScanPage({
|
||||
super.key,
|
||||
required this.tour,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ScanPage> createState() => _ScanPageState();
|
||||
}
|
||||
|
||||
class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin {
|
||||
late MobileScannerController controller;
|
||||
bool isFlashOn = false;
|
||||
bool isManualEntry = false;
|
||||
final TextEditingController barcodeController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = MobileScannerController();
|
||||
|
||||
// Initialize scan bloc with tour
|
||||
context.read<ScanBloc>().add(InitializeScan(widget.tour));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
barcodeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.black.withValues(alpha: 128),
|
||||
elevation: 0,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.tour.locationName ?? 'Station',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_getTypeLabel(widget.tour.type),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
isFlashOn = !isFlashOn;
|
||||
controller.toggleTorch();
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
isFlashOn
|
||||
? Icons.flashlight_on
|
||||
: Icons.flashlight_off,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
controller.switchCamera();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.flip_camera_android,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: BlocListener<ScanBloc, ScanState>(
|
||||
listener: (context, state) {
|
||||
if (state is ScanObjectDetected) {
|
||||
_showScanResult(context, state);
|
||||
} else if (state is ScanFehlKassetteDetected) {
|
||||
_showFehlKassetteDialog(context, state);
|
||||
} else if (state is ScanContainerObjectDetected) {
|
||||
_showContainerObjectResult(context, state);
|
||||
} else if (state is ScanContainerDetected) {
|
||||
_showContainerDetectedSnackBar(context, state);
|
||||
} else if (state is ScanContainerCloseDetected) {
|
||||
_showContainerCloseDialog(context, state);
|
||||
} else if (state is ScanUnknownObject) {
|
||||
_showUnknownObjectDialog(context, state);
|
||||
} else if (state is ScanPageDetected) {
|
||||
_showPageDetectedSnackBar(context, state);
|
||||
} else if (state is ScanObjectUpdated) {
|
||||
_showSuccessSnackBar(context, state);
|
||||
} else if (state is ScanObjectCreated) {
|
||||
_showObjectCreatedSnackBar(context, state);
|
||||
} else if (state is ScanError) {
|
||||
_showErrorSnackBar(context, state.message);
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Camera Preview
|
||||
MobileScanner(
|
||||
controller: controller,
|
||||
onDetect: (capture) {
|
||||
final barcodes = capture.barcodes;
|
||||
if (barcodes.isNotEmpty && barcodes.first.rawValue != null) {
|
||||
HapticFeedback.mediumImpact();
|
||||
context.read<ScanBloc>().add(
|
||||
ProcessBarcode(barcode: barcodes.first.rawValue!),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
// Scan Overlay
|
||||
CustomPaint(
|
||||
size: Size.infinite,
|
||||
painter: ScanOverlayPainter(),
|
||||
),
|
||||
|
||||
// Bottom Controls
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Colors.black.withValues(alpha: 230),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Barcode in den Rahmen halten',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Manual Entry Button
|
||||
ElevatedButton.icon(
|
||||
onPressed: _showManualEntryDialog,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.keyboard),
|
||||
label: const Text('Manuelle Eingabe'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Loading Overlay
|
||||
BlocBuilder<ScanBloc, ScanState>(
|
||||
builder: (context, state) {
|
||||
if (state is ScanProcessing) {
|
||||
return Container(
|
||||
color: Colors.black54,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showScanResult(BuildContext context, ScanObjectDetected state) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => ScanResultSheet(
|
||||
object: state.object,
|
||||
suggestedState: state.suggestedState,
|
||||
tour: state.tour,
|
||||
onConfirm: () {
|
||||
context.read<ScanBloc>().add(UpdateObjectState(
|
||||
object: state.object,
|
||||
newState: state.suggestedState,
|
||||
));
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onCancel: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showUnknownObjectDialog(BuildContext context, ScanUnknownObject state) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Unbekanntes Objekt'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Barcode: ${state.barcode}'),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Dieses Objekt ist nicht im System vorhanden. '
|
||||
'Möchten Sie es neu anlegen?',
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: Navigate to create object page
|
||||
},
|
||||
child: const Text('Anlegen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPageDetectedSnackBar(BuildContext context, ScanPageDetected state) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Seite erkannt: ${state.label}'),
|
||||
backgroundColor: Colors.green,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
action: SnackBarAction(
|
||||
label: 'OK',
|
||||
textColor: Colors.white,
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSuccessSnackBar(BuildContext context, ScanObjectUpdated state) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Status aktualisiert: ${state.object.code}'),
|
||||
backgroundColor: Colors.green,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showFehlKassetteDialog(BuildContext context, ScanFehlKassetteDetected state) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Fehlkassette'),
|
||||
content: Text(
|
||||
'Die Kassette ${state.object.code} wird als Fehlkassette markiert. '
|
||||
'Der Status wird auf "Fehler - zur Dienststelle" geändert.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<ScanBloc>().add(UpdateObjectState(
|
||||
object: state.object,
|
||||
newState: state.suggestedState,
|
||||
));
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Bestätigen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showContainerObjectResult(BuildContext context, ScanContainerObjectDetected state) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => ScanResultSheet(
|
||||
object: state.object,
|
||||
suggestedState: state.suggestedState,
|
||||
tour: state.tour,
|
||||
containerInfo: 'Container: ${state.containerId} (${state.containerType == 'a' ? 'Geldinstitut' : 'Dienststelle'})',
|
||||
onConfirm: () {
|
||||
context.read<ScanBloc>().add(UpdateObjectState(
|
||||
object: state.object,
|
||||
newState: state.suggestedState,
|
||||
containerCode: state.containerId,
|
||||
));
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onCancel: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showContainerDetectedSnackBar(BuildContext context, ScanContainerDetected state) {
|
||||
final containerTypeLabel = state.containerType == 'a' ? 'Geldinstitut' : 'Dienststelle';
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Container erkannt: ${state.containerId} ($containerTypeLabel)'),
|
||||
backgroundColor: Colors.blue,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 3),
|
||||
action: SnackBarAction(
|
||||
label: 'OK',
|
||||
textColor: Colors.white,
|
||||
onPressed: () {
|
||||
context.read<ScanBloc>().add(UpdateObjectState(
|
||||
object: state.object,
|
||||
newState: state.suggestedState,
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showContainerCloseDialog(BuildContext context, ScanContainerCloseDetected state) {
|
||||
final containerTypeLabel = state.containerType == 'a' ? 'Geldinstitut' : 'Dienststelle';
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Container schließen'),
|
||||
content: Text(
|
||||
'Container ${state.object.code} ($containerTypeLabel) wird geschlossen. '
|
||||
'Alle enthaltenen Objekte werden auf den entsprechenden Status aktualisiert.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<ScanBloc>().add(UpdateObjectState(
|
||||
object: state.object,
|
||||
newState: state.suggestedState,
|
||||
));
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Schließen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showObjectCreatedSnackBar(BuildContext context, ScanObjectCreated state) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Objekt erstellt: ${state.barcode}'),
|
||||
backgroundColor: Colors.green,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showErrorSnackBar(BuildContext context, String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.red,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showManualEntryDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Barcode manuell eingeben'),
|
||||
content: TextField(
|
||||
controller: barcodeController,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Barcode eingeben',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
textCapitalization: TextCapitalization.characters,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
barcodeController.clear();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (barcodeController.text.isNotEmpty) {
|
||||
context.read<ScanBloc>().add(
|
||||
ProcessBarcode(barcode: barcodeController.text),
|
||||
);
|
||||
barcodeController.clear();
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: const Text('Suchen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getTypeLabel(String type) {
|
||||
switch (type) {
|
||||
case 'stock_start':
|
||||
return 'Lager - Beladung';
|
||||
case 'stock_end':
|
||||
return 'Lager - Rückgabe';
|
||||
case 'start':
|
||||
return 'Dienststelle';
|
||||
case 'st':
|
||||
return 'Haltestelle';
|
||||
case 'hls':
|
||||
return 'Hochbahnstation';
|
||||
case 'fsa':
|
||||
return 'Fahrscheinautomat';
|
||||
case 'vs':
|
||||
return 'Versorgungsstelle';
|
||||
case 'gi':
|
||||
return 'Geldinstitut';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ScanOverlayPainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = Colors.black.withValues(alpha: 128)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final scanAreaSize = size.width * 0.7;
|
||||
final scanAreaLeft = (size.width - scanAreaSize) / 2;
|
||||
final scanAreaTop = (size.height - scanAreaSize) / 2;
|
||||
|
||||
// Draw dark overlay
|
||||
final path = Path()
|
||||
..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
|
||||
|
||||
final cutout = Path()
|
||||
..addRRect(RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(scanAreaLeft, scanAreaTop, scanAreaSize, scanAreaSize),
|
||||
const Radius.circular(20),
|
||||
));
|
||||
|
||||
final overlayPath = Path.combine(
|
||||
PathOperation.difference,
|
||||
path,
|
||||
cutout,
|
||||
);
|
||||
|
||||
canvas.drawPath(overlayPath, paint);
|
||||
|
||||
// Draw corner markers
|
||||
final markerPaint = Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 4;
|
||||
|
||||
final cornerLength = scanAreaSize * 0.15;
|
||||
const cornerRadius = 20.0;
|
||||
|
||||
// Top-left corner
|
||||
_drawCorner(
|
||||
canvas,
|
||||
Offset(scanAreaLeft, scanAreaTop),
|
||||
cornerLength,
|
||||
markerPaint,
|
||||
true,
|
||||
true,
|
||||
cornerRadius,
|
||||
);
|
||||
|
||||
// Top-right corner
|
||||
_drawCorner(
|
||||
canvas,
|
||||
Offset(scanAreaLeft + scanAreaSize, scanAreaTop),
|
||||
cornerLength,
|
||||
markerPaint,
|
||||
false,
|
||||
true,
|
||||
cornerRadius,
|
||||
);
|
||||
|
||||
// Bottom-left corner
|
||||
_drawCorner(
|
||||
canvas,
|
||||
Offset(scanAreaLeft, scanAreaTop + scanAreaSize),
|
||||
cornerLength,
|
||||
markerPaint,
|
||||
true,
|
||||
false,
|
||||
cornerRadius,
|
||||
);
|
||||
|
||||
// Bottom-right corner
|
||||
_drawCorner(
|
||||
canvas,
|
||||
Offset(scanAreaLeft + scanAreaSize, scanAreaTop + scanAreaSize),
|
||||
cornerLength,
|
||||
markerPaint,
|
||||
false,
|
||||
false,
|
||||
cornerRadius,
|
||||
);
|
||||
}
|
||||
|
||||
void _drawCorner(
|
||||
Canvas canvas,
|
||||
Offset position,
|
||||
double length,
|
||||
Paint paint,
|
||||
bool isLeft,
|
||||
bool isTop,
|
||||
double radius,
|
||||
) {
|
||||
final path = Path();
|
||||
|
||||
if (isLeft && isTop) {
|
||||
path.moveTo(position.dx + length, position.dy);
|
||||
path.lineTo(position.dx + radius, position.dy);
|
||||
path.arcToPoint(
|
||||
Offset(position.dx, position.dy + radius),
|
||||
radius: Radius.circular(radius),
|
||||
clockwise: false,
|
||||
);
|
||||
path.lineTo(position.dx, position.dy + length);
|
||||
} else if (!isLeft && isTop) {
|
||||
path.moveTo(position.dx - length, position.dy);
|
||||
path.lineTo(position.dx - radius, position.dy);
|
||||
path.arcToPoint(
|
||||
Offset(position.dx, position.dy + radius),
|
||||
radius: Radius.circular(radius),
|
||||
);
|
||||
path.lineTo(position.dx, position.dy + length);
|
||||
} else if (isLeft && !isTop) {
|
||||
path.moveTo(position.dx + length, position.dy);
|
||||
path.lineTo(position.dx + radius, position.dy);
|
||||
path.arcToPoint(
|
||||
Offset(position.dx, position.dy - radius),
|
||||
radius: Radius.circular(radius),
|
||||
);
|
||||
path.lineTo(position.dx, position.dy - length);
|
||||
} else {
|
||||
path.moveTo(position.dx - length, position.dy);
|
||||
path.lineTo(position.dx - radius, position.dy);
|
||||
path.arcToPoint(
|
||||
Offset(position.dx, position.dy - radius),
|
||||
radius: Radius.circular(radius),
|
||||
clockwise: false,
|
||||
);
|
||||
path.lineTo(position.dx, position.dy - length);
|
||||
}
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
317
app/lib/presentation/pages/scan/scan_result_sheet.dart
Normal file
317
app/lib/presentation/pages/scan/scan_result_sheet.dart
Normal file
@@ -0,0 +1,317 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import '../../../domain/entities/logistic_object.dart';
|
||||
import '../../../domain/entities/tour.dart';
|
||||
|
||||
class ScanResultSheet extends StatelessWidget {
|
||||
final LogisticObject object;
|
||||
final String suggestedState;
|
||||
final Tour? tour;
|
||||
final String? containerInfo;
|
||||
final VoidCallback onConfirm;
|
||||
final VoidCallback onCancel;
|
||||
|
||||
const ScanResultSheet({
|
||||
super.key,
|
||||
required this.object,
|
||||
required this.suggestedState,
|
||||
this.tour,
|
||||
this.containerInfo,
|
||||
required this.onConfirm,
|
||||
required this.onCancel,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final currentStateColor = ObjectStateInfo.getColorForState(object.state);
|
||||
final suggestedStateColor = ObjectStateInfo.getColorForState(suggestedState);
|
||||
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Handle
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Success Icon
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.green.shade400,
|
||||
Colors.green.shade600,
|
||||
],
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.green.withValues(alpha: 77),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.check,
|
||||
color: Colors.white,
|
||||
size: 40,
|
||||
),
|
||||
).animate().scale(duration: 300.ms, curve: Curves.elasticOut),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Title
|
||||
Text(
|
||||
'Objekt gefunden',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Object Info Card
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
color: Colors.grey.shade50,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildInfoRow(
|
||||
context,
|
||||
'Code',
|
||||
object.code,
|
||||
Icons.qr_code,
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
'Typ',
|
||||
object.typeName ?? object.subtype.toUpperCase(),
|
||||
Icons.inventory_2,
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildStateRow(
|
||||
context,
|
||||
'Aktueller Status',
|
||||
object.state,
|
||||
currentStateColor,
|
||||
),
|
||||
if (containerInfo != null) ...[
|
||||
const Divider(height: 24),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
'Container',
|
||||
containerInfo!,
|
||||
Icons.inventory_2,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// State Transition
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 24),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
suggestedStateColor.withValues(alpha: 26),
|
||||
suggestedStateColor.withValues(alpha: 13),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: suggestedStateColor.withValues(alpha: 77)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Status wird geändert zu:',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: suggestedStateColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
ObjectStateInfo.getDisplayName(suggestedState),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: suggestedStateColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Action Buttons
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: onCancel,
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ElevatedButton(
|
||||
onPressed: onConfirm,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
child: const Text('Bestätigen'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(BuildContext context, String label, String value, IconData icon) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary.withValues(alpha: 26),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStateRow(BuildContext context, String label, String state, Color color) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 26),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.info,
|
||||
size: 20,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 26),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
ObjectStateInfo.getDisplayName(state),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
252
app/lib/presentation/pages/tour_types/fsa_page.dart
Normal file
252
app/lib/presentation/pages/tour_types/fsa_page.dart
Normal file
@@ -0,0 +1,252 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../domain/entities/tour.dart';
|
||||
import '../../blocs/scan/scan_bloc.dart';
|
||||
import '../scan/scan_page.dart';
|
||||
|
||||
/// FSA Page - Fahrscheinautomat
|
||||
/// Entspricht Lua: ShowFsaScreen + CreateLoadingFsaView
|
||||
class FsaPage extends StatefulWidget {
|
||||
final Tour tour;
|
||||
|
||||
const FsaPage({
|
||||
super.key,
|
||||
required this.tour,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FsaPage> createState() => _FsaPageState();
|
||||
}
|
||||
|
||||
class _FsaPageState extends State<FsaPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<ScanBloc>().add(InitializeScan(widget.tour));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.tour.locationName ?? 'Fahrscheinautomat'),
|
||||
const Text(
|
||||
'Objekt-Einbuchung',
|
||||
style: TextStyle(fontSize: 14, color: Colors.white70),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
onPressed: () => _openScanner(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Header
|
||||
_buildHeader(context),
|
||||
|
||||
// FSA-spezifische Info
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildFsaInfo(context),
|
||||
_buildObjectTypes(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Scan Button
|
||||
_buildScanButton(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: const Color(0xFFA4D4F0),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.confirmation_number,
|
||||
size: 48,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Fahrscheinautomat',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.tour.locationName ?? 'FSA',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
if (widget.tour.remark != null)
|
||||
Text(
|
||||
widget.tour.remark!,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFsaInfo(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.blue.shade200),
|
||||
),
|
||||
child: const Column(
|
||||
children: [
|
||||
Text(
|
||||
'Gültige Objekte:',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'• GK (Geldkassette): station → in_fa → ret_fail\n'
|
||||
'• HP (Hauptkasse): station ↔ in_fa (Wechsel)\n'
|
||||
'• FR (Fahrkartenrolle): station ↔ in_fa',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildObjectTypes(BuildContext context) {
|
||||
final objectTypes = [
|
||||
_ObjectTypeInfo('Geldkassette (GK)', 'MEK, BEK', Icons.money, Colors.green),
|
||||
_ObjectTypeInfo('Hauptkasse (HP)', 'H1, H2, H3', Icons.print, Colors.blue),
|
||||
_ObjectTypeInfo('Fahrkartenrolle (FR)', 'P', Icons.receipt, Colors.orange),
|
||||
];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Objekttypen',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...objectTypes.map((type) => _buildObjectTypeCard(context, type)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildObjectTypeCard(BuildContext context, _ObjectTypeInfo type) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: type.color.withValues(alpha: 26),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(type.icon, color: type.color),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
type.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
type.subtypes,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScanButton(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _openScanner(context),
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
label: const Text('Barcode scannen'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 56),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openScanner(BuildContext context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ScanPage(tour: widget.tour),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ObjectTypeInfo {
|
||||
final String name;
|
||||
final String subtypes;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
|
||||
_ObjectTypeInfo(this.name, this.subtypes, this.icon, this.color);
|
||||
}
|
||||
264
app/lib/presentation/pages/tour_types/gi_page.dart
Normal file
264
app/lib/presentation/pages/tour_types/gi_page.dart
Normal file
@@ -0,0 +1,264 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../domain/entities/tour.dart';
|
||||
import '../../blocs/scan/scan_bloc.dart';
|
||||
import '../scan/scan_page.dart';
|
||||
|
||||
/// GI Page - Geldinstitut
|
||||
/// Entspricht Lua: ShowGiScreen + CreateLoadingGiView
|
||||
class GiPage extends StatefulWidget {
|
||||
final Tour tour;
|
||||
|
||||
const GiPage({
|
||||
super.key,
|
||||
required this.tour,
|
||||
});
|
||||
|
||||
@override
|
||||
State<GiPage> createState() => _GiPageState();
|
||||
}
|
||||
|
||||
class _GiPageState extends State<GiPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<ScanBloc>().add(InitializeScan(widget.tour));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.tour.locationName ?? 'Geldinstitut'),
|
||||
const Text(
|
||||
'Übergabe',
|
||||
style: TextStyle(fontSize: 14, color: Colors.white70),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
onPressed: () => _openScanner(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Header
|
||||
_buildHeader(context),
|
||||
|
||||
// GI-spezifische Info
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildGiInfo(context),
|
||||
_buildExpectedObjects(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Scan Button
|
||||
_buildScanButton(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: const Color(0xFFA4D4F0),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.account_balance,
|
||||
size: 48,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Geldinstitut',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.tour.locationName ?? 'Bank',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGiInfo(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.green.shade200),
|
||||
),
|
||||
child: const Column(
|
||||
children: [
|
||||
Text(
|
||||
'Erwartete Objekte:',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'• GK mit Status "ret_gi_fzg" → "fin_gi"\n'
|
||||
'• SB (Safebag) mit Status "ret_gi_fzg" → "fin_gi"\n'
|
||||
'• Leere Kassetten: "unknown" → "ret_ds_empty"',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExpectedObjects(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Zu übergebende Objekte',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Geldkassetten
|
||||
_buildObjectStatusCard(
|
||||
context,
|
||||
'Geldkassetten (GK)',
|
||||
'ret_gi_fzg → fin_gi',
|
||||
Icons.money,
|
||||
Colors.green,
|
||||
),
|
||||
|
||||
// Safebags
|
||||
_buildObjectStatusCard(
|
||||
context,
|
||||
'Safebags (SB)',
|
||||
'ret_gi_fzg → fin_gi',
|
||||
Icons.shopping_bag,
|
||||
Colors.blue,
|
||||
),
|
||||
|
||||
// Leere Kassetten
|
||||
_buildObjectStatusCard(
|
||||
context,
|
||||
'Leere Kassetten',
|
||||
'unknown → ret_ds_empty',
|
||||
Icons.remove_circle_outline,
|
||||
Colors.orange,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildObjectStatusCard(
|
||||
BuildContext context,
|
||||
String title,
|
||||
String transition,
|
||||
IconData icon,
|
||||
Color color,
|
||||
) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 26),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: color),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
transition,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScanButton(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _openScanner(context),
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
label: const Text('Barcode scannen'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 56),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openScanner(BuildContext context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ScanPage(tour: widget.tour),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
274
app/lib/presentation/pages/tour_types/stock_end_page.dart
Normal file
274
app/lib/presentation/pages/tour_types/stock_end_page.dart
Normal file
@@ -0,0 +1,274 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../domain/entities/tour.dart';
|
||||
import '../../blocs/scan/scan_bloc.dart';
|
||||
import '../scan/scan_page.dart';
|
||||
|
||||
/// Stock End Page - Lager Rückgabe
|
||||
/// Entspricht Lua: ShowStockEndScreen + CreateLoadingStockEndView
|
||||
class StockEndPage extends StatefulWidget {
|
||||
final Tour tour;
|
||||
|
||||
const StockEndPage({
|
||||
super.key,
|
||||
required this.tour,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StockEndPage> createState() => _StockEndPageState();
|
||||
}
|
||||
|
||||
class _StockEndPageState extends State<StockEndPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<ScanBloc>().add(InitializeScan(widget.tour));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.tour.locationName ?? 'Lager'),
|
||||
const Text(
|
||||
'Rückgabe',
|
||||
style: TextStyle(fontSize: 14, color: Colors.white70),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
onPressed: () => _openScanner(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Header
|
||||
_buildHeader(context),
|
||||
|
||||
// Rückgabe-Übersicht
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildReturnInfo(context),
|
||||
_buildObjectSummary(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Scan Button
|
||||
_buildScanButton(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: const Color(0xFFA4D4F0),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.warehouse,
|
||||
size: 48,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Lager Rückgabe',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.tour.locationName ?? 'Hauptlager',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReturnInfo(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.green.shade200),
|
||||
),
|
||||
child: const Column(
|
||||
children: [
|
||||
Text(
|
||||
'Rückgabe-Status:',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Abschluss der Tour - Objekte werden finalisiert:\n'
|
||||
'• ret_fail_stk → fin_ds_fail\n'
|
||||
'• ret_ds_stk → fin_ds\n'
|
||||
'• ret_gi_stk → fin_gi_tmp',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildObjectSummary(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Objekte zur Rückgabe',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Fehlkassetten
|
||||
_buildObjectStatusCard(
|
||||
context,
|
||||
'Fehlkassetten',
|
||||
'ret_fail_stk → fin_ds_fail',
|
||||
Icons.error,
|
||||
Colors.red,
|
||||
),
|
||||
|
||||
// DS-Normal
|
||||
_buildObjectStatusCard(
|
||||
context,
|
||||
'Zur Dienststelle',
|
||||
'ret_ds_stk → fin_ds',
|
||||
Icons.account_balance,
|
||||
Colors.blue,
|
||||
),
|
||||
|
||||
// DS-Fehler
|
||||
_buildObjectStatusCard(
|
||||
context,
|
||||
'Fehlerhafte DS',
|
||||
'ret_ds_err → fin_ds_err',
|
||||
Icons.warning,
|
||||
Colors.orange,
|
||||
),
|
||||
|
||||
// GI
|
||||
_buildObjectStatusCard(
|
||||
context,
|
||||
'Zum Geldinstitut',
|
||||
'ret_gi_stk → fin_gi_tmp',
|
||||
Icons.account_balance_wallet,
|
||||
Colors.green,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildObjectStatusCard(
|
||||
BuildContext context,
|
||||
String title,
|
||||
String transition,
|
||||
IconData icon,
|
||||
Color color,
|
||||
) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 26),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: color),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
transition,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScanButton(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _openScanner(context),
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
label: const Text('Barcode scannen'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 56),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openScanner(BuildContext context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ScanPage(tour: widget.tour),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
323
app/lib/presentation/pages/tour_types/stock_start_page.dart
Normal file
323
app/lib/presentation/pages/tour_types/stock_start_page.dart
Normal file
@@ -0,0 +1,323 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../domain/entities/tour.dart';
|
||||
import '../../../domain/entities/counter.dart';
|
||||
import '../../../core/constants/app_constants.dart';
|
||||
import '../../blocs/scan/scan_bloc.dart';
|
||||
import '../../widgets/counter_grid.dart';
|
||||
import '../../widgets/recent_scans_list.dart';
|
||||
import '../scan/scan_page.dart';
|
||||
|
||||
/// Stock Start Page - Lager Beladung
|
||||
/// Entspricht Lua: ShowStockStartScreen + CreateLoadingStockStartView
|
||||
class StockStartPage extends StatefulWidget {
|
||||
final Tour tour;
|
||||
|
||||
const StockStartPage({
|
||||
super.key,
|
||||
required this.tour,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StockStartPage> createState() => _StockStartPageState();
|
||||
}
|
||||
|
||||
class _StockStartPageState extends State<StockStartPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize scan bloc with tour
|
||||
context.read<ScanBloc>().add(InitializeScan(widget.tour));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.tour.locationName ?? 'Lager'),
|
||||
Text(
|
||||
'Beladung',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
onPressed: () => _openScanner(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Header mit Lager-Icon
|
||||
_buildHeader(context),
|
||||
|
||||
// Zähler-Übersicht (wie in Lua CreateLoadingStockStartView)
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
// Bestand Fzg (aktueller Bestand im Fahrzeug)
|
||||
_buildCounterSection(
|
||||
context,
|
||||
title: 'Bestand Fzg',
|
||||
counters: const [
|
||||
CounterItem('MEK', 0),
|
||||
CounterItem('BEK', 0),
|
||||
CounterItem('H1', 0),
|
||||
CounterItem('H2', 0),
|
||||
CounterItem('H3', 0),
|
||||
CounterItem('P', 0),
|
||||
],
|
||||
backgroundColor: const Color(0xFFA4D4F0), // Lua-Farbe
|
||||
),
|
||||
|
||||
// Beladezähler (Soll-Zahlen)
|
||||
_buildCounterSection(
|
||||
context,
|
||||
title: 'Beladezähler',
|
||||
counters: const [
|
||||
CounterItem('MEK', 0),
|
||||
CounterItem('BEK', 0),
|
||||
CounterItem('H1', 0),
|
||||
CounterItem('H2', 0),
|
||||
CounterItem('H3', 0),
|
||||
CounterItem('P', 0),
|
||||
],
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
),
|
||||
|
||||
// HADAG
|
||||
_buildCounterSection(
|
||||
context,
|
||||
title: 'HADAG',
|
||||
counters: const [
|
||||
CounterItem('', null),
|
||||
CounterItem('BEK-B', 0),
|
||||
CounterItem('H1-B', 0),
|
||||
CounterItem('H2-B', 0),
|
||||
CounterItem('', null),
|
||||
CounterItem('', null),
|
||||
],
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
),
|
||||
|
||||
// SST (Schnellbahn)
|
||||
_buildCounterSection(
|
||||
context,
|
||||
title: 'SST',
|
||||
counters: const [
|
||||
CounterItem('MEK-SST', 0),
|
||||
CounterItem('BEK-SST', 0),
|
||||
CounterItem('H1-SST', 0),
|
||||
CounterItem('H2-SST', 0),
|
||||
CounterItem('', null),
|
||||
CounterItem('', null),
|
||||
],
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
),
|
||||
|
||||
// CR (CityRail)
|
||||
_buildCounterSection(
|
||||
context,
|
||||
title: 'CR',
|
||||
counters: const [
|
||||
CounterItem('MEK-CR', 0),
|
||||
CounterItem('BEK-CR', 0),
|
||||
CounterItem('', null),
|
||||
CounterItem('', null),
|
||||
CounterItem('', null),
|
||||
CounterItem('', null),
|
||||
],
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
),
|
||||
|
||||
// Zuletzt gescannte Objekte
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: RecentScansList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Scan Button
|
||||
_buildScanButton(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: const Color(0xFFA4D4F0), // Hellblau wie in Lua
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.warehouse,
|
||||
size: 48,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Lager Beladung',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.tour.locationName ?? 'Hauptlager',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCounterSection(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required List<CounterItem> counters,
|
||||
required Color backgroundColor,
|
||||
}) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Titel
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Zähler-Grid
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: counters.map((counter) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: counter.value == null ? Colors.transparent : Colors.white,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
counter.label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: counter.value == null ? Colors.transparent : Colors.black87,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (counter.value != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${counter.value}',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScanButton(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 10),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _openScanner(context),
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
label: const Text(
|
||||
'Barcode scannen',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 56),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openScanner(BuildContext context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ScanPage(tour: widget.tour),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Hilfsklasse für Zähler-Darstellung
|
||||
class CounterItem {
|
||||
final String label;
|
||||
final int? value;
|
||||
|
||||
const CounterItem(this.label, this.value);
|
||||
}
|
||||
274
app/lib/presentation/pages/tour_types/veh_end_page.dart
Normal file
274
app/lib/presentation/pages/tour_types/veh_end_page.dart
Normal file
@@ -0,0 +1,274 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../domain/entities/tour.dart';
|
||||
import '../../blocs/scan/scan_bloc.dart';
|
||||
import '../scan/scan_page.dart';
|
||||
|
||||
/// Veh End Page - Fahrzeug Rückgabe
|
||||
/// Entspricht Lua: ShowVehEndScreen + CreateLoadingVehEndView
|
||||
class VehEndPage extends StatefulWidget {
|
||||
final Tour tour;
|
||||
|
||||
const VehEndPage({
|
||||
super.key,
|
||||
required this.tour,
|
||||
});
|
||||
|
||||
@override
|
||||
State<VehEndPage> createState() => _VehEndPageState();
|
||||
}
|
||||
|
||||
class _VehEndPageState extends State<VehEndPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<ScanBloc>().add(InitializeScan(widget.tour));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.tour.locationName ?? 'Fahrzeugende'),
|
||||
const Text(
|
||||
'Rückgabe',
|
||||
style: TextStyle(fontSize: 14, color: Colors.white70),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
onPressed: () => _openScanner(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Header
|
||||
_buildHeader(context),
|
||||
|
||||
// Rückgabe-Übersicht
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildReturnInfo(context),
|
||||
_buildObjectSummary(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Scan Button
|
||||
_buildScanButton(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: const Color(0xFFA4D4F0),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.local_shipping,
|
||||
size: 48,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Fahrzeug Rückgabe',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.tour.locationName ?? 'Dienststelle',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReturnInfo(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.orange.shade200),
|
||||
),
|
||||
child: const Column(
|
||||
children: [
|
||||
Text(
|
||||
'Rückgabe-Status:',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Alle Objekte im Fahrzeug werden zurückgebucht:\n'
|
||||
'• ret_fail_fzg → ret_fail_stk\n'
|
||||
'• ret_ds_fzg → ret_ds_stk\n'
|
||||
'• ret_gi_fzg → ret_gi_stk',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildObjectSummary(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Objekte im Fahrzeug',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Fehlkassetten
|
||||
_buildObjectStatusCard(
|
||||
context,
|
||||
'Fehlkassetten',
|
||||
'ret_fail_fzg → ret_fail_stk',
|
||||
Icons.error,
|
||||
Colors.red,
|
||||
),
|
||||
|
||||
// DS-Objekte
|
||||
_buildObjectStatusCard(
|
||||
context,
|
||||
'Zur Dienststelle',
|
||||
'ret_ds_fzg → ret_ds_stk',
|
||||
Icons.account_balance,
|
||||
Colors.blue,
|
||||
),
|
||||
|
||||
// GI-Objekte
|
||||
_buildObjectStatusCard(
|
||||
context,
|
||||
'Zum Geldinstitut',
|
||||
'ret_gi_fzg → ret_gi_stk',
|
||||
Icons.account_balance_wallet,
|
||||
Colors.green,
|
||||
),
|
||||
|
||||
// Noch im Fahrzeug
|
||||
_buildObjectStatusCard(
|
||||
context,
|
||||
'Noch im Fahrzeug (Rest)',
|
||||
'delivery → ret_ds_stk',
|
||||
Icons.local_shipping,
|
||||
Colors.orange,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildObjectStatusCard(
|
||||
BuildContext context,
|
||||
String title,
|
||||
String transition,
|
||||
IconData icon,
|
||||
Color color,
|
||||
) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 26),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: color),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
transition,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScanButton(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _openScanner(context),
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
label: const Text('Barcode scannen'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 56),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openScanner(BuildContext context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ScanPage(tour: widget.tour),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
256
app/lib/presentation/pages/tour_types/veh_page.dart
Normal file
256
app/lib/presentation/pages/tour_types/veh_page.dart
Normal file
@@ -0,0 +1,256 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../domain/entities/tour.dart';
|
||||
import '../../blocs/scan/scan_bloc.dart';
|
||||
import '../scan/scan_page.dart';
|
||||
|
||||
/// Veh Page - Station (Haltestelle)
|
||||
/// Entspricht Lua: ShowVehScreen + CreateLoadingVehView
|
||||
class VehPage extends StatefulWidget {
|
||||
final Tour tour;
|
||||
|
||||
const VehPage({
|
||||
super.key,
|
||||
required this.tour,
|
||||
});
|
||||
|
||||
@override
|
||||
State<VehPage> createState() => _VehPageState();
|
||||
}
|
||||
|
||||
class _VehPageState extends State<VehPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<ScanBloc>().add(InitializeScan(widget.tour));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.tour.locationName ?? 'Station'),
|
||||
const Text(
|
||||
'Objekt-Wechsel',
|
||||
style: TextStyle(fontSize: 14, color: Colors.white70),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
onPressed: () => _openScanner(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Header mit Stations-Info
|
||||
_buildHeader(context),
|
||||
|
||||
// Zähler-Bereiche
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
// Wechselzähler (Leer/Voll)
|
||||
_buildSwapCounters(context),
|
||||
|
||||
// Abholzähler
|
||||
_buildPickupCounters(context),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Scan Button
|
||||
_buildScanButton(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: const Color(0xFFA4D4F0),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.train,
|
||||
size: 48,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.tour.locationName ?? 'Haltestelle',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (widget.tour.remark != null)
|
||||
Text(
|
||||
widget.tour.remark!,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSwapCounters(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade100,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
|
||||
),
|
||||
child: const Text(
|
||||
'Wechselzähler',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Leer/Voll/HADAG Reihen
|
||||
_buildCounterRow('Leer', [0, 0, 0, 0, 0, 0]),
|
||||
const Divider(height: 1),
|
||||
_buildCounterRow('Voll', [0, 0, 0, 0, 0, 0]),
|
||||
const Divider(height: 1),
|
||||
_buildCounterRow('HADAG', [null, 0, 0, 0, null, null]),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPickupCounters(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade100,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
|
||||
),
|
||||
child: const Text(
|
||||
'Abholzähler',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
_buildCounterRow('Abholung', [0, 0, 0, 0, 0, 0]),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCounterRow(String label, List<int?> values) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: values.map((value) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: value == null ? Colors.transparent : Colors.white,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: value != null
|
||||
? Text(
|
||||
'$value',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScanButton(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _openScanner(context),
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
label: const Text('Barcode scannen'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 56),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openScanner(BuildContext context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ScanPage(tour: widget.tour),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
233
app/lib/presentation/pages/tour_types/veh_start_page.dart
Normal file
233
app/lib/presentation/pages/tour_types/veh_start_page.dart
Normal file
@@ -0,0 +1,233 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../domain/entities/tour.dart';
|
||||
import '../../blocs/scan/scan_bloc.dart';
|
||||
import '../scan/scan_page.dart';
|
||||
|
||||
/// Veh Start Page - Fahrzeug Beladung
|
||||
/// Entspricht Lua: ShowVehStartScreen + CreateLoadingVehStartView
|
||||
class VehStartPage extends StatefulWidget {
|
||||
final Tour tour;
|
||||
|
||||
const VehStartPage({
|
||||
super.key,
|
||||
required this.tour,
|
||||
});
|
||||
|
||||
@override
|
||||
State<VehStartPage> createState() => _VehStartPageState();
|
||||
}
|
||||
|
||||
class _VehStartPageState extends State<VehStartPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<ScanBloc>().add(InitializeScan(widget.tour));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.tour.locationName ?? 'Dienststelle'),
|
||||
const Text(
|
||||
'Fahrzeug Beladung',
|
||||
style: TextStyle(fontSize: 14, color: Colors.white70),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
onPressed: () => _openScanner(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Header
|
||||
_buildHeader(context),
|
||||
|
||||
// Beladezähler (wie in Lua CreateLoadingVehStartView)
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildCounterSection(
|
||||
context,
|
||||
title: 'Beladezähler',
|
||||
counters: const [
|
||||
CounterItem('MEK', 0),
|
||||
CounterItem('BEK', 0),
|
||||
CounterItem('H1', 0),
|
||||
CounterItem('H2', 0),
|
||||
CounterItem('H3', 0),
|
||||
CounterItem('P', 0),
|
||||
],
|
||||
),
|
||||
|
||||
// Zuletzt gescannt
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Text(
|
||||
'Scanne Objekte zum Beladen',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Scan Button
|
||||
_buildScanButton(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: const Color(0xFFA4D4F0),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.local_shipping,
|
||||
size: 48,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Fahrzeug Beladung',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.tour.locationName ?? 'Dienststelle',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCounterSection(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required List<CounterItem> counters,
|
||||
}) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: counters.map((counter) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
counter.label,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${counter.value}',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScanButton(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _openScanner(context),
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
label: const Text('Barcode scannen'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 56),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openScanner(BuildContext context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ScanPage(tour: widget.tour),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CounterItem {
|
||||
final String label;
|
||||
final int value;
|
||||
|
||||
const CounterItem(this.label, this.value);
|
||||
}
|
||||
309
app/lib/presentation/pages/tour_types/vs_page.dart
Normal file
309
app/lib/presentation/pages/tour_types/vs_page.dart
Normal file
@@ -0,0 +1,309 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../domain/entities/tour.dart';
|
||||
import '../../blocs/scan/scan_bloc.dart';
|
||||
import '../scan/scan_page.dart';
|
||||
|
||||
/// VS Page - Verwahrungsstelle
|
||||
/// Entspricht Lua: ShowVsScreen + CreateLoadingVsView
|
||||
/// Spezial: Container-Handling für SB (Safebag) und ABS (Abfallbehälter)
|
||||
class VsPage extends StatefulWidget {
|
||||
final Tour tour;
|
||||
|
||||
const VsPage({
|
||||
super.key,
|
||||
required this.tour,
|
||||
});
|
||||
|
||||
@override
|
||||
State<VsPage> createState() => _VsPageState();
|
||||
}
|
||||
|
||||
class _VsPageState extends State<VsPage> {
|
||||
String? selectedContainerId;
|
||||
String? selectedContainerType; // 'a' = GI, 'b' = DS
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<ScanBloc>().add(InitializeScan(widget.tour));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.tour.locationName ?? 'Verwahrungsstelle'),
|
||||
const Text(
|
||||
'Container-Annahme',
|
||||
style: TextStyle(fontSize: 14, color: Colors.white70),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
onPressed: () => _openScanner(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Header
|
||||
_buildHeader(context),
|
||||
|
||||
// Container-Auswahl
|
||||
_buildContainerSelection(context),
|
||||
|
||||
// Aktueller Container Status
|
||||
if (selectedContainerId != null)
|
||||
_buildContainerStatus(context),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// Scan Button
|
||||
_buildScanButton(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: const Color(0xFFA4D4F0),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.inventory_2,
|
||||
size: 48,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Verwahrungsstelle',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.tour.locationName ?? 'VS',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContainerSelection(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Container auswählen',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Container A - Geldinstitut
|
||||
_buildContainerOption(
|
||||
context,
|
||||
id: 'CONT_A',
|
||||
type: 'a',
|
||||
title: 'Container A',
|
||||
subtitle: 'Für Geldinstitut (GI)',
|
||||
icon: Icons.account_balance,
|
||||
color: Colors.blue,
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Container B - Dienststelle
|
||||
_buildContainerOption(
|
||||
context,
|
||||
id: 'CONT_B',
|
||||
type: 'b',
|
||||
title: 'Container B',
|
||||
subtitle: 'Für Dienststelle (DS)',
|
||||
icon: Icons.business,
|
||||
color: Colors.orange,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContainerOption(
|
||||
BuildContext context, {
|
||||
required String id,
|
||||
required String type,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
}) {
|
||||
final isSelected = selectedContainerId == id;
|
||||
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
selectedContainerId = id;
|
||||
selectedContainerType = type;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? color.withValues(alpha: 20) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isSelected ? color : Colors.grey.shade300,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 26),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: color),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSelected)
|
||||
Icon(Icons.check_circle, color: color),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContainerStatus(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: selectedContainerType == 'a'
|
||||
? Colors.blue.shade50
|
||||
: Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: selectedContainerType == 'a'
|
||||
? Colors.blue
|
||||
: Colors.orange,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Ausgewählt: ${selectedContainerType == 'a' ? 'Container A (GI)' : 'Container B (DS)'}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: selectedContainerType == 'a'
|
||||
? Colors.blue
|
||||
: Colors.orange,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Scannen Sie jetzt SB (Safebag) oder ABS (Abfallbehälter)',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScanButton(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
if (selectedContainerId == null)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 8),
|
||||
child: Text(
|
||||
'Bitte zuerst einen Container auswählen',
|
||||
style: TextStyle(
|
||||
color: Colors.orange,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: selectedContainerId != null
|
||||
? () => _openScanner(context)
|
||||
: null,
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
label: const Text('Barcode scannen'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 56),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openScanner(BuildContext context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ScanPage(tour: widget.tour),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
423
app/lib/presentation/pages/tours/dashboard_page.dart
Normal file
423
app/lib/presentation/pages/tours/dashboard_page.dart
Normal file
@@ -0,0 +1,423 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../blocs/tour/tour_bloc.dart';
|
||||
import '../../widgets/loading_indicator.dart';
|
||||
|
||||
class DashboardPage extends StatelessWidget {
|
||||
const DashboardPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<TourBloc, TourState>(
|
||||
builder: (context, state) {
|
||||
if (state is TourLoading) {
|
||||
return const LoadingIndicator();
|
||||
}
|
||||
|
||||
if (state is ToursLoaded) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
// App Bar
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Guten Morgen,',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Fahrer',
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary.withValues(alpha: 26),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tour vom ${DateTime.now().day}.${DateTime.now().month}.${DateTime.now().year}',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Quick Stats
|
||||
SliverToBoxAdapter(
|
||||
child: _buildQuickStats(context, state),
|
||||
),
|
||||
|
||||
// Section Title
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 32, 24, 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Schnellzugriff',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// Navigate to full tours list
|
||||
},
|
||||
child: const Text('Alle anzeigen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Quick Actions Grid
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
sliver: SliverGrid.count(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 16,
|
||||
crossAxisSpacing: 16,
|
||||
children: [
|
||||
_buildQuickActionCard(
|
||||
context,
|
||||
'Nächste Station',
|
||||
Icons.location_on,
|
||||
Colors.orange,
|
||||
'${state.tours.where((t) => t.state == 0).length} offen',
|
||||
() {},
|
||||
),
|
||||
_buildQuickActionCard(
|
||||
context,
|
||||
'Scan',
|
||||
Icons.qr_code_scanner,
|
||||
Colors.green,
|
||||
'Barcode scannen',
|
||||
() {},
|
||||
),
|
||||
_buildQuickActionCard(
|
||||
context,
|
||||
'Bestand',
|
||||
Icons.warehouse,
|
||||
Colors.blue,
|
||||
'Objekte anzeigen',
|
||||
() {},
|
||||
),
|
||||
_buildQuickActionCard(
|
||||
context,
|
||||
'Sync',
|
||||
Icons.sync,
|
||||
Colors.purple,
|
||||
state.isSyncing ? 'Synchronisiert...' : 'Daten aktualisieren',
|
||||
() {
|
||||
context.read<TourBloc>().add(const SyncData());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Recent Activity
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 32, 24, 16),
|
||||
child: Text(
|
||||
'Letzte Aktivitäten',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return _buildActivityItem(
|
||||
context,
|
||||
'Geldkassette gescannt',
|
||||
'Station: Hauptbahnhof Nord',
|
||||
'10:23 Uhr',
|
||||
Icons.qr_code_scanner,
|
||||
Colors.green,
|
||||
);
|
||||
},
|
||||
childCount: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SliverPadding(padding: EdgeInsets.only(bottom: 100)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(child: Text('Willkommen bei HHA Logistics'));
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickStats(BuildContext context, ToursLoaded state) {
|
||||
final theme = Theme.of(context);
|
||||
final completionPercentage = state.completionPercentage;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 24),
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
theme.colorScheme.primary,
|
||||
theme.colorScheme.primary.withValues(alpha: 204),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: theme.colorScheme.primary.withValues(alpha: 77),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 51),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.route,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Tour-Fortschritt',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: Colors.white.withValues(alpha: 204),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${state.completedCount} / ${state.totalCount} Stationen',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: LinearProgressIndicator(
|
||||
value: completionPercentage / 100,
|
||||
backgroundColor: Colors.white.withValues(alpha: 51),
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
minHeight: 10,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${completionPercentage.toStringAsFixed(0)}% abgeschlossen',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.white.withValues(alpha: 230),
|
||||
),
|
||||
),
|
||||
if (state.isSyncing)
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white.withValues(alpha: 204),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Sync...',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.white.withValues(alpha: 230),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickActionCard(
|
||||
BuildContext context,
|
||||
String title,
|
||||
IconData icon,
|
||||
Color color,
|
||||
String subtitle,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
side: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 26),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActivityItem(
|
||||
BuildContext context,
|
||||
String title,
|
||||
String subtitle,
|
||||
String time,
|
||||
IconData icon,
|
||||
Color color,
|
||||
) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 26),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
subtitle,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
trailing: Text(
|
||||
time,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
327
app/lib/presentation/pages/tours/tours_page.dart
Normal file
327
app/lib/presentation/pages/tours/tours_page.dart
Normal file
@@ -0,0 +1,327 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../domain/entities/tour.dart';
|
||||
import '../../../core/constants/app_constants.dart';
|
||||
import '../../blocs/tour/tour_bloc.dart';
|
||||
import '../../widgets/tour_list_item.dart';
|
||||
import '../../widgets/loading_indicator.dart';
|
||||
import '../../widgets/error_view.dart';
|
||||
import '../tour_types/stock_start_page.dart';
|
||||
import '../tour_types/veh_start_page.dart';
|
||||
import '../tour_types/veh_page.dart';
|
||||
import '../tour_types/fsa_page.dart';
|
||||
import '../tour_types/vs_page.dart';
|
||||
import '../tour_types/gi_page.dart';
|
||||
import '../tour_types/veh_end_page.dart';
|
||||
import '../tour_types/stock_end_page.dart';
|
||||
import '../scan/scan_page.dart';
|
||||
|
||||
class ToursPage extends StatelessWidget {
|
||||
const ToursPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: BlocBuilder<TourBloc, TourState>(
|
||||
builder: (context, state) {
|
||||
if (state is TourLoading) {
|
||||
return const LoadingIndicator(message: 'Touren werden geladen...');
|
||||
}
|
||||
|
||||
if (state is TourError) {
|
||||
return ErrorView(
|
||||
message: state.message,
|
||||
onRetry: () => context.read<TourBloc>().add(const RefreshTours()),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is ToursLoaded) {
|
||||
return _ToursListView(state: state);
|
||||
}
|
||||
|
||||
return const LoadingIndicator();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ToursListView extends StatelessWidget {
|
||||
final ToursLoaded state;
|
||||
|
||||
const _ToursListView({required this.state});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
// Header mit Fortschritt
|
||||
SliverToBoxAdapter(
|
||||
child: _buildHeader(context),
|
||||
),
|
||||
|
||||
// Offene Touren
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.location_on,
|
||||
color: theme.colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Offene Stationen',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary.withValues(alpha: 26),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'${state.tours.length}',
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final tour = state.tours[index];
|
||||
return TourListItem(
|
||||
tour: tour,
|
||||
onTap: () => _onTourSelected(context, tour),
|
||||
);
|
||||
},
|
||||
childCount: state.tours.length,
|
||||
),
|
||||
),
|
||||
|
||||
// Erledigte Touren (falls aktiviert)
|
||||
if (state.showCompleted && state.completedTours.isNotEmpty) ...[
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.green,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Erledigte Stationen',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withValues(alpha: 26),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'${state.completedTours.length}',
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: Colors.green,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final tour = state.completedTours[index];
|
||||
return TourListItem(
|
||||
tour: tour,
|
||||
onTap: () => _onTourSelected(context, tour),
|
||||
isCompleted: true,
|
||||
);
|
||||
},
|
||||
childCount: state.completedTours.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SliverPadding(padding: EdgeInsets.only(bottom: 100)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final completionPercentage = state.completionPercentage;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
theme.colorScheme.primary,
|
||||
theme.colorScheme.primary.withValues(alpha: 204),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: theme.colorScheme.primary.withValues(alpha: 77),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 51),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.route,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Tagesübersicht',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: Colors.white.withValues(alpha: 204),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${state.completedCount} / ${state.totalCount} Stationen',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: LinearProgressIndicator(
|
||||
value: completionPercentage / 100,
|
||||
backgroundColor: Colors.white.withValues(alpha: 51),
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
minHeight: 8,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${completionPercentage.toStringAsFixed(0)}% abgeschlossen',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.white.withValues(alpha: 230),
|
||||
),
|
||||
),
|
||||
if (state.isSyncing)
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white.withValues(alpha: 204),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Sync...',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.white.withValues(alpha: 230),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onTourSelected(BuildContext context, Tour tour) {
|
||||
context.read<TourBloc>().add(SelectTour(tour));
|
||||
|
||||
// Navigation zur tour-spezifischen Page
|
||||
final page = _getPageForTourType(tour);
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => page),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getPageForTourType(Tour tour) {
|
||||
switch (tour.type) {
|
||||
case TourTypes.stockStart:
|
||||
return StockStartPage(tour: tour);
|
||||
case TourTypes.vehStart:
|
||||
return VehStartPage(tour: tour);
|
||||
case TourTypes.veh:
|
||||
return VehPage(tour: tour);
|
||||
case TourTypes.fsa:
|
||||
return FsaPage(tour: tour);
|
||||
case TourTypes.vs:
|
||||
return VsPage(tour: tour);
|
||||
case TourTypes.gi:
|
||||
return GiPage(tour: tour);
|
||||
case TourTypes.vehEnd:
|
||||
return VehEndPage(tour: tour);
|
||||
case TourTypes.stockEnd:
|
||||
return StockEndPage(tour: tour);
|
||||
case TourTypes.stock:
|
||||
// Stock (HADAG) uses similar UI to stock_start
|
||||
return StockStartPage(tour: tour);
|
||||
default:
|
||||
// Fallback to generic scan page for unknown types
|
||||
return ScanPage(tour: tour);
|
||||
}
|
||||
}
|
||||
}
|
||||
186
app/lib/presentation/widgets/counter_grid.dart
Normal file
186
app/lib/presentation/widgets/counter_grid.dart
Normal file
@@ -0,0 +1,186 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../domain/entities/counter.dart';
|
||||
|
||||
/// Zähler-Grid wie in Lua CreateLoadingStockStartView etc.
|
||||
class CounterGrid extends StatelessWidget {
|
||||
final List<CounterGroup> groups;
|
||||
final bool showDifferences;
|
||||
|
||||
const CounterGrid({
|
||||
super.key,
|
||||
required this.groups,
|
||||
this.showDifferences = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: groups.map((group) => _buildGroup(context, group)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGroup(BuildContext context, CounterGroup group) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _getGroupColor(group.title),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Titel
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
group.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Zähler
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: group.counters.map((counter) {
|
||||
return Expanded(
|
||||
child: _buildCounterCell(context, counter),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCounterCell(BuildContext context, ObjectCounter counter) {
|
||||
final color = counter.isOver
|
||||
? Colors.red.shade100
|
||||
: counter.isComplete
|
||||
? Colors.green.shade100
|
||||
: Colors.white;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
counter.label,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${counter.currentCount}',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: counter.isOver ? Colors.red : Colors.black87,
|
||||
),
|
||||
),
|
||||
if (showDifferences && counter.targetCount > 0) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${counter.difference > 0 ? "+" : ""}${counter.difference}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: counter.difference == 0
|
||||
? Colors.green
|
||||
: counter.difference > 0
|
||||
? Colors.orange
|
||||
: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getGroupColor(String title) {
|
||||
switch (title.toLowerCase()) {
|
||||
case 'bestand fzg':
|
||||
return const Color(0xFFA4D4F0); // Hellblau wie in Lua
|
||||
case 'beladezähler':
|
||||
case 'hadag':
|
||||
case 'sst':
|
||||
case 'cr':
|
||||
return Colors.grey.shade200;
|
||||
default:
|
||||
return Colors.grey.shade200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Vereinfachte Zähler-Anzeige für eine Zeile
|
||||
class CounterRow extends StatelessWidget {
|
||||
final String label;
|
||||
final int count;
|
||||
final Color? backgroundColor;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const CounterRow({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.count,
|
||||
this.backgroundColor,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor ?? Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Text(
|
||||
'$count',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
141
app/lib/presentation/widgets/error_view.dart
Normal file
141
app/lib/presentation/widgets/error_view.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ErrorView extends StatelessWidget {
|
||||
final String message;
|
||||
final VoidCallback? onRetry;
|
||||
final String? retryText;
|
||||
final IconData? icon;
|
||||
|
||||
const ErrorView({
|
||||
super.key,
|
||||
required this.message,
|
||||
this.onRetry,
|
||||
this.retryText,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.error.withValues(alpha: 26),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
icon ?? Icons.error_outline,
|
||||
size: 48,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Oops!',
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
message,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 179),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (onRetry != null) ...[
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton.icon(
|
||||
onPressed: onRetry,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: Text(retryText ?? 'Erneut versuchen'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EmptyStateView extends StatelessWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final IconData icon;
|
||||
final VoidCallback? onAction;
|
||||
final String? actionText;
|
||||
|
||||
const EmptyStateView({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.icon = Icons.inbox,
|
||||
this.onAction,
|
||||
this.actionText,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 48,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 153),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
if (onAction != null && actionText != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: onAction,
|
||||
child: Text(actionText!),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
134
app/lib/presentation/widgets/loading_indicator.dart
Normal file
134
app/lib/presentation/widgets/loading_indicator.dart
Normal file
@@ -0,0 +1,134 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LoadingIndicator extends StatelessWidget {
|
||||
final String? message;
|
||||
final bool showAnimation;
|
||||
|
||||
const LoadingIndicator({
|
||||
super.key,
|
||||
this.message,
|
||||
this.showAnimation = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (showAnimation) ...[
|
||||
// Optional: Lottie Animation für Loading
|
||||
SizedBox(
|
||||
width: 120,
|
||||
height: 120,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (message != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
message!,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 179),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SkeletonLoading extends StatelessWidget {
|
||||
final int itemCount;
|
||||
final EdgeInsets padding;
|
||||
|
||||
const SkeletonLoading({
|
||||
super.key,
|
||||
this.itemCount = 5,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
padding: padding,
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: _SkeletonCard(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SkeletonCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: 120,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
173
app/lib/presentation/widgets/recent_scans_list.dart
Normal file
173
app/lib/presentation/widgets/recent_scans_list.dart
Normal file
@@ -0,0 +1,173 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../domain/entities/counter.dart';
|
||||
|
||||
/// Zeigt zuletzt gescannte Objekte an
|
||||
/// Entspricht Lua: Die Liste in ShowStockStartScreen etc.
|
||||
class RecentScansList extends StatelessWidget {
|
||||
final List<RecentScan>? scans;
|
||||
|
||||
const RecentScansList({
|
||||
super.key,
|
||||
this.scans,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Demo-Daten falls keine vorhanden
|
||||
final demoScans = scans ?? const [
|
||||
// RecentScan(
|
||||
// objectCode: 'MEKA123456',
|
||||
// objectName: 'MEK A',
|
||||
// state: 'to_delivery',
|
||||
// scanTime: '',
|
||||
// ),
|
||||
];
|
||||
|
||||
if (demoScans.isEmpty) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Noch keine Objekte gescannt',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Zuletzt gescannt',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...demoScans.map((scan) => _buildScanItem(context, scan)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScanItem(BuildContext context, RecentScan scan) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Objekt-Icon
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
_getIconForObject(scan.objectName),
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Objekt-Info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
scan.objectName,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'#${scan.objectCode}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Status
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _getColorForState(scan.state),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
_getShortStateName(scan.state),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getIconForObject(String objectName) {
|
||||
final name = objectName.toLowerCase();
|
||||
if (name.contains('mek')) return Icons.money;
|
||||
if (name.contains('bek')) return Icons.money_off;
|
||||
if (name.contains('hp')) return Icons.print;
|
||||
if (name.contains('fr')) return Icons.receipt;
|
||||
if (name.contains('sb')) return Icons.shopping_bag;
|
||||
if (name.contains('abs')) return Icons.delete;
|
||||
return Icons.inventory_2;
|
||||
}
|
||||
|
||||
Color _getColorForState(String state) {
|
||||
switch (state) {
|
||||
case 'to_delivery':
|
||||
return Colors.grey.shade300;
|
||||
case 'delivery':
|
||||
return Colors.grey.shade400;
|
||||
case 'station':
|
||||
return const Color(0xFFFFDD00);
|
||||
case 'in_fa':
|
||||
return const Color(0xFF9CDA7A);
|
||||
case 'ret_fail':
|
||||
return const Color(0xFFFF9081);
|
||||
default:
|
||||
return Colors.grey.shade200;
|
||||
}
|
||||
}
|
||||
|
||||
String _getShortStateName(String state) {
|
||||
switch (state) {
|
||||
case 'to_delivery':
|
||||
return 'Zum Fzg';
|
||||
case 'delivery':
|
||||
return 'Im Fzg';
|
||||
case 'station':
|
||||
return 'Station';
|
||||
case 'in_fa':
|
||||
return 'Im FA';
|
||||
case 'ret_fail':
|
||||
return 'Fehler';
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
231
app/lib/presentation/widgets/tour_list_item.dart
Normal file
231
app/lib/presentation/widgets/tour_list_item.dart
Normal file
@@ -0,0 +1,231 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import '../../domain/entities/tour.dart';
|
||||
import '../../core/constants/app_constants.dart';
|
||||
|
||||
class TourListItem extends StatelessWidget {
|
||||
final Tour tour;
|
||||
final VoidCallback onTap;
|
||||
final bool isCompleted;
|
||||
|
||||
const TourListItem({
|
||||
super.key,
|
||||
required this.tour,
|
||||
required this.onTap,
|
||||
this.isCompleted = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final iconData = _getIconForTourType(tour.type);
|
||||
final color = _getColorForTourType(tour.type);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
child: Card(
|
||||
elevation: isCompleted ? 0 : 2,
|
||||
shadowColor: Colors.black.withValues(alpha: 26),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: isCompleted
|
||||
? BorderSide(color: Colors.grey.shade300)
|
||||
: BorderSide.none,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: isCompleted ? null : onTap,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
// Icon Container
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
color,
|
||||
color.withValues(alpha: 204),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withValues(alpha: 77),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
iconData,
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Content
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tour.locationName ?? 'Station ${tour.locationId}',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isCompleted ? Colors.grey : null,
|
||||
decoration: isCompleted ? TextDecoration.lineThrough : null,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_getTypeLabel(tour.type),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
if (tour.remark != null && tour.remark!.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
tour.remark!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Status Indicator
|
||||
if (isCompleted)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withValues(alpha: 26),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.check,
|
||||
color: Colors.green,
|
||||
size: 20,
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Colors.grey.shade600,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
).animate().fadeIn(duration: 300.ms).slideX(begin: 0.1, end: 0);
|
||||
}
|
||||
|
||||
IconData _getIconForTourType(String type) {
|
||||
switch (type) {
|
||||
case TourTypes.stockStart:
|
||||
return Icons.warehouse;
|
||||
case TourTypes.stockEnd:
|
||||
return Icons.archive;
|
||||
case TourTypes.start:
|
||||
return Icons.business;
|
||||
case TourTypes.end:
|
||||
return Icons.flag;
|
||||
case TourTypes.station:
|
||||
return Icons.directions_bus;
|
||||
case TourTypes.hls:
|
||||
return Icons.train;
|
||||
case TourTypes.fsa:
|
||||
return Icons.confirmation_number;
|
||||
case TourTypes.vs:
|
||||
return Icons.store;
|
||||
case TourTypes.gi:
|
||||
return Icons.account_balance;
|
||||
case TourTypes.veh:
|
||||
case TourTypes.vehStart:
|
||||
case TourTypes.vehEnd:
|
||||
return Icons.local_shipping;
|
||||
default:
|
||||
return Icons.location_on;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getColorForTourType(String type) {
|
||||
switch (type) {
|
||||
case TourTypes.stockStart:
|
||||
case TourTypes.stockEnd:
|
||||
return const Color(0xFF2196F3);
|
||||
case TourTypes.start:
|
||||
case TourTypes.end:
|
||||
return const Color(0xFF4CAF50);
|
||||
case TourTypes.station:
|
||||
return const Color(0xFFFF9800);
|
||||
case TourTypes.hls:
|
||||
return const Color(0xFF9C27B0);
|
||||
case TourTypes.fsa:
|
||||
return const Color(0xFFE91E63);
|
||||
case TourTypes.vs:
|
||||
return const Color(0xFF00BCD4);
|
||||
case TourTypes.gi:
|
||||
return const Color(0xFF3F51B5);
|
||||
case TourTypes.veh:
|
||||
case TourTypes.vehStart:
|
||||
case TourTypes.vehEnd:
|
||||
return const Color(0xFF795548);
|
||||
default:
|
||||
return const Color(0xFF607D8B);
|
||||
}
|
||||
}
|
||||
|
||||
String _getTypeLabel(String type) {
|
||||
switch (type) {
|
||||
case TourTypes.stockStart:
|
||||
return 'Lager - Beladung';
|
||||
case TourTypes.stockEnd:
|
||||
return 'Lager - Rückgabe';
|
||||
case TourTypes.start:
|
||||
return 'Dienststelle';
|
||||
case TourTypes.end:
|
||||
return 'Tour Ende';
|
||||
case TourTypes.station:
|
||||
return 'Haltestelle';
|
||||
case TourTypes.hls:
|
||||
return 'Hochbahnstation';
|
||||
case TourTypes.fsa:
|
||||
return 'Fahrscheinautomat';
|
||||
case TourTypes.vs:
|
||||
return 'Versorgungsstelle';
|
||||
case TourTypes.gi:
|
||||
return 'Geldinstitut';
|
||||
case TourTypes.veh:
|
||||
return 'Fahrzeug';
|
||||
case TourTypes.vehStart:
|
||||
return 'Fahrzeug - Beladung';
|
||||
case TourTypes.vehEnd:
|
||||
return 'Fahrzeug - Entladung';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user