fix: fix menu generation errors and show planned meals on home screen

Backend fixes:
- migration 003: add 'menu' value to recipe_source enum (was causing SQLSTATE 22P02)
- migration 004: rename recipe_products→recipe_ingredients, product_id→ingredient_id (was causing SQLSTATE 42P01)
- dish/repository.go: fix INSERT INTO tags using $1/$1 for two columns → $1/$2 (was causing SQLSTATE 42P08)
- home/handler.go: replace non-existent saved_recipes table with correct joins (recipes→dishes→dish_translations, user_saved_recipes) so today's plan and recommendations load correctly
- reqlog: new slog.Handler wrapper that adds request_id and stack trace to ERROR-level logs
- all handlers: slog.Error→slog.ErrorContext so error logs include request context; writeError includes request_id in response body

Client:
- home_screen.dart: extend home screen to future dates, show planned meals as ghost entries
- l10n: add new localisation keys for home screen date navigation and planned meal UI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dbastrikin
2026-03-22 00:35:11 +02:00
parent 9306d59d36
commit 5096df2102
49 changed files with 824 additions and 299 deletions

View File

@@ -96,7 +96,7 @@ class HomeScreen extends ConsumerWidget {
),
const SizedBox(height: 16),
if (isFutureDate)
_PlanningBanner(dateString: dateString)
_FutureDayHeader(dateString: dateString)
else ...[
_CaloriesCard(
loggedCalories: loggedCalories,
@@ -1143,6 +1143,163 @@ class _DiaryEntryTile extends StatelessWidget {
}
}
// ── Future day header (wraps banner + menu-gen CTA) ────────────
class _FutureDayHeader extends ConsumerWidget {
final String dateString;
const _FutureDayHeader({required this.dateString});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final date = DateTime.parse(dateString);
final weekString = isoWeekString(date);
final menuState = ref.watch(menuProvider(weekString));
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_PlanningBanner(dateString: dateString),
const SizedBox(height: 8),
menuState.when(
loading: () => _GenerateLoadingCard(l10n: l10n),
error: (_, __) => _GenerateActionCard(
l10n: l10n,
onGenerate: () =>
ref.read(menuProvider(weekString).notifier).generate(),
),
data: (plan) => plan == null
? _GenerateActionCard(
l10n: l10n,
onGenerate: () =>
ref.read(menuProvider(weekString).notifier).generate(),
)
: _WeekPlannedChip(l10n: l10n),
),
],
);
}
}
class _GenerateActionCard extends StatelessWidget {
final AppLocalizations l10n;
final VoidCallback onGenerate;
const _GenerateActionCard({required this.l10n, required this.onGenerate});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.auto_awesome,
color: theme.colorScheme.primary, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
l10n.generateWeekLabel,
style: theme.textTheme.titleSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: 4),
Text(
l10n.generateWeekSubtitle,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
FilledButton(
onPressed: onGenerate,
child: Text(l10n.generateWeekLabel),
),
],
),
);
}
}
class _GenerateLoadingCard extends StatelessWidget {
final AppLocalizations l10n;
const _GenerateLoadingCard({required this.l10n});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(theme.colorScheme.primary),
),
),
const SizedBox(width: 12),
Text(
l10n.generatingMenu,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
);
}
}
class _WeekPlannedChip extends StatelessWidget {
final AppLocalizations l10n;
const _WeekPlannedChip({required this.l10n});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: theme.colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check_circle_outline,
color: theme.colorScheme.onSecondaryContainer, size: 18),
const SizedBox(width: 8),
Text(
l10n.weekPlannedLabel,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSecondaryContainer,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
}
// ── Planning banner (future dates) ────────────────────────────
class _PlanningBanner extends StatelessWidget {

View File

@@ -130,6 +130,10 @@
"date": { "type": "String" }
}
},
"markAsEaten": "",
"plannedMealLabel": ""
"markAsEaten": "وضع علامة كمأكول",
"plannedMealLabel": "مخطط",
"generateWeekLabel": "تخطيط الأسبوع",
"generateWeekSubtitle": "سيقوم الذكاء الاصطناعي بإنشاء قائمة طعام تشمل الإفطار والغداء والعشاء لكامل الأسبوع",
"generatingMenu": "جارٍ إنشاء القائمة...",
"weekPlannedLabel": "تم تخطيط الأسبوع"
}

View File

