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:
parent
06aed2a586
commit
3954c62aa6
16 changed files with 6174 additions and 32 deletions
1495
apps/app_seeds/drift_schemas/drift_schema_v4.json
Normal file
1495
apps/app_seeds/drift_schemas/drift_schema_v4.json
Normal file
File diff suppressed because it is too large
Load diff
1505
apps/app_seeds/drift_schemas/drift_schema_v5.json
Normal file
1505
apps/app_seeds/drift_schemas/drift_schema_v5.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
1408
apps/app_seeds/test/db/schema/schema_v4.dart
Normal file
1408
apps/app_seeds/test/db/schema/schema_v4.dart
Normal file
File diff suppressed because it is too large
Load diff
1418
apps/app_seeds/test/db/schema/schema_v5.dart
Normal file
1418
apps/app_seeds/test/db/schema/schema_v5.dart
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -71,8 +71,10 @@ Zero or more per `Variety`, each with its **own year and its own unit** — and
|
|||
| `id` | UUID | |
|
||||
| `variety_id` | UUID → `Variety` | |
|
||||
| `harvest_year` | INTEGER | Nullable. **This is where "year" lives** (germination decays with lot age). |
|
||||
| `type` | enum `LotType` | The **form** held — see §2.3.2. |
|
||||
| `quantity_label` | TEXT + `quantity_kind` | Qualitative by default. **Not only kitchen units** — see §2.3.1. |
|
||||
| `quantity_precise` | REAL + `quantity_unit` | Optional precise amount (grams / seed count). |
|
||||
| `presentation` | enum `Presentation` | Nullable. How living material is packaged — see §2.3.2. |
|
||||
| `storage_location` | TEXT | Nullable. Free text with suggestions ("fridge", "village bank"). |
|
||||
| `offer_status` | enum | `private` / `shared` / `exchange` / `sell` — the visibility from PLAN §3 Layer 2. Per lot. |
|
||||
| `seedbank_id` | UUID → `SeedBank` | Nullable. If this lot belongs to a collective bank. |
|
||||
|
|
@ -90,6 +92,19 @@ Quantity is deliberately *rough and human*, not precise. But the informal vocabu
|
|||
|
||||
Modelling: store `quantity_kind` (a stable enum key like `pod`, `cob`, `head`, `packet`, `handful`, `grams`, `count`) + an optional numeric `quantity_precise` (and a free-text `quantity_label` for a rough amount when no key fits). The **display label is localized** from the key, so "pod"→"vaina"/"beina"… Units are *labels for a rough amount*, generally **not convertible** between each other or to grams — and that's fine. Suggestion logic (e.g. Poaceae → ear/cob, Fabaceae → pod, Helianthus → flower head) lives in the catalog mapping, not hardcoded, so it stays extensible and translatable. **To think through further:** the full starter list per family, and whether users can add their own unit keys (probably yes, as free `quantity_label` text when no key fits).
|
||||
|
||||
#### 2.3.2 Lot form (`LotType`) and packaging (`Presentation`) — design note
|
||||
|
||||
A lot isn't always dry seed. Reviewing the vocabulary of real seed shops and nurseries (viveros) — where "plantel" is only the *seedbed/batch*, not a unit — the **form** a lot is held in is a small, append-only enum `LotType` (stored by name, data-model §5):
|
||||
|
||||
- `seed` — dry seed. **The only form germination tests apply to.**
|
||||
- `seedling` — a young plant grown from seed, to transplant (*plantón* / plug).
|
||||
- `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).
|
||||
|
||||
**Form drives the offered units**, and only `seed` gets the rich plant-form scale of §2.3.1; living forms are counted as **whole individuals** (`seedling`, `plant`, `tree`, `cutting`; `bulb` also allows grams). The *packaging* is deliberately a **separate optional attribute** `Presentation` (`pot`, `tray`, `plug`, `bareRoot`, `rootBall`) so "how many" is never conflated with "in what format" — that conflation was the original flaw (the old `pot`/`tray` "amounts", now retained only for old rows). Presentation is offered only for `seedling`/`plant`/`tree`. **Left open:** grafts/divisions/forestry seedlings (nicho) fold into `cutting`/`plant` for now, or a free note; revisit if real exchanges demand their own key.
|
||||
|
||||
### 2.4 `Movement` — the append-only event log
|
||||
Immutable events on a `Lot`. Entries/exits are just types. This *is* the history tab and the provenance chain.
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ Estas fijan `schemaVersion = 1` y el arranque técnico:
|
|||
- **Cifrado en reposo (el "cómo" técnico):** SQLCipher + llave aleatoria en el almacén del sistema + QR de recuperación. Conceptualmente decidido; falta bajar el mecanismo. → [security-privacy.md](security-privacy.md)
|
||||
- **Identidad → ✅ una sola clave** raíz (Duniter/Ğ1), de la que se deriva la secp256k1 para Nostr. La clave simétrica que cifra la BD en reposo es un **detalle interno aparte** (no es una identidad), así que no contradice el "una sola identidad". → [g1-integration.md](g1-integration.md), [security-privacy.md](security-privacy.md)
|
||||
- **Detalles de modelo que fijan el esquema → ✅** `Quantity` como **tipo compartido** (Lot y Movement); `offer_status` **solo en Lot**; `category` **texto libre** prerrellenado desde `Species.family`. → [data-model.md](data-model.md) §6
|
||||
- **Forma del lote (`LotType`) y envase (`Presentation`) → ✅** (schema v4). `LotType` pasa de `{seed, plant}` a **6 formas** append-only: `seed, seedling(plantón), plant, tree, bulb, cutting` — tras repasar el vocabulario de viveros/tiendas ("plantel" era mal nombre: es el semillero, no una unidad). El **envase** (`pot/tray/plug/bareRoot/rootBall`) es un **atributo opcional aparte**, no una cantidad, para no mezclar "cuántas" con "en qué formato". Germinación sigue siendo solo-`seed`. → [data-model.md](data-model.md) §2.3.2
|
||||
- **Licencia** del código → ✅ **AGPL-3.0** (GPLv3 + cláusula de red; compatible con GPLv3 y con Duniter/Ğ1nkgo). Falta añadir el fichero `LICENSE` al abrir el repo.
|
||||
- **Varilla v1 → ✅ viable** con datos abiertos: nombres/identificación (Wikidata+GBIF, fácil), **conservación desde Kew SID** (dominio público, ~52k taxones), cuidados enlazados de Permapeople. Falta: confirmar acceso/descarga de Kew SID y el subconjunto curado a empaquetar. → [data-notes.md](data-notes.md) §3-ter
|
||||
- **Estructura del workspace** (`commons_core` + `app_seeds`, pub workspaces) — decidido; falta materializarlo.
|
||||
|
|
|
|||
|
|
@ -33,10 +33,15 @@ enum QuantityKind {
|
|||
tuber(QuantityGroup.seedForm, countable: true),
|
||||
seedHead(QuantityGroup.seedForm, countable: true),
|
||||
bunch(QuantityGroup.seedForm, countable: true),
|
||||
// Seedlings / plants (for a plant lot).
|
||||
// Living material counted as whole individuals (seedling / plant / tree /
|
||||
// bulb / cutting lots). [pot] and [tray] are retained for old rows but are no
|
||||
// longer offered as amounts — packaging now lives in [Presentation] instead.
|
||||
plant(QuantityGroup.plant, countable: true),
|
||||
pot(QuantityGroup.plant, countable: true),
|
||||
tray(QuantityGroup.plant, countable: true),
|
||||
seedling(QuantityGroup.plant, countable: true),
|
||||
tree(QuantityGroup.plant, countable: true),
|
||||
cutting(QuantityGroup.plant, countable: true),
|
||||
// Precise (optional).
|
||||
grams(QuantityGroup.precise, countable: true),
|
||||
count(QuantityGroup.precise, countable: true);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue