first commit
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user