@@ -130,6 +130,10 @@
"date": { "type": "String" }
}
},
"markAsEaten": "",
"plannedMealLabel": ""
"markAsEaten": "Als gegessen markieren",
"plannedMealLabel": "Geplant",
"generateWeekLabel": "Woche planen",
"generateWeekSubtitle": "KI erstellt einen Menüplan mit Frühstück, Mittagessen und Abendessen für die ganze Woche",
"generatingMenu": "Menü wird erstellt...",
"weekPlannedLabel": "Woche geplant"
}

View File

@@ -129,5 +129,9 @@
}
},
"markAsEaten": "Mark as eaten",
"plannedMealLabel": "Planned"
"plannedMealLabel": "Planned",
"generateWeekLabel": "Plan the week",
"generateWeekSubtitle": "AI will create a menu with breakfast, lunch and dinner for the whole week",
"generatingMenu": "Generating menu...",
"weekPlannedLabel": "Week planned"
}

View File

@@ -130,6 +130,10 @@
"date": { "type": "String" }
}
},
"markAsEaten": "",
"plannedMealLabel": ""
"markAsEaten": "Marcar como comido",
"plannedMealLabel": "Planificado",
"generateWeekLabel": "Planificar la semana",
"generateWeekSubtitle": "La IA creará un menú con desayuno, comida y cena para toda la semana",
"generatingMenu": "Generando menú...",
"weekPlannedLabel": "Semana planificada"
}

View File

@@ -130,6 +130,10 @@
"date": { "type": "String" }
}
},
"markAsEaten": "",
"plannedMealLabel": ""
"markAsEaten": "Marquer comme mangé",
"plannedMealLabel": "Planifié",
"generateWeekLabel": "Planifier la semaine",
"generateWeekSubtitle": "L'IA créera un menu avec petit-déjeuner, déjeuner et dîner pour toute la semaine",
"generatingMenu": "Génération du menu...",
"weekPlannedLabel": "Semaine planifiée"
}

View File

@@ -130,6 +130,10 @@
"date": { "type": "String" }
}
},
"markAsEaten": "",
"plannedMealLabel": ""
"markAsEaten": "खाया हुआ चिह्नित करें",
"plannedMealLabel": "नियोजित",
"generateWeekLabel": "सप्ताह की योजना बनाएं",
"generateWeekSubtitle": "AI पूरे सप्ताह के लिए नाश्ता, दोपहर का खाना और रात के खाने के साथ मेनू बनाएगा",
"generatingMenu": "मेनू बना रहे हैं...",
"weekPlannedLabel": "सप्ताह की योजना बनाई गई"
}

View File

@@ -130,6 +130,10 @@
"date": { "type": "String" }
}
},
"markAsEaten": "",
"plannedMealLabel": ""
"markAsEaten": "Segna come mangiato",
"plannedMealLabel": "Pianificato",
"generateWeekLabel": "Pianifica la settimana",
"generateWeekSubtitle": "L'AI creerà un menu con colazione, pranzo e cena per tutta la settimana",
"generatingMenu": "Generazione menu...",
"weekPlannedLabel": "Settimana pianificata"
}

View File

@@ -130,6 +130,10 @@
"date": { "type": "String" }
}
},
"markAsEaten": "",
"plannedMealLabel": ""
"markAsEaten": "食べた印をつける",
"plannedMealLabel": "予定済み",
"generateWeekLabel": "週を計画する",
"generateWeekSubtitle": "AIが一週間の朝食・昼食・夕食のメニューを作成します",
"generatingMenu": "メニューを生成中...",
"weekPlannedLabel": "週の計画済み"
}

View File

@@ -130,6 +130,10 @@
"date": { "type": "String" }
}
},
"markAsEaten": "",
"plannedMealLabel": ""
"markAsEaten": "먹은 것으로 표시",
"plannedMealLabel": "계획됨",
"generateWeekLabel": "주간 계획하기",
"generateWeekSubtitle": "AI가 한 주 동안 아침, 점심, 저녁 식사 메뉴를 만들어 드립니다",
"generatingMenu": "메뉴 생성 중...",
"weekPlannedLabel": "주간 계획 완료"
}

View File

@@ -807,6 +807,30 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Planned'**
String get plannedMealLabel;
/// No description provided for @generateWeekLabel.
///
/// In en, this message translates to:
/// **'Plan the week'**
String get generateWeekLabel;
/// No description provided for @generateWeekSubtitle.
///
/// In en, this message translates to:
/// **'AI will create a menu with breakfast, lunch and dinner for the whole week'**
String get generateWeekSubtitle;
/// No description provided for @generatingMenu.
///
/// In en, this message translates to:
/// **'Generating menu...'**
String get generatingMenu;
/// No description provided for @weekPlannedLabel.
///
/// In en, this message translates to:
/// **'Week planned'**
String get weekPlannedLabel;
}
class _AppLocalizationsDelegate

