added Finder and Release information

This commit is contained in:
2026-06-26 10:21:24 +02:00
parent da7ef93649
commit 2a211c8820
10 changed files with 865 additions and 136 deletions
@@ -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`;
+40 -8
View File
@@ -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]);
}
+8
View File
@@ -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;
@@ -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,
+46 -2
View File
@@ -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<dynamic>('/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<dynamic>('/igel/$id', body);
}
@@ -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,
);
+51
View File
@@ -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,
);
@@ -80,9 +80,15 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
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<IgelImage> images = [];
@@ -128,10 +134,18 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
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<IgelDetailScreen> {
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<IgelDetailScreen> {
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<IgelDetailScreen> {
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<IgelDetailScreen> {
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<IgelDetailScreen> {
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<IgelDetailScreen> {
'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<IgelDetailScreen> {
_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<IgelDetailScreen> {
_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<IgelDetailScreen> {
// --- Export PDF ---
Future<Uint8List?> _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<IgelDetailScreen> {
}
// 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<Uint8List?> _renderChartOffscreen(List<Messwert> data,
@@ -843,7 +916,8 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
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<IgelDetailScreen> {
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<IgelDetailScreen> {
),
);
pw.Widget infoDivider() => pw.Container(
height: 0.4,
margin: const pw.EdgeInsets.symmetric(vertical: 3),
color: PdfColors.grey300,
);
const infoGridWidths = <int, pw.TableColumnWidth>{
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 = <pw.Widget>[
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 = <pw.Widget>[];
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<IgelDetailScreen> {
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<IgelDetailScreen> {
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<IgelDetailScreen> {
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<IgelDetailScreen> {
),
),
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<IgelDetailScreen> {
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<IgelDetailScreen> {
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;
}
}
@@ -45,9 +45,17 @@ class _IgelListState extends ConsumerState<IgelListScreen> {
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<IgelListScreen> {
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<IgelListScreen> {
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<IgelListScreen> {
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<IgelListScreen> {
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<IgelListScreen> {
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<IgelListScreen> {
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<IgelListScreen> {
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<IgelListScreen> {
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<IgelListScreen> {
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)
+1 -1
View File
@@ -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 =