feat(abschluss): interne Notiz nach den Unterschriften abfragen
- Nach "Abschliessen" oeffnet sich ein Blatt "Interne Notiz" (optional, max. 2000 Zeichen). Ohne Eingabe genuegt ein Tipp auf "Ohne Notiz abschliessen"; "Zurueck zur Unterschrift" bricht nur das Abschliessen ab - Die Notiz reist im Abschluss-Aufruf mit (internalNote) und landet im Lieferbericht, nicht in den Kunden-Notizen - API-Client aus aktueller Backend-Spec neu generiert - Widget-Tests fuer das Blatt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@ -0,0 +1,145 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Entscheidung im Blatt „Interne Notiz". `null` als Ergebnis von
|
||||
/// [showInternalNoteSheet] heißt: zurück zur Unterschrift, nichts abschließen.
|
||||
class InternalNoteDecision {
|
||||
const InternalNoteDecision(this.note);
|
||||
|
||||
/// Getrimmter Text oder `null` = ohne Notiz abschließen.
|
||||
final String? note;
|
||||
}
|
||||
|
||||
/// Maximale Länge — deckungsgleich mit dem Backend (2000 Zeichen).
|
||||
const int internalNoteMaxLength = 2000;
|
||||
|
||||
/// Fragt nach beiden Unterschriften optional eine interne Notiz ab.
|
||||
///
|
||||
/// Ohne Eingabe ist der große Button „Ohne Notiz abschließen" — der Fahrer
|
||||
/// braucht also nur einen Tipp, wenn er nichts notieren will. Die Notiz ist
|
||||
/// nur für interne Zwecke (Lieferbericht) und nicht Teil der Bestätigung
|
||||
/// durch den Kunden.
|
||||
Future<InternalNoteDecision?> showInternalNoteSheet(BuildContext context) {
|
||||
return showModalBottomSheet<InternalNoteDecision>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
isDismissible: false,
|
||||
enableDrag: false,
|
||||
builder: (_) => const _InternalNoteSheet(),
|
||||
);
|
||||
}
|
||||
|
||||
class _InternalNoteSheet extends StatefulWidget {
|
||||
const _InternalNoteSheet();
|
||||
|
||||
@override
|
||||
State<_InternalNoteSheet> createState() => _InternalNoteSheetState();
|
||||
}
|
||||
|
||||
class _InternalNoteSheetState extends State<_InternalNoteSheet> {
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Button-Label hängt vom Text ab („Ohne" vs. „Mit Notiz").
|
||||
_controller.addListener(() => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String? get _note {
|
||||
final text = _controller.text.trim();
|
||||
return text.isEmpty ? null : text;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final hasNote = _note != null;
|
||||
return Padding(
|
||||
// Tastatur schiebt das Blatt hoch, statt die Buttons zu verdecken.
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
// Unten Abstand zur System-Navigationsleiste, sonst liegt
|
||||
// „Zurück zur Unterschrift" darunter.
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.sticky_note_2_outlined,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'Interne Notiz',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Optional. Nur für interne Zwecke, der Kunde sieht sie nicht. '
|
||||
'Die Notiz erscheint im Lieferbericht.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _controller,
|
||||
minLines: 3,
|
||||
maxLines: 6,
|
||||
maxLength: internalNoteMaxLength,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'z. B. Kunde wünscht Rückruf wegen Montage',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FilledButton.icon(
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(56),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
onPressed:
|
||||
() =>
|
||||
Navigator.of(context).pop(InternalNoteDecision(_note)),
|
||||
icon: const Icon(Icons.check),
|
||||
label: Text(
|
||||
hasNote ? 'Mit Notiz abschließen' : 'Ohne Notiz abschließen',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(44),
|
||||
),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Zurück zur Unterschrift'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user