diff --git a/api/2026-06-26_add_igel_stammdaten_fields.sql b/api/2026-06-26_add_igel_stammdaten_fields.sql new file mode 100644 index 0000000..99a7493 --- /dev/null +++ b/api/2026-06-26_add_igel_stammdaten_fields.sql @@ -0,0 +1,9 @@ +ALTER TABLE `igel` + ADD COLUMN `marking` varchar(255) DEFAULT NULL AFTER `location`, + ADD COLUMN `finder` varchar(255) DEFAULT NULL AFTER `marking`, + ADD COLUMN `phone` varchar(255) DEFAULT NULL AFTER `finder`, + ADD COLUMN `email` varchar(255) DEFAULT NULL AFTER `phone`, + ADD COLUMN `admission_reason` varchar(255) DEFAULT NULL AFTER `email`, + ADD COLUMN `animal_condition` varchar(255) DEFAULT NULL AFTER `admission_reason`, + ADD COLUMN `released_at` date DEFAULT NULL AFTER `animal_condition`, + ADD COLUMN `release_location` varchar(255) DEFAULT NULL AFTER `released_at`; diff --git a/api/hedgehogs.php b/api/hedgehogs.php index 7428985..d18ff47 100644 --- a/api/hedgehogs.php +++ b/api/hedgehogs.php @@ -348,7 +348,7 @@ function can_edit(PDO $pdo, int $userId, int $hedgehogId): bool function igel_list(PDO $pdo, int $uid): void { - $stmt = $pdo->prepare('SELECT id, name, gender, feature, note, rescued_at, location, created_at, updated_at FROM igel WHERE user_id = ? ORDER BY created_at DESC'); + $stmt = $pdo->prepare('SELECT id, name, gender, feature, note, rescued_at, location, marking, finder, phone, email, admission_reason, animal_condition, released_at, release_location, created_at, updated_at FROM igel WHERE user_id = ? ORDER BY created_at DESC'); $stmt->execute([$uid]); json($stmt->fetchAll(PDO::FETCH_ASSOC)); } @@ -362,6 +362,14 @@ function igel_create(PDO $pdo, int $uid): void $note = isset($in['note']) ? (string) $in['note'] : null; $rescuedAt = isset($in['rescued_at']) ? (string) $in['rescued_at'] : null; // "YYYY-MM-DD" $location = isset($in['location']) ? trim((string) $in['location']) : null; + $marking = isset($in['marking']) ? trim((string) $in['marking']) : null; + $finder = isset($in['finder']) ? trim((string) $in['finder']) : null; + $phone = isset($in['phone']) ? trim((string) $in['phone']) : null; + $email = isset($in['email']) ? trim((string) $in['email']) : null; + $admissionReason = isset($in['admission_reason']) ? trim((string) $in['admission_reason']) : null; + $animalCondition = isset($in['animal_condition']) ? trim((string) $in['animal_condition']) : null; + $releasedAt = isset($in['released_at']) ? (string) $in['released_at'] : null; + $releaseLocation = isset($in['release_location']) ? trim((string) $in['release_location']) : null; if ($name === '') { json(['error' => 'Name required'], 422); @@ -375,11 +383,15 @@ function igel_create(PDO $pdo, int $uid): void json(['error' => 'Location too long (max 255)'], 422); return; } + if ($releasedAt !== null && $releasedAt !== '' && strtotime($releasedAt) === false) { + json(['error' => 'Invalid released_at (expected YYYY-MM-DD)'], 422); + return; + } $stmt = $pdo->prepare( - 'INSERT INTO igel(user_id, name, gender, feature, note, rescued_at, location) - VALUES(?,?,?,?,?,?,?)' + 'INSERT INTO igel(user_id, name, gender, feature, note, rescued_at, location, marking, finder, phone, email, admission_reason, animal_condition, released_at, release_location) + VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)' ); - $stmt->execute([$uid, $name, $gender, $feature, $note, $rescuedAt ?: null, $location ?: null]); + $stmt->execute([$uid, $name, $gender, $feature, $note, $rescuedAt ?: null, $location ?: null, $marking ?: null, $finder ?: null, $phone ?: null, $email ?: null, $admissionReason ?: null, $animalCondition ?: null, $releasedAt ?: null, $releaseLocation ?: null]); $id = (int) $pdo->lastInsertId(); json([ @@ -389,7 +401,15 @@ function igel_create(PDO $pdo, int $uid): void 'feature' => $feature, 'note' => $note, 'rescued_at' => $rescuedAt, - 'location' => $location + 'location' => $location, + 'marking' => $marking, + 'finder' => $finder, + 'phone' => $phone, + 'email' => $email, + 'admission_reason' => $admissionReason, + 'animal_condition' => $animalCondition, + 'released_at' => $releasedAt, + 'release_location' => $releaseLocation ], 201); } @@ -399,7 +419,7 @@ function igel_get(PDO $pdo, int $uid, int $id): void json(['error' => 'Not found'], 404); return; } - $stmt = $pdo->prepare('SELECT id, name, gender, feature, note, rescued_at, location, created_at, updated_at FROM igel WHERE id=?'); + $stmt = $pdo->prepare('SELECT id, name, gender, feature, note, rescued_at, location, marking, finder, phone, email, admission_reason, animal_condition, released_at, release_location, created_at, updated_at FROM igel WHERE id=?'); $stmt->execute([$id]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row) { @@ -423,6 +443,14 @@ function igel_update(PDO $pdo, int $uid, int $id): void $note = isset($in['note']) ? (string) $in['note'] : null; $rescuedAt = array_key_exists('rescued_at', $in) ? (string) $in['rescued_at'] : null; $location = array_key_exists('location', $in) ? trim((string) $in['location']) : null; + $marking = array_key_exists('marking', $in) ? trim((string) $in['marking']) : null; + $finder = array_key_exists('finder', $in) ? trim((string) $in['finder']) : null; + $phone = array_key_exists('phone', $in) ? trim((string) $in['phone']) : null; + $email = array_key_exists('email', $in) ? trim((string) $in['email']) : null; + $admissionReason = array_key_exists('admission_reason', $in) ? trim((string) $in['admission_reason']) : null; + $animalCondition = array_key_exists('animal_condition', $in) ? trim((string) $in['animal_condition']) : null; + $releasedAt = array_key_exists('released_at', $in) ? (string) $in['released_at'] : null; + $releaseLocation = array_key_exists('release_location', $in) ? trim((string) $in['release_location']) : null; if ($name === '') { json(['error' => 'Name required'], 422); @@ -436,11 +464,15 @@ function igel_update(PDO $pdo, int $uid, int $id): void json(['error' => 'Location too long (max 255)'], 422); return; } + if ($releasedAt !== null && $releasedAt !== '' && strtotime($releasedAt) === false) { + json(['error' => 'Invalid released_at (expected YYYY-MM-DD)'], 422); + return; + } $stmt = $pdo->prepare( - 'UPDATE igel SET name=?, gender=?, feature=?, note=?, rescued_at=?, location=? WHERE id=?' + 'UPDATE igel SET name=?, gender=?, feature=?, note=?, rescued_at=?, location=?, marking=?, finder=?, phone=?, email=?, admission_reason=?, animal_condition=?, released_at=?, release_location=? WHERE id=?' ); - $stmt->execute([$name, $gender, $feature, $note, $rescuedAt ?: null, $location ?: null, $id]); + $stmt->execute([$name, $gender, $feature, $note, $rescuedAt ?: null, $location ?: null, $marking ?: null, $finder ?: null, $phone ?: null, $email ?: null, $admissionReason ?: null, $animalCondition ?: null, $releasedAt ?: null, $releaseLocation ?: null, $id]); json(['ok' => true]); } diff --git a/api/hedgehogs.sql b/api/hedgehogs.sql index 9044277..5b3ae66 100644 --- a/api/hedgehogs.sql +++ b/api/hedgehogs.sql @@ -53,6 +53,14 @@ CREATE TABLE `igel` ( `feature` text DEFAULT NULL, `rescued_at` date DEFAULT NULL, `location` varchar(255) DEFAULT NULL, + `marking` varchar(255) DEFAULT NULL, + `finder` varchar(255) DEFAULT NULL, + `phone` varchar(255) DEFAULT NULL, + `email` varchar(255) DEFAULT NULL, + `admission_reason` varchar(255) DEFAULT NULL, + `animal_condition` varchar(255) DEFAULT NULL, + `released_at` date DEFAULT NULL, + `release_location` varchar(255) DEFAULT NULL, `created_at` timestamp NOT NULL DEFAULT current_timestamp(), `updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; diff --git a/lib/features/igel/data/export_service.dart b/lib/features/igel/data/export_service.dart index a71e811..33f25ae 100644 --- a/lib/features/igel/data/export_service.dart +++ b/lib/features/igel/data/export_service.dart @@ -52,6 +52,14 @@ class IgelExportService { 'igel_note', 'igel_rescued_at', 'igel_location', + 'igel_marking', + 'igel_finder', + 'igel_phone', + 'igel_email', + 'igel_admission_reason', + 'igel_animal_condition', + 'igel_released_at', + 'igel_release_location', // Messwert 'messwert_id', 'messwert_datum', @@ -78,6 +86,14 @@ class IgelExportService { _csvEscape(ig.note, delimiter: sep), _fmtDate(ig.rescuedAt), _csvEscape(ig.location, delimiter: sep), + _csvEscape(ig.marking, delimiter: sep), + _csvEscape(ig.finder, delimiter: sep), + _csvEscape(ig.phone, delimiter: sep), + _csvEscape(ig.email, delimiter: sep), + _csvEscape(ig.admissionReason, delimiter: sep), + _csvEscape(ig.animalCondition, delimiter: sep), + _fmtDate(ig.releasedAt), + _csvEscape(ig.releaseLocation, delimiter: sep), // Messwert-Spalten leer '', @@ -98,6 +114,14 @@ class IgelExportService { _csvEscape(ig.note, delimiter: sep), _fmtDate(ig.rescuedAt), _csvEscape(ig.location, delimiter: sep), + _csvEscape(ig.marking, delimiter: sep), + _csvEscape(ig.finder, delimiter: sep), + _csvEscape(ig.phone, delimiter: sep), + _csvEscape(ig.email, delimiter: sep), + _csvEscape(ig.admissionReason, delimiter: sep), + _csvEscape(ig.animalCondition, delimiter: sep), + _fmtDate(ig.releasedAt), + _csvEscape(ig.releaseLocation, delimiter: sep), m.id, _fmtDateTime(m.datum), m.gewicht, diff --git a/lib/features/igel/data/igel_repository.dart b/lib/features/igel/data/igel_repository.dart index 83a538c..c1008a9 100644 --- a/lib/features/igel/data/igel_repository.dart +++ b/lib/features/igel/data/igel_repository.dart @@ -20,7 +20,15 @@ class IgelRepository { String? gender, String? feature, DateTime? rescuedAt, - String? location}) async { + String? location, + String? marking, + String? finder, + String? phone, + String? email, + String? admissionReason, + String? animalCondition, + DateTime? releasedAt, + String? releaseLocation}) async { String? fmtDate(DateTime? d) => d?.toIso8601String().split('T').first; final res = await api.post('/igel', { @@ -30,6 +38,20 @@ class IgelRepository { 'feature': feature, 'rescued_at': fmtDate(rescuedAt), 'location': (location?.trim().isEmpty ?? true) ? null : location!.trim(), + 'marking': (marking?.trim().isEmpty ?? true) ? null : marking!.trim(), + 'finder': (finder?.trim().isEmpty ?? true) ? null : finder!.trim(), + 'phone': (phone?.trim().isEmpty ?? true) ? null : phone!.trim(), + 'email': (email?.trim().isEmpty ?? true) ? null : email!.trim(), + 'admission_reason': (admissionReason?.trim().isEmpty ?? true) + ? null + : admissionReason!.trim(), + 'animal_condition': (animalCondition?.trim().isEmpty ?? true) + ? null + : animalCondition!.trim(), + 'released_at': fmtDate(releasedAt), + 'release_location': (releaseLocation?.trim().isEmpty ?? true) + ? null + : releaseLocation!.trim(), }); try { final id = (res['id'] as num).toInt(); @@ -54,7 +76,15 @@ class IgelRepository { String? gender, String? feature, DateTime? rescuedAt, - String? location}) async { + String? location, + String? marking, + String? finder, + String? phone, + String? email, + String? admissionReason, + String? animalCondition, + DateTime? releasedAt, + String? releaseLocation}) async { String? fmtDate(DateTime? d) => d?.toIso8601String().split('T').first; final body = { @@ -64,6 +94,20 @@ class IgelRepository { 'feature': feature, 'rescued_at': fmtDate(rescuedAt), 'location': (location?.trim().isEmpty ?? true) ? null : location!.trim(), + 'marking': (marking?.trim().isEmpty ?? true) ? null : marking!.trim(), + 'finder': (finder?.trim().isEmpty ?? true) ? null : finder!.trim(), + 'phone': (phone?.trim().isEmpty ?? true) ? null : phone!.trim(), + 'email': (email?.trim().isEmpty ?? true) ? null : email!.trim(), + 'admission_reason': (admissionReason?.trim().isEmpty ?? true) + ? null + : admissionReason!.trim(), + 'animal_condition': (animalCondition?.trim().isEmpty ?? true) + ? null + : animalCondition!.trim(), + 'released_at': fmtDate(releasedAt), + 'release_location': (releaseLocation?.trim().isEmpty ?? true) + ? null + : releaseLocation!.trim(), }; await api.put('/igel/$id', body); } diff --git a/lib/features/igel/data/import_service.dart b/lib/features/igel/data/import_service.dart index b6ba9c9..baa58dd 100644 --- a/lib/features/igel/data/import_service.dart +++ b/lib/features/igel/data/import_service.dart @@ -246,6 +246,14 @@ class IgelCsvImportService { final hIgelNote = idx('igel_note'); final hIgelRescuedAt = idx('igel_rescued_at'); final hIgelLocation = idx('igel_location'); + final hIgelMarking = idx('igel_marking'); + final hIgelFinder = idx('igel_finder'); + final hIgelPhone = idx('igel_phone'); + final hIgelEmail = idx('igel_email'); + final hIgelAdmissionReason = idx('igel_admission_reason'); + final hIgelAnimalCondition = idx('igel_animal_condition'); + final hIgelReleasedAt = idx('igel_released_at'); + final hIgelReleaseLocation = idx('igel_release_location'); final hMwDatum = idx('messwert_datum'); final hMwGewicht = idx('messwert_gewicht'); @@ -304,8 +312,16 @@ class IgelCsvImportService { final feature = s(hIgelFeature); final note = s(hIgelNote); final location = s(hIgelLocation); + final marking = s(hIgelMarking); + final finder = s(hIgelFinder); + final phone = s(hIgelPhone); + final email = s(hIgelEmail); + final admissionReason = s(hIgelAdmissionReason); + final animalCondition = s(hIgelAnimalCondition); final rescuedAt = _parseDate(s(hIgelRescuedAt)); // yyyy-MM-dd (vom Export) + final releasedAt = _parseDate(s(hIgelReleasedAt)); + final releaseLocation = s(hIgelReleaseLocation); final newId = await igelRepo.create( name, @@ -314,6 +330,14 @@ class IgelCsvImportService { feature: feature, rescuedAt: rescuedAt, location: location, + marking: marking, + finder: finder, + phone: phone, + email: email, + admissionReason: admissionReason, + animalCondition: animalCondition, + releasedAt: releasedAt, + releaseLocation: releaseLocation, ); // Holen (oder minimal zusammensetzen) ig = Igel( @@ -324,6 +348,14 @@ class IgelCsvImportService { feature: feature, rescuedAt: rescuedAt, location: location, + marking: marking, + finder: finder, + phone: phone, + email: email, + admissionReason: admissionReason, + animalCondition: animalCondition, + releasedAt: releasedAt, + releaseLocation: releaseLocation, createdAt: null, updatedAt: null, ); diff --git a/lib/features/igel/domain/igel.dart b/lib/features/igel/domain/igel.dart index d0cc005..6d51fb4 100644 --- a/lib/features/igel/domain/igel.dart +++ b/lib/features/igel/domain/igel.dart @@ -8,6 +8,14 @@ class Igel { // 🆕 NEU: final DateTime? rescuedAt; // YYYY-MM-DD (Datum) final String? location; // Ort / Fundstelle + final String? marking; + final String? finder; + final String? phone; + final String? email; + final String? admissionReason; + final String? animalCondition; + final DateTime? releasedAt; // YYYY-MM-DD (Datum) + final String? releaseLocation; final DateTime? createdAt; final DateTime? updatedAt; @@ -20,6 +28,14 @@ class Igel { this.note, this.rescuedAt, this.location, + this.marking, + this.finder, + this.phone, + this.email, + this.admissionReason, + this.animalCondition, + this.releasedAt, + this.releaseLocation, this.createdAt, this.updatedAt, }); @@ -42,6 +58,14 @@ class Igel { note: m['note'] as String?, rescuedAt: _parseDateOrNull(m['rescued_at'] as String?), location: m['location'] as String?, + marking: m['marking'] as String?, + finder: m['finder'] as String?, + phone: m['phone'] as String?, + email: m['email'] as String?, + admissionReason: m['admission_reason'] as String?, + animalCondition: m['animal_condition'] as String?, + releasedAt: _parseDateOrNull(m['released_at'] as String?), + releaseLocation: m['release_location'] as String?, createdAt: _parseDateOrNull(m['created_at'] as String?), updatedAt: _parseDateOrNull(m['updated_at'] as String?), ); @@ -58,6 +82,17 @@ class Igel { 'note': note, 'rescued_at': fmtDate(rescuedAt), 'location': location?.trim().isEmpty == true ? null : location, + 'marking': marking?.trim().isEmpty == true ? null : marking, + 'finder': finder?.trim().isEmpty == true ? null : finder, + 'phone': phone?.trim().isEmpty == true ? null : phone, + 'email': email?.trim().isEmpty == true ? null : email, + 'admission_reason': + admissionReason?.trim().isEmpty == true ? null : admissionReason, + 'animal_condition': + animalCondition?.trim().isEmpty == true ? null : animalCondition, + 'released_at': fmtDate(releasedAt), + 'release_location': + releaseLocation?.trim().isEmpty == true ? null : releaseLocation, }; } @@ -68,6 +103,14 @@ class Igel { String? note, DateTime? rescuedAt, String? location, + String? marking, + String? finder, + String? phone, + String? email, + String? admissionReason, + String? animalCondition, + DateTime? releasedAt, + String? releaseLocation, DateTime? createdAt, DateTime? updatedAt, }) { @@ -79,6 +122,14 @@ class Igel { note: note ?? this.note, rescuedAt: rescuedAt ?? this.rescuedAt, location: location ?? this.location, + marking: marking ?? this.marking, + finder: finder ?? this.finder, + phone: phone ?? this.phone, + email: email ?? this.email, + admissionReason: admissionReason ?? this.admissionReason, + animalCondition: animalCondition ?? this.animalCondition, + releasedAt: releasedAt ?? this.releasedAt, + releaseLocation: releaseLocation ?? this.releaseLocation, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, ); diff --git a/lib/features/igel/presentation/igel_detail_screen.dart b/lib/features/igel/presentation/igel_detail_screen.dart index ccc2bce..a7a9582 100644 --- a/lib/features/igel/presentation/igel_detail_screen.dart +++ b/lib/features/igel/presentation/igel_detail_screen.dart @@ -80,9 +80,15 @@ class _IgelDetailState extends ConsumerState { final featureC = TextEditingController(); final noteC = TextEditingController(); final locationC = TextEditingController(); + final markingC = TextEditingController(); + final finderC = TextEditingController(); + final phoneC = TextEditingController(); + final emailC = TextEditingController(); + final admissionReasonC = TextEditingController(); + final animalConditionC = TextEditingController(); + final releaseLocationC = TextEditingController(); DateTime? rescuedAt; - DateTime? _initRescuedAt; - String? _initLocation; + DateTime? releasedAt; String? gender; List images = []; @@ -128,10 +134,18 @@ class _IgelDetailState extends ConsumerState { void onAnyTextChanged() { if (mounted) setState(() {}); } + nameC.addListener(onAnyTextChanged); featureC.addListener(onAnyTextChanged); noteC.addListener(onAnyTextChanged); locationC.addListener(onAnyTextChanged); + markingC.addListener(onAnyTextChanged); + finderC.addListener(onAnyTextChanged); + phoneC.addListener(onAnyTextChanged); + emailC.addListener(onAnyTextChanged); + admissionReasonC.addListener(onAnyTextChanged); + animalConditionC.addListener(onAnyTextChanged); + releaseLocationC.addListener(onAnyTextChanged); } @override @@ -140,6 +154,13 @@ class _IgelDetailState extends ConsumerState { featureC.dispose(); noteC.dispose(); locationC.dispose(); + markingC.dispose(); + finderC.dispose(); + phoneC.dispose(); + emailC.dispose(); + admissionReasonC.dispose(); + animalConditionC.dispose(); + releaseLocationC.dispose(); mwGewichtC.dispose(); mwBehandlungC.dispose(); mwBemerkungC.dispose(); @@ -159,8 +180,14 @@ class _IgelDetailState extends ConsumerState { gender = igel!.gender; rescuedAt = igel!.rescuedAt; locationC.text = igel!.location ?? ''; - _initRescuedAt = igel!.rescuedAt; - _initLocation = igel!.location; + markingC.text = igel!.marking ?? ''; + finderC.text = igel!.finder ?? ''; + phoneC.text = igel!.phone ?? ''; + emailC.text = igel!.email ?? ''; + admissionReasonC.text = igel!.admissionReason ?? ''; + animalConditionC.text = igel!.animalCondition ?? ''; + releasedAt = igel!.releasedAt; + releaseLocationC.text = igel!.releaseLocation ?? ''; images = await imagesRepo.list(widget.igelId); messwerte = await messRepo.list(widget.igelId); @@ -176,11 +203,19 @@ class _IgelDetailState extends ConsumerState { bool changed = nameC.text.trim() != igel!.name || featureC.text.trim() != (igel!.feature ?? '') || noteC.text.trim() != (igel!.note ?? '') || + locationC.text.trim() != (igel!.location ?? '') || + markingC.text.trim() != (igel!.marking ?? '') || + finderC.text.trim() != (igel!.finder ?? '') || + phoneC.text.trim() != (igel!.phone ?? '') || + emailC.text.trim() != (igel!.email ?? '') || + admissionReasonC.text.trim() != (igel!.admissionReason ?? '') || + animalConditionC.text.trim() != (igel!.animalCondition ?? '') || + releaseLocationC.text.trim() != (igel!.releaseLocation ?? '') || gender != igel!.gender; String? fmt(DateTime? d) => d == null ? null : DateFormat('yyyy-MM-dd').format(d); - if (fmt(rescuedAt) != fmt(_initRescuedAt)) changed = true; - if (locationC.text.trim() != (_initLocation ?? '')) changed = true; + if (fmt(rescuedAt) != fmt(igel!.rescuedAt)) changed = true; + if (fmt(releasedAt) != fmt(igel!.releasedAt)) changed = true; return changed; } @@ -201,10 +236,23 @@ class _IgelDetailState extends ConsumerState { feature: featureC.text.trim().isEmpty ? null : featureC.text.trim(), rescuedAt: rescuedAt, location: locationC.text.trim().isEmpty ? null : locationC.text.trim(), + marking: markingC.text.trim().isEmpty ? null : markingC.text.trim(), + finder: finderC.text.trim().isEmpty ? null : finderC.text.trim(), + phone: phoneC.text.trim().isEmpty ? null : phoneC.text.trim(), + email: emailC.text.trim().isEmpty ? null : emailC.text.trim(), + admissionReason: admissionReasonC.text.trim().isEmpty + ? null + : admissionReasonC.text.trim(), + animalCondition: animalConditionC.text.trim().isEmpty + ? null + : animalConditionC.text.trim(), + releasedAt: releasedAt, + releaseLocation: releaseLocationC.text.trim().isEmpty + ? null + : releaseLocationC.text.trim(), ); + await _loadAll(); _snack('Gespeichert'); - _initRescuedAt = rescuedAt; - _initLocation = locationC.text.trim(); } catch (e) { _snack('Speichern fehlgeschlagen: $e'); } @@ -352,8 +400,8 @@ class _IgelDetailState extends ConsumerState { fallback(Icons.videocam_outlined), Container(color: const Color(0x33000000)), const Center( - child: Icon(Icons.play_circle_fill, - color: Colors.white70, size: 32), + child: + Icon(Icons.play_circle_fill, color: Colors.white70, size: 32), ), ], ); @@ -654,6 +702,14 @@ class _IgelDetailState extends ConsumerState { 'igel_note', 'igel_rescued_at', 'igel_location', + 'igel_marking', + 'igel_finder', + 'igel_phone', + 'igel_email', + 'igel_admission_reason', + 'igel_animal_condition', + 'igel_released_at', + 'igel_release_location', 'messwert_id', 'messwert_datum', 'messwert_gewicht', @@ -670,6 +726,14 @@ class _IgelDetailState extends ConsumerState { _csvEscape(igel!.note, delimiter: sep), _fmtDate(igel!.rescuedAt), _csvEscape(igel!.location, delimiter: sep), + _csvEscape(igel!.marking, delimiter: sep), + _csvEscape(igel!.finder, delimiter: sep), + _csvEscape(igel!.phone, delimiter: sep), + _csvEscape(igel!.email, delimiter: sep), + _csvEscape(igel!.admissionReason, delimiter: sep), + _csvEscape(igel!.animalCondition, delimiter: sep), + _fmtDate(igel!.releasedAt), + _csvEscape(igel!.releaseLocation, delimiter: sep), '', '', '', @@ -686,6 +750,14 @@ class _IgelDetailState extends ConsumerState { _csvEscape(igel!.note, delimiter: sep), _fmtDate(igel!.rescuedAt), _csvEscape(igel!.location, delimiter: sep), + _csvEscape(igel!.marking, delimiter: sep), + _csvEscape(igel!.finder, delimiter: sep), + _csvEscape(igel!.phone, delimiter: sep), + _csvEscape(igel!.email, delimiter: sep), + _csvEscape(igel!.admissionReason, delimiter: sep), + _csvEscape(igel!.animalCondition, delimiter: sep), + _fmtDate(igel!.releasedAt), + _csvEscape(igel!.releaseLocation, delimiter: sep), m.id, _fmtDateTimeIso(m.datum), m.gewicht, @@ -711,16 +783,18 @@ class _IgelDetailState extends ConsumerState { // --- Export PDF --- Future _captureChartPngBytes() async { - final sortedData = [...messwerte]..sort((a, b) => a.datum.compareTo(b.datum)); + final sortedData = [...messwerte] + ..sort((a, b) => a.datum.compareTo(b.datum)); Size? renderSize; final dpr = MediaQuery.maybeOf(context)?.devicePixelRatio ?? (WidgetsBinding.instance.platformDispatcher.views.isNotEmpty - ? WidgetsBinding.instance.platformDispatcher.views.first.devicePixelRatio + ? WidgetsBinding + .instance.platformDispatcher.views.first.devicePixelRatio : 3.0); try { - final boundary = - _chartKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; + final boundary = _chartKey.currentContext?.findRenderObject() + as RenderRepaintBoundary?; if (boundary != null) { renderSize = boundary.size; // Ensure the chart has been painted before capturing, otherwise toImage can return empty. @@ -741,8 +815,7 @@ class _IgelDetailState extends ConsumerState { } // Fallback for web: render the CustomPainter off-screen to avoid missing charts in PDF. - return _renderChartOffscreen(sortedData, - size: renderSize, pixelRatio: dpr); + return _renderChartOffscreen(sortedData, size: renderSize, pixelRatio: dpr); } Future _renderChartOffscreen(List data, @@ -843,7 +916,8 @@ class _IgelDetailState extends ConsumerState { value: includeBilder, onChanged: (v) => setStateDialog(() => includeBilder = v ?? false), - title: const Text('Medien aufnehmen (Videos werden ausgelassen)'), + title: const Text( + 'Medien aufnehmen (Videos werden ausgelassen)'), controlAffinity: ListTileControlAffinity.leading, ), ], @@ -874,8 +948,7 @@ class _IgelDetailState extends ConsumerState { final chartDataPdf = [...mw]..sort((a, b) => a.datum.compareTo(b.datum)); const pageFormat = PdfPageFormat.a4; const pageMargin = pw.EdgeInsets.all(24); - final contentWidth = - pageFormat.width - pageMargin.left - pageMargin.right; + final contentWidth = pageFormat.width - pageMargin.left - pageMargin.right; const double chartHeight = 220; const chartRatio = 1.0; final Size chartOffscreenSize = Size(contentWidth * 2, chartHeight); @@ -909,24 +982,102 @@ class _IgelDetailState extends ConsumerState { ), ); + pw.Widget infoDivider() => pw.Container( + height: 0.4, + margin: const pw.EdgeInsets.symmetric(vertical: 3), + color: PdfColors.grey300, + ); + + const infoGridWidths = { + 0: pw.FixedColumnWidth(110), + 1: pw.FlexColumnWidth(), + 2: pw.FixedColumnWidth(24), + 3: pw.FixedColumnWidth(110), + 4: pw.FlexColumnWidth(), + }; + + pw.Widget infoPair( + String leftLabel, + String leftValue, + String rightLabel, + String rightValue, + ) => + pw.Padding( + padding: const pw.EdgeInsets.symmetric(vertical: 2), + child: pw.Table( + columnWidths: infoGridWidths, + children: [ + pw.TableRow( + verticalAlignment: pw.TableCellVerticalAlignment.top, + children: [ + pw.Padding( + padding: const pw.EdgeInsets.only(right: 6), + child: pw.Text(leftLabel, style: labelStyle), + ), + pw.Padding( + padding: const pw.EdgeInsets.only(right: 16), + child: pw.Text(leftValue, style: textStyle), + ), + pw.SizedBox(), + pw.Padding( + padding: const pw.EdgeInsets.only(right: 6), + child: pw.Text(rightLabel, style: labelStyle), + ), + pw.Padding( + padding: const pw.EdgeInsets.only(right: 12), + child: pw.Text(rightValue, style: textStyle), + ), + ], + ), + ], + ), + ); + doc.addPage( pw.MultiPage( pageFormat: pageFormat, margin: pageMargin, build: (context) { - return [ - pw.Text('Igel-Bericht', style: headerStyle), - pw.SizedBox(height: 10), - infoRow('Name', igel!.name), - infoRow('Geschlecht', igel!.gender ?? '—'), + final infoItems = [ + infoPair('Name', igel!.name, 'Geschlecht', igel!.gender ?? '—'), infoRow('Merkmal', igel!.feature ?? '—'), - infoRow( + infoPair( 'Gerettet am', rescuedAt == null ? '—' - : DateFormat('dd.MM.yyyy').format(rescuedAt!)), - infoRow('Ort / Fundstelle', igel!.location ?? '—'), + : DateFormat('dd.MM.yyyy').format(rescuedAt!), + 'Ort / Fundstelle', + igel!.location ?? '—'), + infoRow('Markierung', igel!.marking ?? '—'), + infoRow('Finder', igel!.finder ?? '—'), + infoPair( + 'Telefon', + igel!.phone ?? '—', + 'Email', + igel!.email ?? '—', + ), + infoRow('Grund der Aufnahme', igel!.admissionReason ?? '—'), + infoRow('Zustand', igel!.animalCondition ?? '—'), + infoPair( + 'Ausgewildert', + releasedAt == null + ? '—' + : DateFormat('dd.MM.yyyy').format(releasedAt!), + 'Auswilderungsort', + igel!.releaseLocation ?? '—'), infoRow('Information', igel!.note ?? '—'), + ]; + + final infoSection = []; + for (var i = 0; i < infoItems.length; i++) { + if (i > 0) infoSection.add(infoDivider()); + infoSection.add(infoItems[i]); + } + + return [ + pw.Text('Igel-Bericht', style: headerStyle), + pw.SizedBox(height: 10), + ...infoSection, pw.SizedBox(height: 4), pw.Text('Gewichtsverlauf', style: @@ -944,8 +1095,8 @@ class _IgelDetailState extends ConsumerState { if (includeMesswerte) ...[ pw.SizedBox(height: 4), pw.Text('Messwerte', - style: - pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold)), + style: pw.TextStyle( + fontSize: 14, fontWeight: pw.FontWeight.bold)), pw.SizedBox(height: 6), if (mw.isEmpty) pw.Text('Keine Messwerte vorhanden.', style: labelStyle) @@ -976,8 +1127,8 @@ class _IgelDetailState extends ConsumerState { if (includeBilder) ...[ pw.SizedBox(height: 16), pw.Text('Medien (Fotos)', - style: - pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold)), + style: pw.TextStyle( + fontSize: 14, fontWeight: pw.FontWeight.bold)), pw.SizedBox(height: 6), if (imageBytesList.isEmpty) pw.Text('Keine Medien vorhanden.', style: labelStyle) @@ -1008,8 +1159,8 @@ class _IgelDetailState extends ConsumerState { border: pw.Border.all(color: PdfColors.grey300), borderRadius: pw.BorderRadius.circular(6), ), - child: - pw.Text('Medium nicht verfügbar', style: labelStyle), + child: pw.Text('Medium nicht verfügbar', + style: labelStyle), ), ], ), @@ -1254,56 +1405,221 @@ class _IgelDetailState extends ConsumerState { ), ), const SizedBox(height: 12), - // Gerettet am - ListTile( - enabled: canEdit, - contentPadding: EdgeInsets.zero, - leading: - const Icon(Icons.calendar_today), - title: const Text('Gerettet am'), - subtitle: Text( - rescuedAt == null - ? '—' - : DateFormat('dd.MM.yyyy') - .format(rescuedAt!), - ), - onTap: !canEdit - ? null - : () async { - final now = DateTime.now(); - final init = rescuedAt ?? now; - final picked = - await showDatePicker( - context: context, - initialDate: init, - firstDate: DateTime(2000), - lastDate: DateTime( - now.year + 1, 12, 31), - ); - if (picked != null) { - setState( - () => rescuedAt = picked); - } - }, - onLongPress: !canEdit - ? null - : () => setState( - () => rescuedAt = null), + Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Expanded( + child: ListTile( + enabled: canEdit, + contentPadding: EdgeInsets.zero, + leading: const Icon( + Icons.calendar_today), + title: const Text('Gerettet am'), + subtitle: Text( + rescuedAt == null + ? '—' + : DateFormat('dd.MM.yyyy') + .format(rescuedAt!), + ), + onTap: !canEdit + ? null + : () async { + final now = + DateTime.now(); + final init = + rescuedAt ?? now; + final picked = + await showDatePicker( + context: context, + initialDate: init, + firstDate: + DateTime(2000), + lastDate: DateTime( + now.year + 1, + 12, + 31), + ); + if (picked != null) { + setState(() => + rescuedAt = picked); + } + }, + onLongPress: !canEdit + ? null + : () => setState( + () => rescuedAt = null), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: locationC, + enabled: canEdit, + readOnly: !canEdit, + decoration: const InputDecoration( + labelText: + 'Ort / Fundstelle (optional)', + prefixIcon: + Icon(Icons.place_outlined), + ), + ), + ), + ], ), const SizedBox(height: 12), - // Ort / Fundstelle TextField( - controller: locationC, + controller: markingC, + enabled: canEdit, + readOnly: !canEdit, + decoration: const InputDecoration( + labelText: 'Markierung (optional)', + prefixIcon: Icon(Icons.sell_outlined), + ), + ), + const SizedBox(height: 12), + Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + controller: finderC, + enabled: canEdit, + readOnly: !canEdit, + decoration: const InputDecoration( + labelText: 'Finder (optional)', + prefixIcon: + Icon(Icons.person_search), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: phoneC, + enabled: canEdit, + readOnly: !canEdit, + keyboardType: TextInputType.phone, + decoration: const InputDecoration( + labelText: 'Telefon (optional)', + prefixIcon: + Icon(Icons.phone_outlined), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: emailC, + enabled: canEdit, + readOnly: !canEdit, + keyboardType: + TextInputType.emailAddress, + decoration: const InputDecoration( + labelText: 'Email (optional)', + prefixIcon: + Icon(Icons.email_outlined), + ), + ), + ), + ], + ), + const SizedBox(height: 12), + TextField( + controller: admissionReasonC, enabled: canEdit, readOnly: !canEdit, decoration: const InputDecoration( labelText: - 'Ort / Fundstelle (optional)', + 'Grund der Aufnahme (optional)', prefixIcon: - Icon(Icons.place_outlined), + Icon(Icons.playlist_add_check), ), ), const SizedBox(height: 12), + TextField( + controller: animalConditionC, + enabled: canEdit, + readOnly: !canEdit, + decoration: const InputDecoration( + labelText: 'Zustand (optional)', + prefixIcon: + Icon(Icons.health_and_safety), + ), + ), + const SizedBox(height: 12), + Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Expanded( + child: ListTile( + enabled: canEdit, + contentPadding: EdgeInsets.zero, + leading: const Icon( + Icons.event_available), + title: const Text('Ausgewildert'), + subtitle: Text( + releasedAt == null + ? '—' + : DateFormat('dd.MM.yyyy') + .format(releasedAt!), + ), + trailing: !canEdit || + releasedAt == null + ? null + : IconButton( + tooltip: 'Datum löschen', + onPressed: () => setState( + () => releasedAt = + null), + icon: const Icon( + Icons.clear), + ), + onTap: !canEdit + ? null + : () async { + final now = + DateTime.now(); + final init = + releasedAt ?? now; + final picked = + await showDatePicker( + context: context, + initialDate: init, + firstDate: + DateTime(2000), + lastDate: DateTime( + now.year + 1, + 12, + 31), + ); + if (picked != null) { + setState(() => + releasedAt = + picked); + } + }, + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: releaseLocationC, + enabled: canEdit, + readOnly: !canEdit, + decoration: const InputDecoration( + labelText: + 'Auswilderungsort (optional)', + prefixIcon: Icon(Icons + .nature_people_outlined), + ), + ), + ), + ], + ), + const SizedBox(height: 12), TextField( controller: noteC, enabled: canEdit, @@ -1359,8 +1675,7 @@ class _IgelDetailState extends ConsumerState { child: TextButton.icon( onPressed: _saveChartPngDebug, icon: const Icon(Icons.download), - label: - const Text('Chart als PNG'), + label: const Text('Chart als PNG'), ), ), ], @@ -1661,8 +1976,8 @@ class _IgelDetailState extends ConsumerState { borderRadius: BorderRadius .circular(10), - child: _mediaPreview( - img), + child: + _mediaPreview(img), ), ), ), @@ -1914,7 +2229,8 @@ class _WeightChartPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - const paddingLeft = 64.0; // Platz für Y-Labels, damit nichts abgeschnitten wird + const paddingLeft = + 64.0; // Platz für Y-Labels, damit nichts abgeschnitten wird const paddingBottom = 18.0; // Platz für X-Labels (kompakter) const paddingTop = 4.0; const paddingRight = 28.0; // Platz für letzte X-Labels @@ -2049,7 +2365,3 @@ class _WeightChartPainter extends CustomPainter { return false; } } - - - - diff --git a/lib/features/igel/presentation/igel_list_screen.dart b/lib/features/igel/presentation/igel_list_screen.dart index df21427..6e4fd78 100644 --- a/lib/features/igel/presentation/igel_list_screen.dart +++ b/lib/features/igel/presentation/igel_list_screen.dart @@ -45,9 +45,17 @@ class _IgelListState extends ConsumerState { final featureC = TextEditingController(); String? gender; // 'männlich' | 'weiblich' | 'unbekannt' | null (keine Angabe) - // ⬇️ NEU: Felder für „Gerettet am“ + „Ort / Fundstelle“ + // Stammdaten final locationC = TextEditingController(); + final markingC = TextEditingController(); + final finderC = TextEditingController(); + final phoneC = TextEditingController(); + final emailC = TextEditingController(); + final admissionReasonC = TextEditingController(); + final animalConditionC = TextEditingController(); + final releaseLocationC = TextEditingController(); DateTime? rescuedAt; + DateTime? releasedAt; // Undo Igel? _lastDeleted; @@ -75,6 +83,13 @@ class _IgelListState extends ConsumerState { noteC.dispose(); featureC.dispose(); locationC.dispose(); // ⬅️ NEU + markingC.dispose(); + finderC.dispose(); + phoneC.dispose(); + emailC.dispose(); + admissionReasonC.dispose(); + animalConditionC.dispose(); + releaseLocationC.dispose(); super.dispose(); } @@ -100,8 +115,7 @@ class _IgelListState extends ConsumerState { try { final list = await messRepo.list(ig.id); if (list.isEmpty) return; - final latest = list.reduce( - (a, b) => a.datum.isAfter(b.datum) ? a : b); + final latest = list.reduce((a, b) => a.datum.isAfter(b.datum) ? a : b); latestById[ig.id] = latest; } catch (_) { // Messwerte optional: bei Fehler einfach weglassen. @@ -117,6 +131,19 @@ class _IgelListState extends ConsumerState { final feature = featureC.text.trim().isEmpty ? null : featureC.text.trim(); final location = locationC.text.trim().isEmpty ? null : locationC.text.trim(); // ⬅️ NEU + final marking = markingC.text.trim().isEmpty ? null : markingC.text.trim(); + final finder = finderC.text.trim().isEmpty ? null : finderC.text.trim(); + final phone = phoneC.text.trim().isEmpty ? null : phoneC.text.trim(); + final email = emailC.text.trim().isEmpty ? null : emailC.text.trim(); + final admissionReason = admissionReasonC.text.trim().isEmpty + ? null + : admissionReasonC.text.trim(); + final animalCondition = animalConditionC.text.trim().isEmpty + ? null + : animalConditionC.text.trim(); + final releaseLocation = releaseLocationC.text.trim().isEmpty + ? null + : releaseLocationC.text.trim(); if (name.isEmpty) { _snack('Bitte einen Namen eingeben'); return; @@ -130,14 +157,30 @@ class _IgelListState extends ConsumerState { feature: feature, rescuedAt: rescuedAt, location: location, + marking: marking, + finder: finder, + phone: phone, + email: email, + admissionReason: admissionReason, + animalCondition: animalCondition, + releasedAt: releasedAt, + releaseLocation: releaseLocation, ); // Felder leeren/zurücksetzen nameC.clear(); noteC.clear(); featureC.clear(); locationC.clear(); // ⬅️ NEU + markingC.clear(); + finderC.clear(); + phoneC.clear(); + emailC.clear(); + admissionReasonC.clear(); + animalConditionC.clear(); + releaseLocationC.clear(); gender = null; rescuedAt = null; // ⬅️ NEU + releasedAt = null; setState(() => showForm = false); await _load(); _snack('Igel angelegt'); @@ -166,10 +209,26 @@ class _IgelListState extends ConsumerState { final note = (x.note ?? '').toLowerCase(); final feat = (x.feature ?? '').toLowerCase(); final gen = (x.gender ?? '').toLowerCase(); + final loc = (x.location ?? '').toLowerCase(); + final marking = (x.marking ?? '').toLowerCase(); + final finder = (x.finder ?? '').toLowerCase(); + final phone = (x.phone ?? '').toLowerCase(); + final email = (x.email ?? '').toLowerCase(); + final admissionReason = (x.admissionReason ?? '').toLowerCase(); + final animalCondition = (x.animalCondition ?? '').toLowerCase(); + final releaseLocation = (x.releaseLocation ?? '').toLowerCase(); return n.contains(query) || note.contains(query) || feat.contains(query) || - gen.contains(query); + gen.contains(query) || + loc.contains(query) || + marking.contains(query) || + finder.contains(query) || + phone.contains(query) || + email.contains(query) || + admissionReason.contains(query) || + animalCondition.contains(query) || + releaseLocation.contains(query); }).toList(); } @@ -451,49 +510,183 @@ class _IgelListState extends ConsumerState { labelText: 'Merkmal (optional)'), ), const SizedBox(height: 8), - - // ⬇️ „Gerettet am“ - ListTile( - contentPadding: EdgeInsets.zero, - leading: - const Icon(Icons.calendar_today), - title: const Text('Gerettet am'), - subtitle: Text( - rescuedAt == null - ? '—' - : df.format(rescuedAt!), - ), - onTap: () async { - final now = DateTime.now(); - final init = rescuedAt ?? now; - final picked = await showDatePicker( - context: context, - initialDate: init, - firstDate: DateTime(2000), - lastDate: - DateTime(now.year + 1, 12, 31), - ); - if (picked != null) { - setState(() => rescuedAt = picked); - } - }, - onLongPress: () => - setState(() => rescuedAt = null), + Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Expanded( + child: ListTile( + contentPadding: EdgeInsets.zero, + leading: const Icon( + Icons.calendar_today), + title: const Text('Gerettet am'), + subtitle: Text( + rescuedAt == null + ? '—' + : df.format(rescuedAt!), + ), + onTap: () async { + final now = DateTime.now(); + final init = rescuedAt ?? now; + final picked = + await showDatePicker( + context: context, + initialDate: init, + firstDate: DateTime(2000), + lastDate: DateTime( + now.year + 1, 12, 31), + ); + if (picked != null) { + setState( + () => rescuedAt = picked); + } + }, + onLongPress: () => setState( + () => rescuedAt = null), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: locationC, + decoration: const InputDecoration( + labelText: + 'Ort / Fundstelle (optional)', + prefixIcon: + Icon(Icons.place_outlined), + ), + ), + ), + ], ), const SizedBox(height: 8), - - // ⬇️ „Ort / Fundstelle“ TextField( - controller: locationC, + controller: markingC, + decoration: const InputDecoration( + labelText: 'Markierung (optional)', + prefixIcon: Icon(Icons.sell_outlined), + ), + ), + const SizedBox(height: 8), + Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + controller: finderC, + decoration: const InputDecoration( + labelText: 'Finder (optional)', + prefixIcon: + Icon(Icons.person_search), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: phoneC, + keyboardType: TextInputType.phone, + decoration: const InputDecoration( + labelText: 'Telefon (optional)', + prefixIcon: + Icon(Icons.phone_outlined), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: emailC, + keyboardType: + TextInputType.emailAddress, + decoration: const InputDecoration( + labelText: 'Email (optional)', + prefixIcon: + Icon(Icons.email_outlined), + ), + ), + ), + ], + ), + const SizedBox(height: 8), + TextField( + controller: admissionReasonC, decoration: const InputDecoration( labelText: - 'Ort / Fundstelle (optional)', + 'Grund der Aufnahme (optional)', prefixIcon: - Icon(Icons.place_outlined), + Icon(Icons.playlist_add_check), ), ), const SizedBox(height: 8), - + TextField( + controller: animalConditionC, + decoration: const InputDecoration( + labelText: 'Zustand (optional)', + prefixIcon: + Icon(Icons.health_and_safety), + ), + ), + const SizedBox(height: 8), + Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Expanded( + child: ListTile( + contentPadding: EdgeInsets.zero, + leading: const Icon( + Icons.event_available), + title: const Text('Ausgewildert'), + subtitle: Text( + releasedAt == null + ? '—' + : df.format(releasedAt!), + ), + trailing: releasedAt == null + ? null + : IconButton( + tooltip: 'Datum löschen', + onPressed: () => setState( + () => releasedAt = + null), + icon: const Icon( + Icons.clear), + ), + onTap: () async { + final now = DateTime.now(); + final init = releasedAt ?? now; + final picked = + await showDatePicker( + context: context, + initialDate: init, + firstDate: DateTime(2000), + lastDate: DateTime( + now.year + 1, 12, 31), + ); + if (picked != null) { + setState(() => + releasedAt = picked); + } + }, + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: releaseLocationC, + decoration: const InputDecoration( + labelText: + 'Auswilderungsort (optional)', + prefixIcon: Icon(Icons + .nature_people_outlined), + ), + ), + ), + ], + ), + const SizedBox(height: 8), TextField( controller: noteC, decoration: const InputDecoration( @@ -514,10 +707,18 @@ class _IgelListState extends ConsumerState { noteC.clear(); featureC.clear(); locationC.clear(); // ⬅️ + markingC.clear(); + finderC.clear(); + phoneC.clear(); + emailC.clear(); + admissionReasonC.clear(); + animalConditionC.clear(); + releaseLocationC.clear(); setState(() { showForm = false; gender = null; rescuedAt = null; // ⬅️ + releasedAt = null; }); }, child: const Text('Abbrechen'), @@ -553,6 +754,16 @@ class _IgelListState extends ConsumerState { note: result.note, gender: result.gender, feature: result.feature, + rescuedAt: x.rescuedAt, + location: x.location, + marking: x.marking, + finder: x.finder, + phone: x.phone, + email: x.email, + admissionReason: x.admissionReason, + animalCondition: x.animalCondition, + releasedAt: x.releasedAt, + releaseLocation: x.releaseLocation, ); await _load(); _snack('Gespeichert'); @@ -568,10 +779,17 @@ class _IgelListState extends ConsumerState { await repo.create(ig.name, note: ig.note, gender: ig.gender, - feature: ig.feature - // Hinweis: rescuedAt/location könnten hier - // ebenfalls rekonstruiert werden, wenn Model vorhanden. - ); + feature: ig.feature, + rescuedAt: ig.rescuedAt, + location: ig.location, + marking: ig.marking, + finder: ig.finder, + phone: ig.phone, + email: ig.email, + admissionReason: ig.admissionReason, + animalCondition: ig.animalCondition, + releasedAt: ig.releasedAt, + releaseLocation: ig.releaseLocation); await _load(); }); }, @@ -659,8 +877,7 @@ class _IgelTile extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (feature != null) - Text(feature, - style: Theme.of(context).textTheme.bodyMedium), + Text(feature, style: Theme.of(context).textTheme.bodyMedium), if (weightLabel != null) Text(weightLabel!, style: Theme.of(context) diff --git a/lib/main.dart b/lib/main.dart index d0ceac2..c717e8d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,7 +14,7 @@ const kApiBase = String.fromEnvironment( const kBuildNumber = String.fromEnvironment( 'BUILD_NUMBER', - defaultValue: '2025.01.07.1', + defaultValue: '2026.06.26.1', ); final tokenStorageProvider =