feat(db): lot presentation + attachment sort order (schema v4→v5)

Expand LotType from {seed, plant} to six append-only forms (seed,
seedling, plant, tree, bulb, cutting) and add an optional Presentation
attribute (pot/tray/plug/bareRoot/rootBall) kept separate from quantity
so 'how many' and 'in what packaging' never conflate.

Add attachments.sortOrder so the cover photo is the lowest-ordered one,
enabling reordering from the full-screen viewer. Expand QuantityKind
with the new plant-aware forms.

Migrations are additive (v1→v5 all covered by migration_test); enum
values are stored by name and appended only, keeping CRDT sync safe.
This commit is contained in:
vjrj 2026-07-09 11:51:21 +02:00
parent 06aed2a586
commit 3954c62aa6
16 changed files with 6174 additions and 32 deletions

View file

@ -67,6 +67,7 @@ class VarietyLot extends Equatable {
this.harvestYear,
this.harvestMonth,
this.quantity,
this.presentation,
this.storageLocation,
this.germinationTests = const [],
});
@ -78,6 +79,9 @@ class VarietyLot extends Equatable {
/// Optional harvest month (1..12). Only meaningful when [harvestYear] is set.
final int? harvestMonth;
final Quantity? quantity;
/// How living material is packaged (pot, tray, bare-root); null for seeds.
final Presentation? presentation;
final String? storageLocation;
final List<GerminationEntry> germinationTests;
@ -92,6 +96,7 @@ class VarietyLot extends Equatable {
harvestYear,
harvestMonth,
quantity,
presentation,
storageLocation,
germinationTests,
];
@ -252,13 +257,18 @@ class VarietyRepository {
) async {
if (varietyIds.isEmpty) return const {};
final rows =
await (_db.select(_db.attachments)..where(
(a) =>
a.parentId.isIn(varietyIds) &
a.parentType.equalsValue(ParentType.variety) &
a.kind.equalsValue(AttachmentKind.photo) &
a.isDeleted.equals(false),
))
await (_db.select(_db.attachments)
..where(
(a) =>
a.parentId.isIn(varietyIds) &
a.parentType.equalsValue(ParentType.variety) &
a.kind.equalsValue(AttachmentKind.photo) &
a.isDeleted.equals(false),
)
..orderBy([
(a) => OrderingTerm(expression: a.sortOrder),
(a) => OrderingTerm(expression: a.createdAt),
]))
.get();
final byVariety = <String, Uint8List>{};
for (final row in rows) {
@ -431,7 +441,10 @@ class VarietyRepository {
a.kind.equalsValue(AttachmentKind.photo) &
a.isDeleted.equals(false),
)
..orderBy([(a) => OrderingTerm(expression: a.createdAt)]))
..orderBy([
(a) => OrderingTerm(expression: a.sortOrder),
(a) => OrderingTerm(expression: a.createdAt),
]))
.get();
final links =
@ -527,6 +540,7 @@ class VarietyRepository {
int? harvestYear,
int? harvestMonth,
Quantity? quantity,
Presentation? presentation,
String? storageLocation,
}) async {
final (created, updated) = _stamp();
@ -546,6 +560,7 @@ class VarietyRepository {
quantityKind: Value(quantity?.kind.name),
quantityPrecise: Value(quantity?.count?.toDouble()),
quantityLabel: Value(quantity?.label),
presentation: Value(presentation),
storageLocation: Value(storageLocation),
),
);
@ -560,6 +575,7 @@ class VarietyRepository {
int? harvestYear,
int? harvestMonth,
Quantity? quantity,
Presentation? presentation,
String? storageLocation,
}) async {
final (_, updated) = _stamp();
@ -571,6 +587,7 @@ class VarietyRepository {
quantityKind: Value(quantity?.kind.name),
quantityPrecise: Value(quantity?.count?.toDouble()),
quantityLabel: Value(quantity?.label),
presentation: Value(presentation),
storageLocation: Value(storageLocation),
updatedAt: Value(updated),
lastAuthor: Value(nodeId),
@ -666,6 +683,34 @@ class VarietyRepository {
);
}
/// Makes [attachmentId] the cover (preferred) attachment of [varietyId] by
/// giving it a [sortOrder] below every current sibling. The cover is what the
/// inventory list shows as the avatar and the detail gallery shows first.
Future<void> setPreferredPhoto(String varietyId, String attachmentId) async {
final siblings =
await (_db.select(_db.attachments)..where(
(a) =>
a.parentId.equals(varietyId) &
a.parentType.equalsValue(ParentType.variety) &
a.isDeleted.equals(false),
))
.get();
var minOrder = 0;
for (final a in siblings) {
if (a.sortOrder < minOrder) minOrder = a.sortOrder;
}
final (_, updated) = _stamp();
await (_db.update(
_db.attachments,
)..where((a) => a.id.equals(attachmentId))).write(
AttachmentsCompanion(
sortOrder: Value(minOrder - 1),
updatedAt: Value(updated),
lastAuthor: Value(nodeId),
),
);
}
/// Adds an external link (URL, optional title) to a variety. Returns its id.
Future<String> addExternalLink(
String varietyId,
@ -756,6 +801,7 @@ class VarietyRepository {
type: l.type,
harvestYear: l.harvestYear,
harvestMonth: l.harvestMonth,
presentation: l.presentation,
storageLocation: l.storageLocation,
germinationTests: germinationTests,
quantity: hasQuantity