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:
vjrj 2026-07-09 22:19:55 +02:00
parent e3ec855630
commit ba87bf2719
20 changed files with 982 additions and 36 deletions

View file

@ -18,6 +18,7 @@ class InventoryState extends Equatable {
this.typeFilter = const {},
this.organicOnly = false,
this.needsReproductionOnly = false,
this.sharingOnly = false,
this.loading = true,
});
@ -42,6 +43,10 @@ class InventoryState extends Equatable {
/// When true, keep only varieties flagged "needs reproducing this season".
final bool needsReproductionOnly;
/// When true, keep only varieties with some lot offered to others the
/// "what I share" view.
final bool sharingOnly;
final bool loading;
/// Categories present across all items, in display order (deduped), so the
@ -60,15 +65,16 @@ class InventoryState extends Equatable {
final q = query.trim().toLowerCase();
return items.where((i) {
if (q.isNotEmpty && !i.label.toLowerCase().contains(q)) return false;
if (categoryFilter.isNotEmpty &&
!categoryFilter.contains(i.category)) {
if (categoryFilter.isNotEmpty && !categoryFilter.contains(i.category)) {
return false;
}
if (typeFilter.isNotEmpty && i.lotTypes.intersection(typeFilter).isEmpty) {
if (typeFilter.isNotEmpty &&
i.lotTypes.intersection(typeFilter).isEmpty) {
return false;
}
if (organicOnly && !i.isOrganic) return false;
if (needsReproductionOnly && !i.needsReproduction) return false;
if (sharingOnly && !i.isShared) return false;
return true;
}).toList();
}
@ -81,6 +87,10 @@ class InventoryState extends Equatable {
/// that filter chip when it would match something.
bool get hasNeedsReproduction => items.any((i) => i.needsReproduction);
/// Whether any variety has a lot offered to others, so the UI only offers
/// the "sharing" chip and the printable catalog when they'd show something.
bool get hasShared => items.any((i) => i.isShared);
InventoryState copyWith({
List<VarietyListItem>? items,
List<VarietyListItem>? drafts,
@ -89,6 +99,7 @@ class InventoryState extends Equatable {
Set<LotType>? typeFilter,
bool? organicOnly,
bool? needsReproductionOnly,
bool? sharingOnly,
bool? loading,
}) {
return InventoryState(
@ -100,6 +111,7 @@ class InventoryState extends Equatable {
organicOnly: organicOnly ?? this.organicOnly,
needsReproductionOnly:
needsReproductionOnly ?? this.needsReproductionOnly,
sharingOnly: sharingOnly ?? this.sharingOnly,
loading: loading ?? this.loading,
);
}
@ -113,6 +125,7 @@ class InventoryState extends Equatable {
typeFilter,
organicOnly,
needsReproductionOnly,
sharingOnly,
loading,
];
}
@ -157,9 +170,12 @@ class InventoryCubit extends Cubit<InventoryState> {
emit(state.copyWith(organicOnly: !state.organicOnly));
/// Toggles the "needs reproducing only" filter.
void toggleNeedsReproductionOnly() => emit(
state.copyWith(needsReproductionOnly: !state.needsReproductionOnly),
);
void toggleNeedsReproductionOnly() =>
emit(state.copyWith(needsReproductionOnly: !state.needsReproductionOnly));
/// Toggles the "what I share" filter.
void toggleSharingOnly() =>
emit(state.copyWith(sharingOnly: !state.sharingOnly));
/// Clears all filters (search is left untouched).
void clearFilters() => emit(
@ -168,6 +184,7 @@ class InventoryCubit extends Cubit<InventoryState> {
typeFilter: const {},
organicOnly: false,
needsReproductionOnly: false,
sharingOnly: false,
),
);

View file

@ -80,6 +80,7 @@ class VarietyDetailCubit extends Cubit<VarietyDetailState> {
String? originPlace,
Abundance? abundance,
PreservationFormat? preservationFormat,
OfferStatus offerStatus = OfferStatus.private,
}) => _repo.addLot(
varietyId: varietyId,
type: type,
@ -91,6 +92,7 @@ class VarietyDetailCubit extends Cubit<VarietyDetailState> {
originPlace: originPlace,
abundance: abundance,
preservationFormat: preservationFormat,
offerStatus: offerStatus,
);
Future<void> updateLot({
@ -104,6 +106,7 @@ class VarietyDetailCubit extends Cubit<VarietyDetailState> {
String? originPlace,
Abundance? abundance,
PreservationFormat? preservationFormat,
OfferStatus offerStatus = OfferStatus.private,
}) => _repo.updateLot(
lotId: lotId,
type: type,
@ -115,6 +118,7 @@ class VarietyDetailCubit extends Cubit<VarietyDetailState> {
originPlace: originPlace,
abundance: abundance,
preservationFormat: preservationFormat,
offerStatus: offerStatus,
);
Future<void> deleteLot(String lotId) => _repo.softDeleteLot(lotId);