feat: Adressbuch mit Kundennummer, Update-Flow und interne Einträge

- Menüpunkt "Kunden" in "Adressbuch" umbenannt und App-Label
  "Verfügbare Jobs" zu "Auftragsliste" geändert (alle 10 Sprachen)
- Fortlaufende Kundennummer (usrId) ab 10000 über neuen
  SequenceGeneratorService und Counter-Dokument in misc-Collection
- Abholung/Lieferstation-Dialog: Änderungen an verknüpften
  Stammdaten aktualisieren den bestehenden Adressbuch-Eintrag
  statt einen neuen zu erzeugen; Checkbox-Label wechselt zu
  "Adresse im Adressbuch aktualisieren"
- Geänderte Adressen ohne Checkbox werden als interner Customer
  (internal=true) gesichert und im Adressbuch ausgeblendet
- E-Mail in AddCustomer und in Stations-Dialogen kein Pflichtfeld
  mehr; "(Login)" aus profile.email entfernt
- Manuelles Beenden eines Auftrags öffnet neue Seite
  JobManualCompleteView statt eines Dialogs
This commit is contained in:
2026-04-20 12:42:56 +02:00
parent 6e8bedd9b4
commit 704d1e7378
42 changed files with 720 additions and 196 deletions

View File

@@ -55,7 +55,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get jobs => 'Jobs';
@override
String get availableJobs => 'Verfügbare Jobs';
String get availableJobs => 'Auftragsliste';
@override
String get chats => 'Chats';

View File

@@ -55,7 +55,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get jobs => 'Jobs';
@override
String get availableJobs => 'Available Jobs';
String get availableJobs => 'Order List';
@override
String get chats => 'Chats';

View File

@@ -42,7 +42,7 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get jobs => 'Trabajos';
@override
String get availableJobs => 'Trabajos Disponibles';
String get availableJobs => 'Lista de pedidos';
@override
String get chats => 'Chats';
@override

View File

@@ -42,7 +42,7 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get jobs => 'Tööd';
@override
String get availableJobs => 'Saadaolevad tööd';
String get availableJobs => 'Tellimuste loend';
@override
String get chats => 'Vestlused';
@override

View File

@@ -42,7 +42,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get jobs => 'Emplois';
@override
String get availableJobs => 'Emplois Disponibles';
String get availableJobs => 'Liste des commandes';
@override
String get chats => 'Discussions';
@override

View File

@@ -42,7 +42,7 @@ class AppLocalizationsLt extends AppLocalizations {
@override
String get jobs => 'Darbai';
@override
String get availableJobs => 'Galimi darbai';
String get availableJobs => 'Užsakymų sąrašas';
@override
String get chats => 'Pokalbiai';
@override

View File

@@ -42,7 +42,7 @@ class AppLocalizationsLv extends AppLocalizations {
@override
String get jobs => 'Darbi';
@override
String get availableJobs => 'Pieejamie darbi';
String get availableJobs => 'Pasūtījumu saraksts';
@override
String get chats => 'Tērzēšanas';
@override

View File

@@ -42,7 +42,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get jobs => 'Zadania';
@override
String get availableJobs => 'Dostępne Zadania';
String get availableJobs => 'Lista zleceń';
@override
String get chats => 'Czaty';
@override

View File

@@ -42,7 +42,7 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get jobs => 'Задания';
@override
String get availableJobs => 'Доступные задания';
String get availableJobs => 'Список заказов';
@override
String get chats => 'Чаты';
@override

View File

@@ -42,7 +42,7 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get jobs => 'İşler';
@override
String get availableJobs => 'Mevcut İşler';
String get availableJobs => 'Sipariş Listesi';
@override
String get chats => 'Sohbetler';
@override

View File

@@ -2,6 +2,8 @@ import '../task.dart';
// Signature Task
class SignatureTask extends Task {
final String? note;
SignatureTask({
required super.id,
required super.jobId,
@@ -14,11 +16,19 @@ class SignatureTask extends Task {
super.title,
super.description,
super.displayName,
this.note,
});
factory SignatureTask.fromJson(Map<String, dynamic> json) {
final commonProps = Task.parseCommonProperties(json);
String? note;
final taskSpecificData = json['taskSpecificData'];
if (taskSpecificData is Map<String, dynamic>) {
final n = taskSpecificData['note'];
if (n is String) note = n;
}
return SignatureTask(
id: commonProps['id'],
jobId: commonProps['jobId'],
@@ -31,6 +41,7 @@ class SignatureTask extends Task {
title: commonProps['title'],
description: commonProps['description'],
displayName: commonProps['displayName'],
note: note,
);
}
@@ -47,7 +58,11 @@ class SignatureTask extends Task {
'taskOrder': taskOrder,
'description': description,
'displayName': displayName,
'taskSpecificData': {'taskType': 'SIGNATURE', 'title': title},
'taskSpecificData': {
'taskType': 'SIGNATURE',
'title': title,
'note': note,
},
};
}
@@ -64,6 +79,7 @@ class SignatureTask extends Task {
String? title,
String? description,
String? displayName,
String? note,
}) {
return SignatureTask(
id: id ?? this.id,
@@ -77,6 +93,7 @@ class SignatureTask extends Task {
title: title ?? this.title,
description: description ?? this.description,
displayName: displayName ?? this.displayName,
note: note ?? this.note,
);
}
}

View File

@@ -831,6 +831,42 @@ class DatabaseService {
}
}
/// Save signature note (Bemerkung) for a task into user_data table
Future<void> saveTaskSignatureNote(String taskId, String note) async {
try {
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return;
}
final key = 'task_signature_note:$taskId';
await saveKeyValue(key, note);
} catch (e, st) {
developer.log(
'Error saving task signature note: $e',
name: 'DatabaseService',
);
developer.log('Stack trace: $st', name: 'DatabaseService');
}
}
/// Load signature note (Bemerkung) for a task from user_data table
Future<String?> loadTaskSignatureNote(String taskId) async {
try {
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return null;
}
return await loadKeyValue('task_signature_note:$taskId');
} catch (e, st) {
developer.log(
'Error loading task signature note: $e',
name: 'DatabaseService',
);
developer.log('Stack trace: $st', name: 'DatabaseService');
return null;
}
}
/// Load signature SVG for a task from user_data table
Future<String?> loadTaskSignature(String taskId) async {
try {

View File

@@ -622,10 +622,11 @@ class _TaskViewState extends State<TaskView> {
builder:
(context) => SignatureCaptureScreen(
task: task,
onSignatureCompleted: (String svg) async {
onSignatureCompleted: (String svg, String note) async {
try {
// Persist SVG only (no PNG)
await _databaseService.saveTaskSignature(task.id, svg);
await _databaseService.saveTaskSignatureNote(task.id, note);
} catch (e, stackTrace) {
developer.log(
'Error saving task signature: $e',
@@ -649,6 +650,7 @@ class _TaskViewState extends State<TaskView> {
'signatureSvg': svg,
'svgLength': svg.length,
'hasSignature': true,
'signatureNote': note,
},
);
},
@@ -896,6 +898,10 @@ class _TaskViewState extends State<TaskView> {
task.description != null
? localizeKnownText(context, task.description!)
: null;
final String? signatureNote =
(task is SignatureTask && task.note != null && task.note!.trim().isNotEmpty)
? task.note!.trim()
: null;
if (displayName?.isNotEmpty == true) {
return Column(
@@ -906,14 +912,39 @@ class _TaskViewState extends State<TaskView> {
const SizedBox(height: 2),
Text(description!, style: subtitleStyle),
],
if (signatureNote != null) ...[
const SizedBox(height: 2),
Text(signatureNote, style: subtitleStyle),
],
],
);
}
if (description?.isNotEmpty == true) {
if (signatureNote != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(description!, style: titleStyle),
const SizedBox(height: 2),
Text(signatureNote, style: subtitleStyle),
],
);
}
return Text(description!, style: titleStyle);
}
if (signatureNote != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_getStandardTaskDisplayText(task), style: titleStyle),
const SizedBox(height: 2),
Text(signatureNote, style: subtitleStyle),
],
);
}
// Fall back to standard text based on task type
return Text(_getStandardTaskDisplayText(task), style: titleStyle);
}

View File

@@ -9,7 +9,7 @@ import '../widgets/offline_banner.dart';
class SignatureCaptureScreen extends StatefulWidget {
final SignatureTask task;
final void Function(String svg) onSignatureCompleted;
final void Function(String svg, String note) onSignatureCompleted;
const SignatureCaptureScreen({
super.key,
@@ -23,6 +23,7 @@ class SignatureCaptureScreen extends StatefulWidget {
class _SignatureCaptureScreenState extends State<SignatureCaptureScreen> {
late final SignatureController _controller;
final TextEditingController _noteController = TextEditingController();
bool _hasSignature = false;
bool _isMobilePlatform = false;
@@ -84,6 +85,7 @@ class _SignatureCaptureScreenState extends State<SignatureCaptureScreen> {
void dispose() {
_controller.removeListener(_onSignatureChanged);
_controller.dispose();
_noteController.dispose();
_restoreOrientation();
super.dispose();
}
@@ -155,14 +157,15 @@ class _SignatureCaptureScreenState extends State<SignatureCaptureScreen> {
// Build SVG from the captured signature points
final String svg = _buildSvgFromPoints(_controller.points);
final String note = _noteController.text.trim();
// Close this screen first to show the updated TaskView quickly
if (!mounted) return;
_restoreOrientation();
Navigator.of(context).pop();
// Then notify the caller (SVG only)
widget.onSignatureCompleted(svg);
// Then notify the caller (SVG + Bemerkung)
widget.onSignatureCompleted(svg, note);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
@@ -233,6 +236,16 @@ class _SignatureCaptureScreenState extends State<SignatureCaptureScreen> {
),
),
),
const SizedBox(height: 12),
TextField(
controller: _noteController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).completeTaskNote,
border: const OutlineInputBorder(),
),
maxLines: 2,
minLines: 1,
),
const SizedBox(height: 16),
Row(
children: [