717 lines
23 KiB
Dart
717 lines
23 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
import 'package:image_picker/image_picker.dart';
|
||
|
||
import 'package:hl_lieferservice/domain/entity/delivery.dart';
|
||
import 'package:hl_lieferservice/domain/entity/delivery_note.dart';
|
||
import 'package:hl_lieferservice/domain/entity/tour_details.dart';
|
||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
|
||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart';
|
||
import 'package:hl_lieferservice/widget/attachment_image.dart';
|
||
|
||
/// Step 2 — Notizen & Fotos.
|
||
///
|
||
/// Die UI trennt bewusst in **zwei Sektionen**, weil es zwei
|
||
/// unterschiedliche Dinge sind:
|
||
/// * **Notizen** (Text): anlegen / bearbeiten / löschen über den
|
||
/// `TourBloc` (Backend-Endpoints vorhanden).
|
||
/// * **Fotos** (Bild-Notizen): `image_picker` → Upload über
|
||
/// `TourBloc.UploadDeliveryNoteImage`. Das Backend schiebt das Bild nach
|
||
/// DOCUframe und legt eine Notiz mit der Referenz an. Fotos werden als
|
||
/// Thumbnail angezeigt (Tap → formatfüllend) und können nur gelöscht,
|
||
/// nicht inline bearbeitet werden.
|
||
///
|
||
/// Datenmodell: beides sind `DeliveryNote`s. Unterschieden wird über
|
||
/// `imageAttachment != null` (Foto) bzw. `text != null` (Notiz).
|
||
class StepNotes extends StatelessWidget {
|
||
const StepNotes({super.key, required this.delivery, required this.details});
|
||
|
||
final Delivery delivery;
|
||
final TourDetails details;
|
||
|
||
void _openAddNoteDialog(BuildContext context) {
|
||
final tourBloc = context.read<TourBloc>();
|
||
showDialog<void>(
|
||
context: context,
|
||
builder: (_) => _NoteEditorDialog(
|
||
title: 'Notiz hinzufügen',
|
||
onSubmit: (text) => tourBloc.add(
|
||
AddDeliveryNote(deliveryId: delivery.id, text: text),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _pickImage(BuildContext context) async {
|
||
// Bloc vor dem await greifen — danach kein context-Zugriff über den
|
||
// async-Gap.
|
||
final tourBloc = context.read<TourBloc>();
|
||
final picker = ImagePicker();
|
||
// Bild schon on-device runterskalieren + JPEG-komprimieren: Foto-Notizen
|
||
// brauchen keine 12-MP-Originale. Spart Upload-/Speicher-/Report-Größe
|
||
// (ein 4080×3060-Foto ~2,9 MB → ~200–400 KB). 1600 px / Q82 deckt sich mit
|
||
// dem Backend-Report-Renderer.
|
||
final file = await picker.pickImage(
|
||
source: ImageSource.camera,
|
||
maxWidth: 1600,
|
||
maxHeight: 1600,
|
||
imageQuality: 82,
|
||
);
|
||
if (file == null) return;
|
||
final bytes = await file.readAsBytes();
|
||
tourBloc.add(
|
||
UploadDeliveryNoteImage(
|
||
deliveryId: delivery.id,
|
||
filename: file.name,
|
||
mime: file.mimeType ?? _mimeFromName(file.name),
|
||
bytes: bytes,
|
||
),
|
||
);
|
||
}
|
||
|
||
/// Grober MIME-Fallback, wenn der Picker keinen Typ liefert (Kamera gibt
|
||
/// meist JPEG). Reicht für das `Content-Type` des Multipart-Felds.
|
||
String _mimeFromName(String name) {
|
||
final lower = name.toLowerCase();
|
||
if (lower.endsWith('.png')) return 'image/png';
|
||
if (lower.endsWith('.heic')) return 'image/heic';
|
||
if (lower.endsWith('.webp')) return 'image/webp';
|
||
return 'image/jpeg';
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final notes = details.notesOf(delivery.id);
|
||
// Notizen & Fotos sind nur bei aktiver Lieferung änderbar. Ist die
|
||
// Lieferung beendet (abgeschlossen/abgebrochen/pausiert), bleiben sie
|
||
// sichtbar, aber read-only: kein FAB, keine Aktions-Menüs, kein Löschen.
|
||
final active = delivery.state == DeliveryState.active;
|
||
// Sauber in Text-Notizen und Fotos aufteilen — getrennte Sektionen.
|
||
final textNotes =
|
||
notes.where((n) => n.imageAttachment == null).toList(growable: false);
|
||
final photoNotes =
|
||
notes.where((n) => n.imageAttachment != null).toList(growable: false);
|
||
return Stack(
|
||
children: [
|
||
ListView(
|
||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 100),
|
||
children: [
|
||
if (!active) ...[
|
||
const _ReadOnlyBanner(),
|
||
const SizedBox(height: 16),
|
||
],
|
||
_SectionHeader(text: 'Notizen (${textNotes.length})'),
|
||
const SizedBox(height: 8),
|
||
if (textNotes.isEmpty)
|
||
const _EmptyHint(
|
||
icon: Icons.notes,
|
||
text: 'Noch keine Notizen erfasst.',
|
||
)
|
||
else
|
||
for (final n in textNotes)
|
||
_NoteCard(note: n, deliveryId: delivery.id, active: active),
|
||
const SizedBox(height: 24),
|
||
_SectionHeader(text: 'Fotos (${photoNotes.length})'),
|
||
const SizedBox(height: 8),
|
||
if (photoNotes.isEmpty)
|
||
const _EmptyHint(
|
||
icon: Icons.photo_camera_outlined,
|
||
text: 'Noch keine Fotos aufgenommen.',
|
||
)
|
||
else
|
||
for (final n in photoNotes)
|
||
_PhotoCard(note: n, deliveryId: delivery.id, active: active),
|
||
],
|
||
),
|
||
// FAB nur bei aktiver Lieferung — sonst ist Hinzufügen gesperrt.
|
||
if (active)
|
||
Align(
|
||
alignment: Alignment.bottomRight,
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: _AddMenu(
|
||
onAddNote: () => _openAddNoteDialog(context),
|
||
onAddImage: () => _pickImage(context),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Hinweis-Balken oben in der Notiz-Sektion, wenn die Lieferung nicht mehr
|
||
/// aktiv ist — Notizen/Fotos sind dann reine Anzeige.
|
||
class _ReadOnlyBanner extends StatelessWidget {
|
||
const _ReadOnlyBanner();
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||
decoration: BoxDecoration(
|
||
color: theme.colorScheme.surfaceContainerHighest,
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Icon(Icons.lock_outline,
|
||
size: 18, color: theme.colorScheme.onSurfaceVariant),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Text(
|
||
'Lieferung beendet — Notizen & Fotos können nicht mehr '
|
||
'hinzugefügt, geändert oder gelöscht werden.',
|
||
style: theme.textTheme.bodySmall?.copyWith(
|
||
color: theme.colorScheme.onSurfaceVariant,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _SectionHeader extends StatelessWidget {
|
||
const _SectionHeader({required this.text});
|
||
final String text;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Text(
|
||
text,
|
||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _EmptyHint extends StatelessWidget {
|
||
const _EmptyHint({required this.icon, required this.text});
|
||
final IconData icon;
|
||
final String text;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
return Card(
|
||
margin: EdgeInsets.zero,
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Row(
|
||
children: [
|
||
Icon(icon, color: theme.colorScheme.onSurfaceVariant),
|
||
const SizedBox(width: 12),
|
||
Text(
|
||
text,
|
||
style: TextStyle(
|
||
color: theme.colorScheme.onSurfaceVariant,
|
||
fontStyle: FontStyle.italic,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
enum _NoteAction { edit, delete }
|
||
|
||
/// Geteiltes Zeitformat für Notiz- und Foto-Karten.
|
||
String _formatNoteTime(DateTime t) =>
|
||
'${t.day.toString().padLeft(2, "0")}.${t.month.toString().padLeft(2, "0")}.${t.year} '
|
||
'${t.hour.toString().padLeft(2, "0")}:${t.minute.toString().padLeft(2, "0")}';
|
||
|
||
/// Geteilter Lösch-Bestätigungsdialog. Wording variiert je nachdem, ob eine
|
||
/// Text-Notiz oder ein Foto entfernt wird; gefeuert wird in beiden Fällen
|
||
/// dasselbe `DeleteDeliveryNote`-Event (Foto ist intern auch eine Notiz).
|
||
Future<void> _confirmDeleteNote(
|
||
BuildContext context, {
|
||
required String deliveryId,
|
||
required String noteId,
|
||
required bool isPhoto,
|
||
}) async {
|
||
final tourBloc = context.read<TourBloc>();
|
||
final confirmed = await showDialog<bool>(
|
||
context: context,
|
||
builder: (ctx) => AlertDialog(
|
||
title: Text(isPhoto ? 'Foto löschen?' : 'Notiz löschen?'),
|
||
content: Text(
|
||
isPhoto
|
||
? 'Das Foto wird dauerhaft entfernt.'
|
||
: 'Die Notiz wird dauerhaft entfernt.',
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(ctx).pop(false),
|
||
child: const Text('Abbrechen'),
|
||
),
|
||
FilledButton(
|
||
style: FilledButton.styleFrom(
|
||
backgroundColor: Theme.of(ctx).colorScheme.error,
|
||
foregroundColor: Theme.of(ctx).colorScheme.onError,
|
||
),
|
||
onPressed: () => Navigator.of(ctx).pop(true),
|
||
child: const Text('Löschen'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
if (confirmed != true) return;
|
||
tourBloc.add(DeleteDeliveryNote(deliveryId: deliveryId, noteId: noteId));
|
||
}
|
||
|
||
/// Karte einer **Text-Notiz**. Normale Notizen sind bearbeitbar und löschbar.
|
||
/// **System-verwaltete** Grund-Notizen (Mengen-Gutschrift via
|
||
/// `creditDeliveryItemId` oder Betrags-Gutschrift via `isAmountCreditNote`)
|
||
/// dürfen vom Fahrer nicht manuell geändert/gelöscht werden — sie werden
|
||
/// automatisch mit der jeweiligen Gutschrift angelegt und wieder entfernt.
|
||
class _NoteCard extends StatelessWidget {
|
||
const _NoteCard({
|
||
required this.note,
|
||
required this.deliveryId,
|
||
required this.active,
|
||
});
|
||
final DeliveryNote note;
|
||
final String deliveryId;
|
||
|
||
/// Nur bei aktiver Lieferung darf bearbeitet/gelöscht werden.
|
||
final bool active;
|
||
|
||
void _openEditDialog(BuildContext context) {
|
||
final tourBloc = context.read<TourBloc>();
|
||
showDialog<void>(
|
||
context: context,
|
||
builder: (_) => _NoteEditorDialog(
|
||
title: 'Notiz bearbeiten',
|
||
initialText: note.text,
|
||
onSubmit: (text) => tourBloc.add(
|
||
UpdateDeliveryNote(
|
||
deliveryId: deliveryId,
|
||
noteId: note.id,
|
||
text: text,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
// System-verwaltete Grund-Notiz: kein manuelles Bearbeiten/Löschen.
|
||
final isSystemManaged =
|
||
note.creditDeliveryItemId != null || note.isAmountCreditNote;
|
||
// Aktions-Menü nur bei aktiver Lieferung UND nicht-System-Notiz.
|
||
final canEdit = active && !isSystemManaged;
|
||
return Card(
|
||
margin: const EdgeInsets.only(bottom: 8),
|
||
child: Padding(
|
||
padding: const EdgeInsets.fromLTRB(12, 12, 4, 12),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Expanded(
|
||
child: Padding(
|
||
padding: const EdgeInsets.only(top: 4),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
note.text ?? '',
|
||
style: const TextStyle(fontSize: 14),
|
||
),
|
||
const SizedBox(height: 6),
|
||
Text(
|
||
'Personalnr. ${note.authorPersonalnummer} '
|
||
'· ${_formatNoteTime(note.createdAt)}',
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
color: theme.colorScheme.onSurfaceVariant,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
if (!canEdit)
|
||
Padding(
|
||
padding: const EdgeInsets.only(right: 8, top: 6),
|
||
child: Tooltip(
|
||
message: isSystemManaged
|
||
? 'Automatisch verwaltet – wird mit der Gutschrift '
|
||
'angelegt und beim Zurücknehmen wieder entfernt.'
|
||
: 'Lieferung beendet – Notiz nicht mehr änderbar.',
|
||
child: Icon(
|
||
Icons.lock_outline,
|
||
size: 18,
|
||
color: theme.colorScheme.onSurfaceVariant,
|
||
),
|
||
),
|
||
)
|
||
else
|
||
PopupMenuButton<_NoteAction>(
|
||
icon: const Icon(Icons.more_vert),
|
||
tooltip: 'Notiz-Aktionen',
|
||
onSelected: (action) {
|
||
switch (action) {
|
||
case _NoteAction.edit:
|
||
_openEditDialog(context);
|
||
case _NoteAction.delete:
|
||
_confirmDeleteNote(
|
||
context,
|
||
deliveryId: deliveryId,
|
||
noteId: note.id,
|
||
isPhoto: false,
|
||
);
|
||
}
|
||
},
|
||
itemBuilder: (context) => [
|
||
const PopupMenuItem(
|
||
value: _NoteAction.edit,
|
||
child: Row(
|
||
children: [
|
||
Icon(Icons.edit_outlined),
|
||
SizedBox(width: 12),
|
||
Text('Bearbeiten'),
|
||
],
|
||
),
|
||
),
|
||
PopupMenuItem(
|
||
value: _NoteAction.delete,
|
||
child: Row(
|
||
children: [
|
||
Icon(Icons.delete_outline,
|
||
color: theme.colorScheme.error),
|
||
const SizedBox(width: 12),
|
||
Text(
|
||
'Löschen',
|
||
style: TextStyle(color: theme.colorScheme.error),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Karte eines **Fotos** — Thumbnail (Tap → formatfüllend) plus Metazeile
|
||
/// mit Lösch-Button. Kein Inline-Edit (ein Foto bearbeitet man nicht).
|
||
class _PhotoCard extends StatelessWidget {
|
||
const _PhotoCard({
|
||
required this.note,
|
||
required this.deliveryId,
|
||
required this.active,
|
||
});
|
||
final DeliveryNote note;
|
||
final String deliveryId;
|
||
|
||
/// Nur bei aktiver Lieferung darf das Foto gelöscht werden.
|
||
final bool active;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
return Card(
|
||
margin: const EdgeInsets.only(bottom: 8),
|
||
clipBehavior: Clip.antiAlias,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
_NoteImageThumb(
|
||
attachmentId: note.imageAttachment!,
|
||
deleted: note.imageAttachmentDeleted,
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(12, 8, 4, 8),
|
||
child: Row(
|
||
children: [
|
||
Icon(
|
||
Icons.photo_camera_outlined,
|
||
size: 16,
|
||
color: theme.colorScheme.onSurfaceVariant,
|
||
),
|
||
const SizedBox(width: 6),
|
||
Expanded(
|
||
child: Text(
|
||
'Personalnr. ${note.authorPersonalnummer} '
|
||
'· ${_formatNoteTime(note.createdAt)}',
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
color: theme.colorScheme.onSurfaceVariant,
|
||
),
|
||
),
|
||
),
|
||
if (active)
|
||
IconButton(
|
||
icon: Icon(
|
||
Icons.delete_outline,
|
||
color: theme.colorScheme.error,
|
||
),
|
||
tooltip: 'Foto löschen',
|
||
onPressed: () => _confirmDeleteNote(
|
||
context,
|
||
deliveryId: deliveryId,
|
||
noteId: note.id,
|
||
isPhoto: true,
|
||
),
|
||
)
|
||
else
|
||
Padding(
|
||
padding: const EdgeInsets.only(right: 8),
|
||
child: Tooltip(
|
||
message: 'Lieferung beendet – Foto nicht mehr löschbar.',
|
||
child: Icon(
|
||
Icons.lock_outline,
|
||
size: 18,
|
||
color: theme.colorScheme.onSurfaceVariant,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Thumbnail einer Bild-Notiz; Tap öffnet das Bild formatfüllend mit
|
||
/// Zoom/Pan.
|
||
class _NoteImageThumb extends StatelessWidget {
|
||
const _NoteImageThumb({required this.attachmentId, this.deleted = false});
|
||
|
||
final String attachmentId;
|
||
|
||
/// Lokale Bilddatei nach Report-Upload gelöscht → Hinweis statt Vorschau.
|
||
final bool deleted;
|
||
|
||
void _openFull(BuildContext context) {
|
||
Navigator.of(context).push(
|
||
MaterialPageRoute(
|
||
builder: (_) => Scaffold(
|
||
backgroundColor: Colors.black,
|
||
appBar: AppBar(
|
||
backgroundColor: Colors.black,
|
||
foregroundColor: Colors.white,
|
||
),
|
||
body: Center(
|
||
child: InteractiveViewer(
|
||
minScale: 0.5,
|
||
maxScale: 5,
|
||
child: AttachmentImage(
|
||
attachmentId: attachmentId,
|
||
width: 2048,
|
||
height: 2048,
|
||
quality: 90,
|
||
fit: BoxFit.contain,
|
||
deleted: deleted,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
// Kein eigenes Clipping — die umgebende `_PhotoCard` clippt bereits
|
||
// (Clip.antiAlias), sonst gäbe es doppelt gerundete Ecken.
|
||
return GestureDetector(
|
||
// Gelöschtes Bild → kein Vollbild öffnen (es gibt nichts zu laden).
|
||
onTap: deleted ? null : () => _openFull(context),
|
||
child: SizedBox(
|
||
height: 160,
|
||
width: double.infinity,
|
||
child: AttachmentImage(
|
||
attachmentId: attachmentId,
|
||
width: 600,
|
||
height: 600,
|
||
deleted: deleted,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ─── Add-Menu (FAB) ─────────────────────────────────────────────────────
|
||
|
||
enum _AddAction { note, image }
|
||
|
||
class _AddMenu extends StatelessWidget {
|
||
const _AddMenu({required this.onAddNote, required this.onAddImage});
|
||
|
||
final VoidCallback onAddNote;
|
||
final VoidCallback onAddImage;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return PopupMenuButton<_AddAction>(
|
||
tooltip: 'Hinzufügen',
|
||
onSelected: (action) {
|
||
switch (action) {
|
||
case _AddAction.note:
|
||
onAddNote();
|
||
case _AddAction.image:
|
||
onAddImage();
|
||
}
|
||
},
|
||
itemBuilder: (context) => const [
|
||
PopupMenuItem(
|
||
value: _AddAction.note,
|
||
child: Row(
|
||
children: [
|
||
Icon(Icons.edit_note),
|
||
SizedBox(width: 12),
|
||
Text('Notiz schreiben'),
|
||
],
|
||
),
|
||
),
|
||
PopupMenuItem(
|
||
value: _AddAction.image,
|
||
child: Row(
|
||
children: [
|
||
Icon(Icons.camera_alt_outlined),
|
||
SizedBox(width: 12),
|
||
Text('Foto aufnehmen'),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
child: FloatingActionButton.extended(
|
||
onPressed: null, // PopupMenuButton fängt den Tap
|
||
icon: const Icon(Icons.add),
|
||
label: const Text('Hinzufügen'),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ─── Notiz-Dialog (Text) ────────────────────────────────────────────────
|
||
|
||
/// Editor-Dialog für Text-Notizen — geteilt zwischen „Hinzufügen" und
|
||
/// „Bearbeiten". Liefert den getrimmten Text per [onSubmit]; der Aufrufer
|
||
/// entscheidet, ob daraus ein `AddDeliveryNote` oder `UpdateDeliveryNote`
|
||
/// wird.
|
||
class _NoteEditorDialog extends StatefulWidget {
|
||
const _NoteEditorDialog({
|
||
required this.title,
|
||
required this.onSubmit,
|
||
this.initialText,
|
||
});
|
||
|
||
final String title;
|
||
final void Function(String text) onSubmit;
|
||
final String? initialText;
|
||
|
||
@override
|
||
State<_NoteEditorDialog> createState() => _NoteEditorDialogState();
|
||
}
|
||
|
||
class _NoteEditorDialogState extends State<_NoteEditorDialog> {
|
||
late final TextEditingController _controller =
|
||
TextEditingController(text: widget.initialText ?? '');
|
||
bool _empty = true;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_empty = _controller.text.trim().isEmpty;
|
||
_controller.addListener(() {
|
||
setState(() => _empty = _controller.text.trim().isEmpty);
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_controller.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
void _save() {
|
||
final text = _controller.text.trim();
|
||
if (text.isEmpty) return;
|
||
widget.onSubmit(text);
|
||
Navigator.of(context).pop();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Dialog(
|
||
insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||
child: ConstrainedBox(
|
||
constraints: BoxConstraints(
|
||
maxHeight: MediaQuery.of(context).size.height * 0.55,
|
||
),
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(20),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
widget.title,
|
||
style: Theme.of(context).textTheme.titleLarge,
|
||
),
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.close),
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 12),
|
||
// TODO(B6): Templates-Dropdown ergänzen, sobald Backend
|
||
// Notiz-Templates als Stammdaten anbietet.
|
||
Expanded(
|
||
child: TextField(
|
||
controller: _controller,
|
||
autofocus: true,
|
||
maxLines: null,
|
||
expands: true,
|
||
textAlignVertical: TextAlignVertical.top,
|
||
decoration: const InputDecoration(
|
||
labelText: 'Notiz',
|
||
border: OutlineInputBorder(),
|
||
alignLabelWithHint: true,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.end,
|
||
children: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
child: const Text('Abbrechen'),
|
||
),
|
||
const SizedBox(width: 8),
|
||
FilledButton.icon(
|
||
onPressed: _empty ? null : _save,
|
||
icon: const Icon(Icons.save),
|
||
label: const Text('Speichern'),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|