feat(inventory): local sharing — per-lot share terms, filter and printable catalog
The local half of the original Fase 2 (no network, no Offer entity yet): - Lot sheet gains a 'do you share it?' choice (gift/swap first, sale last, never the default); declaring plenty of abundance reveals it with a nudge. - Lot lines and list tiles badge shared batches; an 'I share' filter and a printable PDF catalog (paper bridge for fairs) round out the view. - Catalog PDF embeds DejaVu (Latin-ext/Cyrillic/Greek); fonts are a per-locale resource like the OCR packs.
This commit is contained in:
parent
e3ec855630
commit
ba87bf2719
20 changed files with 982 additions and 36 deletions
|
|
@ -179,6 +179,8 @@ String _lotSubtitle(Translations t, VarietyLot lot) {
|
|||
else if (lot.abundance != null)
|
||||
abundanceLabel(t, lot.abundance!),
|
||||
if (lot.presentation != null) presentationLabel(t, lot.presentation!),
|
||||
if (lot.offerStatus != OfferStatus.private)
|
||||
shareStatusLabel(t, lot.offerStatus),
|
||||
];
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
|
@ -885,6 +887,14 @@ String abundanceLabel(Translations t, Abundance a) => switch (a) {
|
|||
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,
|
||||
OfferStatus.shared => t.share.gift,
|
||||
OfferStatus.exchange => t.share.exchange,
|
||||
OfferStatus.sell => t.share.sell,
|
||||
};
|
||||
|
||||
/// Human label for a seed preservation format.
|
||||
String preservationLabel(Translations t, PreservationFormat p) => switch (p) {
|
||||
PreservationFormat.jarWithDesiccant => t.preservation.jarWithDesiccant,
|
||||
|
|
@ -930,6 +940,41 @@ class _AbundanceSelector extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// (sharing-model §4.1). There is always a selection ("just for me" default).
|
||||
class _ShareSelector extends StatelessWidget {
|
||||
const _ShareSelector({required this.value, required this.onChanged});
|
||||
|
||||
final OfferStatus value;
|
||||
final ValueChanged<OfferStatus> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
for (final s in const [
|
||||
OfferStatus.private,
|
||||
OfferStatus.shared,
|
||||
OfferStatus.exchange,
|
||||
OfferStatus.sell,
|
||||
])
|
||||
ChoiceChip(
|
||||
key: Key('share.${s.name}'),
|
||||
label: Text(shareStatusLabel(t, s)),
|
||||
selected: value == s,
|
||||
onSelected: (sel) {
|
||||
if (sel) onChanged(s);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Single-select chips for the preservation format; tapping the current one
|
||||
/// clears it (null).
|
||||
class _PreservationSelector extends StatelessWidget {
|
||||
|
|
@ -1113,15 +1158,19 @@ Future<void> _showLotSheet(
|
|||
final editing = existing != null;
|
||||
// Provenance + abundance + preservation: hidden behind reveal-on-tap chips so
|
||||
// the default form stays small (progressive disclosure).
|
||||
final originNameCtrl = TextEditingController(text: existing?.originName ?? '');
|
||||
final originNameCtrl = TextEditingController(
|
||||
text: existing?.originName ?? '',
|
||||
);
|
||||
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;
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
|
|
@ -1227,11 +1276,24 @@ Future<void> _showLotSheet(
|
|||
if (!showAbundance)
|
||||
ActionChip(
|
||||
key: const Key('lot.addAbundance'),
|
||||
avatar: const Icon(Icons.inventory_2_outlined, size: 18),
|
||||
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'),
|
||||
avatar: const Icon(
|
||||
Icons.volunteer_activism_outlined,
|
||||
size: 18,
|
||||
),
|
||||
label: Text(t.share.add),
|
||||
onPressed: () => setState(() => showShare = true),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showOrigin) ...[
|
||||
|
|
@ -1271,9 +1333,41 @@ Future<void> _showLotSheet(
|
|||
const SizedBox(height: 8),
|
||||
_AbundanceSelector(
|
||||
value: selectedAbundance,
|
||||
onChanged: (a) => setState(() => selectedAbundance = a),
|
||||
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(
|
||||
t.share.title,
|
||||
style: Theme.of(sheetContext).textTheme.labelLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_ShareSelector(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
// Layer 2: advanced seed-bank details, collapsed by default.
|
||||
// Only for seed lots, and (condition checks) an existing lot.
|
||||
if (selectedType == LotType.seed)
|
||||
|
|
@ -1289,8 +1383,9 @@ Future<void> _showLotSheet(
|
|||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
t.preservation.title,
|
||||
style:
|
||||
Theme.of(sheetContext).textTheme.labelLarge,
|
||||
style: Theme.of(
|
||||
sheetContext,
|
||||
).textTheme.labelLarge,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
|
@ -1334,6 +1429,7 @@ Future<void> _showLotSheet(
|
|||
originPlace: originPlace,
|
||||
abundance: selectedAbundance,
|
||||
preservationFormat: selectedPreservation,
|
||||
offerStatus: selectedShare,
|
||||
);
|
||||
} else {
|
||||
cubit.addLot(
|
||||
|
|
@ -1346,6 +1442,7 @@ Future<void> _showLotSheet(
|
|||
originPlace: originPlace,
|
||||
abundance: selectedAbundance,
|
||||
preservationFormat: selectedPreservation,
|
||||
offerStatus: selectedShare,
|
||||
);
|
||||
}
|
||||
Navigator.of(sheetContext).pop();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue