polish(detail): TODO review — calendar intro, drop abundance, collapse seed-saving

From the owner's TODO.org review of the shipped features:
- Crop calendar now says the months are YOUR notes: a hint line in the
  editor (cropCalendar.editorHint) and a caption under the 'this month'
  screen (calendar.selfNote) — it read as if the app knew when to sow.
- Dropped the 'how much I have' (abundance) input + display: it competed
  with the lot quantity as a second 'how much'. Removed from the lot sheet
  and the lot summary; the DB column + backup/CSV/catalog plumbing stay
  dormant (no destructive migration). offer_status carries 'do I share'.
- Seed-saving guidance moved into a collapsed expander (detail.seedSaving)
  — reference facts about the species shouldn't sit open among the
  grower's own records; opt-in depth, per progressive disclosure.
- Condition checks already live behind the collapsed 'advanced' expander
  (seed lots only) — left as the discreet power-user surface.

Tests: dropped the obsolete abundance→share nudge test; the seed-saving
section test now expands before asserting. analyze clean; suites green.
This commit is contained in:
vjrj 2026-07-11 14:05:31 +02:00
parent 00db9d4b6a
commit 6898e385e8
13 changed files with 73 additions and 124 deletions

View file

@ -147,9 +147,22 @@ class _DetailView extends StatelessWidget {
scientificName: detail.scientificName,
family: detail.family ?? detail.category,
) case final guide? when guide.hasAny) ...[
const SizedBox(height: 16),
_SectionTitle(t.seedSaving.title),
_SeedSavingView(guide: guide),
const SizedBox(height: 8),
// Reference data (facts about the species, not the grower's own
// records) collapsed by default so it's opt-in depth, not noise.
ExpansionTile(
key: const Key('detail.seedSaving'),
tilePadding: EdgeInsets.zero,
childrenPadding: const EdgeInsets.only(bottom: 8),
leading: const Icon(Icons.spa_outlined, color: seedGreen),
title: Text(t.seedSaving.title),
children: [
Align(
alignment: AlignmentDirectional.centerStart,
child: _SeedSavingView(guide: guide),
),
],
),
],
if (_referenceLinks(context, detail) case final references
when references.isNotEmpty) ...[
@ -234,10 +247,7 @@ String _lotSubtitle(Translations t, VarietyLot lot) {
harvestDateLabel(t, year: lot.harvestYear!, month: lot.harvestMonth)
else
t.detail.noYear,
if (lot.quantity != null)
quantityDisplay(t, lot.quantity!)
else if (lot.abundance != null)
abundanceLabel(t, lot.abundance!),
if (lot.quantity != null) quantityDisplay(t, lot.quantity!),
if (lot.presentation != null) presentationLabel(t, lot.presentation!),
if (lot.offerStatus != OfferStatus.private)
shareStatusLabel(t, lot.offerStatus),
@ -879,6 +889,17 @@ class _EditVarietySheetState extends State<_EditVarietySheet> {
title: Text(t.cropCalendar.title),
onExpansionChanged: (v) => _calendarOpen = v,
children: [
Align(
alignment: AlignmentDirectional.centerStart,
child: Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
t.cropCalendar.editorHint,
style: const TextStyle(
color: seedMuted, fontSize: 12),
),
),
),
_MonthMultiSelect(
label: t.cropCalendar.sow,
mask: _sowMonths,
@ -1176,14 +1197,6 @@ class _DifficultyChip extends StatelessWidget {
}
}
/// Human label for a coarse-abundance level.
String abundanceLabel(Translations t, Abundance a) => switch (a) {
Abundance.plentyToShare => t.abundance.plentyToShare,
Abundance.enoughToShare => t.abundance.enoughToShare,
Abundance.enoughForMe => t.abundance.enoughForMe,
Abundance.runningLow => t.abundance.runningLow,
};
/// Human label for how (whether) a lot is offered to others.
String shareStatusLabel(Translations t, OfferStatus s) => switch (s) {
OfferStatus.private => t.share.private,
@ -1210,32 +1223,6 @@ String desiccantLabel(Translations t, DesiccantState d) => switch (d) {
DesiccantState.fresh => t.desiccant.fresh,
};
/// Single-select chips for the coarse-abundance level; tapping the current one
/// clears it (null).
class _AbundanceSelector extends StatelessWidget {
const _AbundanceSelector({required this.value, required this.onChanged});
final Abundance? value;
final ValueChanged<Abundance?> onChanged;
@override
Widget build(BuildContext context) {
final t = context.t;
return Wrap(
spacing: 8,
runSpacing: 4,
children: [
for (final a in Abundance.values)
ChoiceChip(
key: Key('abundance.${a.name}'),
label: Text(abundanceLabel(t, a)),
selected: value == a,
onSelected: (sel) => onChanged(sel ? a : null),
),
],
);
}
}
/// Single-select chips for the share terms. Gift and swap come first and sale
/// last, never preselected the gift is first-class, the sale a choice
@ -1464,12 +1451,10 @@ Future<void> _showLotSheet(
final originPlaceCtrl = TextEditingController(
text: existing?.originPlace ?? '',
);
var selectedAbundance = existing?.abundance;
var selectedPreservation = existing?.preservationFormat;
var selectedShare = existing?.offerStatus ?? OfferStatus.private;
var showOrigin =
existing?.originName != null || existing?.originPlace != null;
var showAbundance = existing?.abundance != null;
var showShare = selectedShare != OfferStatus.private;
final existingPrice = existing?.priceAmount;
final priceAmountCtrl = TextEditingController(
@ -1585,17 +1570,6 @@ Future<void> _showLotSheet(
label: Text(t.provenance.section),
onPressed: () => setState(() => showOrigin = true),
),
if (!showAbundance)
ActionChip(
key: const Key('lot.addAbundance'),
avatar: const Icon(
Icons.inventory_2_outlined,
size: 18,
),
label: Text(t.abundance.add),
onPressed: () =>
setState(() => showAbundance = true),
),
if (!showShare)
ActionChip(
key: const Key('lot.addShare'),
@ -1636,27 +1610,6 @@ Future<void> _showLotSheet(
),
),
],
if (showAbundance) ...[
const SizedBox(height: 16),
Text(
t.abundance.title,
style: Theme.of(sheetContext).textTheme.labelLarge,
),
const SizedBox(height: 8),
_AbundanceSelector(
value: selectedAbundance,
onChanged: (a) => setState(() {
selectedAbundance = a;
// The bridge from "how much" to "do you share it":
// declaring plenty reveals the sharing choice. A
// nudge, never a change of the choice itself.
if (a == Abundance.plentyToShare ||
a == Abundance.enoughToShare) {
showShare = true;
}
}),
),
],
if (showShare) ...[
const SizedBox(height: 16),
Text(
@ -1668,17 +1621,6 @@ Future<void> _showLotSheet(
value: selectedShare,
onChanged: (s) => setState(() => selectedShare = s),
),
if (selectedShare == OfferStatus.private &&
(selectedAbundance == Abundance.plentyToShare ||
selectedAbundance == Abundance.enoughToShare))
Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(
t.share.nudge,
key: const Key('share.nudge'),
style: Theme.of(sheetContext).textTheme.bodySmall,
),
),
// Price only when selling informational, agreed and
// paid off-app. Empty amount = "to be agreed".
if (selectedShare == OfferStatus.sell) ...[
@ -1786,7 +1728,6 @@ Future<void> _showLotSheet(
presentation: selectedPresentation,
originName: originName,
originPlace: originPlace,
abundance: selectedAbundance,
preservationFormat: selectedPreservation,
offerStatus: selectedShare,
priceAmount: priceAmount,
@ -1801,7 +1742,6 @@ Future<void> _showLotSheet(
presentation: selectedPresentation,
originName: originName,
originPlace: originPlace,
abundance: selectedAbundance,
preservationFormat: selectedPreservation,
offerStatus: selectedShare,
priceAmount: priceAmount,