fix: Stationsabschluss absichern und Kundenformulare anpassen

Sperrt nachträgliche Aufgabenabschlüsse in App und Backend und zeigt erledigte Aufträge trotz offener optionaler Aufgaben grün an. Ergänzt Regressionstests, die Anredeauswahl und optionale Steuerangaben für Kunden. Enthält App-Version 0.9.25, Backend-Version 0.9.26 und das aktualisierte Frontend-Bundle.
This commit is contained in:
Sven committed 2026-10-06 15:09:51 +02:00
1 parent 8758c53e5b
commit 5e6044e4b1
16 files changed
+638 -69

No files matched your search

+74 -15
View File
@@ -6,9 +6,11 @@ import 'package:image/image.dart' as img;
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'app_theme.dart';
import 'app_state.dart';
import 'l10n/app_localizations.dart';
import 'l10n/localization_helpers.dart';
import 'models/job.dart';
import 'models/job_progress.dart';
import 'models/task.dart';
import 'models/tasks/confirmation_task.dart';
import 'models/tasks/photo_task.dart';
@@ -42,6 +44,9 @@ class TaskView extends StatefulWidget {
class _TaskViewState extends State<TaskView> {
final Set<String> _completedTasks = {};
final DatabaseService _databaseService = DatabaseService();
late Job _job;
Set<int> _localCompletedStations = {};
StreamSubscription<void>? _jobsUpdatedSubscription;
bool _statusesLoaded = false;
bool _stationCompleted = false;
bool _isCompletingStation = false;
@@ -51,31 +56,73 @@ class _TaskViewState extends State<TaskView> {
@override
void initState() {
super.initState();
_job = widget.job;
_jobsUpdatedSubscription = AppState().jobsUpdated.listen(
(_) => _loadTaskStatuses(),
);
_loadTaskStatuses();
}
@override
void didUpdateWidget(covariant TaskView oldWidget) {
super.didUpdateWidget(oldWidget);
if (!identical(oldWidget.job, widget.job) ||
oldWidget.stationOrder != widget.stationOrder) {
_job = widget.job;
_statusesLoaded = false;
_loadTaskStatuses();
}
}
@override
void dispose() {
_jobsUpdatedSubscription?.cancel();
super.dispose();
}
JobProgress get _progress => JobProgress(
_job,
taskStatuses: {for (final id in _completedTasks) id: true},
localCompletedStations: _localCompletedStations,
);
bool _canCompleteTask(Task task) =>
mounted &&
_statusesLoaded &&
!_isCompletingStation &&
!_stationCompleted &&
_progress.canCompleteTask(task);
List<Task> get _visibleTasks {
final stationOrder = widget.stationOrder;
if (stationOrder == null) {
return widget.job.tasks;
return _job.tasks;
}
return widget.job.tasks
return _job.tasks
.where((task) => task.stationOrder == stationOrder)
.toList();
}
String get _stationCompletionKey =>
'station_completed:${widget.job.id}:${widget.stationOrder ?? 0}';
'station_completed:${_job.id}:${widget.stationOrder ?? 0}';
/// Load task completion and skipped statuses from database and merge with JSON task states
Future<void> _loadTaskStatuses() async {
var latestJob = _job;
for (final savedJob in await _databaseService.loadJobs()) {
if (savedJob.id == _job.id &&
!savedJob.updatedAt.isBefore(latestJob.updatedAt)) {
latestJob = savedJob;
}
}
final statuses = await _databaseService.loadAllTaskStatuses();
final stationCompleted =
await _databaseService.loadKeyValue(_stationCompletionKey) == '1';
final completedStations = await _databaseService.loadCompletedStationOrders(
latestJob,
);
if (!mounted) return;
setState(() {
_stationCompleted =
stationCompleted || widget.job.status.toUpperCase() == 'COMPLETED';
_job = latestJob;
_localCompletedStations = completedStations;
_statusesLoaded = true;
_completedTasks.clear();
// 1) Add all completed from DB
@@ -85,11 +132,12 @@ class _TaskViewState extends State<TaskView> {
}
}
// 2) Merge: also mark tasks completed if the job JSON already had them completed
for (final t in widget.job.tasks) {
for (final t in _job.tasks) {
if (t.completed) {
_completedTasks.add(t.id);
}
}
_stationCompleted = _progress.isStationClosed(widget.stationOrder ?? 0);
});
}
@@ -100,7 +148,7 @@ class _TaskViewState extends State<TaskView> {
title: Text(
widget.stationTitle?.isNotEmpty == true
? '${AppLocalizations.of(context).tasks} - ${widget.stationTitle}'
: '${AppLocalizations.of(context).tasks} - ${widget.job.jobNumber}',
: '${AppLocalizations.of(context).tasks} - ${_job.jobNumber}',
),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
@@ -189,7 +237,9 @@ class _TaskViewState extends State<TaskView> {
child: ElevatedButton.icon(
onPressed: enabled ? _onCompleteStationPressed : null,
icon: const Icon(Icons.flag),
label: const Text('Station abschließen'),
label: Text(
_stationCompleted ? 'Station abgeschlossen' : 'Station abschließen',
),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
),
@@ -235,12 +285,13 @@ class _TaskViewState extends State<TaskView> {
final stationOrder = widget.stationOrder ?? 0;
try {
await StompService().sendStationCompleted(
jobId: widget.job.id,
jobNumber: widget.job.jobNumber,
jobId: _job.id,
jobNumber: _job.jobNumber,
stationOrder: stationOrder,
hasIncompleteOptionalTasks: _hasIncompleteOptionalTasks,
);
_stationCompleted = true;
_localCompletedStations.add(stationOrder);
await _databaseService.saveKeyValue(_stationCompletionKey, '1');
if (!mounted) return;
ScaffoldMessenger.of(
@@ -289,7 +340,7 @@ class _TaskViewState extends State<TaskView> {
final task = _visibleTasks[index];
final isCompleted = _completedTasks.contains(task.id);
final canBeCompletedNow =
!isCompleted && _arePreviousTasksCompleted(index);
_canCompleteTask(task) && _arePreviousTasksCompleted(index);
// Hintergrundfarbe je nach Status:
// abgeschlossen → hellgrün, bearbeitbar → weiß, gesperrt → hellgrau
@@ -429,6 +480,7 @@ class _TaskViewState extends State<TaskView> {
}
void _showTaskCompletionDialog(Task task, int taskIndex) {
if (!_canCompleteTask(task)) return;
switch (task) {
case ConfirmationTask():
_showConfirmationDialog(task, taskIndex);
@@ -542,10 +594,12 @@ class _TaskViewState extends State<TaskView> {
(context) => PhotoCaptureScreen(
task: task,
onPhotosCompleted: (List<Uint8List> photoData) async {
if (!_canCompleteTask(task)) return;
// Compress + encode photos for network send (limit payload)
final List<String> base64List = await _compressAndEncodePhotos(
photoData,
);
if (!_canCompleteTask(task)) return;
final bool truncated = base64List.length < photoData.length;
// Try to persist full-quality (encoded) photos to DB for offline/backup
@@ -647,6 +701,7 @@ class _TaskViewState extends State<TaskView> {
(context) => SignatureCaptureScreen(
task: task,
onSignatureCompleted: (String svg, String note) async {
if (!_canCompleteTask(task)) return;
try {
// Persist SVG only (no PNG)
await _databaseService.saveTaskSignature(task.id, svg);
@@ -658,6 +713,7 @@ class _TaskViewState extends State<TaskView> {
);
developer.log('Stack trace: $stackTrace', name: 'TaskView');
}
if (!_canCompleteTask(task)) return;
// Store SVG for later use in this TaskView session
setState(() {
_signatureSvgByTask[task.id] = svg;
@@ -690,6 +746,7 @@ class _TaskViewState extends State<TaskView> {
(context) => BarcodeCaptureScreen(
task: task,
onBarcodesCompleted: (List<String> barcodes) async {
if (!_canCompleteTask(task)) return;
try {
// Save barcodes to database for later use
await _databaseService.saveTaskBarcodes(task.id, barcodes);
@@ -758,6 +815,8 @@ class _TaskViewState extends State<TaskView> {
String? taskType,
Map<String, dynamic>? extraData,
}) {
final tasks = _visibleTasks.where((task) => task.id == taskId);
if (tasks.isEmpty || !_canCompleteTask(tasks.first)) return;
final bool hadOpenMandatoryBefore = _hasOpenMandatoryTasks;
setState(() {
_completedTasks.add(taskId);
@@ -1000,7 +1059,7 @@ class _TaskViewState extends State<TaskView> {
}
}
String _getRemark() => widget.job.remark;
String _getRemark() => _job.remark;
String? _getTaskStationLabel(Task task) {
if (widget.stationOrder != null) {
@@ -1011,7 +1070,7 @@ class _TaskViewState extends State<TaskView> {
return null;
}
for (final station in widget.job.deliveryStations) {
for (final station in _job.deliveryStations) {
if (station.stationOrder == stationOrder) {
final suffix =
station.displayName.isNotEmpty ? station.displayName : station.city;