feat: flexible meal planning wizard — plan 1 meal, 1 day, several days, or a week
Backend:
- migration 005: expand menu_items.meal_type CHECK to all 6 types (second_breakfast, afternoon_snack, snack)
- ai/types.go: add Days and MealTypes to MenuRequest for partial generation
- openai/menu.go: parametrize GenerateMenu — use requested meal types and day count; add caloric fractions for all 6 meal types
- menu/repository.go: add UpsertItemsInTx for partial upsert (preserves existing slots); fix meal_type sort order in GetByWeek
- menu/handler.go: add dates+meal_types path to POST /ai/generate-menu; extract fetchImages/saveRecipes helpers; returns {"plans":[...]} for dates mode; backward-compatible with week mode
Client:
- PlanMenuSheet: bottom sheet with 4 planning horizon options
- PlanDatePickerSheet: adaptive sheet with date strip (single day/meal) or custom CalendarRangePicker (multi-day/week); sliding 7-day window for week mode
- menu_service.dart: add generateForDates
- menu_provider.dart: add PlanMenuService (generates + invalidates week providers), lastPlannedDateProvider
- home_screen.dart: add _PlanMenuButton card below quick actions; opens planning wizard
- l10n: 16 new keys for planning UI across all 12 languages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,8 @@ import '../../shared/models/meal_type.dart';
|
||||
import '../../shared/models/menu.dart';
|
||||
import '../diary/food_search_sheet.dart';
|
||||
import '../menu/menu_provider.dart';
|
||||
import '../menu/plan_date_picker_sheet.dart';
|
||||
import '../menu/plan_menu_sheet.dart';
|
||||
import '../profile/profile_provider.dart';
|
||||
import '../scan/dish_result_screen.dart';
|
||||
import '../scan/recognition_service.dart';
|
||||
@@ -126,6 +128,8 @@ class HomeScreen extends ConsumerWidget {
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
_QuickActionsRow(),
|
||||
const SizedBox(height: 8),
|
||||
_PlanMenuButton(),
|
||||
if (!isFutureDate && recommendations.isNotEmpty) ...[
|
||||
const SizedBox(height: 20),
|
||||
_SectionTitle(l10n.recommendCook),
|
||||
@@ -1641,6 +1645,68 @@ class _ActionButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Plan menu button ──────────────────────────────────────────
|
||||
|
||||
class _PlanMenuButton extends ConsumerWidget {
|
||||
const _PlanMenuButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
child: InkWell(
|
||||
onTap: () => _openPlanSheet(context, ref),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.edit_calendar_outlined,
|
||||
color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.planMenuButton,
|
||||
style: theme.textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
Icon(Icons.chevron_right,
|
||||
color: theme.colorScheme.onSurfaceVariant),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openPlanSheet(BuildContext context, WidgetRef ref) {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder: (_) => PlanMenuSheet(
|
||||
onModeSelected: (mode) {
|
||||
final lastPlanned = ref.read(lastPlannedDateProvider);
|
||||
final defaultStart = lastPlanned != null
|
||||
? lastPlanned.add(const Duration(days: 1))
|
||||
: DateTime.now().add(const Duration(days: 1));
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder: (_) => PlanDatePickerSheet(
|
||||
mode: mode,
|
||||
defaultStart: defaultStart,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Section title ─────────────────────────────────────────────
|
||||
|
||||
class _SectionTitle extends StatelessWidget {
|
||||
|
||||
Reference in New Issue
Block a user