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

+54 -21
View File
@@ -8,6 +8,7 @@ import 'services/dart_mq.dart';
import 'services/chat_service.dart';
import 'models/delivery_station.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';
@@ -23,6 +24,13 @@ import 'services/database_service.dart';
import 'navigation_observer.dart';
import 'routing_view.dart';
@visibleForTesting
Color? jobCardBackgroundColor(JobProgress progress) {
if (progress.isCompleted) return AppColors.successSoft;
if (progress.hasProgress) return AppColors.warningSoft;
return null;
}
class JobsView extends StatefulWidget {
const JobsView({super.key});
@@ -63,6 +71,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
bool _isLoggingOut = false;
final DatabaseService _databaseService = DatabaseService();
Map<String, bool> _taskStatuses = const {};
Map<String, Set<int>> _completedStations = const {};
Map<String, bool> _jobSeen = const {};
Map<String, double> _jobSwipeOffsets = const {};
String? _openJobId;
@@ -479,9 +488,15 @@ class _JobsViewState extends State<JobsView> with RouteAware {
Future<void> _loadLocalTaskStatuses() async {
final map = await _databaseService.loadAllTaskStatuses();
final completedStations = <String, Set<int>>{};
for (final job in _appState.assignedJobs) {
completedStations[job.id] = await _databaseService
.loadCompletedStationOrders(job);
}
if (!mounted) return;
setState(() {
_taskStatuses = map;
_completedStations = completedStations;
});
}
@@ -950,25 +965,15 @@ class _JobsViewState extends State<JobsView> with RouteAware {
statusColor = AppColors.textMuted;
}
// Determine card background color based on task completion
final progress = JobProgress(
job,
taskStatuses: _taskStatuses,
localCompletedStations: _completedStations[job.id] ?? const {},
);
final totalTasks = job.tasks.length;
int completedTasks = 0;
for (final t in job.tasks) {
final isCompleted = _taskStatuses[t.id] ?? t.completed;
if (isCompleted) completedTasks++;
}
// Check if all tasks are completed (job is done)
final bool isJobCompleted = totalTasks > 0 && completedTasks == totalTasks;
Color? cardBg;
if (totalTasks == 0 || completedTasks == 0) {
cardBg = null; // unchanged (default)
} else if (completedTasks > 0 && completedTasks < totalTasks) {
cardBg = AppColors.warningSoft;
} else if (completedTasks == totalTasks) {
cardBg = AppColors.successSoft;
}
final completedTasks = progress.completedTaskCount;
final isJobCompleted = progress.isCompleted;
final cardBg = jobCardBackgroundColor(progress);
// Build robust display strings with fallbacks
final pickupName = _joinNonEmpty([job.pickupFirstName, job.pickupLastName]);
final pickupDisplayName =
@@ -1211,7 +1216,17 @@ class _JobsViewState extends State<JobsView> with RouteAware {
),
],
),
// Progress bar for tasks
if (isJobCompleted) ...[
const SizedBox(height: 8),
Text(
AppLocalizations.of(context).jobCompleted,
style: const TextStyle(
color: AppColors.success,
fontWeight: FontWeight.w600,
),
),
],
// Progress bar for tasks (optional tasks retain their actual state).
if (totalTasks > 0) ...[
const SizedBox(height: 8),
Text(
@@ -1235,7 +1250,8 @@ class _JobsViewState extends State<JobsView> with RouteAware {
minHeight: 8,
backgroundColor: AppColors.border,
valueColor: AlwaysStoppedAnimation<Color>(
completedTasks >= totalTasks
isJobCompleted ||
completedTasks >= totalTasks
? AppColors.success
: (completedTasks > 0
? AppColors.warning
@@ -1447,6 +1463,14 @@ class _JobsViewState extends State<JobsView> with RouteAware {
}
void _toggleTaskCompletion(Job job, int taskIndex) {
final progress = JobProgress(
job,
taskStatuses: _taskStatuses,
localCompletedStations: _completedStations[job.id] ?? const {},
);
if (progress.isStationClosed(job.tasks[taskIndex].stationOrder ?? 0)) {
return;
}
// Create a new task with toggled completion status
final updatedTask = job.tasks[taskIndex].copyWith(
completed: !job.tasks[taskIndex].completed,
@@ -1488,6 +1512,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
id: job.id,
jobNumber: job.jobNumber,
status: job.status,
completedStationOrders: job.completedStationOrders,
createdAt: job.createdAt,
updatedAt: job.updatedAt,
createdBy: job.createdBy,
@@ -1690,7 +1715,15 @@ class _JobsViewState extends State<JobsView> with RouteAware {
const SizedBox(height: 8),
...job.tasks.asMap().entries.map(
(entry) => GestureDetector(
onTap: () => _toggleTaskCompletion(job, entry.key),
onTap:
JobProgress(
job,
taskStatuses: _taskStatuses,
localCompletedStations:
_completedStations[job.id] ?? const {},
).isStationClosed(entry.value.stationOrder ?? 0)
? null
: () => _toggleTaskCompletion(job, entry.key),
child: Container(
margin: const EdgeInsets.only(bottom: 4),
padding: const EdgeInsets.symmetric(