leafittome/lib/features/plants/presentation/plant_form_screen.dart
cschlaefke 53ee30c598 Firestore und Auth: echte Daten statt Demo, Login mit E-Mail+Passwort, Security Rules
- Datenmodell: users/{uid}, households/{id} mit memberUids, plants/locations als Subcollections
- StreamProvider liefern Live-Daten, Repositories kapseln Schreibzugriffe
- Login-Screen mit Registrierung (legt Haushalt automatisch an), Logout in Einstellungen
- Router-Redirect bei Login/Logout, firestore.rules mit Haushalts-Prinzip
- Tests auf firebase_auth_mocks + fake_cloud_firestore umgestellt
- Verifiziert: analyze/test grün, Android-Debug-Build erfolgreich

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 22:53:09 +02:00

189 lines
6.6 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../l10n/generated/app_localizations.dart';
import '../../locations/data/locations_provider.dart';
import '../data/plants_provider.dart';
import '../domain/plant.dart';
/// Manuelles Anlegen und Bearbeiten einer Pflanze.
///
/// Im KI-Block von V1 wird dieser Screen um den Foto-Flow ergänzt:
/// Foto → PlantNet-Erkennung → Claude befüllt Art, Beschreibung und
/// Intervall-Vorschläge vor der Nutzer bestätigt hier nur noch.
class PlantFormScreen extends ConsumerStatefulWidget {
const PlantFormScreen({super.key, this.existing});
final Plant? existing;
@override
ConsumerState<PlantFormScreen> createState() => _PlantFormScreenState();
}
class _PlantFormScreenState extends ConsumerState<PlantFormScreen> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _nicknameController;
late final TextEditingController _speciesController;
late final TextEditingController _wateringController;
late final TextEditingController _fertilizingController;
String? _locationId;
@override
void initState() {
super.initState();
final existing = widget.existing;
_nicknameController = TextEditingController(text: existing?.nickname ?? '');
_speciesController = TextEditingController(text: existing?.species ?? '');
_wateringController = TextEditingController(
text: existing?.wateringIntervalDays.toString() ?? '7');
_fertilizingController = TextEditingController(
text: existing?.fertilizingIntervalDays.toString() ?? '28');
_locationId = existing?.locationId;
}
@override
void dispose() {
_nicknameController.dispose();
_speciesController.dispose();
_wateringController.dispose();
_fertilizingController.dispose();
super.dispose();
}
void _save() {
if (!_formKey.currentState!.validate()) return;
// Firestore puffert Schreibzugriffe lokal (Offline-Persistenz)
// wir müssen hier nicht auf den Server warten.
final repository = ref.read(plantRepositoryProvider);
final existing = widget.existing;
if (existing != null) {
repository.updatePlant(existing.copyWith(
nickname: _nicknameController.text.trim(),
species: _speciesController.text.trim(),
locationId: () => _locationId,
wateringIntervalDays: int.parse(_wateringController.text.trim()),
fertilizingIntervalDays: int.parse(_fertilizingController.text.trim()),
));
} else {
repository.addPlant(Plant(
id: '', // wird von Firestore vergeben
nickname: _nicknameController.text.trim(),
species: _speciesController.text.trim(),
locationId: _locationId,
wateringIntervalDays: int.parse(_wateringController.text.trim()),
fertilizingIntervalDays: int.parse(_fertilizingController.text.trim()),
));
}
context.pop();
}
String? _validateRequired(String? value) {
final l10n = AppLocalizations.of(context);
if (value == null || value.trim().isEmpty) return l10n.requiredField;
return null;
}
String? _validateInterval(String? value) {
final l10n = AppLocalizations.of(context);
final parsed = int.tryParse(value?.trim() ?? '');
if (parsed == null || parsed <= 0) return l10n.invalidNumber;
return null;
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final locations = ref.watch(locationsProvider).value ?? const [];
return Scaffold(
appBar: AppBar(
title: Text(
widget.existing != null ? l10n.plantDetailTitle : l10n.addPlant),
),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
TextFormField(
controller: _nicknameController,
decoration: InputDecoration(
labelText: l10n.nicknameLabel,
border: const OutlineInputBorder(),
),
textInputAction: TextInputAction.next,
validator: _validateRequired,
),
const SizedBox(height: 16),
TextFormField(
controller: _speciesController,
decoration: InputDecoration(
labelText: l10n.speciesLabel,
border: const OutlineInputBorder(),
),
textInputAction: TextInputAction.next,
validator: _validateRequired,
),
const SizedBox(height: 16),
DropdownButtonFormField<String?>(
initialValue: _locationId,
decoration: InputDecoration(
labelText: l10n.locationLabel,
border: const OutlineInputBorder(),
),
items: [
DropdownMenuItem<String?>(
value: null,
child: Text(l10n.locationNone),
),
for (final location in locations)
DropdownMenuItem<String?>(
value: location.id,
child: Text(location.name),
),
],
onChanged: (value) => setState(() => _locationId = value),
),
const SizedBox(height: 16),
TextFormField(
controller: _wateringController,
decoration: InputDecoration(
labelText: l10n.wateringIntervalLabel,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.water_drop),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: _validateInterval,
),
const SizedBox(height: 16),
TextFormField(
controller: _fertilizingController,
decoration: InputDecoration(
labelText: l10n.fertilizingIntervalLabel,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.compost),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.done,
validator: _validateInterval,
),
const SizedBox(height: 24),
FilledButton.icon(
onPressed: _save,
icon: const Icon(Icons.check),
label: Text(l10n.save),
),
const SizedBox(height: 8),
TextButton(
onPressed: () => context.pop(),
child: Text(l10n.cancel),
),
],
),
),
);
}
}