diet-guard/app/lib/widgets/log_meal_actions_row.dart
Krzysztof kuhy Rudnicki c897279c8b fix: clear every macro field before a prefill, add the phone's delivery button
`fillControllersFromDish` set the dish's name, four macros and `grams` but
left `perGrams` untouched. A `100` left there by an earlier food-bank pick
made `nutritionForPortion` read the dish's per-portion macros as per-100g and
rescale them: one real entry on 2026-08-23 logged 472 kcal as 1217.8 (258/100).

The PC never had this -- `_gatelock_delivery.py` calls `_clear_inputs()` first
-- so the fix is that same discipline, as one `MacroControllers.clear()` call
rather than a `perGrams.clear()` line a seventh field could forget. `fillFrom`
now clears too, and `edit_entry_screen` routes through it instead of six
hand-written assignments; its `initState` fill had the same shape.

The old tests asserted controller *text* and passed throughout the bug, so the
new ones assert the computed `Nutrition` -- verified failing (400 -> 1200.0)
before the fix. `clear()` gets an enumerated test so a new field fails until
it is cleared too.

Also adds the log screen's "Today's delivery" button, matching the PC lock
screen's affordance: explicit taps are unguarded per the Triggers table, still
skip dishes already logged, and show the queue count. The whole action row
moved to its own widget, which put `log_meal_screen.dart` back under the cap.
The label is a FittedBox in an Expanded -- measured, since a Row clips an
oversized label without ever throwing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQpCiPaSbyRpwsLkH3dgsX
2026-08-23 09:34:47 +02:00

96 lines
3.6 KiB
Dart

/// The log form's action row: load today's delivery, then log the meal.
///
/// Its own file because `log_meal_screen.dart` sits at the repo's 250-line
/// ceiling; lifting the whole row rather than just the delivery button keeps
/// both controls' layout in one place and returns budget to the screen.
library;
import 'package:diet_guard_app/services/kuchnia_client.dart';
import 'package:diet_guard_app/ui/theme.dart';
import 'package:flutter/material.dart';
/// A delivery button beside the log button.
///
/// The delivery control mirrors the PC lock screen's "🍱 Today's delivery":
/// standing in the kitchen, there must be something to tap. It stays visible
/// and enabled even when there is nothing to load — the outcome arrives as a
/// message rather than as a disabled button, because a disabled control gives
/// no way to retry after a late delivery and a hidden one reads as broken.
class LogMealActionsRow extends StatelessWidget {
/// Creates the action row.
const LogMealActionsRow({
required this.onLoadDelivery,
required this.onLog,
this.deliveryBusy = false,
this.dishesQueued = 0,
this.canFetchDelivery,
super.key,
});
/// Loads today's delivery and prefills the first/next dish.
final Future<void> Function() onLoadDelivery;
/// Writes the current form as a log entry.
final VoidCallback onLog;
/// Whether a delivery fetch is in flight; disables the button meanwhile so
/// a second tap cannot start a concurrent login against the caterer.
final bool deliveryBusy;
/// How many delivered dishes are still queued, shown as the PC's
/// "(N more to go)" so the user knows the walk is not finished.
final int dishesQueued;
/// Whether the platform can fetch at all. Defaults to
/// [kuchniaFetchSupported] — false on web, where the desktop build runs and
/// the caterer's panel blocks browser requests, so the control is hidden
/// rather than shipped dead.
final bool? canFetchDelivery;
@override
Widget build(BuildContext context) {
final canFetch = canFetchDelivery ?? kuchniaFetchSupported;
return Row(
children: [
// Flexible, not Spacer-and-hope: `OutlinedButton.icon` lays its label
// out unbounded, so on a narrow phone the queue suffix renders wider
// than the row and is clipped without ever throwing an overflow. The
// Flexible caps the width and the FittedBox scales the text into it.
if (canFetch)
Expanded(
child: Tooltip(
message: "Load today's catering delivery",
child: OutlinedButton.icon(
onPressed: deliveryBusy ? null : onLoadDelivery,
icon: const Icon(Icons.restaurant),
label: FittedBox(
fit: BoxFit.scaleDown,
child: Text(_label, maxLines: 1),
),
),
),
),
// No Spacer: it is a flex child too, so it would split the free space
// with the Flexible above and re-create the clipping this guards
// against. The Flexible takes the slack and pushes the log button
// right on its own.
if (!canFetch) const Spacer(),
const SizedBox(width: AppSpacing.sm),
Tooltip(
message: 'Log meal',
child: FilledButton(
onPressed: onLog,
child: const Icon(Icons.check_circle),
),
),
],
);
}
String get _label {
if (deliveryBusy) return 'Loading…';
if (dishesQueued > 0) return "Today's delivery ($dishesQueued more to go)";
return "Today's delivery";
}
}