leafittome/lib/features/today/presentation/today_screen.dart
cschlaefke 81d990dd7e V3: Umtopf-Erinnerungen (opt-in, Intervall in Monaten)
Dritter Aufgabentyp repotting im Intervall-Modell: optionales
repottingIntervalMonths + lastRepotted/By am Pflanzenprofil, Formular
mit Intervall-Feld und Datums-Auswahl, Umtopf-Zeile im Detail,
Aufgaben in Checkliste und Sammel-Push (reminders.ts gespiegelt),
Sitter dürfen bestätigen (Rules-Allowlist). Monats-Arithmetik kürzt
den 31. auf den Monatsletzten. Ende-zu-Ende-Widget-Test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 23:40:21 +02:00

224 lines
7.1 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 '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 tasks = ref.watch(dueTasksProvider);
final Widget body;
if (ref.watch(dueTasksLoadingProvider)) {
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);
final theme = Theme.of(context);
// Zwischenüberschriften nur, wenn Aufgaben aus mehreren Haushalten da
// sind im Normalfall (ein Haushalt) bleibt die Liste wie gewohnt.
final householdIds = {for (final task in tasks) task.householdId};
final showHouseholdHeaders = householdIds.length > 1;
final children = <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
l10n.todayOpenTasks(tasks.length),
style: theme.textTheme.titleMedium,
),
),
];
String? currentHouseholdId;
for (final task in tasks) {
if (showHouseholdHeaders && task.householdId != currentHouseholdId) {
currentHouseholdId = task.householdId;
children.add(Padding(
padding: const EdgeInsets.only(top: 12, bottom: 4),
child: Row(
children: [
Icon(Icons.home_outlined,
size: 18, color: theme.colorScheme.primary),
const SizedBox(width: 6),
Expanded(
child: Text(
task.householdName,
style: theme.textTheme.titleSmall
?.copyWith(color: theme.colorScheme.primary),
overflow: TextOverflow.ellipsis,
),
),
],
),
));
}
children.add(_TaskCard(task: task));
}
return ListView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 96),
children: children,
);
}
}
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),
CareTaskType.repotting => l10n.taskRepotTitle(task.plant.nickname),
};
final icon = switch (task.type) {
CareTaskType.watering => Icons.water_drop,
CareTaskType.fertilizing => Icons.compost,
CareTaskType.repotting => Icons.yard,
};
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.householdId, 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,
),
],
),
),
);
}
}