mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 19:07:15 +01:00
Catering fill: honour the "Log for later" date
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
file-length / file length <= 250 lines (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Has been cancelled
Tests / test (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
repo-contract / repo contract (push) Has been cancelled
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
file-length / file length <= 250 lines (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Has been cancelled
Tests / test (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
repo-contract / repo contract (push) Has been cancelled
Fill all with catering always fetched and filled today, so picking 24.10 on 23.10 reported "No catering delivery today." FillAllFlow.tap now takes the picked day: fetch, slot schedule, occupancy and dedup all resolve against it, and logDishes stamps each dish at its slot time on that day. - A plan only confirms onto the day it was proposed for; changing the date (or back to today) disarms it. - A later-day fill leaves today's delivery queue and prefill alone and returns the form to today, as a manual "Log for later" does. - Only today's fetch records the single-day "already fetched" marker; recording a later date would skip that day's automatic fetch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VwYgigocPkPWa73QZRtU38
This commit is contained in:
parent
84c78a0b1d
commit
66650ca3cc
@ -6,6 +6,7 @@
|
||||
/// `kuchnia_fill_flow.dart`; this only connects them to the screen.
|
||||
library;
|
||||
|
||||
import 'package:diet_guard_app/screens/log_meal_future_mixin.dart';
|
||||
import 'package:diet_guard_app/screens/log_meal_kuchnia_mixin.dart';
|
||||
import 'package:diet_guard_app/screens/log_meal_sync_mixin.dart';
|
||||
import 'package:diet_guard_app/services/kuchnia_fill_flow.dart';
|
||||
@ -15,7 +16,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
/// Drives the screen's [FillAllFlow] and tidies the screen after a fill.
|
||||
mixin LogMealFillMixin<T extends StatefulWidget>
|
||||
on LogMealSyncMixin<T>, LogMealKuchniaMixin<T> {
|
||||
on LogMealSyncMixin<T>, LogMealKuchniaMixin<T>, LogMealFutureMixin<T> {
|
||||
/// The flow behind the button. Per-screen, so leaving the screen drops an
|
||||
/// armed proposal along with it.
|
||||
FillAllFlow fillFlow = FillAllFlow();
|
||||
@ -23,9 +24,19 @@ mixin LogMealFillMixin<T extends StatefulWidget>
|
||||
/// Shows [message] in the form's status line. Supplied by the screen.
|
||||
void showFillStatus(String message);
|
||||
|
||||
/// A proposal belongs to the date it was fetched for; picking another (or
|
||||
/// going back to today) must not leave "Confirm (N)" offering it.
|
||||
@override
|
||||
void onLogDateChanged() {
|
||||
super.onLogDateChanged();
|
||||
fillFlow.cancel();
|
||||
}
|
||||
|
||||
/// Runs one tap: the first proposes, a second on the same day confirms.
|
||||
/// Fills the "Log for later" date when one is picked, else today.
|
||||
Future<void> onFillAll() async {
|
||||
final pending = fillFlow.tap();
|
||||
final day = futureDate;
|
||||
final pending = fillFlow.tap(day: day);
|
||||
setState(() {}); // show the busy label while the fetch/writes run
|
||||
final FillOutcome? outcome;
|
||||
try {
|
||||
@ -44,7 +55,9 @@ mixin LogMealFillMixin<T extends StatefulWidget>
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
if (outcome == null) return;
|
||||
if (outcome.written.isNotEmpty) await _afterFill(outcome.written);
|
||||
if (outcome.written.isNotEmpty) {
|
||||
await _afterFill(outcome.written, later: day != null);
|
||||
}
|
||||
if (!mounted) return;
|
||||
showFillStatus(outcome.message);
|
||||
}
|
||||
@ -58,7 +71,20 @@ mixin LogMealFillMixin<T extends StatefulWidget>
|
||||
/// Publishes the batch once and drops the filled dishes from the delivery
|
||||
/// queue, so "Today's delivery (N more to go)" and the prefilled form do
|
||||
/// not keep offering meals that were just logged.
|
||||
Future<void> _afterFill(List<SlottedDish> written) async {
|
||||
///
|
||||
/// A [later] fill logged another day's delivery: today's queue and form are
|
||||
/// left alone, and the screen goes back to today, as after a manual
|
||||
/// "Log for later" -- the picked day's slot row would otherwise be stale.
|
||||
Future<void> _afterFill(
|
||||
List<SlottedDish> written, {
|
||||
required bool later,
|
||||
}) async {
|
||||
if (later) {
|
||||
resetFutureLog();
|
||||
await publishAfterLog();
|
||||
await refreshSlots();
|
||||
return;
|
||||
}
|
||||
if (KuchniaQueueService.isInitialized) {
|
||||
for (final item in written) {
|
||||
KuchniaQueueService.instance.markLogged(item.dish);
|
||||
|
||||
@ -33,6 +33,10 @@ mixin LogMealFutureMixin<T extends StatefulWidget> on State<T> {
|
||||
/// The slot chosen for [futureDate], or null until one is picked.
|
||||
int? get futureSlot => _futureSlot;
|
||||
|
||||
/// Called after the picked date changes (including back to today), for
|
||||
/// state keyed to the old date to drop itself. No-op unless overridden.
|
||||
void onLogDateChanged() {}
|
||||
|
||||
/// Clears back to "log for today", dropping the picked date and slot.
|
||||
void resetFutureLog() {
|
||||
setState(() {
|
||||
@ -41,6 +45,7 @@ mixin LogMealFutureMixin<T extends StatefulWidget> on State<T> {
|
||||
_futureLoggedSlots = {};
|
||||
_futureSchedule = null;
|
||||
});
|
||||
onLogDateChanged();
|
||||
}
|
||||
|
||||
Future<void> _pickFutureDate() async {
|
||||
@ -53,6 +58,12 @@ mixin LogMealFutureMixin<T extends StatefulWidget> on State<T> {
|
||||
initialDate: _futureDate ?? tomorrow,
|
||||
);
|
||||
if (picked == null || !mounted) return;
|
||||
await selectLogDate(picked);
|
||||
}
|
||||
|
||||
/// Switches the form to log against [picked] (strictly after today): what
|
||||
/// the date picker does once a date is chosen.
|
||||
Future<void> selectLogDate(DateTime picked) async {
|
||||
final log = await LogStorageService.instance.readLog();
|
||||
if (!mounted) return;
|
||||
final day = localDateKey(picked);
|
||||
@ -70,6 +81,7 @@ mixin LogMealFutureMixin<T extends StatefulWidget> on State<T> {
|
||||
_futureLoggedSlots = logged;
|
||||
_futureSlot = null;
|
||||
});
|
||||
onLogDateChanged();
|
||||
}
|
||||
|
||||
/// The "When" row: a "Log for later" button, or the picked date with a
|
||||
|
||||
@ -47,8 +47,8 @@ class _LogMealScreenState extends State<LogMealScreen>
|
||||
LogMealSyncMixin<LogMealScreen>,
|
||||
LogMealNavMixin<LogMealScreen>,
|
||||
LogMealKuchniaMixin<LogMealScreen>,
|
||||
LogMealFillMixin<LogMealScreen>,
|
||||
LogMealFutureMixin<LogMealScreen> {
|
||||
LogMealFutureMixin<LogMealScreen>,
|
||||
LogMealFillMixin<LogMealScreen> {
|
||||
@override
|
||||
http.Client? get syncHttpClient => widget.httpClient;
|
||||
|
||||
|
||||
@ -9,11 +9,14 @@
|
||||
/// reached from the second tap of `FillAllFlow`.
|
||||
library;
|
||||
|
||||
import 'package:diet_guard_app/models/food_entry.dart';
|
||||
import 'package:diet_guard_app/models/kuchnia_dish.dart';
|
||||
import 'package:diet_guard_app/models/local_time.dart';
|
||||
import 'package:diet_guard_app/models/nutrition.dart';
|
||||
import 'package:diet_guard_app/models/slot.dart';
|
||||
import 'package:diet_guard_app/services/kuchnia_spread.dart';
|
||||
import 'package:diet_guard_app/services/log_storage_service.dart';
|
||||
import 'package:diet_guard_app/services/meal_schedule_service.dart';
|
||||
|
||||
/// Marks entries this feature created, the same tag the PC's Fill all writes
|
||||
/// (`_kuchnia_log.SOURCE`), so both devices' batch fills read alike.
|
||||
@ -61,23 +64,49 @@ Nutrition dishNutrition(KuchniaDish dish) => nutritionForPortion(
|
||||
String _dedupKey(String desc, int? slotMinute) =>
|
||||
'${desc.trim().toLowerCase()}\u0000$slotMinute';
|
||||
|
||||
/// Logs each chosen dish, skipping any already logged today in that slot.
|
||||
/// Returns [day]'s live entries; today's when [day] is null.
|
||||
Future<List<FoodEntry>> dayEntries(
|
||||
LogStorageService store,
|
||||
DateTime? day,
|
||||
) async {
|
||||
if (day == null) return await store.todayEntries();
|
||||
final log = await store.readLog();
|
||||
return [
|
||||
for (final entry in log[localDateKey(day)] ?? const <FoodEntry>[])
|
||||
if (!entry.deleted) entry,
|
||||
];
|
||||
}
|
||||
|
||||
/// [day]'s satisfied slot minutes under the schedule in force *that* day --
|
||||
/// `LogStorageService.loggedSlotsToday` for any date, as the "Log for later"
|
||||
/// slot row computes it.
|
||||
Future<Set<int>> loggedSlotsOn(LogStorageService store, DateTime day) async {
|
||||
final entries = await dayEntries(store, day);
|
||||
return satisfiedSlots([
|
||||
for (final entry in entries) ?entry.slotMinute,
|
||||
], MealScheduleService.history.forDay(localDateKey(day)));
|
||||
}
|
||||
|
||||
/// Logs each chosen dish, skipping any already logged that day in that slot.
|
||||
///
|
||||
/// Mirrors `log_dishes`, including its intra-batch skip: two identical dishes
|
||||
/// spread into the same slot land once, because the write is reflected in the
|
||||
/// seen set before the next dish is checked.
|
||||
///
|
||||
/// Writes through [LogStorageService.logMeal], the same call a manual log
|
||||
/// makes; the caller then publishes once for the whole batch.
|
||||
/// makes; the caller then publishes once for the whole batch. With [day]
|
||||
/// (a "Log for later" date, strictly after today) each dish is stamped at its
|
||||
/// slot's time on that day; without it, now -- as a manual log is.
|
||||
///
|
||||
/// Returns the dishes actually written, in order.
|
||||
Future<List<SlottedDish>> logDishes(
|
||||
List<SlottedDish> chosen, {
|
||||
LogStorageService? storage,
|
||||
DateTime? day,
|
||||
}) async {
|
||||
final store = storage ?? LogStorageService.instance;
|
||||
final seen = {
|
||||
for (final entry in await store.todayEntries())
|
||||
for (final entry in await dayEntries(store, day))
|
||||
_dedupKey(entry.desc, entry.slotMinute),
|
||||
};
|
||||
final written = <SlottedDish>[];
|
||||
@ -88,6 +117,15 @@ Future<List<SlottedDish>> logDishes(
|
||||
item.dish.name,
|
||||
dishNutrition(item.dish),
|
||||
slotMinute: item.slot,
|
||||
when: day == null
|
||||
? null
|
||||
: DateTime(
|
||||
day.year,
|
||||
day.month,
|
||||
day.day,
|
||||
item.slot ~/ 60,
|
||||
item.slot % 60,
|
||||
),
|
||||
);
|
||||
written.add(item);
|
||||
}
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
/// The two-tap state machine behind "Fill all with catering".
|
||||
///
|
||||
/// Mirrors `diet_guard/_gatelock_fillall.py`'s `FillAllFlow`. Two taps, never
|
||||
/// one: the first fetches today's delivery and proposes exactly what would be
|
||||
/// written; only the second writes. No modal dialog -- the proposal is a
|
||||
/// status line and the button relabels to "✓ Confirm (N)".
|
||||
/// one: the first fetches the day's delivery (today's, or the date picked
|
||||
/// through "Log for later") and proposes exactly what would be written; only
|
||||
/// the second writes. No modal dialog -- the proposal is a status line and
|
||||
/// the button relabels to "✓ Confirm (N)".
|
||||
///
|
||||
/// Widget-free so the flow's guarantees (first tap writes nothing, confirm
|
||||
/// re-checks occupancy, a plan from before midnight refetches) are testable
|
||||
/// with an injected clock and fetch.
|
||||
library;
|
||||
|
||||
import 'package:diet_guard_app/models/local_time.dart';
|
||||
import 'package:diet_guard_app/models/slot.dart';
|
||||
import 'package:diet_guard_app/services/kuchnia_fill.dart';
|
||||
import 'package:diet_guard_app/services/kuchnia_import.dart';
|
||||
@ -17,6 +19,7 @@ import 'package:diet_guard_app/services/kuchnia_queue.dart';
|
||||
import 'package:diet_guard_app/services/kuchnia_spread.dart';
|
||||
import 'package:diet_guard_app/services/log_storage_service.dart';
|
||||
import 'package:diet_guard_app/services/meal_schedule_service.dart';
|
||||
import 'package:flutter/foundation.dart' show visibleForTesting;
|
||||
|
||||
/// Fetches one day's delivery.
|
||||
typedef DeliveryFetch = Future<KuchniaRefresh> Function(DateTime day);
|
||||
@ -31,11 +34,19 @@ const _none = <SlottedDish>[];
|
||||
const nothingToFillMessage = 'Every slot already has a meal — nothing to fill.';
|
||||
|
||||
/// The explicit tap's fetch: unguarded, like "Today's delivery", and it
|
||||
/// records the fetch so the automatic path is spared a second walk.
|
||||
Future<KuchniaRefresh> _fetchAndRecord(DateTime day) async {
|
||||
final result = await refreshDelivery(day);
|
||||
/// records today's fetch so the automatic path is spared a second walk.
|
||||
///
|
||||
/// Only today's: the marker holds a single day, so recording a "Log for
|
||||
/// later" date would make the automatic fetch skip that day when it comes.
|
||||
@visibleForTesting
|
||||
Future<KuchniaRefresh> fetchAndRecord(
|
||||
DateTime day, {
|
||||
DeliveryFetch fetch = refreshDelivery,
|
||||
}) async {
|
||||
final result = await fetch(day);
|
||||
if (result.ok &&
|
||||
result.dishes.isNotEmpty &&
|
||||
localDateKey(day) == localDateKey(DateTime.now()) &&
|
||||
KuchniaQueueService.isInitialized) {
|
||||
await KuchniaQueueService.instance.recordFetched(day);
|
||||
}
|
||||
@ -48,21 +59,24 @@ class FillAllFlow {
|
||||
FillAllFlow({
|
||||
DeliveryFetch? fetch,
|
||||
DateTime Function()? now,
|
||||
List<int> Function()? slots,
|
||||
Future<Set<int>> Function()? occupiedSlots,
|
||||
List<int> Function(DateTime day)? slots,
|
||||
Future<Set<int>> Function(DateTime day)? occupiedSlots,
|
||||
LogStorageService? storage,
|
||||
}) : _fetch = fetch ?? _fetchAndRecord,
|
||||
}) : _fetch = fetch ?? fetchAndRecord,
|
||||
_now = now ?? DateTime.now,
|
||||
_slots = slots ?? (() => daySlots(MealScheduleService.current)),
|
||||
_slots =
|
||||
slots ??
|
||||
((day) =>
|
||||
daySlots(MealScheduleService.history.forDay(localDateKey(day)))),
|
||||
_occupied =
|
||||
occupiedSlots ??
|
||||
(() => (storage ?? LogStorageService.instance).loggedSlotsToday()),
|
||||
((day) => loggedSlotsOn(storage ?? LogStorageService.instance, day)),
|
||||
_storage = storage;
|
||||
|
||||
final DeliveryFetch _fetch;
|
||||
final DateTime Function() _now;
|
||||
final List<int> Function() _slots;
|
||||
final Future<Set<int>> Function() _occupied;
|
||||
final List<int> Function(DateTime day) _slots;
|
||||
final Future<Set<int>> Function(DateTime day) _occupied;
|
||||
final LogStorageService? _storage;
|
||||
|
||||
List<SlottedDish> _plan = const [];
|
||||
@ -82,37 +96,43 @@ class FillAllFlow {
|
||||
|
||||
/// First tap fetches and proposes; a second tap on the same day confirms.
|
||||
///
|
||||
/// [day] is the "Log for later" date when one is picked; null means today.
|
||||
///
|
||||
/// Returns null when the tap was ignored because one is already in flight,
|
||||
/// or when [cancel] ran while its fetch was.
|
||||
Future<FillOutcome?> tap() async {
|
||||
Future<FillOutcome?> tap({DateTime? day}) async {
|
||||
if (busy) return null;
|
||||
final today = _today();
|
||||
if (_plan.isNotEmpty && _planDay == today) return await _confirm();
|
||||
// No plan, or one proposed before midnight: fetch (again). The day is
|
||||
// pinned here, not when the result lands, so a fetch still in flight at
|
||||
// midnight can never confirm yesterday's menu into today.
|
||||
final target = day == null
|
||||
? _today()
|
||||
: DateTime(day.year, day.month, day.day);
|
||||
if (_plan.isNotEmpty && _planDay == target) return await _confirm();
|
||||
// No plan, one for another picked day, or one proposed before midnight:
|
||||
// fetch (again). The day is pinned here, not when the result lands, so a
|
||||
// fetch still in flight at midnight can never confirm yesterday's menu
|
||||
// into today.
|
||||
cancel();
|
||||
_planDay = today;
|
||||
_planDay = target;
|
||||
busy = true;
|
||||
try {
|
||||
return await _propose(today);
|
||||
return await _propose(target);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<FillOutcome?> _propose(DateTime today) async {
|
||||
final result = await _fetch(today);
|
||||
Future<FillOutcome?> _propose(DateTime day) async {
|
||||
final result = await _fetch(day);
|
||||
// Cancelled while the fetch was in flight: arming now would show
|
||||
// "Confirm (N)" for a plan the user already dismissed.
|
||||
if (_planDay != today) return null;
|
||||
if (_planDay != day) return null;
|
||||
if (!result.ok) return (message: result.reason!, written: _none);
|
||||
if (result.dishes.isEmpty) {
|
||||
return (message: 'No catering delivery today.', written: _none);
|
||||
final when = day == _today() ? 'today' : 'on ${localDateKey(day)}';
|
||||
return (message: 'No catering delivery $when.', written: _none);
|
||||
}
|
||||
final plan = fillPlan(
|
||||
assignSlots(result.dishes, _slots()),
|
||||
await _occupied(),
|
||||
assignSlots(result.dishes, _slots(day)),
|
||||
await _occupied(day),
|
||||
);
|
||||
if (plan.isEmpty) return (message: nothingToFillMessage, written: _none);
|
||||
_plan = plan;
|
||||
@ -124,15 +144,20 @@ class FillAllFlow {
|
||||
try {
|
||||
// Re-checked at write time: a meal logged in between the two taps --
|
||||
// here or synced from the PC -- has taken its slot and keeps it.
|
||||
final plan = fillPlan(_plan, await _occupied());
|
||||
final day = _planDay!;
|
||||
final plan = fillPlan(_plan, await _occupied(day));
|
||||
cancel();
|
||||
if (plan.isEmpty) {
|
||||
return (message: nothingToFillMessage, written: _none);
|
||||
}
|
||||
final written = await logDishes(plan, storage: _storage);
|
||||
// A later day's dishes are stamped at their slot times on that day;
|
||||
// today's at now, like any manual log.
|
||||
final later = day == _today() ? null : day;
|
||||
final written = await logDishes(plan, storage: _storage, day: later);
|
||||
final noun = written.length == 1 ? 'dish' : 'dishes';
|
||||
final suffix = later == null ? '' : ' for ${localDateKey(later)}';
|
||||
return (
|
||||
message: 'Logged ${written.length} catering $noun.',
|
||||
message: 'Logged ${written.length} catering $noun$suffix.',
|
||||
written: written,
|
||||
);
|
||||
} finally {
|
||||
|
||||
@ -2,7 +2,7 @@ name: diet_guard_app
|
||||
description: "Companion phone app for diet_guard: log meals on the go."
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.0.120+1
|
||||
version: 1.0.121+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.12.2
|
||||
|
||||
@ -25,8 +25,8 @@ void main() {
|
||||
return KuchniaRefresh(dishes: dishes(4));
|
||||
},
|
||||
now: () => DateTime(2026, 8, 23, 10),
|
||||
slots: () => defaultSlots,
|
||||
occupiedSlots: () async => <int>{},
|
||||
slots: (_) => defaultSlots,
|
||||
occupiedSlots: (_) async => <int>{},
|
||||
storage: freshStorage(),
|
||||
);
|
||||
|
||||
|
||||
150
app/test/kuchnia_fill_flow_future_test.dart
Normal file
150
app/test/kuchnia_fill_flow_future_test.dart
Normal file
@ -0,0 +1,150 @@
|
||||
/// "Fill all with catering" with a later date picked through "Log for later".
|
||||
///
|
||||
/// The flow used to hard-code today: picking 24.10 on 23.10 fetched 23.10's
|
||||
/// delivery and reported "No catering delivery today." Each test pins the
|
||||
/// *picked* day end to end -- the fetch, the occupancy, and where the entries
|
||||
/// land -- by reading the log, not a return value.
|
||||
///
|
||||
/// The clock is the real one: `logMeal` refuses a `when` in the past against
|
||||
/// `DateTime.now()`, so the picked day has to be genuinely in the future.
|
||||
library;
|
||||
|
||||
import 'package:diet_guard_app/models/local_time.dart';
|
||||
import 'package:diet_guard_app/models/nutrition.dart';
|
||||
import 'package:diet_guard_app/services/kuchnia_fill_flow.dart';
|
||||
import 'package:diet_guard_app/services/kuchnia_import.dart';
|
||||
import 'package:diet_guard_app/services/kuchnia_queue.dart';
|
||||
import 'package:diet_guard_app/services/log_storage_service.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'kuchnia_fill_support.dart';
|
||||
|
||||
/// `desc@slot time` for every live entry stored under [day].
|
||||
Future<List<String>> loggedOn(LogStorageService storage, DateTime day) async {
|
||||
final log = await storage.readLog();
|
||||
return [
|
||||
for (final e in log[localDateKey(day)] ?? const [])
|
||||
if (!e.deleted) '${e.desc}@${e.slotMinute} ${e.time.substring(11, 16)}',
|
||||
];
|
||||
}
|
||||
|
||||
void main() {
|
||||
late LogStorageService storage;
|
||||
late List<DateTime> fetched;
|
||||
late KuchniaRefresh delivery;
|
||||
late FillAllFlow flow;
|
||||
final now = DateTime.now();
|
||||
final tomorrow = DateTime(now.year, now.month, now.day + 1);
|
||||
final dayAfter = DateTime(now.year, now.month, now.day + 2);
|
||||
|
||||
setUp(() {
|
||||
storage = freshStorage();
|
||||
fetched = [];
|
||||
delivery = KuchniaRefresh(dishes: dishes(4));
|
||||
flow = FillAllFlow(
|
||||
fetch: (day) async {
|
||||
fetched.add(day);
|
||||
return delivery;
|
||||
},
|
||||
storage: storage,
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(LogStorageService.resetForTesting);
|
||||
|
||||
test('fetches the picked day and logs onto it at slot times', () async {
|
||||
final first = await flow.tap(day: tomorrow);
|
||||
|
||||
expect(fetched, [tomorrow]);
|
||||
expect(first!.message, startsWith('Will log 4'));
|
||||
expect(await loggedOn(storage, tomorrow), isEmpty);
|
||||
|
||||
final second = await flow.tap(day: tomorrow);
|
||||
|
||||
expect(
|
||||
second!.message,
|
||||
'Logged 4 catering dishes for '
|
||||
'${localDateKey(tomorrow)}.',
|
||||
);
|
||||
expect(await loggedOn(storage, tomorrow), [
|
||||
'D1@480 08:00',
|
||||
'D2@720 12:00',
|
||||
'D3@960 16:00',
|
||||
'D4@1200 20:00',
|
||||
]);
|
||||
expect(await loggedToday(storage), isEmpty);
|
||||
});
|
||||
|
||||
test("occupancy is the picked day's, not today's", () async {
|
||||
// Today's 08:00 is taken, which must not block tomorrow's 08:00 ...
|
||||
await storage.logMeal('eaten today', _kcal, slotMinute: 480);
|
||||
// ... while tomorrow's pre-logged 12:00 keeps its meal.
|
||||
await storage.logMeal(
|
||||
'pre-logged',
|
||||
_kcal,
|
||||
slotMinute: 720,
|
||||
when: tomorrow.add(const Duration(hours: 12)),
|
||||
);
|
||||
|
||||
await flow.tap(day: tomorrow);
|
||||
await flow.tap(day: tomorrow);
|
||||
|
||||
expect(await loggedOn(storage, tomorrow), [
|
||||
'pre-logged@720 12:00',
|
||||
'D1@480 08:00',
|
||||
'D3@960 16:00',
|
||||
'D4@1200 20:00',
|
||||
]);
|
||||
});
|
||||
|
||||
test('a plan proposed for one day never confirms onto another', () async {
|
||||
await flow.tap(day: tomorrow);
|
||||
final outcome = await flow.tap(day: dayAfter);
|
||||
|
||||
expect(fetched, [tomorrow, dayAfter]);
|
||||
expect(outcome!.written, isEmpty);
|
||||
expect(await loggedOn(storage, tomorrow), isEmpty);
|
||||
expect(await loggedOn(storage, dayAfter), isEmpty);
|
||||
});
|
||||
|
||||
test('an empty delivery names the picked day', () async {
|
||||
delivery = const KuchniaRefresh(dishes: []);
|
||||
|
||||
final outcome = await flow.tap(day: tomorrow);
|
||||
|
||||
expect(
|
||||
outcome!.message,
|
||||
'No catering delivery on ${localDateKey(tomorrow)}.',
|
||||
);
|
||||
});
|
||||
|
||||
test('no day still means today', () async {
|
||||
await flow.tap();
|
||||
|
||||
expect(fetched, [DateTime(now.year, now.month, now.day)]);
|
||||
});
|
||||
|
||||
test("a later day's fetch never marks that day as already fetched", () async {
|
||||
// The marker holds one day, read by the automatic "Today's delivery"
|
||||
// fetch: recording tomorrow would skip that fetch when tomorrow comes.
|
||||
await KuchniaQueueService.initForTesting(MemoryStore());
|
||||
addTearDown(KuchniaQueueService.resetForTesting);
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
Future<KuchniaRefresh> fake(DateTime _) async => delivery;
|
||||
|
||||
await fetchAndRecord(tomorrow, fetch: fake);
|
||||
expect(KuchniaQueueService.instance.alreadyFetched(tomorrow), isFalse);
|
||||
|
||||
await fetchAndRecord(today, fetch: fake);
|
||||
expect(KuchniaQueueService.instance.alreadyFetched(today), isTrue);
|
||||
});
|
||||
}
|
||||
|
||||
const _kcal = Nutrition(
|
||||
grams: 100,
|
||||
kcal: 100,
|
||||
proteinG: 1,
|
||||
carbsG: 1,
|
||||
fatG: 1,
|
||||
source: 'manual',
|
||||
);
|
||||
@ -49,8 +49,8 @@ void main() {
|
||||
flow = FillAllFlow(
|
||||
fetch: fetcher.call,
|
||||
now: () => clock,
|
||||
slots: () => defaultSlots,
|
||||
occupiedSlots: () async {
|
||||
slots: (_) => defaultSlots,
|
||||
occupiedSlots: (_) async {
|
||||
occupancyReads++;
|
||||
if (occupancyGate != null) await occupancyGate!.future;
|
||||
return {...occupied};
|
||||
|
||||
@ -12,7 +12,9 @@
|
||||
library;
|
||||
|
||||
import 'package:diet_guard_app/models/kuchnia_dish.dart';
|
||||
import 'package:diet_guard_app/models/local_time.dart';
|
||||
import 'package:diet_guard_app/screens/log_meal_fill_mixin.dart';
|
||||
import 'package:diet_guard_app/screens/log_meal_future_mixin.dart';
|
||||
import 'package:diet_guard_app/screens/log_meal_kuchnia_mixin.dart';
|
||||
import 'package:diet_guard_app/screens/log_meal_sync_mixin.dart';
|
||||
import 'package:diet_guard_app/services/kuchnia_fill.dart';
|
||||
@ -42,6 +44,7 @@ class _HostState extends State<_Host>
|
||||
WidgetsBindingObserver,
|
||||
LogMealSyncMixin<_Host>,
|
||||
LogMealKuchniaMixin<_Host>,
|
||||
LogMealFutureMixin<_Host>,
|
||||
LogMealFillMixin<_Host> {
|
||||
final TextEditingController desc = TextEditingController();
|
||||
final MacroControllers macros = MacroControllers();
|
||||
@ -65,7 +68,7 @@ class _HostState extends State<_Host>
|
||||
super.initState();
|
||||
fillFlow = FillAllFlow(
|
||||
fetch: (_) async => KuchniaRefresh(dishes: widget.delivery),
|
||||
slots: () => defaultSlots,
|
||||
slots: (_) => defaultSlots,
|
||||
storage: widget.storage,
|
||||
);
|
||||
}
|
||||
@ -175,8 +178,8 @@ void main() {
|
||||
await pumpHost(t);
|
||||
host.fillFlow = FillAllFlow(
|
||||
fetch: (_) async => KuchniaRefresh(dishes: delivery),
|
||||
slots: () => defaultSlots,
|
||||
occupiedSlots: () async => throw Exception('storage unavailable'),
|
||||
slots: (_) => defaultSlots,
|
||||
occupiedSlots: (_) async => throw Exception('storage unavailable'),
|
||||
storage: storage,
|
||||
);
|
||||
|
||||
@ -186,4 +189,43 @@ void main() {
|
||||
expect(host.publishes, 1, reason: 'a partial batch must still sync');
|
||||
expect(host.fillFlow.busy, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('a later-day fill leaves today alone and returns to it', (
|
||||
t,
|
||||
) async {
|
||||
final now = DateTime.now();
|
||||
final tomorrow = DateTime(now.year, now.month, now.day + 1);
|
||||
await pumpHost(t);
|
||||
await t.runAsync(() async {
|
||||
await host.selectLogDate(tomorrow);
|
||||
await host.onFillAll();
|
||||
await host.onFillAll();
|
||||
});
|
||||
|
||||
expect(
|
||||
host.status,
|
||||
'Logged 4 catering dishes for ${localDateKey(tomorrow)}.',
|
||||
);
|
||||
final log = await t.runAsync(storage.readLog);
|
||||
expect(log![localDateKey(tomorrow)], hasLength(4));
|
||||
expect(await t.runAsync(storage.todayEntries), isEmpty);
|
||||
expect(host.publishes, 1);
|
||||
expect(KuchniaQueueService.remaining, 4, reason: "today's queue untouched");
|
||||
expect(host.desc.text, 'D1');
|
||||
expect(host.futureDate, isNull);
|
||||
});
|
||||
|
||||
testWidgets('changing the date disarms a proposal', (t) async {
|
||||
final now = DateTime.now();
|
||||
await pumpHost(t);
|
||||
await t.runAsync(() async {
|
||||
await host.selectLogDate(DateTime(now.year, now.month, now.day + 1));
|
||||
await host.onFillAll();
|
||||
});
|
||||
expect(host.fillFlow.armedCount, 4);
|
||||
|
||||
host.resetFutureLog();
|
||||
|
||||
expect(host.fillFlow.armedCount, 0);
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user