View File

@@ -355,8 +355,21 @@ class AppLocalizationsAr extends AppLocalizations {
}
@override
String get markAsEaten => '';
String get markAsEaten => 'وضع علامة كمأكول';
@override
String get plannedMealLabel => '';
String get plannedMealLabel => 'مخطط';
@override
String get generateWeekLabel => 'تخطيط الأسبوع';
@override
String get generateWeekSubtitle =>
'سيقوم الذكاء الاصطناعي بإنشاء قائمة طعام تشمل الإفطار والغداء والعشاء لكامل الأسبوع';
@override
String get generatingMenu => 'جارٍ إنشاء القائمة...';
@override
String get weekPlannedLabel => 'تم تخطيط الأسبوع';
}

View File

@@ -357,8 +357,21 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get markAsEaten => '';
String get markAsEaten => 'Als gegessen markieren';
@override
String get plannedMealLabel => '';
String get plannedMealLabel => 'Geplant';
@override
String get generateWeekLabel => 'Woche planen';
@override
String get generateWeekSubtitle =>
'KI erstellt einen Menüplan mit Frühstück, Mittagessen und Abendessen für die ganze Woche';
@override
String get generatingMenu => 'Menü wird erstellt...';
@override
String get weekPlannedLabel => 'Woche geplant';
}

View File

@@ -359,4 +359,17 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get plannedMealLabel => 'Planned';
@override
String get generateWeekLabel => 'Plan the week';
@override
String get generateWeekSubtitle =>
'AI will create a menu with breakfast, lunch and dinner for the whole week';
@override
String get generatingMenu => 'Generating menu...';
@override
String get weekPlannedLabel => 'Week planned';
}

View File

@@ -357,8 +357,21 @@ class AppLocalizationsEs extends AppLocalizations {
}
@override
String get markAsEaten => '';
String get markAsEaten => 'Marcar como comido';
@override
String get plannedMealLabel => '';
String get plannedMealLabel => 'Planificado';
@override
String get generateWeekLabel => 'Planificar la semana';
@override
String get generateWeekSubtitle =>
'La IA creará un menú con desayuno, comida y cena para toda la semana';
@override
String get generatingMenu => 'Generando menú...';
@override
String get weekPlannedLabel => 'Semana planificada';
}

View File

@@ -358,8 +358,21 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get markAsEaten => '';
String get markAsEaten => 'Marquer comme mangé';
@override
String get plannedMealLabel => '';
String get plannedMealLabel => 'Planifié';
@override
String get generateWeekLabel => 'Planifier la semaine';
@override
String get generateWeekSubtitle =>
'L\'IA créera un menu avec petit-déjeuner, déjeuner et dîner pour toute la semaine';
@override
String get generatingMenu => 'Génération du menu...';
@override
String get weekPlannedLabel => 'Semaine planifiée';
}

View File

@@ -356,8 +356,21 @@ class AppLocalizationsHi extends AppLocalizations {
}
@override
String get markAsEaten => '';
String get markAsEaten => 'खाया हुआ चिह्नित करें';
@override
String get plannedMealLabel => '';
String get plannedMealLabel => 'नियोजित';
@override
String get generateWeekLabel => 'सप्ताह की योजना बनाएं';
@override
String get generateWeekSubtitle =>
'AI पूरे सप्ताह के लिए नाश्ता, दोपहर का खाना और रात के खाने के साथ मेनू बनाएगा';
@override
String get generatingMenu => 'मेनू बना रहे हैं...';
@override
String get weekPlannedLabel => 'सप्ताह की योजना बनाई गई';
}

View File

@@ -357,8 +357,21 @@ class AppLocalizationsIt extends AppLocalizations {
}
@override
String get markAsEaten => '';
String get markAsEaten => 'Segna come mangiato';
@override
String get plannedMealLabel => '';
String get plannedMealLabel => 'Pianificato';
@override
String get generateWeekLabel => 'Pianifica la settimana';
@override
String get generateWeekSubtitle =>
'L\'AI creerà un menu con colazione, pranzo e cena per tutta la settimana';
@override
String get generatingMenu => 'Generazione menu...';
@override
String get weekPlannedLabel => 'Settimana pianificata';
}

