V3: Untersuchungshistorie auf eigene Seite ausgelagert
Neue Route /plants/:id/diagnoses mit PlantDiagnosesScreen; im Detail ersetzt der Button „Frühere Untersuchungen" (deaktiviert bei leerer Historie) die Inline-Liste. Ergebnis-Sheet nach diagnosis_sheet.dart ausgelagert und von beiden Screens genutzt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9caaa2ef79
commit
23560768e2
9 changed files with 336 additions and 226 deletions
|
|
@ -40,9 +40,9 @@ Private Flutter-App (iOS+Android) zur Pflanzenpflege für Haushalt + Pflanzen-Si
|
|||
|
||||
**Erweiterung Untersuchungshistorie (2026-07-23):** Jede Diagnose wird automatisch unter `households/{id}/plants/{id}/diagnoses` gespeichert (healthy, matchesSpecies, Texte, `createdAt` Server-Timestamp). Im Pflanzen-Detail unter dem Untersuchen-Button: Liste „Untersuchungen" (neueste zuerst, Ampel-Icon + Kurzbefund + Datum), Tipp öffnet dasselbe Ergebnis-Sheet inkl. „Untersucht am …". Volle Mitglieder können Einträge löschen (Bestätigungsdialog); Sitter dürfen speichern/lesen, Einträge sind nachträglich unveränderbar (Rules). Neuer Widget-Test navigiert bis ins Sheet — alle 11 Tests grün.
|
||||
|
||||
**Noch zu tun (Nutzer):**
|
||||
1. **Rules-Deploy** (neue diagnoses-Regeln; ohne sie schlägt das Speichern der Historie still fehl): `firebase deploy --only firestore --project leaf-it-to-me-app`
|
||||
2. App neu bauen, Untersuchung durchführen → Eintrag erscheint in „Untersuchungen", antippen zeigt den vollen Text, Löschen fragt nach.
|
||||
Rules deployt (2026-07-23). **Auf Nutzerwunsch ausgelagert:** Die Historie liegt jetzt auf einer eigenen Seite (`/plants/:id/diagnoses`, `PlantDiagnosesScreen`) statt inline im Detail; im Detail führt der Button „Frühere Untersuchungen" dorthin (deaktiviert/ausgegraut, solange keine Untersuchungen existieren). Das Ergebnis-Sheet ist in `diagnosis_sheet.dart` ausgelagert und wird von Detail (frische Untersuchung) und Historie-Seite gemeinsam genutzt. 12 Tests grün (neu: Button-deaktiviert-Test).
|
||||
|
||||
**Noch zu tun (Nutzer):** App neu bauen und die ausgelagerte Historie-Seite einmal durchklicken (Button → Liste → Eintrag → Sheet; Löschen).
|
||||
|
||||
**Danach V3 weiter:** Stellplatz-Bewertung (Zwei-Ebenen-Konzept), Umtopf-Erinnerungen.
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import '../../features/household/presentation/household_screen.dart';
|
|||
import '../../features/locations/presentation/locations_screen.dart';
|
||||
import '../../features/plants/domain/plant.dart';
|
||||
import '../../features/plants/presentation/plant_detail_screen.dart';
|
||||
import '../../features/plants/presentation/plant_diagnoses_screen.dart';
|
||||
import '../../features/plants/presentation/plant_form_screen.dart';
|
||||
import '../../features/plants/presentation/plants_screen.dart';
|
||||
import '../../features/settings/presentation/settings_screen.dart';
|
||||
|
|
@ -26,6 +27,10 @@ abstract final class AppRoutes {
|
|||
static const locations = '/locations';
|
||||
static const household = '/household';
|
||||
static const settings = '/settings';
|
||||
|
||||
/// Untersuchungs-Seite einer Pflanze.
|
||||
static String plantDiagnoses(String plantId) =>
|
||||
'/plants/$plantId/diagnoses';
|
||||
}
|
||||
|
||||
/// Übersetzt einen Stream in ein Listenable, damit der Router bei
|
||||
|
|
@ -86,6 +91,11 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
|||
builder: (context, state) =>
|
||||
PlantDetailScreen(plantId: state.pathParameters['id']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/plants/:id/diagnoses',
|
||||
builder: (context, state) =>
|
||||
PlantDiagnosesScreen(plantId: state.pathParameters['id']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: AppRoutes.locations,
|
||||
builder: (context, state) => const LocationsScreen(),
|
||||
|
|
|
|||
127
lib/features/plants/presentation/diagnosis_sheet.dart
Normal file
127
lib/features/plants/presentation/diagnosis_sheet.dart
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../l10n/generated/app_localizations.dart';
|
||||
import '../data/plant_diagnosis_service.dart';
|
||||
|
||||
/// Zeigt das Diagnose-Ergebnis als scrollbares Bottom-Sheet — sowohl für
|
||||
/// eine frische Untersuchung (Detail-Screen) als auch für Einträge aus der
|
||||
/// Untersuchungs-Seite ([createdAt] gesetzt).
|
||||
Future<void> showDiagnosisSheet(
|
||||
BuildContext context, {
|
||||
required DiagnosisResult result,
|
||||
required String plantNickname,
|
||||
DateTime? createdAt,
|
||||
}) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (sheetContext) {
|
||||
final theme = Theme.of(sheetContext);
|
||||
final healthyColor = theme.brightness == Brightness.dark
|
||||
? Colors.green.shade300
|
||||
: Colors.green.shade700;
|
||||
return DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: 0.6,
|
||||
maxChildSize: 0.95,
|
||||
builder: (context, scrollController) => ListView(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.all(24),
|
||||
children: [
|
||||
if (!result.matchesSpecies) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.swap_horiz,
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.diagnosisSpeciesMismatch(plantNickname),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
result.healthy
|
||||
? Icons.check_circle
|
||||
: Icons.warning_amber_rounded,
|
||||
size: 32,
|
||||
color:
|
||||
result.healthy ? healthyColor : theme.colorScheme.error,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
result.summary,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (createdAt != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.diagnosisDate(
|
||||
DateFormat('d. MMMM y, HH:mm', locale).format(createdAt),
|
||||
),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (result.details.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(result.details, style: theme.textTheme.bodyLarge),
|
||||
],
|
||||
if (result.treatment.isNotEmpty) ...[
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
l10n.diagnosisTreatmentTitle,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(result.treatment, style: theme.textTheme.bodyLarge),
|
||||
],
|
||||
if (result.prevention.isNotEmpty) ...[
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
l10n.diagnosisPreventionTitle,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(result.prevention, style: theme.textTheme.bodyLarge),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.diagnosisDisclaimer,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import '../../locations/data/locations_provider.dart';
|
|||
import '../data/plant_diagnosis_service.dart';
|
||||
import '../data/plants_provider.dart';
|
||||
import '../domain/plant.dart';
|
||||
import 'diagnosis_sheet.dart';
|
||||
|
||||
class PlantDetailScreen extends ConsumerWidget {
|
||||
const PlantDetailScreen({super.key, required this.plantId});
|
||||
|
|
@ -150,230 +151,12 @@ class PlantDetailScreen extends ConsumerWidget {
|
|||
],
|
||||
const Divider(height: 32),
|
||||
_DiagnosisSection(plant: plant),
|
||||
_DiagnosisHistory(plant: plant),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Zeigt das Diagnose-Ergebnis als scrollbares Bottom-Sheet — sowohl für
|
||||
/// eine frische Untersuchung als auch für Einträge aus der Historie
|
||||
/// ([createdAt] gesetzt).
|
||||
Future<void> _showDiagnosisSheet(
|
||||
BuildContext context, {
|
||||
required DiagnosisResult result,
|
||||
required String plantNickname,
|
||||
DateTime? createdAt,
|
||||
}) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (sheetContext) {
|
||||
final theme = Theme.of(sheetContext);
|
||||
final healthyColor = theme.brightness == Brightness.dark
|
||||
? Colors.green.shade300
|
||||
: Colors.green.shade700;
|
||||
return DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: 0.6,
|
||||
maxChildSize: 0.95,
|
||||
builder: (context, scrollController) => ListView(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.all(24),
|
||||
children: [
|
||||
if (!result.matchesSpecies) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.swap_horiz,
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.diagnosisSpeciesMismatch(plantNickname),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
result.healthy
|
||||
? Icons.check_circle
|
||||
: Icons.warning_amber_rounded,
|
||||
size: 32,
|
||||
color:
|
||||
result.healthy ? healthyColor : theme.colorScheme.error,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
result.summary,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (createdAt != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.diagnosisDate(
|
||||
DateFormat('d. MMMM y, HH:mm', locale).format(createdAt),
|
||||
),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (result.details.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(result.details, style: theme.textTheme.bodyLarge),
|
||||
],
|
||||
if (result.treatment.isNotEmpty) ...[
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
l10n.diagnosisTreatmentTitle,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(result.treatment, style: theme.textTheme.bodyLarge),
|
||||
],
|
||||
if (result.prevention.isNotEmpty) ...[
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
l10n.diagnosisPreventionTitle,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(result.prevention, style: theme.textTheme.bodyLarge),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.diagnosisDisclaimer,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Liste der gespeicherten Untersuchungen dieser Pflanze (neueste zuerst).
|
||||
class _DiagnosisHistory extends ConsumerWidget {
|
||||
const _DiagnosisHistory({required this.plant});
|
||||
|
||||
final Plant plant;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final entries = ref.watch(plantDiagnosesProvider(plant.id)).value ?? [];
|
||||
if (entries.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
final canEdit = ref.watch(myRoleProvider) == HouseholdRole.member;
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
final dateFormat = DateFormat('d. MMMM y, HH:mm', locale);
|
||||
final healthyColor = theme.brightness == Brightness.dark
|
||||
? Colors.green.shade300
|
||||
: Colors.green.shade700;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Text(l10n.diagnosisHistoryTitle, style: theme.textTheme.titleMedium),
|
||||
for (final entry in entries)
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(
|
||||
entry.result.healthy
|
||||
? Icons.check_circle
|
||||
: Icons.warning_amber_rounded,
|
||||
color: entry.result.healthy
|
||||
? healthyColor
|
||||
: theme.colorScheme.error,
|
||||
),
|
||||
title: Text(
|
||||
entry.result.summary,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: entry.createdAt != null
|
||||
? Text(dateFormat.format(entry.createdAt!))
|
||||
: null,
|
||||
trailing: canEdit
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
tooltip: l10n.delete,
|
||||
onPressed: () => _confirmDelete(context, ref, entry),
|
||||
)
|
||||
: null,
|
||||
onTap: () => _showDiagnosisSheet(
|
||||
context,
|
||||
result: entry.result,
|
||||
plantNickname: plant.nickname,
|
||||
createdAt: entry.createdAt,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmDelete(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
PlantDiagnosisEntry entry,
|
||||
) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
final dateText = entry.createdAt != null
|
||||
? DateFormat('d. MMMM y', locale).format(entry.createdAt!)
|
||||
: '';
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(l10n.deleteDiagnosisQuestion(dateText)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
child: Text(l10n.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, true),
|
||||
child: Text(l10n.delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true) {
|
||||
await ref
|
||||
.read(plantDiagnosisServiceProvider)
|
||||
.deleteDiagnosis(plant.id, entry.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Krankheits-Check (V3): Foto aufnehmen → Claude beurteilt den Zustand
|
||||
/// und gibt Behandlungstipps. Auch für Sitter nutzbar (nur lesend).
|
||||
|
|
@ -437,7 +220,7 @@ class _DiagnosisSectionState extends ConsumerState<_DiagnosisSection> {
|
|||
debugPrint('Untersuchung speichern fehlgeschlagen: $error');
|
||||
}
|
||||
if (!mounted) return;
|
||||
await _showDiagnosisSheet(
|
||||
await showDiagnosisSheet(
|
||||
context,
|
||||
result: result,
|
||||
plantNickname: widget.plant.nickname,
|
||||
|
|
@ -465,6 +248,13 @@ class _DiagnosisSectionState extends ConsumerState<_DiagnosisSection> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
// Für den Historie-Button: deaktiviert, solange keine Untersuchungen
|
||||
// gespeichert sind (oder die Liste noch lädt).
|
||||
final hasHistory = (ref
|
||||
.watch(plantDiagnosesProvider(widget.plant.id))
|
||||
.value ??
|
||||
const [])
|
||||
.isNotEmpty;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
|
|
@ -487,6 +277,15 @@ class _DiagnosisSectionState extends ConsumerState<_DiagnosisSection> {
|
|||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
onPressed: hasHistory
|
||||
? () => context
|
||||
.push(AppRoutes.plantDiagnoses(widget.plant.id))
|
||||
: null,
|
||||
icon: const Icon(Icons.history),
|
||||
label: Text(l10n.diagnosisHistoryButton),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
129
lib/features/plants/presentation/plant_diagnoses_screen.dart
Normal file
129
lib/features/plants/presentation/plant_diagnoses_screen.dart
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../l10n/generated/app_localizations.dart';
|
||||
import '../../household/data/household_providers.dart';
|
||||
import '../../household/domain/household.dart';
|
||||
import '../data/plant_diagnosis_service.dart';
|
||||
import '../data/plants_provider.dart';
|
||||
import 'diagnosis_sheet.dart';
|
||||
|
||||
/// Untersuchungs-Seite einer Pflanze: alle gespeicherten Diagnosen
|
||||
/// (neueste zuerst); Antippen öffnet das volle Ergebnis, volle Mitglieder
|
||||
/// können Einträge löschen.
|
||||
class PlantDiagnosesScreen extends ConsumerWidget {
|
||||
const PlantDiagnosesScreen({super.key, required this.plantId});
|
||||
|
||||
final String plantId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final plant = ref.watch(plantByIdProvider(plantId));
|
||||
|
||||
if (plant == null) {
|
||||
// Pflanze wurde gelöscht, während der Screen offen war.
|
||||
return Scaffold(appBar: AppBar(), body: const SizedBox.shrink());
|
||||
}
|
||||
|
||||
final entries = ref.watch(plantDiagnosesProvider(plantId)).value ?? [];
|
||||
final canEdit = ref.watch(myRoleProvider) == HouseholdRole.member;
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
final dateFormat = DateFormat('d. MMMM y, HH:mm', locale);
|
||||
final healthyColor = theme.brightness == Brightness.dark
|
||||
? Colors.green.shade300
|
||||
: Colors.green.shade700;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('${l10n.diagnosisHistoryTitle} · ${plant.nickname}'),
|
||||
),
|
||||
body: entries.isEmpty
|
||||
// Kommt nur vor, wenn hier der letzte Eintrag gelöscht wurde —
|
||||
// der Button im Detail ist bei leerer Historie deaktiviert.
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
l10n.diagnosisHistoryEmpty,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: entries.length,
|
||||
itemBuilder: (context, index) {
|
||||
final entry = entries[index];
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
entry.result.healthy
|
||||
? Icons.check_circle
|
||||
: Icons.warning_amber_rounded,
|
||||
color: entry.result.healthy
|
||||
? healthyColor
|
||||
: theme.colorScheme.error,
|
||||
),
|
||||
title: Text(
|
||||
entry.result.summary,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: entry.createdAt != null
|
||||
? Text(dateFormat.format(entry.createdAt!))
|
||||
: null,
|
||||
trailing: canEdit
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
tooltip: l10n.delete,
|
||||
onPressed: () => _confirmDelete(context, ref, entry),
|
||||
)
|
||||
: null,
|
||||
onTap: () => showDiagnosisSheet(
|
||||
context,
|
||||
result: entry.result,
|
||||
plantNickname: plant.nickname,
|
||||
createdAt: entry.createdAt,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmDelete(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
PlantDiagnosisEntry entry,
|
||||
) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
final dateText = entry.createdAt != null
|
||||
? DateFormat('d. MMMM y', locale).format(entry.createdAt!)
|
||||
: '';
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(l10n.deleteDiagnosisQuestion(dateText)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
child: Text(l10n.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, true),
|
||||
child: Text(l10n.delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true) {
|
||||
await ref
|
||||
.read(plantDiagnosisServiceProvider)
|
||||
.deleteDiagnosis(plantId, entry.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -241,6 +241,8 @@
|
|||
},
|
||||
"diagnosisFailed": "Die Untersuchung hat nicht geklappt. Bitte versuche es erneut.",
|
||||
"diagnosisHistoryTitle": "Untersuchungen",
|
||||
"diagnosisHistoryButton": "Frühere Untersuchungen",
|
||||
"diagnosisHistoryEmpty": "Noch keine Untersuchungen gespeichert.",
|
||||
"diagnosisDate": "Untersucht am {date}",
|
||||
"@diagnosisDate": {
|
||||
"placeholders": {
|
||||
|
|
|
|||
|
|
@ -880,6 +880,18 @@ abstract class AppLocalizations {
|
|||
/// **'Untersuchungen'**
|
||||
String get diagnosisHistoryTitle;
|
||||
|
||||
/// No description provided for @diagnosisHistoryButton.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Frühere Untersuchungen'**
|
||||
String get diagnosisHistoryButton;
|
||||
|
||||
/// No description provided for @diagnosisHistoryEmpty.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Noch keine Untersuchungen gespeichert.'**
|
||||
String get diagnosisHistoryEmpty;
|
||||
|
||||
/// No description provided for @diagnosisDate.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
|
|
|
|||
|
|
@ -481,6 +481,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get diagnosisHistoryTitle => 'Untersuchungen';
|
||||
|
||||
@override
|
||||
String get diagnosisHistoryButton => 'Frühere Untersuchungen';
|
||||
|
||||
@override
|
||||
String get diagnosisHistoryEmpty => 'Noch keine Untersuchungen gespeichert.';
|
||||
|
||||
@override
|
||||
String diagnosisDate(String date) {
|
||||
return 'Untersucht am $date';
|
||||
|
|
|
|||
|
|
@ -406,10 +406,14 @@ void main() {
|
|||
await tester.tap(find.text('Monstera'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Historie ist sichtbar.
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Die Blätter zeigen braune Flecken.'), 200);
|
||||
expect(find.text('Untersuchungen'), findsOneWidget);
|
||||
// Historie-Button ist aktiv (Untersuchungen vorhanden) → Seite öffnen.
|
||||
await tester.scrollUntilVisible(find.text('Frühere Untersuchungen'), 200);
|
||||
await tester.tap(find.text('Frühere Untersuchungen'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Untersuchungs-Seite listet den Eintrag.
|
||||
expect(find.textContaining('Untersuchungen'), findsWidgets);
|
||||
expect(find.text('Die Blätter zeigen braune Flecken.'), findsOneWidget);
|
||||
expect(find.textContaining('20. Juli 2026'), findsOneWidget);
|
||||
|
||||
// Eintrag öffnen → volles Ergebnis-Sheet mit Behandlungstipps.
|
||||
|
|
@ -419,6 +423,27 @@ void main() {
|
|||
expect(find.text('Stelle die Pflanze etwas schattiger.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Ohne gespeicherte Untersuchungen ist der Historie-Button deaktiviert',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(await buildTestApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byIcon(Icons.menu));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Meine Pflanzen'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Monstera'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.scrollUntilVisible(find.text('Frühere Untersuchungen'), 200);
|
||||
final button = tester.widget<OutlinedButton>(find.ancestor(
|
||||
of: find.text('Frühere Untersuchungen'),
|
||||
matching: find.byType(OutlinedButton),
|
||||
));
|
||||
expect(button.onPressed, isNull);
|
||||
});
|
||||
|
||||
testWidgets('Mitglied kann den Haushalt umbenennen', (tester) async {
|
||||
await tester.pumpWidget(await buildTestApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
|
|
|||
Loading…
Reference in a new issue