fix: unify date limits, fix ISO week calculation, refactor home screen plan button

- Fix _isoWeek: correct Sunday shift (-3 instead of +4), use floor+1 formula,
  match jan1 timezone to input — planned meals now appear correctly for UTC+ users
- Add kPlanningHorizonDays=28 / kMenuPastWeeks=8 constants; apply to home date
  strip, plan picker (strip + calendar), and menu screen prev/next navigation
- Menu screen week nav: disable arrows at min/max limits using compareTo
- Home screen: replace _GenerateActionCard/_WeekPlannedChip conditional with
  always-visible _FutureDayPlanButton(dateString); show _DayPlannedChip only
  when the specific day has planned meals; remove standalone _PlanMenuButton
- _FutureDayPlanButton uses selected date as defaultStart instead of lastPlanned+1
- Rename weekPlannedLabel -> dayPlannedLabel across all 12 locales

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dbastrikin
2026-03-22 23:25:46 +02:00
parent edf587e798
commit 9a6b7800a3
30 changed files with 106 additions and 167 deletions

View File

@@ -12,6 +12,7 @@ import '../../core/storage/local_preferences_provider.dart';
import '../../core/theme/app_colors.dart';
import '../../shared/models/diary_entry.dart';
import '../../shared/models/home_summary.dart';
import '../../shared/constants/date_limits.dart';
import '../../shared/models/meal_type.dart';
import '../../shared/models/menu.dart';
import '../diary/food_search_sheet.dart';
@@ -128,8 +129,6 @@ 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),
@@ -191,10 +190,10 @@ class _DateSelector extends StatefulWidget {
}
class _DateSelectorState extends State<_DateSelector> {
// Strip covers 7 future days + today + 364 past days = 372 items total.
// Strip covers kPlanningHorizonDays future days + today + 364 past days.
// With reverse: true, index 0 is rendered at the RIGHT edge (newest).
// index 0 = today + 7, index 7 = today, index 371 = today - 364.
static const _futureDays = 7;
// index 0 = today + kPlanningHorizonDays, index kPlanningHorizonDays = today.
static const _futureDays = kPlanningHorizonDays;
static const _pastDays = 364;
static const _totalDays = _futureDays + 1 + _pastDays; // 372
static const _pillWidth = 48.0;
@@ -1156,125 +1155,26 @@ class _FutureDayHeader extends ConsumerWidget {
@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));
final plannedMeals = ref.watch(plannedMealsProvider(dateString));
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),
),
_FutureDayPlanButton(dateString: dateString),
if (plannedMeals.isNotEmpty) ...[
const SizedBox(height: 8),
_DayPlannedChip(l10n: l10n),
],
],
);
}
}
class _GenerateActionCard extends StatelessWidget {
class _DayPlannedChip 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});
const _DayPlannedChip({required this.l10n});
@override
Widget build(BuildContext context) {
@@ -1292,7 +1192,7 @@ class _WeekPlannedChip extends StatelessWidget {
color: theme.colorScheme.onSecondaryContainer, size: 18),
const SizedBox(width: 8),
Text(
l10n.weekPlannedLabel,
l10n.dayPlannedLabel,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSecondaryContainer,
fontWeight: FontWeight.w600,
@@ -1647,8 +1547,9 @@ class _ActionButton extends StatelessWidget {
// ── Plan menu button ──────────────────────────────────────────
class _PlanMenuButton extends ConsumerWidget {
const _PlanMenuButton();
class _FutureDayPlanButton extends ConsumerWidget {
final String dateString;
const _FutureDayPlanButton({required this.dateString});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -1682,16 +1583,13 @@ class _PlanMenuButton extends ConsumerWidget {
}
void _openPlanSheet(BuildContext context, WidgetRef ref) {
final defaultStart = DateTime.parse(dateString);
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,