From 903567e7354fb8227a598d0b3efa99d453ede3f0 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 15 Mar 2026 22:17:21 +0100 Subject: [PATCH] feat(02-04): create template picker bottom sheet with German localization - Add TemplatePickerSheet StatefulWidget with checklist UI for task templates - Add showTemplatePickerSheet helper for modal display with DraggableScrollableSheet - Add 4 German localization keys: templatePickerTitle, Skip, Add, Selected count - All templates unchecked by default, confirm button only enabled with selection Co-Authored-By: Claude Opus 4.6 --- .../presentation/template_picker_sheet.dart | 198 ++++++++++++++++++ lib/l10n/app_de.arb | 11 +- lib/l10n/app_localizations.dart | 24 +++ lib/l10n/app_localizations_de.dart | 14 ++ 4 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 lib/features/templates/presentation/template_picker_sheet.dart diff --git a/lib/features/templates/presentation/template_picker_sheet.dart b/lib/features/templates/presentation/template_picker_sheet.dart new file mode 100644 index 0000000..ab47e26 --- /dev/null +++ b/lib/features/templates/presentation/template_picker_sheet.dart @@ -0,0 +1,198 @@ +import 'package:flutter/material.dart'; + +import 'package:household_keeper/features/tasks/domain/effort_level.dart'; +import 'package:household_keeper/features/tasks/domain/frequency.dart'; +import 'package:household_keeper/features/templates/data/task_templates.dart'; +import 'package:household_keeper/l10n/app_localizations.dart'; + +/// Bottom sheet with a checklist of task templates for a detected room type. +/// +/// Displays German-language task templates with frequency and effort labels. +/// All templates start unchecked. The user can select desired templates and +/// confirm, or skip (dismiss) without selecting any. +class TemplatePickerSheet extends StatefulWidget { + const TemplatePickerSheet({ + super.key, + required this.roomType, + required this.templates, + required this.onConfirm, + }); + + /// The detected room type key (e.g. "kueche"). + final String roomType; + + /// The template list for this room type. + final List templates; + + /// Callback invoked with the selected templates when user confirms. + final void Function(List selected) onConfirm; + + @override + State createState() => _TemplatePickerSheetState(); +} + +class _TemplatePickerSheetState extends State { + /// Tracks which template indices are checked. + final Set _selected = {}; + + /// Display name for the detected room type (first letter capitalized). + String get _roomTypeDisplayName { + final key = widget.roomType; + if (key.isEmpty) return key; + return key[0].toUpperCase() + key.substring(1); + } + + /// German label combining frequency and effort for a template. + String _templateSubtitle(TaskTemplate template) { + final freq = FrequencyInterval( + intervalType: template.intervalType, + days: template.intervalDays, + ).label(); + final effort = template.effortLevel.label(); + return '$freq \u00b7 $effort'; + } + + void _onConfirmPressed() { + final selectedTemplates = _selected + .map((index) => widget.templates[index]) + .toList(growable: false); + widget.onConfirm(selectedTemplates); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final theme = Theme.of(context); + + return DraggableScrollableSheet( + initialChildSize: 0.6, + minChildSize: 0.3, + maxChildSize: 0.9, + expand: false, + builder: (context, scrollController) { + return Column( + children: [ + // Drag handle + Padding( + padding: const EdgeInsets.only(top: 12, bottom: 4), + child: Container( + width: 32, + height: 4, + decoration: BoxDecoration( + color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.4), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + // Title and subtitle + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.templatePickerTitle, + style: theme.textTheme.titleLarge, + ), + const SizedBox(height: 4), + Text( + _roomTypeDisplayName, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + const Divider(height: 1), + // Template checklist + Expanded( + child: ListView.builder( + controller: scrollController, + itemCount: widget.templates.length, + itemBuilder: (context, index) { + final template = widget.templates[index]; + final isChecked = _selected.contains(index); + return CheckboxListTile( + value: isChecked, + onChanged: (checked) { + setState(() { + if (checked == true) { + _selected.add(index); + } else { + _selected.remove(index); + } + }); + }, + title: Text(template.name), + subtitle: Text(_templateSubtitle(template)), + ); + }, + ), + ), + // Selected count indicator + if (_selected.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + l10n.templatePickerSelected(_selected.length), + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.primary, + ), + ), + ), + // Action buttons + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.templatePickerSkip), + ), + ), + const SizedBox(width: 12), + Expanded( + child: FilledButton( + onPressed: + _selected.isNotEmpty ? _onConfirmPressed : null, + child: Text(l10n.templatePickerAdd), + ), + ), + ], + ), + ), + ], + ); + }, + ); + } +} + +/// Shows the template picker bottom sheet and returns the selected templates. +/// +/// Returns the list of selected [TaskTemplate]s, or null if user skipped. +Future?> showTemplatePickerSheet({ + required BuildContext context, + required String roomType, + required List templates, +}) { + return showModalBottomSheet>( + context: context, + isScrollControlled: true, + useSafeArea: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (context) { + return TemplatePickerSheet( + roomType: roomType, + templates: templates, + onConfirm: (selected) { + Navigator.of(context).pop(selected); + }, + ); + }, + ); +} diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 7b9f1d0..df73a76 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -59,5 +59,14 @@ "taskDeleteConfirmAction": "L\u00f6schen", "taskEmptyTitle": "Noch keine Aufgaben", "taskEmptyMessage": "Erstelle die erste Aufgabe f\u00fcr diesen Raum.", - "taskEmptyAction": "Aufgabe erstellen" + "taskEmptyAction": "Aufgabe erstellen", + "templatePickerTitle": "Aufgaben aus Vorlagen hinzuf\u00fcgen?", + "templatePickerSkip": "\u00dcberspringen", + "templatePickerAdd": "Hinzuf\u00fcgen", + "templatePickerSelected": "{count} ausgew\u00e4hlt", + "@templatePickerSelected": { + "placeholders": { + "count": { "type": "int" } + } + } } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index eccf7cb..9b95b9c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -393,6 +393,30 @@ abstract class AppLocalizations { /// In de, this message translates to: /// **'Aufgabe erstellen'** String get taskEmptyAction; + + /// No description provided for @templatePickerTitle. + /// + /// In de, this message translates to: + /// **'Aufgaben aus Vorlagen hinzuf\u00fcgen?'** + String get templatePickerTitle; + + /// No description provided for @templatePickerSkip. + /// + /// In de, this message translates to: + /// **'\u00dcberspringen'** + String get templatePickerSkip; + + /// No description provided for @templatePickerAdd. + /// + /// In de, this message translates to: + /// **'Hinzuf\u00fcgen'** + String get templatePickerAdd; + + /// No description provided for @templatePickerSelected. + /// + /// In de, this message translates to: + /// **'{count} ausgew\u00e4hlt'** + String templatePickerSelected(int count); } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 807a17a..a83b62f 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -164,4 +164,18 @@ class AppLocalizationsDe extends AppLocalizations { @override String get taskEmptyAction => 'Aufgabe erstellen'; + + @override + String get templatePickerTitle => 'Aufgaben aus Vorlagen hinzuf\u00fcgen?'; + + @override + String get templatePickerSkip => '\u00dcberspringen'; + + @override + String get templatePickerAdd => 'Hinzuf\u00fcgen'; + + @override + String templatePickerSelected(int count) { + return '$count ausgew\u00e4hlt'; + } }