added Finder and Release information
This commit is contained in:
@@ -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
@@ -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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user