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>
334 lines
10 KiB
Dart
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),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|