View File

@@ -354,8 +354,20 @@ class AppLocalizationsJa extends AppLocalizations {
}
@override
String get markAsEaten => '';
String get markAsEaten => '食べた印をつける';
@override
String get plannedMealLabel => '';
String get plannedMealLabel => '予定済み';
@override
String get generateWeekLabel => '週を計画する';
@override
String get generateWeekSubtitle => 'AIが一週間の朝食・昼食・夕食のメニューを作成します';
@override
String get generatingMenu => 'メニューを生成中...';
@override
String get weekPlannedLabel => '週の計画済み';
}

View File

@@ -354,8 +354,20 @@ class AppLocalizationsKo extends AppLocalizations {
}
@override
String get markAsEaten => '';
String get markAsEaten => '먹은 것으로 표시';
@override
String get plannedMealLabel => '';
String get plannedMealLabel => '계획됨';
@override
String get generateWeekLabel => '주간 계획하기';
@override
String get generateWeekSubtitle => 'AI가 한 주 동안 아침, 점심, 저녁 식사 메뉴를 만들어 드립니다';
@override
String get generatingMenu => '메뉴 생성 중...';
@override
String get weekPlannedLabel => '주간 계획 완료';
}

View File

@@ -357,8 +357,21 @@ class AppLocalizationsPt extends AppLocalizations {
}
@override
String get markAsEaten => '';
String get markAsEaten => 'Marcar como comido';
@override
String get plannedMealLabel => '';
String get plannedMealLabel => 'Planejado';
@override
String get generateWeekLabel => 'Planejar a semana';
@override
String get generateWeekSubtitle =>
'A IA criará um menu com café da manhã, almoço e jantar para a semana inteira';
@override
String get generatingMenu => 'Gerando menu...';
@override
String get weekPlannedLabel => 'Semana planejada';
}

View File

@@ -359,4 +359,17 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get plannedMealLabel => 'Запланировано';
@override
String get generateWeekLabel => 'Запланировать неделю';
@override
String get generateWeekSubtitle =>
'AI составит меню с завтраком, обедом и ужином на всю неделю';
@override
String get generatingMenu => 'Генерируем меню...';
@override
String get weekPlannedLabel => 'Неделя запланирована';
}

View File

@@ -354,8 +354,20 @@ class AppLocalizationsZh extends AppLocalizations {
}
@override
String get markAsEaten => '';
String get markAsEaten => '标记为已吃';
@override
String get plannedMealLabel => '';
String get plannedMealLabel => '已计划';
@override
String get generateWeekLabel => '规划本周';
@override
String get generateWeekSubtitle => 'AI将为整周创建含早餐、午餐和晚餐的菜单';
@override
String get generatingMenu => '正在生成菜单...';
@override
String get weekPlannedLabel => '本周已规划';
}

View File

@@ -130,6 +130,10 @@
"date": { "type": "String" }
}
},
"markAsEaten": "",
"plannedMealLabel": ""
"markAsEaten": "Marcar como comido",
"plannedMealLabel": "Planejado",
"generateWeekLabel": "Planejar a semana",
"generateWeekSubtitle": "A IA criará um menu com café da manhã, almoço e jantar para a semana inteira",
"generatingMenu": "Gerando menu...",
"weekPlannedLabel": "Semana planejada"
}

View File

@@ -129,5 +129,9 @@
}
},
"markAsEaten": "Отметить как съеденное",
"plannedMealLabel": "Запланировано"
"plannedMealLabel": "Запланировано",
"generateWeekLabel": "Запланировать неделю",
"generateWeekSubtitle": "AI составит меню с завтраком, обедом и ужином на всю неделю",
"generatingMenu": "Генерируем меню...",
"weekPlannedLabel": "Неделя запланирована"
}

View File

@@ -130,6 +130,10 @@
"date": { "type": "String" }
}
},
"markAsEaten": "",
"plannedMealLabel": ""
"markAsEaten": "标记为已吃",
"plannedMealLabel": "已计划",
"generateWeekLabel": "规划本周",
"generateWeekSubtitle": "AI将为整周创建含早餐、午餐和晚餐的菜单",
"generatingMenu": "正在生成菜单...",
"weekPlannedLabel": "本周已规划"
}

View File

@@ -125,10 +125,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
checked_yaml:
dependency: transitive
description:
@@ -689,26 +689,26 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.16.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -1078,10 +1078,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.6"
version: "0.7.10"
typed_data:
dependency: transitive
description: