Inserts a new PlanProductsSheet as step 1 of the planning flow. Users see their current products as a multi-select checklist (all selected by default) before choosing the planning mode and dates. - Empty state explains the benefit and offers "Add products" CTA while always allowing "Plan without products" to skip - Selected product IDs flow through PlanMenuSheet → PlanDatePickerSheet → MenuService.generateForDates → backend - Backend: added ProductIDs field to generate-menu request body; uses ListForPromptByIDs when set, ListForPrompt otherwise - Backend: added Repository.ListForPromptByIDs (filtered SQL query) - All 12 ARB locale files updated with planProducts* keys Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
108 lines
3.3 KiB
Dart
108 lines
3.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:food_ai/l10n/app_localizations.dart';
|
|
|
|
/// The planning horizon selected by the user.
|
|
enum PlanMode { singleMeal, singleDay, days, week }
|
|
|
|
/// Bottom sheet that lets the user choose a planning horizon.
|
|
/// Closes itself and calls [onModeSelected] with the chosen mode and the
|
|
/// product IDs selected in the previous step (may be empty).
|
|
class PlanMenuSheet extends StatelessWidget {
|
|
const PlanMenuSheet({
|
|
super.key,
|
|
required this.onModeSelected,
|
|
required this.selectedProductIds,
|
|
});
|
|
|
|
final void Function(PlanMode mode, List<String> productIds) onModeSelected;
|
|
final List<String> selectedProductIds;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final theme = Theme.of(context);
|
|
|
|
return SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Text(
|
|
l10n.planMenuTitle,
|
|
style: theme.textTheme.titleLarge,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 20),
|
|
_PlanOptionTile(
|
|
icon: Icons.restaurant_outlined,
|
|
title: l10n.planOptionSingleMeal,
|
|
subtitle: l10n.planOptionSingleMealDesc,
|
|
onTap: () => _select(context, PlanMode.singleMeal),
|
|
),
|
|
_PlanOptionTile(
|
|
icon: Icons.today_outlined,
|
|
title: l10n.planOptionDay,
|
|
subtitle: l10n.planOptionDayDesc,
|
|
onTap: () => _select(context, PlanMode.singleDay),
|
|
),
|
|
_PlanOptionTile(
|
|
icon: Icons.date_range_outlined,
|
|
title: l10n.planOptionDays,
|
|
subtitle: l10n.planOptionDaysDesc,
|
|
onTap: () => _select(context, PlanMode.days),
|
|
),
|
|
_PlanOptionTile(
|
|
icon: Icons.calendar_month_outlined,
|
|
title: l10n.planOptionWeek,
|
|
subtitle: l10n.planOptionWeekDesc,
|
|
onTap: () => _select(context, PlanMode.week),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _select(BuildContext context, PlanMode mode) {
|
|
Navigator.pop(context);
|
|
onModeSelected(mode, selectedProductIds);
|
|
}
|
|
}
|
|
|
|
class _PlanOptionTile extends StatelessWidget {
|
|
const _PlanOptionTile({
|
|
required this.icon,
|
|
required this.title,
|
|
required this.subtitle,
|
|
required this.onTap,
|
|
});
|
|
|
|
final IconData icon;
|
|
final String title;
|
|
final String subtitle;
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
child: ListTile(
|
|
leading: Icon(icon, color: theme.colorScheme.primary),
|
|
title: Text(title, style: theme.textTheme.titleSmall),
|
|
subtitle: Text(
|
|
subtitle,
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
trailing: Icon(Icons.chevron_right,
|
|
color: theme.colorScheme.onSurfaceVariant),
|
|
onTap: onTap,
|
|
),
|
|
);
|
|
}
|
|
}
|