leafittome/lib/features/today/presentation/today_screen.dart
cschlaefke 1b2c880c89 V2: Haushalt teilen – Einladungscodes, Mitglieder- und Sitter-Rolle, sichtbare Bestätigungen
- Einladen per einmaligem 6-stelligem Code (7 Tage gültig), wahlweise als Mitglied oder Pflanzen-Sitter
- Beitritt über Cloud Function joinHousehold (Transaktion, Admin-Rechte)
- Security Rules: Sitter dürfen an Pflanzen nur Bestätigungs-Felder ändern; invites nur erstellen, nie lesen
- Haushalts-Screen: Mitgliederliste mit Rollen, Einladen, Beitreten mit Wechsel-Warnung
- UI-Gating: Sitter sehen keine Anlegen-/Bearbeiten-/Löschen-Aktionen
- Bestätigungen speichern den Namen (lastWateredBy/lastFertilizedBy), Anzeige im Profil
- Tests: Members-Map im Seed, neuer Sitter-Test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:14:34 +02:00

192 lines
5.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import '../../../core/router/app_router.dart';
import '../../../core/widgets/app_drawer.dart';
import '../../../l10n/generated/app_localizations.dart';
import '../../household/data/household_providers.dart';
import '../../household/domain/household.dart';
import '../../plants/data/plants_provider.dart';
import '../application/due_tasks_provider.dart';
import '../domain/due_task.dart';
/// Start-Screen der App: die Tages-Checkliste.
/// Push-Erinnerungen führen später direkt hierher.
class TodayScreen extends ConsumerWidget {
const TodayScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final plantsAsync = ref.watch(plantsProvider);
final tasks = ref.watch(dueTasksProvider);
final Widget body;
if (plantsAsync.isLoading && !plantsAsync.hasValue) {
body = const Center(child: CircularProgressIndicator());
} else if (tasks.isEmpty) {
body = const _AllDoneView();
} else {
body = _TaskListView(tasks: tasks);
}
final canEdit = ref.watch(myRoleProvider) == HouseholdRole.member;
return Scaffold(
appBar: AppBar(title: Text(l10n.todayTitle)),
drawer: const AppDrawer(),
floatingActionButton: canEdit
? FloatingActionButton.extended(
onPressed: () => context.push(AppRoutes.plantAdd),
icon: const Icon(Icons.add),
label: Text(l10n.addPlant),
)
: null,
body: body,
);
}
}
class _TaskListView extends ConsumerWidget {
const _TaskListView({required this.tasks});
final List<DueTask> tasks;
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
return ListView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 96),
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
l10n.todayOpenTasks(tasks.length),
style: Theme.of(context).textTheme.titleMedium,
),
),
for (final task in tasks) _TaskCard(task: task),
],
);
}
}
class _TaskCard extends ConsumerWidget {
const _TaskCard({required this.task});
final DueTask task;
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final theme = Theme.of(context);
final today = DateTime.now();
final overdueDays =
task.overdueDays(DateTime(today.year, today.month, today.day));
final title = switch (task.type) {
CareTaskType.watering => l10n.taskWaterTitle(task.plant.nickname),
CareTaskType.fertilizing => l10n.taskFertilizeTitle(task.plant.nickname),
};
final icon = switch (task.type) {
CareTaskType.watering => Icons.water_drop,
CareTaskType.fertilizing => Icons.compost,
};
return Card(
margin: const EdgeInsets.symmetric(vertical: 6),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(icon, size: 32, color: theme.colorScheme.primary),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: theme.textTheme.titleMedium),
const SizedBox(height: 4),
Text(
overdueDays > 0
? l10n.taskOverdue(overdueDays)
: l10n.taskDueToday,
style: theme.textTheme.bodyMedium?.copyWith(
color: overdueDays > 0
? theme.colorScheme.error
: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
const SizedBox(width: 12),
FilledButton.icon(
onPressed: () {
ref
.read(plantRepositoryProvider)
.confirmTask(task.plant.id, task.type);
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content:
Text(l10n.taskDoneConfirmation(task.plant.nickname)),
),
);
},
icon: const Icon(Icons.check),
label: Text(l10n.taskDone),
),
],
),
),
);
}
}
class _AllDoneView extends ConsumerWidget {
const _AllDoneView();
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final theme = Theme.of(context);
final nextDue = ref.watch(nextDueDateProvider);
final locale = Localizations.localeOf(context).toString();
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.check_circle,
size: 72,
color: theme.colorScheme.primary,
),
const SizedBox(height: 16),
Text(
l10n.todayAllDone,
style: theme.textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
nextDue != null
? l10n.todayNextTask(
DateFormat('EEEE, d. MMMM', locale).format(nextDue))
: l10n.todayEmptyHint,
style: theme.textTheme.bodyLarge,
textAlign: TextAlign.center,
),
],
),
),
);
}
}