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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

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

View file

@ -27,13 +27,13 @@ class AppDatabase extends _$AppDatabase {
AppDatabase(super.e);
@override
int get schemaVersion => 3;
int get schemaVersion => 5;
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (m) async => m.createAll(),
onUpgrade: (m, from, to) async {
// v2: lots can hold seeds or plants/seedlings (plantel).
// v2: lots can hold seeds or living plant material.
if (from < 2) {
await m.addColumn(lots, lots.type);
}
@ -41,6 +41,14 @@ class AppDatabase extends _$AppDatabase {
if (from < 3) {
await m.addColumn(lots, lots.harvestMonth);
}
// v4: optional packaging/supply form for living lots.
if (from < 4) {
await m.addColumn(lots, lots.presentation);
}
// v5: display order for attachments (cover photo = lowest sortOrder).
if (from < 5) {
await m.addColumn(attachments, attachments.sortOrder);
}
},
);
}

View file

@ -2675,6 +2675,15 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> {
type: DriftSqlType.string,
requiredDuringInsert: false,
);
@override
late final GeneratedColumnWithTypeConverter<Presentation?, String>
presentation = GeneratedColumn<String>(
'presentation',
aliasedName,
true,
type: DriftSqlType.string,
requiredDuringInsert: false,
).withConverter<Presentation?>($LotsTable.$converterpresentationn);
static const VerificationMeta _storageLocationMeta = const VerificationMeta(
'storageLocation',
);
@ -2722,6 +2731,7 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> {
quantityKind,
quantityPrecise,
quantityLabel,
presentation,
storageLocation,
offerStatus,
seedbankId,
@ -2913,6 +2923,12 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> {
DriftSqlType.string,
data['${effectivePrefix}quantity_label'],
),
presentation: $LotsTable.$converterpresentationn.fromSql(
attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}presentation'],
),
),
storageLocation: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}storage_location'],
@ -2937,6 +2953,14 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> {
static JsonTypeConverter2<LotType, String, String> $convertertype =
const EnumNameConverter<LotType>(LotType.values);
static JsonTypeConverter2<Presentation, String, String>
$converterpresentation = const EnumNameConverter<Presentation>(
Presentation.values,
);
static JsonTypeConverter2<Presentation?, String?, String?>
$converterpresentationn = JsonTypeConverter2.asNullable(
$converterpresentation,
);
static JsonTypeConverter2<OfferStatus, String, String> $converterofferStatus =
const EnumNameConverter<OfferStatus>(OfferStatus.values);
}
@ -2955,6 +2979,7 @@ class Lot extends DataClass implements Insertable<Lot> {
final String? quantityKind;
final double? quantityPrecise;
final String? quantityLabel;
final Presentation? presentation;
final String? storageLocation;
final OfferStatus offerStatus;
final String? seedbankId;
@ -2972,6 +2997,7 @@ class Lot extends DataClass implements Insertable<Lot> {
this.quantityKind,
this.quantityPrecise,
this.quantityLabel,
this.presentation,
this.storageLocation,
required this.offerStatus,
this.seedbankId,
@ -3004,6 +3030,11 @@ class Lot extends DataClass implements Insertable<Lot> {
if (!nullToAbsent || quantityLabel != null) {
map['quantity_label'] = Variable<String>(quantityLabel);
}
if (!nullToAbsent || presentation != null) {
map['presentation'] = Variable<String>(
$LotsTable.$converterpresentationn.toSql(presentation),
);
}
if (!nullToAbsent || storageLocation != null) {
map['storage_location'] = Variable<String>(storageLocation);
}
@ -3043,6 +3074,9 @@ class Lot extends DataClass implements Insertable<Lot> {
quantityLabel: quantityLabel == null && nullToAbsent
? const Value.absent()
: Value(quantityLabel),
presentation: presentation == null && nullToAbsent
? const Value.absent()
: Value(presentation),
storageLocation: storageLocation == null && nullToAbsent
? const Value.absent()
: Value(storageLocation),
@ -3074,6 +3108,9 @@ class Lot extends DataClass implements Insertable<Lot> {
quantityKind: serializer.fromJson<String?>(json['quantityKind']),
quantityPrecise: serializer.fromJson<double?>(json['quantityPrecise']),
quantityLabel: serializer.fromJson<String?>(json['quantityLabel']),
presentation: $LotsTable.$converterpresentationn.fromJson(
serializer.fromJson<String?>(json['presentation']),
),
storageLocation: serializer.fromJson<String?>(json['storageLocation']),
offerStatus: $LotsTable.$converterofferStatus.fromJson(
serializer.fromJson<String>(json['offerStatus']),
@ -3098,6 +3135,9 @@ class Lot extends DataClass implements Insertable<Lot> {
'quantityKind': serializer.toJson<String?>(quantityKind),
'quantityPrecise': serializer.toJson<double?>(quantityPrecise),
'quantityLabel': serializer.toJson<String?>(quantityLabel),
'presentation': serializer.toJson<String?>(
$LotsTable.$converterpresentationn.toJson(presentation),
),
'storageLocation': serializer.toJson<String?>(storageLocation),
'offerStatus': serializer.toJson<String>(
$LotsTable.$converterofferStatus.toJson(offerStatus),
@ -3120,6 +3160,7 @@ class Lot extends DataClass implements Insertable<Lot> {
Value<String?> quantityKind = const Value.absent(),
Value<double?> quantityPrecise = const Value.absent(),
Value<String?> quantityLabel = const Value.absent(),
Value<Presentation?> presentation = const Value.absent(),
Value<String?> storageLocation = const Value.absent(),
OfferStatus? offerStatus,
Value<String?> seedbankId = const Value.absent(),
@ -3141,6 +3182,7 @@ class Lot extends DataClass implements Insertable<Lot> {
quantityLabel: quantityLabel.present
? quantityLabel.value
: this.quantityLabel,
presentation: presentation.present ? presentation.value : this.presentation,
storageLocation: storageLocation.present
? storageLocation.value
: this.storageLocation,
@ -3176,6 +3218,9 @@ class Lot extends DataClass implements Insertable<Lot> {
quantityLabel: data.quantityLabel.present
? data.quantityLabel.value
: this.quantityLabel,
presentation: data.presentation.present
? data.presentation.value
: this.presentation,
storageLocation: data.storageLocation.present
? data.storageLocation.value
: this.storageLocation,
@ -3204,6 +3249,7 @@ class Lot extends DataClass implements Insertable<Lot> {
..write('quantityKind: $quantityKind, ')
..write('quantityPrecise: $quantityPrecise, ')
..write('quantityLabel: $quantityLabel, ')
..write('presentation: $presentation, ')
..write('storageLocation: $storageLocation, ')
..write('offerStatus: $offerStatus, ')
..write('seedbankId: $seedbankId')
@ -3226,6 +3272,7 @@ class Lot extends DataClass implements Insertable<Lot> {
quantityKind,
quantityPrecise,
quantityLabel,
presentation,
storageLocation,
offerStatus,
seedbankId,
@ -3247,6 +3294,7 @@ class Lot extends DataClass implements Insertable<Lot> {
other.quantityKind == this.quantityKind &&
other.quantityPrecise == this.quantityPrecise &&
other.quantityLabel == this.quantityLabel &&
other.presentation == this.presentation &&
other.storageLocation == this.storageLocation &&
other.offerStatus == this.offerStatus &&
other.seedbankId == this.seedbankId);
@ -3266,6 +3314,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
final Value<String?> quantityKind;
final Value<double?> quantityPrecise;
final Value<String?> quantityLabel;
final Value<Presentation?> presentation;
final Value<String?> storageLocation;
final Value<OfferStatus> offerStatus;
final Value<String?> seedbankId;
@ -3284,6 +3333,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
this.quantityKind = const Value.absent(),
this.quantityPrecise = const Value.absent(),
this.quantityLabel = const Value.absent(),
this.presentation = const Value.absent(),
this.storageLocation = const Value.absent(),
this.offerStatus = const Value.absent(),
this.seedbankId = const Value.absent(),
@ -3303,6 +3353,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
this.quantityKind = const Value.absent(),
this.quantityPrecise = const Value.absent(),
this.quantityLabel = const Value.absent(),
this.presentation = const Value.absent(),
this.storageLocation = const Value.absent(),
this.offerStatus = const Value.absent(),
this.seedbankId = const Value.absent(),
@ -3326,6 +3377,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
Expression<String>? quantityKind,
Expression<double>? quantityPrecise,
Expression<String>? quantityLabel,
Expression<String>? presentation,
Expression<String>? storageLocation,
Expression<String>? offerStatus,
Expression<String>? seedbankId,
@ -3345,6 +3397,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
if (quantityKind != null) 'quantity_kind': quantityKind,
if (quantityPrecise != null) 'quantity_precise': quantityPrecise,
if (quantityLabel != null) 'quantity_label': quantityLabel,
if (presentation != null) 'presentation': presentation,
if (storageLocation != null) 'storage_location': storageLocation,
if (offerStatus != null) 'offer_status': offerStatus,
if (seedbankId != null) 'seedbank_id': seedbankId,
@ -3366,6 +3419,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
Value<String?>? quantityKind,
Value<double?>? quantityPrecise,
Value<String?>? quantityLabel,
Value<Presentation?>? presentation,
Value<String?>? storageLocation,
Value<OfferStatus>? offerStatus,
Value<String?>? seedbankId,
@ -3385,6 +3439,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
quantityKind: quantityKind ?? this.quantityKind,
quantityPrecise: quantityPrecise ?? this.quantityPrecise,
quantityLabel: quantityLabel ?? this.quantityLabel,
presentation: presentation ?? this.presentation,
storageLocation: storageLocation ?? this.storageLocation,
offerStatus: offerStatus ?? this.offerStatus,
seedbankId: seedbankId ?? this.seedbankId,
@ -3436,6 +3491,11 @@ class LotsCompanion extends UpdateCompanion<Lot> {
if (quantityLabel.present) {
map['quantity_label'] = Variable<String>(quantityLabel.value);
}
if (presentation.present) {
map['presentation'] = Variable<String>(
$LotsTable.$converterpresentationn.toSql(presentation.value),
);
}
if (storageLocation.present) {
map['storage_location'] = Variable<String>(storageLocation.value);
}
@ -3469,6 +3529,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
..write('quantityKind: $quantityKind, ')
..write('quantityPrecise: $quantityPrecise, ')
..write('quantityLabel: $quantityLabel, ')
..write('presentation: $presentation, ')
..write('storageLocation: $storageLocation, ')
..write('offerStatus: $offerStatus, ')
..write('seedbankId: $seedbankId, ')
@ -5752,6 +5813,18 @@ class $AttachmentsTable extends Attachments
type: DriftSqlType.string,
requiredDuringInsert: false,
);
static const VerificationMeta _sortOrderMeta = const VerificationMeta(
'sortOrder',
);
@override
late final GeneratedColumn<int> sortOrder = GeneratedColumn<int>(
'sort_order',
aliasedName,
false,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const Constant(0),
);
@override
List<GeneratedColumn> get $columns => [
id,
@ -5766,6 +5839,7 @@ class $AttachmentsTable extends Attachments
uri,
bytes,
mimeType,
sortOrder,
];
@override
String get aliasedName => _alias ?? actualTableName;
@ -5849,6 +5923,12 @@ class $AttachmentsTable extends Attachments
mimeType.isAcceptableOrUnknown(data['mime_type']!, _mimeTypeMeta),
);
}
if (data.containsKey('sort_order')) {
context.handle(
_sortOrderMeta,
sortOrder.isAcceptableOrUnknown(data['sort_order']!, _sortOrderMeta),
);
}
return context;
}
@ -5910,6 +5990,10 @@ class $AttachmentsTable extends Attachments
DriftSqlType.string,
data['${effectivePrefix}mime_type'],
),
sortOrder: attachedDatabase.typeMapping.read(
DriftSqlType.int,
data['${effectivePrefix}sort_order'],
)!,
);
}
@ -5937,6 +6021,12 @@ class Attachment extends DataClass implements Insertable<Attachment> {
final String? uri;
final Uint8List? bytes;
final String? mimeType;
/// Display order among sibling attachments (lower first). The lowest-ordered
/// photo is the "preferred"/cover used as the variety avatar and shown
/// first. New photos append at the end; "set as cover" moves one to the
/// front. A plain int (not a bool flag) so it generalizes to full reordering.
final int sortOrder;
const Attachment({
required this.id,
required this.createdAt,
@ -5950,6 +6040,7 @@ class Attachment extends DataClass implements Insertable<Attachment> {
this.uri,
this.bytes,
this.mimeType,
required this.sortOrder,
});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
@ -5980,6 +6071,7 @@ class Attachment extends DataClass implements Insertable<Attachment> {
if (!nullToAbsent || mimeType != null) {
map['mime_type'] = Variable<String>(mimeType);
}
map['sort_order'] = Variable<int>(sortOrder);
return map;
}
@ -6001,6 +6093,7 @@ class Attachment extends DataClass implements Insertable<Attachment> {
mimeType: mimeType == null && nullToAbsent
? const Value.absent()
: Value(mimeType),
sortOrder: Value(sortOrder),
);
}
@ -6026,6 +6119,7 @@ class Attachment extends DataClass implements Insertable<Attachment> {
uri: serializer.fromJson<String?>(json['uri']),
bytes: serializer.fromJson<Uint8List?>(json['bytes']),
mimeType: serializer.fromJson<String?>(json['mimeType']),
sortOrder: serializer.fromJson<int>(json['sortOrder']),
);
}
@override
@ -6048,6 +6142,7 @@ class Attachment extends DataClass implements Insertable<Attachment> {
'uri': serializer.toJson<String?>(uri),
'bytes': serializer.toJson<Uint8List?>(bytes),
'mimeType': serializer.toJson<String?>(mimeType),
'sortOrder': serializer.toJson<int>(sortOrder),
};
}
@ -6064,6 +6159,7 @@ class Attachment extends DataClass implements Insertable<Attachment> {
Value<String?> uri = const Value.absent(),
Value<Uint8List?> bytes = const Value.absent(),
Value<String?> mimeType = const Value.absent(),
int? sortOrder,
}) => Attachment(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
@ -6077,6 +6173,7 @@ class Attachment extends DataClass implements Insertable<Attachment> {
uri: uri.present ? uri.value : this.uri,
bytes: bytes.present ? bytes.value : this.bytes,
mimeType: mimeType.present ? mimeType.value : this.mimeType,
sortOrder: sortOrder ?? this.sortOrder,
);
Attachment copyWithCompanion(AttachmentsCompanion data) {
return Attachment(
@ -6098,6 +6195,7 @@ class Attachment extends DataClass implements Insertable<Attachment> {
uri: data.uri.present ? data.uri.value : this.uri,
bytes: data.bytes.present ? data.bytes.value : this.bytes,
mimeType: data.mimeType.present ? data.mimeType.value : this.mimeType,
sortOrder: data.sortOrder.present ? data.sortOrder.value : this.sortOrder,
);
}
@ -6115,7 +6213,8 @@ class Attachment extends DataClass implements Insertable<Attachment> {
..write('kind: $kind, ')
..write('uri: $uri, ')
..write('bytes: $bytes, ')
..write('mimeType: $mimeType')
..write('mimeType: $mimeType, ')
..write('sortOrder: $sortOrder')
..write(')'))
.toString();
}
@ -6134,6 +6233,7 @@ class Attachment extends DataClass implements Insertable<Attachment> {
uri,
$driftBlobEquality.hash(bytes),
mimeType,
sortOrder,
);
@override
bool operator ==(Object other) =>
@ -6150,7 +6250,8 @@ class Attachment extends DataClass implements Insertable<Attachment> {
other.kind == this.kind &&
other.uri == this.uri &&
$driftBlobEquality.equals(other.bytes, this.bytes) &&
other.mimeType == this.mimeType);
other.mimeType == this.mimeType &&
other.sortOrder == this.sortOrder);
}
class AttachmentsCompanion extends UpdateCompanion<Attachment> {
@ -6166,6 +6267,7 @@ class AttachmentsCompanion extends UpdateCompanion<Attachment> {
final Value<String?> uri;
final Value<Uint8List?> bytes;
final Value<String?> mimeType;
final Value<int> sortOrder;
final Value<int> rowid;
const AttachmentsCompanion({
this.id = const Value.absent(),
@ -6180,6 +6282,7 @@ class AttachmentsCompanion extends UpdateCompanion<Attachment> {
this.uri = const Value.absent(),
this.bytes = const Value.absent(),
this.mimeType = const Value.absent(),
this.sortOrder = const Value.absent(),
this.rowid = const Value.absent(),
});
AttachmentsCompanion.insert({
@ -6195,6 +6298,7 @@ class AttachmentsCompanion extends UpdateCompanion<Attachment> {
this.uri = const Value.absent(),
this.bytes = const Value.absent(),
this.mimeType = const Value.absent(),
this.sortOrder = const Value.absent(),
this.rowid = const Value.absent(),
}) : id = Value(id),
createdAt = Value(createdAt),
@ -6216,6 +6320,7 @@ class AttachmentsCompanion extends UpdateCompanion<Attachment> {
Expression<String>? uri,
Expression<Uint8List>? bytes,
Expression<String>? mimeType,
Expression<int>? sortOrder,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
@ -6231,6 +6336,7 @@ class AttachmentsCompanion extends UpdateCompanion<Attachment> {
if (uri != null) 'uri': uri,
if (bytes != null) 'bytes': bytes,
if (mimeType != null) 'mime_type': mimeType,
if (sortOrder != null) 'sort_order': sortOrder,
if (rowid != null) 'rowid': rowid,
});
}
@ -6248,6 +6354,7 @@ class AttachmentsCompanion extends UpdateCompanion<Attachment> {
Value<String?>? uri,
Value<Uint8List?>? bytes,
Value<String?>? mimeType,
Value<int>? sortOrder,
Value<int>? rowid,
}) {
return AttachmentsCompanion(
@ -6263,6 +6370,7 @@ class AttachmentsCompanion extends UpdateCompanion<Attachment> {
uri: uri ?? this.uri,
bytes: bytes ?? this.bytes,
mimeType: mimeType ?? this.mimeType,
sortOrder: sortOrder ?? this.sortOrder,
rowid: rowid ?? this.rowid,
);
}
@ -6310,6 +6418,9 @@ class AttachmentsCompanion extends UpdateCompanion<Attachment> {
if (mimeType.present) {
map['mime_type'] = Variable<String>(mimeType.value);
}
if (sortOrder.present) {
map['sort_order'] = Variable<int>(sortOrder.value);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
@ -6331,6 +6442,7 @@ class AttachmentsCompanion extends UpdateCompanion<Attachment> {
..write('uri: $uri, ')
..write('bytes: $bytes, ')
..write('mimeType: $mimeType, ')
..write('sortOrder: $sortOrder, ')
..write('rowid: $rowid')
..write(')'))
.toString();
@ -8246,6 +8358,7 @@ typedef $$LotsTableCreateCompanionBuilder =
Value<String?> quantityKind,
Value<double?> quantityPrecise,
Value<String?> quantityLabel,
Value<Presentation?> presentation,
Value<String?> storageLocation,
Value<OfferStatus> offerStatus,
Value<String?> seedbankId,
@ -8266,6 +8379,7 @@ typedef $$LotsTableUpdateCompanionBuilder =
Value<String?> quantityKind,
Value<double?> quantityPrecise,
Value<String?> quantityLabel,
Value<Presentation?> presentation,
Value<String?> storageLocation,
Value<OfferStatus> offerStatus,
Value<String?> seedbankId,
@ -8346,6 +8460,12 @@ class $$LotsTableFilterComposer extends Composer<_$AppDatabase, $LotsTable> {
builder: (column) => ColumnFilters(column),
);
ColumnWithTypeConverterFilters<Presentation?, Presentation, String>
get presentation => $composableBuilder(
column: $table.presentation,
builder: (column) => ColumnWithTypeConverterFilters(column),
);
ColumnFilters<String> get storageLocation => $composableBuilder(
column: $table.storageLocation,
builder: (column) => ColumnFilters(column),
@ -8436,6 +8556,11 @@ class $$LotsTableOrderingComposer extends Composer<_$AppDatabase, $LotsTable> {
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get presentation => $composableBuilder(
column: $table.presentation,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get storageLocation => $composableBuilder(
column: $table.storageLocation,
builder: (column) => ColumnOrderings(column),
@ -8514,6 +8639,12 @@ class $$LotsTableAnnotationComposer
builder: (column) => column,
);
GeneratedColumnWithTypeConverter<Presentation?, String> get presentation =>
$composableBuilder(
column: $table.presentation,
builder: (column) => column,
);
GeneratedColumn<String> get storageLocation => $composableBuilder(
column: $table.storageLocation,
builder: (column) => column,
@ -8572,6 +8703,7 @@ class $$LotsTableTableManager
Value<String?> quantityKind = const Value.absent(),
Value<double?> quantityPrecise = const Value.absent(),
Value<String?> quantityLabel = const Value.absent(),
Value<Presentation?> presentation = const Value.absent(),
Value<String?> storageLocation = const Value.absent(),
Value<OfferStatus> offerStatus = const Value.absent(),
Value<String?> seedbankId = const Value.absent(),
@ -8590,6 +8722,7 @@ class $$LotsTableTableManager
quantityKind: quantityKind,
quantityPrecise: quantityPrecise,
quantityLabel: quantityLabel,
presentation: presentation,
storageLocation: storageLocation,
offerStatus: offerStatus,
seedbankId: seedbankId,
@ -8610,6 +8743,7 @@ class $$LotsTableTableManager
Value<String?> quantityKind = const Value.absent(),
Value<double?> quantityPrecise = const Value.absent(),
Value<String?> quantityLabel = const Value.absent(),
Value<Presentation?> presentation = const Value.absent(),
Value<String?> storageLocation = const Value.absent(),
Value<OfferStatus> offerStatus = const Value.absent(),
Value<String?> seedbankId = const Value.absent(),
@ -8628,6 +8762,7 @@ class $$LotsTableTableManager
quantityKind: quantityKind,
quantityPrecise: quantityPrecise,
quantityLabel: quantityLabel,
presentation: presentation,
storageLocation: storageLocation,
offerStatus: offerStatus,
seedbankId: seedbankId,
@ -9677,6 +9812,7 @@ typedef $$AttachmentsTableCreateCompanionBuilder =
Value<String?> uri,
Value<Uint8List?> bytes,
Value<String?> mimeType,
Value<int> sortOrder,
Value<int> rowid,
});
typedef $$AttachmentsTableUpdateCompanionBuilder =
@ -9693,6 +9829,7 @@ typedef $$AttachmentsTableUpdateCompanionBuilder =
Value<String?> uri,
Value<Uint8List?> bytes,
Value<String?> mimeType,
Value<int> sortOrder,
Value<int> rowid,
});
@ -9766,6 +9903,11 @@ class $$AttachmentsTableFilterComposer
column: $table.mimeType,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<int> get sortOrder => $composableBuilder(
column: $table.sortOrder,
builder: (column) => ColumnFilters(column),
);
}
class $$AttachmentsTableOrderingComposer
@ -9836,6 +9978,11 @@ class $$AttachmentsTableOrderingComposer
column: $table.mimeType,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<int> get sortOrder => $composableBuilder(
column: $table.sortOrder,
builder: (column) => ColumnOrderings(column),
);
}
class $$AttachmentsTableAnnotationComposer
@ -9889,6 +10036,9 @@ class $$AttachmentsTableAnnotationComposer
GeneratedColumn<String> get mimeType =>
$composableBuilder(column: $table.mimeType, builder: (column) => column);
GeneratedColumn<int> get sortOrder =>
$composableBuilder(column: $table.sortOrder, builder: (column) => column);
}
class $$AttachmentsTableTableManager
@ -9934,6 +10084,7 @@ class $$AttachmentsTableTableManager
Value<String?> uri = const Value.absent(),
Value<Uint8List?> bytes = const Value.absent(),
Value<String?> mimeType = const Value.absent(),
Value<int> sortOrder = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) => AttachmentsCompanion(
id: id,
@ -9948,6 +10099,7 @@ class $$AttachmentsTableTableManager
uri: uri,
bytes: bytes,
mimeType: mimeType,
sortOrder: sortOrder,
rowid: rowid,
),
createCompanionCallback:
@ -9964,6 +10116,7 @@ class $$AttachmentsTableTableManager
Value<String?> uri = const Value.absent(),
Value<Uint8List?> bytes = const Value.absent(),
Value<String?> mimeType = const Value.absent(),
Value<int> sortOrder = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) => AttachmentsCompanion.insert(
id: id,
@ -9978,6 +10131,7 @@ class $$AttachmentsTableTableManager
uri: uri,
bytes: bytes,
mimeType: mimeType,
sortOrder: sortOrder,
rowid: rowid,
),
withReferenceMapper: (p0) => p0

View file

@ -3,9 +3,26 @@
// may only be appended, never renumbered or reused; when sync arrives, readers
// must tolerate unknown values (map to a safe default). No sync yet in Block 1.
/// Whether a lot holds seeds or living plants/seedlings (plantel). Germination
/// tests apply only to seed lots.
enum LotType { seed, plant }
/// The form in which a lot is held. `seed` is dry seed (the only form
/// germination tests apply to); the rest are living/vegetative material. Values
/// are stored by name and append-only (data-model §5): never renamed/reordered.
///
/// - `seedling` a young plant grown from seed, to transplant (plantón).
/// - `plant` an established plant (potted or bare-root).
/// - `tree` a woody plant: tree or shrub.
/// - `bulb` a storage organ: bulb, corm, tuber or rhizome.
/// - `cutting` vegetative propagation material (cutting, scion).
enum LotType { seed, plant, seedling, tree, bulb, cutting }
/// How living plant material is packaged/supplied an optional attribute of a
/// non-seed [Lot], kept separate from the amount. Append-only by name.
///
/// - `pot` in a pot/container (maceta/contenedor).
/// - `tray` in a plug/cell tray (bandeja de alvéolos).
/// - `plug` a single plug/cell.
/// - `bareRoot` dormant, roots exposed, no soil (raíz desnuda).
/// - `rootBall` dug with an intact soil ball (cepellón).
enum Presentation { pot, tray, plug, bareRoot, rootBall }
/// Per-lot visibility (data-model §2.3). Used by the future sharing layer.
enum OfferStatus { private, shared, exchange, sell }

View file

@ -50,6 +50,8 @@ class Lots extends Table with SyncColumns {
TextColumn get quantityKind => text().nullable()(); // QuantityKind.name
RealColumn get quantityPrecise => real().nullable()();
TextColumn get quantityLabel => text().nullable()();
// How living (non-seed) material is packaged; null for seed lots or unset.
TextColumn get presentation => textEnum<Presentation>().nullable()();
TextColumn get storageLocation => text().nullable()();
TextColumn get offerStatus =>
textEnum<OfferStatus>().withDefault(const Constant('private'))();
@ -99,6 +101,12 @@ class Attachments extends Table with SyncColumns {
TextColumn get uri => text().nullable()();
BlobColumn get bytes => blob().nullable()();
TextColumn get mimeType => text().nullable()();
/// Display order among sibling attachments (lower first). The lowest-ordered
/// photo is the "preferred"/cover used as the variety avatar and shown
/// first. New photos append at the end; "set as cover" moves one to the
/// front. A plain int (not a bool flag) so it generalizes to full reordering.
IntColumn get sortOrder => integer().withDefault(const Constant(0))();
}
/// Any pasted URL (Wikipedia, forum). Polymorphic parent.

View file

@ -57,12 +57,14 @@ class VarietyDetailCubit extends Cubit<VarietyDetailState> {
int? year,
int? month,
Quantity? quantity,
Presentation? presentation,
}) => _repo.addLot(
varietyId: varietyId,
type: type,
harvestYear: year,
harvestMonth: month,
quantity: quantity,
presentation: presentation,
);
Future<void> updateLot({
@ -71,12 +73,14 @@ class VarietyDetailCubit extends Cubit<VarietyDetailState> {
int? year,
int? month,
Quantity? quantity,
Presentation? presentation,
}) => _repo.updateLot(
lotId: lotId,
type: type,
harvestYear: year,
harvestMonth: month,
quantity: quantity,
presentation: presentation,
);
Future<void> deleteLot(String lotId) => _repo.softDeleteLot(lotId);
@ -92,6 +96,9 @@ class VarietyDetailCubit extends Cubit<VarietyDetailState> {
Future<void> removePhoto(String attachmentId) =>
_repo.removePhoto(attachmentId);
Future<void> setPreferredPhoto(String attachmentId) =>
_repo.setPreferredPhoto(varietyId, attachmentId);
Future<void> addExternalLink(String url, {String? title}) =>
_repo.addExternalLink(varietyId, url, title: title);

View file

@ -201,6 +201,43 @@ void main() {
]);
});
test('setPreferredPhoto moves a photo to the front (cover)', () async {
final id = await repo.addQuickVariety(label: 'Maize');
await repo.addPhoto(id, Uint8List.fromList([1]));
await repo.addPhoto(id, Uint8List.fromList([2]));
final p3 = await repo.addPhoto(id, Uint8List.fromList([3]));
// Insertion order until a cover is chosen.
expect(
(await repo.watchVariety(id).first)!.photos.map((p) => p.bytes.toList()),
[
[1],
[2],
[3],
],
);
await repo.setPreferredPhoto(id, p3);
final photos = (await repo.watchVariety(id).first)!.photos;
expect(photos.map((p) => p.bytes.toList()), [
[3],
[1],
[2],
]);
});
test('setPreferredPhoto changes the inventory avatar to the cover', () async {
final id = await repo.addQuickVariety(label: 'Maize');
await repo.addPhoto(id, Uint8List.fromList([1]));
final p2 = await repo.addPhoto(id, Uint8List.fromList([2]));
expect((await repo.watchInventory().first).single.photo!.toList(), [1]);
await repo.setPreferredPhoto(id, p2);
expect((await repo.watchInventory().first).single.photo!.toList(), [2]);
});
test('watchInventory avatar refreshes when a photo is added later', () async {
final id = await repo.addQuickVariety(label: 'Maize');
final queue = StreamQueue(repo.watchInventory());

View file

@ -11,28 +11,40 @@ void main() {
verifier = SchemaVerifier(GeneratedHelper());
});
test('freshly created database matches the exported schema v3', () async {
final schema = await verifier.schemaAt(3);
test('freshly created database matches the exported schema v5', () async {
final schema = await verifier.schemaAt(5);
final db = AppDatabase(schema.newConnection());
await verifier.migrateAndValidate(db, 3);
await verifier.migrateAndValidate(db, 5);
await db.close();
});
test('upgrades v1 → v3 (adds Lot.type, then harvestMonth) and matches the '
'fresh schema', () async {
test('upgrades v1 → v5 (Lot.type, harvestMonth, presentation, '
'Attachment.sortOrder) and matches the fresh schema', () async {
final connection = await verifier.startAt(1);
final db = AppDatabase(connection);
await verifier.migrateAndValidate(db, 3);
await verifier.migrateAndValidate(db, 5);
await db.close();
});
test(
'upgrades v2 → v3 (adds Lot.harvestMonth) and matches the fresh schema',
() async {
final connection = await verifier.startAt(2);
final db = AppDatabase(connection);
await verifier.migrateAndValidate(db, 3);
await db.close();
},
);
test('upgrades v2 → v5 and matches the fresh schema', () async {
final connection = await verifier.startAt(2);
final db = AppDatabase(connection);
await verifier.migrateAndValidate(db, 5);
await db.close();
});
test('upgrades v3 → v5 and matches the fresh schema', () async {
final connection = await verifier.startAt(3);
final db = AppDatabase(connection);
await verifier.migrateAndValidate(db, 5);
await db.close();
});
test('upgrades v4 → v5 (adds Attachment.sortOrder) and matches the fresh '
'schema', () async {
final connection = await verifier.startAt(4);
final db = AppDatabase(connection);
await verifier.migrateAndValidate(db, 5);
await db.close();
});
}

View file

@ -7,6 +7,8 @@ import 'package:drift/internal/migrations.dart';
import 'schema_v1.dart' as v1;
import 'schema_v2.dart' as v2;
import 'schema_v3.dart' as v3;
import 'schema_v4.dart' as v4;
import 'schema_v5.dart' as v5;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
@ -18,10 +20,14 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v2.DatabaseAtV2(db);
case 3:
return v3.DatabaseAtV3(db);
case 4:
return v4.DatabaseAtV4(db);
case 5:
return v5.DatabaseAtV5(db);
default:
throw MissingSchemaException(version, versions);
}
}
static const versions = const [1, 2, 3];
static const versions = const [1, 2, 3, 4, 5];
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff