Files
votianlt/app/lib/tasks/barcode_capture_screen.dart
Sven Carstensen 6e8bedd9b4 feat: Drag-and-Drop-Reihenfolge, Station-Abschluss-Flow und UI-Verbesserungen
Lieferstationen-Dialog (Backend/Vaadin):
- Aufgaben per Drag & Drop neu anordnen, inkl. Drag-Handle, komprimierter
  Kachelansicht während des Drags und horizontaler Einfügelinie als Drop-Target
- Drop-Indikator wird unterdrückt, wenn der Drop keine Positionsänderung bewirken
  würde, und nach dem Abschluss clientseitig zuverlässig aufgeräumt
- Drag-Handle, Aufgabentyp-Label und Close-Button auf einheitlicher Position
  ausgerichtet; Abstände in der Kachel komprimiert

Station-Abschluss-Flow (Flutter-App + Backend):
- Neuer Button "Station abschließen" unter den Aufgaben; deaktiviert, solange
  Pflichtaufgaben offen sind, ansonsten aktiv (auch wenn nur optionale Aufgaben
  existieren)
- Hinweisdialog nach Erledigung der letzten Pflichtaufgabe sowie Warnung bei
  offenen optionalen Aufgaben vor dem Senden
- Neue station_completed-Nachricht (jobId, jobNumber, stationOrder,
  completedAt, hasIncompleteOptionalTasks) wird an den Server gesendet
- Backend: Auftrag wird nicht mehr automatisch beim Erledigen der letzten
  Pflichtaufgabe abgeschlossen, sondern erst beim Empfang der
  station_completed-Nachricht (neuer Handler in MessageController und
  MessagingConfig)

Aufgabenliste in der App:
- Farbcodierung optionaler Aufgaben entfernt; stattdessen vertikal zentrierter
  "Optional"-Chip am rechten Kartenrand

Weitere UI-Überarbeitungen über Login, Jobs, Chats, Settings, Aufgaben-Capture-
Screens, Offline-Banner und zugehörige Widgets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:26:30 +02:00

