vor Import/Export

This commit is contained in:
2025-10-24 12:39:10 +02:00
parent 77ad6b3823
commit 8abe0ca966
6 changed files with 214 additions and 113 deletions
+36 -12
View File
@@ -1,3 +1,4 @@
// lib/features/igel/data/igel_repository.dart
import '../../../shared/api_client.dart';
import '../domain/igel.dart';
@@ -12,24 +13,27 @@ class IgelRepository {
}
Future<Igel> create(String name,
{String? note, String? gender, String? feature}) async {
{String? note,
String? gender,
String? feature,
DateTime? rescuedAt,
String? location}) async {
String? fmtDate(DateTime? d) =>
d == null ? null : d.toIso8601String().split('T').first;
final res = await api.post<dynamic>('/igel', {
'name': name,
'note': note,
'gender': gender,
'feature': feature,
'rescued_at': fmtDate(rescuedAt),
'location': (location?.trim().isEmpty ?? true) ? null : location!.trim(),
});
return Igel.fromMap(res as Map<String, dynamic>);
}
Future<void> update(int id, String name,
{String? note, String? gender, String? feature}) async {
await api.put<dynamic>('/igel/$id', {
'name': name,
'note': note,
'gender': gender,
'feature': feature,
});
try {
final id = (res['id'] as num).toInt();
return await get(id);
} catch (_) {
return Igel.fromMap((res as Map).cast<String, dynamic>());
}
}
Future<void> delete(int id) async {
@@ -40,4 +44,24 @@ class IgelRepository {
final map = await api.get<Map<String, dynamic>>('/igel/$id');
return Igel.fromMap(map);
}
/// Update hedgehog data. Backend requires name; other fields optional.
Future<void> update(int id, String name,
{String? note,
String? gender,
String? feature,
DateTime? rescuedAt,
String? location}) async {
String? fmtDate(DateTime? d) =>
d == null ? null : d.toIso8601String().split('T').first;
final body = {
'name': name,
'note': note,
'gender': gender,
'feature': feature,
'rescued_at': fmtDate(rescuedAt),
'location': (location?.trim().isEmpty ?? true) ? null : location!.trim(),
};
await api.put<dynamic>('/igel/$id', body);
}
}
+47 -27
View File
@@ -4,6 +4,11 @@ class Igel {
final String? gender; // 'männlich' | 'weiblich' | 'unbekannt'
final String? feature; // Merkmal
final String? note; // Information
// 🆕 NEU:
final DateTime? rescuedAt; // YYYY-MM-DD (Datum)
final String? location; // Ort / Fundstelle
final DateTime? createdAt;
final DateTime? updatedAt;
@@ -13,52 +18,67 @@ class Igel {
this.gender,
this.feature,
this.note,
this.rescuedAt,
this.location,
this.createdAt,
this.updatedAt,
});
// ---------- Factory fromMap ----------
factory Igel.fromMap(Map<String, dynamic> map) => Igel(
id: (map['id'] as num).toInt(),
name: map['name'] as String,
gender: map['gender'] as String?,
feature: map['feature'] as String?,
note: map['note'] as String?,
createdAt: map['created_at'] != null
? DateTime.parse(map['created_at'])
: null,
updatedAt: map['updated_at'] != null
? DateTime.parse(map['updated_at'])
: null,
);
static DateTime? _parseDateOrNull(String? s) {
if (s == null || s.isEmpty) return null;
try {
return DateTime.parse(s);
} catch (_) {
return null;
}
}
// ---------- toMap (optional für PUT/POST) ----------
Map<String, dynamic> toMap() => {
'id': id,
'name': name,
if (gender != null) 'gender': gender,
if (feature != null) 'feature': feature,
if (note != null) 'note': note,
if (createdAt != null) 'created_at': createdAt!.toIso8601String(),
if (updatedAt != null) 'updated_at': updatedAt!.toIso8601String(),
};
factory Igel.fromMap(Map<String, dynamic> m) {
return Igel(
id: (m['id'] as num).toInt(),
name: (m['name'] ?? '') as String,
gender: m['gender'] as String?,
feature: m['feature'] as String?,
note: m['note'] as String?,
rescuedAt: _parseDateOrNull(m['rescued_at'] as String?),
location: m['location'] as String?,
createdAt: _parseDateOrNull(m['created_at'] as String?),
updatedAt: _parseDateOrNull(m['updated_at'] as String?),
);
}
Map<String, dynamic> toMapForCreateUpdate() {
String? fmtDate(DateTime? d) =>
d == null ? null : d.toIso8601String().split('T').first;
return {
'name': name,
'gender': gender,
'feature': feature,
'note': note,
'rescued_at': fmtDate(rescuedAt),
'location': location?.trim().isEmpty == true ? null : location,
};
}
// ---------- copyWith ----------
Igel copyWith({
int? id,
String? name,
String? gender,
String? feature,
String? note,
DateTime? rescuedAt,
String? location,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return Igel(
id: id ?? this.id,
id: id,
name: name ?? this.name,
gender: gender ?? this.gender,
feature: feature ?? this.feature,
note: note ?? this.note,
rescuedAt: rescuedAt ?? this.rescuedAt,
location: location ?? this.location,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
@@ -43,6 +43,13 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
final nameC = TextEditingController();
final featureC = TextEditingController();
final noteC = TextEditingController();
// NEU: Ort/Fundstelle + Gerettet am
final locationC = TextEditingController();
DateTime? rescuedAt;
DateTime? _initRescuedAt;
String? _initLocation;
String? gender; // 'männlich' | 'weiblich' | 'unbekannt' | null
// Bilder
@@ -70,6 +77,7 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
featureC.addListener(_markDirtyBasics);
noteC.addListener(_markDirtyBasics);
locationC.addListener(_markDirtyBasics); // NEU
_loadAll();
}
@@ -79,6 +87,7 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
nameC.dispose();
featureC.dispose();
noteC.dispose();
locationC.dispose(); // NEU
mwGewichtC.dispose();
mwBehandlungC.dispose();
mwBemerkungC.dispose();
@@ -91,14 +100,25 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
bool get _basicsChanged {
if (igel == null) return false;
final newName = nameC.text.trim();
final newFeature =
featureC.text.trim().isEmpty ? null : featureC.text.trim();
final newNote = noteC.text.trim().isEmpty ? null : noteC.text.trim();
final changed = newName != igel!.name ||
bool changed = newName != igel!.name ||
newFeature != igel!.feature ||
newNote != igel!.note ||
gender != igel!.gender;
// NEU: rescuedAt & location berücksichtigen
String? fmt(DateTime? d) =>
d == null ? null : DateFormat('yyyy-MM-dd').format(d);
if (fmt(rescuedAt) != fmt(_initRescuedAt)) changed = true;
final newLoc = locationC.text.trim().isEmpty ? null : locationC.text.trim();
if (newLoc != (_initLocation?.trim())) changed = true;
return changed;
}
@@ -116,6 +136,12 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
noteC.text = data.note ?? '';
gender = data.gender;
// NEU: rescuedAt + location aus Domain (falls Backend schon erweitert)
rescuedAt = data.rescuedAt;
locationC.text = data.location ?? '';
_initRescuedAt = data.rescuedAt;
_initLocation = data.location;
// Bilder
images = await imagesRepo.list(widget.igelId);
if (mounted) await _prefetchImages(context);
@@ -158,15 +184,31 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
return;
}
try {
// NEU: rescuedAt + location mitsenden (Repo wurde entsprechend erweitert)
await igelRepo.update(
widget.igelId,
newName,
note: newNote,
gender: gender,
feature: newFeature,
rescuedAt: rescuedAt,
location: locationC.text.trim().isEmpty ? null : locationC.text.trim(),
);
igel = igel!.copyWith(
name: newName, note: newNote, gender: gender, feature: newFeature);
name: newName,
note: newNote,
gender: gender,
feature: newFeature,
rescuedAt: rescuedAt,
location: locationC.text.trim().isEmpty ? null : locationC.text.trim(),
);
// NEU: Init-Stand für Change-Detection aktualisieren
_initRescuedAt = rescuedAt;
_initLocation =
locationC.text.trim().isEmpty ? null : locationC.text.trim();
setState(() {});
_snack('Gespeichert');
} catch (e) {
@@ -547,6 +589,50 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
prefixIcon: Icon(Icons.style),
),
),
const SizedBox(height: 12),
// NEU: Gerettet am (Datum)
ListTile(
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: () 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(height: 12),
// NEU: Ort / Fundstelle
TextField(
controller: locationC,
decoration: const InputDecoration(
labelText:
'Ort / Fundstelle (optional)',
prefixIcon:
Icon(Icons.place_outlined),
),
),
const SizedBox(height: 12),
TextField(
controller: noteC,
@@ -758,7 +844,7 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
),
],
)),
]),
]), // for
],
),
),