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

@ -0,0 +1,104 @@
import 'package:commons_core/commons_core.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tane/data/species_repository.dart';
import 'package:tane/data/variety_repository.dart';
import 'package:tane/db/database.dart';
import 'package:tane/db/enums.dart';
import '../support/test_support.dart';
void main() {
late AppDatabase db;
late VarietyRepository repo;
setUp(() {
db = newTestDatabase();
repo = newTestRepository(db);
});
tearDown(() => db.close());
test('addLot and updateLot persist the share terms', () async {
final id = await repo.addQuickVariety(label: 'Maize');
final lotId = await repo.addLot(
varietyId: id,
offerStatus: OfferStatus.shared,
);
var detail = await repo.watchVariety(id).first;
expect(detail!.lots.single.offerStatus, OfferStatus.shared);
await repo.updateLot(
lotId: lotId,
type: LotType.seed,
offerStatus: OfferStatus.exchange,
);
detail = await repo.watchVariety(id).first;
expect(detail!.lots.single.offerStatus, OfferStatus.exchange);
});
test('list items flag varieties with some lot offered', () async {
final sharedId = await repo.addQuickVariety(label: 'Shared bean');
await repo.addLot(varietyId: sharedId, offerStatus: OfferStatus.sell);
final privateId = await repo.addQuickVariety(label: 'Private bean');
await repo.addLot(varietyId: privateId);
final items = await repo.watchInventoryView().first;
final byLabel = {for (final i in items.items) i.label: i};
expect(byLabel['Shared bean']!.isShared, isTrue);
expect(byLabel['Private bean']!.isShared, isFalse);
});
test('sharedCatalog lists only offered lots, sorted and joined', () async {
final species = newTestSpeciesRepository(db);
await species.seedBundled(const [
SpeciesSeed(
scientificName: 'Zea mays',
family: 'Poaceae',
commonNames: {
'en': ['Maize'],
},
),
]);
final maizeId = await repo.addQuickVariety(label: 'Rebordanes maize');
final matches = await species.search('Zea', languageCode: 'en');
await repo.linkSpecies(maizeId, matches.single.id);
await repo.addLot(
varietyId: maizeId,
harvestYear: 2024,
quantity: const Quantity(kind: QuantityKind.cob, count: 2),
offerStatus: OfferStatus.shared,
);
// A private lot of the same variety stays out of the catalog.
await repo.addLot(varietyId: maizeId, harvestYear: 2023);
final beanId = await repo.addQuickVariety(label: 'Alubia de Tolosa');
await repo.addLot(
varietyId: beanId,
abundance: Abundance.plentyToShare,
offerStatus: OfferStatus.exchange,
);
// A deleted shared lot disappears with its tombstone.
final goneId = await repo.addQuickVariety(label: 'Gone');
final goneLot = await repo.addLot(
varietyId: goneId,
offerStatus: OfferStatus.shared,
);
await repo.softDeleteLot(goneLot);
final catalog = await repo.sharedCatalog();
expect(catalog, hasLength(2));
// Sorted by label, case-insensitively.
expect(catalog.first.varietyLabel, 'Alubia de Tolosa');
expect(catalog.first.offerStatus, OfferStatus.exchange);
expect(catalog.first.abundance, Abundance.plentyToShare);
final maize = catalog.last;
expect(maize.varietyLabel, 'Rebordanes maize');
expect(maize.scientificName, 'Zea mays');
expect(maize.category, 'Poaceae');
expect(maize.harvestYear, 2024);
expect(maize.quantity!.count, 2);
});
}