334 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import '../l10n/app_localizations.dart';
import '../models/tasks/barcode_task.dart';
import '../widgets/offline_banner.dart';
class BarcodeCaptureScreen extends StatefulWidget {
final BarcodeTask task;
final Function(List<String>) onBarcodesCompleted;
const BarcodeCaptureScreen({
super.key,
required this.task,
required this.onBarcodesCompleted,
});
@override
State<BarcodeCaptureScreen> createState() => _BarcodeCaptureScreenState();
}
class _BarcodeCaptureScreenState extends State<BarcodeCaptureScreen> {
final List<String> _scannedBarcodes = [];
final List<TextEditingController> _textControllers = [];
MobileScannerController? _scannerController;
bool _isMobilePlatform = false;
bool _isScannerInitialized = false;
@override
void initState() {
super.initState();
_detectPlatformAndInit();
}
@override
void dispose() {
_scannerController?.dispose();
for (final controller in _textControllers) {
controller.dispose();
}
super.dispose();
}
void _detectPlatformAndInit() {
// Determine if we're on a mobile platform
if (kIsWeb) {
_isMobilePlatform = false;
_initializeDesktopMode();
} else {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
_isMobilePlatform = true;
_initializeMobileScanner();
break;
case TargetPlatform.macOS:
case TargetPlatform.windows:
case TargetPlatform.linux:
_isMobilePlatform = false;
_initializeDesktopMode();
break;
default:
_isMobilePlatform = false;
_initializeDesktopMode();
}
}
}
void _initializeMobileScanner() {
try {
_scannerController = MobileScannerController();
setState(() {
_isScannerInitialized = true;
});
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${AppLocalizations.of(context).cameraError}: $e'),
),
);
}
}
}
void _initializeDesktopMode() {
// Create text controllers for desktop input fields
for (int i = 0; i < widget.task.maxBarcodeCount; i++) {
_textControllers.add(TextEditingController());
}
setState(() {
_isScannerInitialized = true;
});
}
void _onBarcodeDetected(BarcodeCapture capture) {
final List<Barcode> barcodes = capture.barcodes;
for (final barcode in barcodes) {
final String? code = barcode.rawValue;
if (code != null && code.isNotEmpty && !_scannedBarcodes.contains(code)) {
if (_scannedBarcodes.length < widget.task.maxBarcodeCount) {
setState(() {
_scannedBarcodes.add(code);
});
}
}
}
}
void _removeBarcode(int index) {
setState(() {
_scannedBarcodes.removeAt(index);
});
}
void _finishTask() {
final List<String> barcodes;
if (_isMobilePlatform) {
barcodes = _scannedBarcodes;
} else {
// Collect barcodes from text fields
barcodes = [];
for (final controller in _textControllers) {
if (controller.text.trim().isNotEmpty) {
barcodes.add(controller.text.trim());
}
}
}
// Navigate back to task view first
Navigator.of(context).pop();
// Then call the completion callback
widget.onBarcodesCompleted(barcodes);
}
bool _canFinish() {
if (_isMobilePlatform) {
return _scannedBarcodes.length >= widget.task.minBarcodeCount;
} else {
int filledFields = 0;
for (final controller in _textControllers) {
if (controller.text.trim().isNotEmpty) {
filledFields++;
}
}
return filledFields >= widget.task.minBarcodeCount;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).barcodeScan),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(),
),
),
body: Column(
children: [
const OfflineBanner(),
Expanded(
child:
_isScannerInitialized
? (_isMobilePlatform
? _buildMobileView()
: _buildDesktopView())
: const Center(child: CircularProgressIndicator()),
),
],
),
);
}
Widget _buildMobileView() {
return Column(
children: [
// Scanner view
Expanded(
flex: 3,
child: Stack(
children: [
MobileScanner(
controller: _scannerController,
onDetect: _onBarcodeDetected,
),
// Overlay with scanning frame
Container(
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.5),
),
child: Center(
child: Container(
width: 250,
height: 250,
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 2),
borderRadius: BorderRadius.circular(12),
),
child: Container(
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
border: Border.all(color: Colors.green, width: 2),
borderRadius: BorderRadius.circular(8),
),
),
),
),
),
],
),
),
// Scanned barcodes list
Expanded(
flex: 2,
child: Container(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${AppLocalizations.of(context).scannedBarcodes} (${_scannedBarcodes.length}/${widget.task.maxBarcodeCount})',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'${AppLocalizations.of(context).minBarcodes} ${widget.task.minBarcodeCount} ${AppLocalizations.of(context).barcodesRequired}',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
const SizedBox(height: 16),
Expanded(
child: ListView.builder(
itemCount: _scannedBarcodes.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
leading: const Icon(Icons.qr_code),
title: Text(_scannedBarcodes[index]),
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _removeBarcode(index),
),
),
);
},
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _canFinish() ? _finishTask : null,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(AppLocalizations.of(context).finish),
),
),
],
),
),
),
],
);
}
Widget _buildDesktopView() {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context).enterBarcode,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
'${AppLocalizations.of(context).barcodeEnterDescription} (${widget.task.minBarcodeCount}-${widget.task.maxBarcodeCount})',
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
const SizedBox(height: 24),
Expanded(
child: ListView.builder(
itemCount: widget.task.maxBarcodeCount,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: TextField(
controller: _textControllers[index],
decoration: InputDecoration(
labelText:
index < widget.task.minBarcodeCount
? AppLocalizations.of(
context,
).barcodeNumberRequired(index + 1)
: AppLocalizations.of(
context,
).barcodeNumberOptional(index + 1),
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.qr_code),
),
onChanged: (value) {
setState(() {
// Trigger rebuild to update button state
});
},
),
);
},
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _canFinish() ? _finishTask : null,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(AppLocalizations.of(context).finish),
),
),
],
),
);
}
}