feat(inventory): month/year harvest date picker (schema v3)
Replace the free-text harvest year with a themed month/year picker: a tappable field opens a green-styled dialog with month and year grids (navigable, paginated years), month optional. Lots now read e.g. "September 2021" or just "Year 2021". - schema v3: add nullable Lot.harvest_month (1..12) + migration + tests - repo/cubit propagate harvestMonth; localized month names (en/es) via slang
This commit is contained in:
parent
9e5c82184b
commit
5d4053157f
17 changed files with 3799 additions and 57 deletions
1481
apps/app_seeds/drift_schemas/drift_schema_v3.json
Normal file
1481
apps/app_seeds/drift_schemas/drift_schema_v3.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -65,6 +65,7 @@ class VarietyLot extends Equatable {
|
||||||
required this.id,
|
required this.id,
|
||||||
this.type = LotType.seed,
|
this.type = LotType.seed,
|
||||||
this.harvestYear,
|
this.harvestYear,
|
||||||
|
this.harvestMonth,
|
||||||
this.quantity,
|
this.quantity,
|
||||||
this.storageLocation,
|
this.storageLocation,
|
||||||
this.germinationTests = const [],
|
this.germinationTests = const [],
|
||||||
|
|
@ -73,6 +74,9 @@ class VarietyLot extends Equatable {
|
||||||
final String id;
|
final String id;
|
||||||
final LotType type;
|
final LotType type;
|
||||||
final int? harvestYear;
|
final int? harvestYear;
|
||||||
|
|
||||||
|
/// Optional harvest month (1..12). Only meaningful when [harvestYear] is set.
|
||||||
|
final int? harvestMonth;
|
||||||
final Quantity? quantity;
|
final Quantity? quantity;
|
||||||
final String? storageLocation;
|
final String? storageLocation;
|
||||||
final List<GerminationEntry> germinationTests;
|
final List<GerminationEntry> germinationTests;
|
||||||
|
|
@ -86,6 +90,7 @@ class VarietyLot extends Equatable {
|
||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
harvestYear,
|
harvestYear,
|
||||||
|
harvestMonth,
|
||||||
quantity,
|
quantity,
|
||||||
storageLocation,
|
storageLocation,
|
||||||
germinationTests,
|
germinationTests,
|
||||||
|
|
@ -110,7 +115,18 @@ class VernacularName extends Equatable {
|
||||||
List<Object?> get props => [id, name, language, region];
|
List<Object?> get props => [id, name, language, region];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The full detail of one variety (identity + its lots, names and first photo).
|
/// One stored photo of a variety (encrypted BLOB in the DB).
|
||||||
|
class VarietyPhoto extends Equatable {
|
||||||
|
const VarietyPhoto({required this.id, required this.bytes});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final Uint8List bytes;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [id, bytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The full detail of one variety (identity + its lots, names and photos).
|
||||||
class VarietyDetail extends Equatable {
|
class VarietyDetail extends Equatable {
|
||||||
const VarietyDetail({
|
const VarietyDetail({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
|
@ -121,7 +137,7 @@ class VarietyDetail extends Equatable {
|
||||||
this.scientificName,
|
this.scientificName,
|
||||||
this.lots = const [],
|
this.lots = const [],
|
||||||
this.vernacularNames = const [],
|
this.vernacularNames = const [],
|
||||||
this.photo,
|
this.photos = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
|
|
@ -132,7 +148,7 @@ class VarietyDetail extends Equatable {
|
||||||
final String? scientificName;
|
final String? scientificName;
|
||||||
final List<VarietyLot> lots;
|
final List<VarietyLot> lots;
|
||||||
final List<VernacularName> vernacularNames;
|
final List<VernacularName> vernacularNames;
|
||||||
final Uint8List? photo;
|
final List<VarietyPhoto> photos;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
|
|
@ -144,7 +160,7 @@ class VarietyDetail extends Equatable {
|
||||||
scientificName,
|
scientificName,
|
||||||
lots,
|
lots,
|
||||||
vernacularNames,
|
vernacularNames,
|
||||||
photo,
|
photos,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -380,7 +396,7 @@ class VarietyRepository {
|
||||||
a.kind.equalsValue(AttachmentKind.photo) &
|
a.kind.equalsValue(AttachmentKind.photo) &
|
||||||
a.isDeleted.equals(false),
|
a.isDeleted.equals(false),
|
||||||
)
|
)
|
||||||
..limit(1))
|
..orderBy([(a) => OrderingTerm(expression: a.createdAt)]))
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
return VarietyDetail(
|
return VarietyDetail(
|
||||||
|
|
@ -401,7 +417,10 @@ class VarietyRepository {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
photo: photos.isEmpty ? null : photos.first.bytes,
|
photos: [
|
||||||
|
for (final p in photos)
|
||||||
|
if (p.bytes != null) VarietyPhoto(id: p.id, bytes: p.bytes!),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -457,6 +476,7 @@ class VarietyRepository {
|
||||||
required String varietyId,
|
required String varietyId,
|
||||||
LotType type = LotType.seed,
|
LotType type = LotType.seed,
|
||||||
int? harvestYear,
|
int? harvestYear,
|
||||||
|
int? harvestMonth,
|
||||||
Quantity? quantity,
|
Quantity? quantity,
|
||||||
String? storageLocation,
|
String? storageLocation,
|
||||||
}) async {
|
}) async {
|
||||||
|
|
@ -473,6 +493,7 @@ class VarietyRepository {
|
||||||
lastAuthor: nodeId,
|
lastAuthor: nodeId,
|
||||||
type: Value(type),
|
type: Value(type),
|
||||||
harvestYear: Value(harvestYear),
|
harvestYear: Value(harvestYear),
|
||||||
|
harvestMonth: Value(harvestMonth),
|
||||||
quantityKind: Value(quantity?.kind.name),
|
quantityKind: Value(quantity?.kind.name),
|
||||||
quantityPrecise: Value(quantity?.count?.toDouble()),
|
quantityPrecise: Value(quantity?.count?.toDouble()),
|
||||||
quantityLabel: Value(quantity?.label),
|
quantityLabel: Value(quantity?.label),
|
||||||
|
|
@ -482,12 +503,13 @@ class VarietyRepository {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates an existing lot's fields (LWW). The [quantity] and [harvestYear]
|
/// Updates an existing lot's fields (LWW). The [quantity], [harvestYear] and
|
||||||
/// are set as given (null clears them).
|
/// [harvestMonth] are set as given (null clears them).
|
||||||
Future<void> updateLot({
|
Future<void> updateLot({
|
||||||
required String lotId,
|
required String lotId,
|
||||||
required LotType type,
|
required LotType type,
|
||||||
int? harvestYear,
|
int? harvestYear,
|
||||||
|
int? harvestMonth,
|
||||||
Quantity? quantity,
|
Quantity? quantity,
|
||||||
String? storageLocation,
|
String? storageLocation,
|
||||||
}) async {
|
}) async {
|
||||||
|
|
@ -496,6 +518,7 @@ class VarietyRepository {
|
||||||
LotsCompanion(
|
LotsCompanion(
|
||||||
type: Value(type),
|
type: Value(type),
|
||||||
harvestYear: Value(harvestYear),
|
harvestYear: Value(harvestYear),
|
||||||
|
harvestMonth: Value(harvestMonth),
|
||||||
quantityKind: Value(quantity?.kind.name),
|
quantityKind: Value(quantity?.kind.name),
|
||||||
quantityPrecise: Value(quantity?.count?.toDouble()),
|
quantityPrecise: Value(quantity?.count?.toDouble()),
|
||||||
quantityLabel: Value(quantity?.label),
|
quantityLabel: Value(quantity?.label),
|
||||||
|
|
@ -558,6 +581,42 @@ class VarietyRepository {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds a photo (encrypted BLOB) to a variety. Returns the new attachment id.
|
||||||
|
Future<String> addPhoto(String varietyId, Uint8List bytes) async {
|
||||||
|
final (created, updated) = _stamp();
|
||||||
|
final id = idGen.newId();
|
||||||
|
await _db
|
||||||
|
.into(_db.attachments)
|
||||||
|
.insert(
|
||||||
|
AttachmentsCompanion.insert(
|
||||||
|
id: id,
|
||||||
|
createdAt: created,
|
||||||
|
updatedAt: updated,
|
||||||
|
lastAuthor: nodeId,
|
||||||
|
parentType: ParentType.variety,
|
||||||
|
parentId: varietyId,
|
||||||
|
kind: AttachmentKind.photo,
|
||||||
|
bytes: Value(bytes),
|
||||||
|
mimeType: const Value('image/jpeg'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Soft-deletes a photo (tombstone).
|
||||||
|
Future<void> removePhoto(String attachmentId) async {
|
||||||
|
final (_, updated) = _stamp();
|
||||||
|
await (_db.update(
|
||||||
|
_db.attachments,
|
||||||
|
)..where((a) => a.id.equals(attachmentId))).write(
|
||||||
|
AttachmentsCompanion(
|
||||||
|
isDeleted: const Value(true),
|
||||||
|
updatedAt: Value(updated),
|
||||||
|
lastAuthor: Value(nodeId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Soft-deletes a variety (tombstone); it disappears from the inventory but
|
/// Soft-deletes a variety (tombstone); it disappears from the inventory but
|
||||||
/// the row survives for correct CRDT merges later.
|
/// the row survives for correct CRDT merges later.
|
||||||
Future<void> softDeleteVariety(String id) async {
|
Future<void> softDeleteVariety(String id) async {
|
||||||
|
|
@ -608,6 +667,7 @@ class VarietyRepository {
|
||||||
id: l.id,
|
id: l.id,
|
||||||
type: l.type,
|
type: l.type,
|
||||||
harvestYear: l.harvestYear,
|
harvestYear: l.harvestYear,
|
||||||
|
harvestMonth: l.harvestMonth,
|
||||||
storageLocation: l.storageLocation,
|
storageLocation: l.storageLocation,
|
||||||
germinationTests: germinationTests,
|
germinationTests: germinationTests,
|
||||||
quantity: hasQuantity
|
quantity: hasQuantity
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class AppDatabase extends _$AppDatabase {
|
||||||
AppDatabase(super.e);
|
AppDatabase(super.e);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 2;
|
int get schemaVersion => 3;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
|
|
@ -37,6 +37,10 @@ class AppDatabase extends _$AppDatabase {
|
||||||
if (from < 2) {
|
if (from < 2) {
|
||||||
await m.addColumn(lots, lots.type);
|
await m.addColumn(lots, lots.type);
|
||||||
}
|
}
|
||||||
|
// v3: optional harvest month alongside the harvest year.
|
||||||
|
if (from < 3) {
|
||||||
|
await m.addColumn(lots, lots.harvestMonth);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2631,6 +2631,17 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> {
|
||||||
type: DriftSqlType.int,
|
type: DriftSqlType.int,
|
||||||
requiredDuringInsert: false,
|
requiredDuringInsert: false,
|
||||||
);
|
);
|
||||||
|
static const VerificationMeta _harvestMonthMeta = const VerificationMeta(
|
||||||
|
'harvestMonth',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<int> harvestMonth = GeneratedColumn<int>(
|
||||||
|
'harvest_month',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
);
|
||||||
static const VerificationMeta _quantityKindMeta = const VerificationMeta(
|
static const VerificationMeta _quantityKindMeta = const VerificationMeta(
|
||||||
'quantityKind',
|
'quantityKind',
|
||||||
);
|
);
|
||||||
|
|
@ -2707,6 +2718,7 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> {
|
||||||
varietyId,
|
varietyId,
|
||||||
type,
|
type,
|
||||||
harvestYear,
|
harvestYear,
|
||||||
|
harvestMonth,
|
||||||
quantityKind,
|
quantityKind,
|
||||||
quantityPrecise,
|
quantityPrecise,
|
||||||
quantityLabel,
|
quantityLabel,
|
||||||
|
|
@ -2787,6 +2799,15 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('harvest_month')) {
|
||||||
|
context.handle(
|
||||||
|
_harvestMonthMeta,
|
||||||
|
harvestMonth.isAcceptableOrUnknown(
|
||||||
|
data['harvest_month']!,
|
||||||
|
_harvestMonthMeta,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (data.containsKey('quantity_kind')) {
|
if (data.containsKey('quantity_kind')) {
|
||||||
context.handle(
|
context.handle(
|
||||||
_quantityKindMeta,
|
_quantityKindMeta,
|
||||||
|
|
@ -2876,6 +2897,10 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> {
|
||||||
DriftSqlType.int,
|
DriftSqlType.int,
|
||||||
data['${effectivePrefix}harvest_year'],
|
data['${effectivePrefix}harvest_year'],
|
||||||
),
|
),
|
||||||
|
harvestMonth: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.int,
|
||||||
|
data['${effectivePrefix}harvest_month'],
|
||||||
|
),
|
||||||
quantityKind: attachedDatabase.typeMapping.read(
|
quantityKind: attachedDatabase.typeMapping.read(
|
||||||
DriftSqlType.string,
|
DriftSqlType.string,
|
||||||
data['${effectivePrefix}quantity_kind'],
|
data['${effectivePrefix}quantity_kind'],
|
||||||
|
|
@ -2926,6 +2951,7 @@ class Lot extends DataClass implements Insertable<Lot> {
|
||||||
final String varietyId;
|
final String varietyId;
|
||||||
final LotType type;
|
final LotType type;
|
||||||
final int? harvestYear;
|
final int? harvestYear;
|
||||||
|
final int? harvestMonth;
|
||||||
final String? quantityKind;
|
final String? quantityKind;
|
||||||
final double? quantityPrecise;
|
final double? quantityPrecise;
|
||||||
final String? quantityLabel;
|
final String? quantityLabel;
|
||||||
|
|
@ -2942,6 +2968,7 @@ class Lot extends DataClass implements Insertable<Lot> {
|
||||||
required this.varietyId,
|
required this.varietyId,
|
||||||
required this.type,
|
required this.type,
|
||||||
this.harvestYear,
|
this.harvestYear,
|
||||||
|
this.harvestMonth,
|
||||||
this.quantityKind,
|
this.quantityKind,
|
||||||
this.quantityPrecise,
|
this.quantityPrecise,
|
||||||
this.quantityLabel,
|
this.quantityLabel,
|
||||||
|
|
@ -2965,6 +2992,9 @@ class Lot extends DataClass implements Insertable<Lot> {
|
||||||
if (!nullToAbsent || harvestYear != null) {
|
if (!nullToAbsent || harvestYear != null) {
|
||||||
map['harvest_year'] = Variable<int>(harvestYear);
|
map['harvest_year'] = Variable<int>(harvestYear);
|
||||||
}
|
}
|
||||||
|
if (!nullToAbsent || harvestMonth != null) {
|
||||||
|
map['harvest_month'] = Variable<int>(harvestMonth);
|
||||||
|
}
|
||||||
if (!nullToAbsent || quantityKind != null) {
|
if (!nullToAbsent || quantityKind != null) {
|
||||||
map['quantity_kind'] = Variable<String>(quantityKind);
|
map['quantity_kind'] = Variable<String>(quantityKind);
|
||||||
}
|
}
|
||||||
|
|
@ -3001,6 +3031,9 @@ class Lot extends DataClass implements Insertable<Lot> {
|
||||||
harvestYear: harvestYear == null && nullToAbsent
|
harvestYear: harvestYear == null && nullToAbsent
|
||||||
? const Value.absent()
|
? const Value.absent()
|
||||||
: Value(harvestYear),
|
: Value(harvestYear),
|
||||||
|
harvestMonth: harvestMonth == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(harvestMonth),
|
||||||
quantityKind: quantityKind == null && nullToAbsent
|
quantityKind: quantityKind == null && nullToAbsent
|
||||||
? const Value.absent()
|
? const Value.absent()
|
||||||
: Value(quantityKind),
|
: Value(quantityKind),
|
||||||
|
|
@ -3037,6 +3070,7 @@ class Lot extends DataClass implements Insertable<Lot> {
|
||||||
serializer.fromJson<String>(json['type']),
|
serializer.fromJson<String>(json['type']),
|
||||||
),
|
),
|
||||||
harvestYear: serializer.fromJson<int?>(json['harvestYear']),
|
harvestYear: serializer.fromJson<int?>(json['harvestYear']),
|
||||||
|
harvestMonth: serializer.fromJson<int?>(json['harvestMonth']),
|
||||||
quantityKind: serializer.fromJson<String?>(json['quantityKind']),
|
quantityKind: serializer.fromJson<String?>(json['quantityKind']),
|
||||||
quantityPrecise: serializer.fromJson<double?>(json['quantityPrecise']),
|
quantityPrecise: serializer.fromJson<double?>(json['quantityPrecise']),
|
||||||
quantityLabel: serializer.fromJson<String?>(json['quantityLabel']),
|
quantityLabel: serializer.fromJson<String?>(json['quantityLabel']),
|
||||||
|
|
@ -3060,6 +3094,7 @@ class Lot extends DataClass implements Insertable<Lot> {
|
||||||
'varietyId': serializer.toJson<String>(varietyId),
|
'varietyId': serializer.toJson<String>(varietyId),
|
||||||
'type': serializer.toJson<String>($LotsTable.$convertertype.toJson(type)),
|
'type': serializer.toJson<String>($LotsTable.$convertertype.toJson(type)),
|
||||||
'harvestYear': serializer.toJson<int?>(harvestYear),
|
'harvestYear': serializer.toJson<int?>(harvestYear),
|
||||||
|
'harvestMonth': serializer.toJson<int?>(harvestMonth),
|
||||||
'quantityKind': serializer.toJson<String?>(quantityKind),
|
'quantityKind': serializer.toJson<String?>(quantityKind),
|
||||||
'quantityPrecise': serializer.toJson<double?>(quantityPrecise),
|
'quantityPrecise': serializer.toJson<double?>(quantityPrecise),
|
||||||
'quantityLabel': serializer.toJson<String?>(quantityLabel),
|
'quantityLabel': serializer.toJson<String?>(quantityLabel),
|
||||||
|
|
@ -3081,6 +3116,7 @@ class Lot extends DataClass implements Insertable<Lot> {
|
||||||
String? varietyId,
|
String? varietyId,
|
||||||
LotType? type,
|
LotType? type,
|
||||||
Value<int?> harvestYear = const Value.absent(),
|
Value<int?> harvestYear = const Value.absent(),
|
||||||
|
Value<int?> harvestMonth = const Value.absent(),
|
||||||
Value<String?> quantityKind = const Value.absent(),
|
Value<String?> quantityKind = const Value.absent(),
|
||||||
Value<double?> quantityPrecise = const Value.absent(),
|
Value<double?> quantityPrecise = const Value.absent(),
|
||||||
Value<String?> quantityLabel = const Value.absent(),
|
Value<String?> quantityLabel = const Value.absent(),
|
||||||
|
|
@ -3097,6 +3133,7 @@ class Lot extends DataClass implements Insertable<Lot> {
|
||||||
varietyId: varietyId ?? this.varietyId,
|
varietyId: varietyId ?? this.varietyId,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
harvestYear: harvestYear.present ? harvestYear.value : this.harvestYear,
|
harvestYear: harvestYear.present ? harvestYear.value : this.harvestYear,
|
||||||
|
harvestMonth: harvestMonth.present ? harvestMonth.value : this.harvestMonth,
|
||||||
quantityKind: quantityKind.present ? quantityKind.value : this.quantityKind,
|
quantityKind: quantityKind.present ? quantityKind.value : this.quantityKind,
|
||||||
quantityPrecise: quantityPrecise.present
|
quantityPrecise: quantityPrecise.present
|
||||||
? quantityPrecise.value
|
? quantityPrecise.value
|
||||||
|
|
@ -3127,6 +3164,9 @@ class Lot extends DataClass implements Insertable<Lot> {
|
||||||
harvestYear: data.harvestYear.present
|
harvestYear: data.harvestYear.present
|
||||||
? data.harvestYear.value
|
? data.harvestYear.value
|
||||||
: this.harvestYear,
|
: this.harvestYear,
|
||||||
|
harvestMonth: data.harvestMonth.present
|
||||||
|
? data.harvestMonth.value
|
||||||
|
: this.harvestMonth,
|
||||||
quantityKind: data.quantityKind.present
|
quantityKind: data.quantityKind.present
|
||||||
? data.quantityKind.value
|
? data.quantityKind.value
|
||||||
: this.quantityKind,
|
: this.quantityKind,
|
||||||
|
|
@ -3160,6 +3200,7 @@ class Lot extends DataClass implements Insertable<Lot> {
|
||||||
..write('varietyId: $varietyId, ')
|
..write('varietyId: $varietyId, ')
|
||||||
..write('type: $type, ')
|
..write('type: $type, ')
|
||||||
..write('harvestYear: $harvestYear, ')
|
..write('harvestYear: $harvestYear, ')
|
||||||
|
..write('harvestMonth: $harvestMonth, ')
|
||||||
..write('quantityKind: $quantityKind, ')
|
..write('quantityKind: $quantityKind, ')
|
||||||
..write('quantityPrecise: $quantityPrecise, ')
|
..write('quantityPrecise: $quantityPrecise, ')
|
||||||
..write('quantityLabel: $quantityLabel, ')
|
..write('quantityLabel: $quantityLabel, ')
|
||||||
|
|
@ -3181,6 +3222,7 @@ class Lot extends DataClass implements Insertable<Lot> {
|
||||||
varietyId,
|
varietyId,
|
||||||
type,
|
type,
|
||||||
harvestYear,
|
harvestYear,
|
||||||
|
harvestMonth,
|
||||||
quantityKind,
|
quantityKind,
|
||||||
quantityPrecise,
|
quantityPrecise,
|
||||||
quantityLabel,
|
quantityLabel,
|
||||||
|
|
@ -3201,6 +3243,7 @@ class Lot extends DataClass implements Insertable<Lot> {
|
||||||
other.varietyId == this.varietyId &&
|
other.varietyId == this.varietyId &&
|
||||||
other.type == this.type &&
|
other.type == this.type &&
|
||||||
other.harvestYear == this.harvestYear &&
|
other.harvestYear == this.harvestYear &&
|
||||||
|
other.harvestMonth == this.harvestMonth &&
|
||||||
other.quantityKind == this.quantityKind &&
|
other.quantityKind == this.quantityKind &&
|
||||||
other.quantityPrecise == this.quantityPrecise &&
|
other.quantityPrecise == this.quantityPrecise &&
|
||||||
other.quantityLabel == this.quantityLabel &&
|
other.quantityLabel == this.quantityLabel &&
|
||||||
|
|
@ -3219,6 +3262,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
||||||
final Value<String> varietyId;
|
final Value<String> varietyId;
|
||||||
final Value<LotType> type;
|
final Value<LotType> type;
|
||||||
final Value<int?> harvestYear;
|
final Value<int?> harvestYear;
|
||||||
|
final Value<int?> harvestMonth;
|
||||||
final Value<String?> quantityKind;
|
final Value<String?> quantityKind;
|
||||||
final Value<double?> quantityPrecise;
|
final Value<double?> quantityPrecise;
|
||||||
final Value<String?> quantityLabel;
|
final Value<String?> quantityLabel;
|
||||||
|
|
@ -3236,6 +3280,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
||||||
this.varietyId = const Value.absent(),
|
this.varietyId = const Value.absent(),
|
||||||
this.type = const Value.absent(),
|
this.type = const Value.absent(),
|
||||||
this.harvestYear = const Value.absent(),
|
this.harvestYear = const Value.absent(),
|
||||||
|
this.harvestMonth = const Value.absent(),
|
||||||
this.quantityKind = const Value.absent(),
|
this.quantityKind = const Value.absent(),
|
||||||
this.quantityPrecise = const Value.absent(),
|
this.quantityPrecise = const Value.absent(),
|
||||||
this.quantityLabel = const Value.absent(),
|
this.quantityLabel = const Value.absent(),
|
||||||
|
|
@ -3254,6 +3299,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
||||||
required String varietyId,
|
required String varietyId,
|
||||||
this.type = const Value.absent(),
|
this.type = const Value.absent(),
|
||||||
this.harvestYear = const Value.absent(),
|
this.harvestYear = const Value.absent(),
|
||||||
|
this.harvestMonth = const Value.absent(),
|
||||||
this.quantityKind = const Value.absent(),
|
this.quantityKind = const Value.absent(),
|
||||||
this.quantityPrecise = const Value.absent(),
|
this.quantityPrecise = const Value.absent(),
|
||||||
this.quantityLabel = const Value.absent(),
|
this.quantityLabel = const Value.absent(),
|
||||||
|
|
@ -3276,6 +3322,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
||||||
Expression<String>? varietyId,
|
Expression<String>? varietyId,
|
||||||
Expression<String>? type,
|
Expression<String>? type,
|
||||||
Expression<int>? harvestYear,
|
Expression<int>? harvestYear,
|
||||||
|
Expression<int>? harvestMonth,
|
||||||
Expression<String>? quantityKind,
|
Expression<String>? quantityKind,
|
||||||
Expression<double>? quantityPrecise,
|
Expression<double>? quantityPrecise,
|
||||||
Expression<String>? quantityLabel,
|
Expression<String>? quantityLabel,
|
||||||
|
|
@ -3294,6 +3341,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
||||||
if (varietyId != null) 'variety_id': varietyId,
|
if (varietyId != null) 'variety_id': varietyId,
|
||||||
if (type != null) 'type': type,
|
if (type != null) 'type': type,
|
||||||
if (harvestYear != null) 'harvest_year': harvestYear,
|
if (harvestYear != null) 'harvest_year': harvestYear,
|
||||||
|
if (harvestMonth != null) 'harvest_month': harvestMonth,
|
||||||
if (quantityKind != null) 'quantity_kind': quantityKind,
|
if (quantityKind != null) 'quantity_kind': quantityKind,
|
||||||
if (quantityPrecise != null) 'quantity_precise': quantityPrecise,
|
if (quantityPrecise != null) 'quantity_precise': quantityPrecise,
|
||||||
if (quantityLabel != null) 'quantity_label': quantityLabel,
|
if (quantityLabel != null) 'quantity_label': quantityLabel,
|
||||||
|
|
@ -3314,6 +3362,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
||||||
Value<String>? varietyId,
|
Value<String>? varietyId,
|
||||||
Value<LotType>? type,
|
Value<LotType>? type,
|
||||||
Value<int?>? harvestYear,
|
Value<int?>? harvestYear,
|
||||||
|
Value<int?>? harvestMonth,
|
||||||
Value<String?>? quantityKind,
|
Value<String?>? quantityKind,
|
||||||
Value<double?>? quantityPrecise,
|
Value<double?>? quantityPrecise,
|
||||||
Value<String?>? quantityLabel,
|
Value<String?>? quantityLabel,
|
||||||
|
|
@ -3332,6 +3381,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
||||||
varietyId: varietyId ?? this.varietyId,
|
varietyId: varietyId ?? this.varietyId,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
harvestYear: harvestYear ?? this.harvestYear,
|
harvestYear: harvestYear ?? this.harvestYear,
|
||||||
|
harvestMonth: harvestMonth ?? this.harvestMonth,
|
||||||
quantityKind: quantityKind ?? this.quantityKind,
|
quantityKind: quantityKind ?? this.quantityKind,
|
||||||
quantityPrecise: quantityPrecise ?? this.quantityPrecise,
|
quantityPrecise: quantityPrecise ?? this.quantityPrecise,
|
||||||
quantityLabel: quantityLabel ?? this.quantityLabel,
|
quantityLabel: quantityLabel ?? this.quantityLabel,
|
||||||
|
|
@ -3374,6 +3424,9 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
||||||
if (harvestYear.present) {
|
if (harvestYear.present) {
|
||||||
map['harvest_year'] = Variable<int>(harvestYear.value);
|
map['harvest_year'] = Variable<int>(harvestYear.value);
|
||||||
}
|
}
|
||||||
|
if (harvestMonth.present) {
|
||||||
|
map['harvest_month'] = Variable<int>(harvestMonth.value);
|
||||||
|
}
|
||||||
if (quantityKind.present) {
|
if (quantityKind.present) {
|
||||||
map['quantity_kind'] = Variable<String>(quantityKind.value);
|
map['quantity_kind'] = Variable<String>(quantityKind.value);
|
||||||
}
|
}
|
||||||
|
|
@ -3412,6 +3465,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
||||||
..write('varietyId: $varietyId, ')
|
..write('varietyId: $varietyId, ')
|
||||||
..write('type: $type, ')
|
..write('type: $type, ')
|
||||||
..write('harvestYear: $harvestYear, ')
|
..write('harvestYear: $harvestYear, ')
|
||||||
|
..write('harvestMonth: $harvestMonth, ')
|
||||||
..write('quantityKind: $quantityKind, ')
|
..write('quantityKind: $quantityKind, ')
|
||||||
..write('quantityPrecise: $quantityPrecise, ')
|
..write('quantityPrecise: $quantityPrecise, ')
|
||||||
..write('quantityLabel: $quantityLabel, ')
|
..write('quantityLabel: $quantityLabel, ')
|
||||||
|
|
@ -8188,6 +8242,7 @@ typedef $$LotsTableCreateCompanionBuilder =
|
||||||
required String varietyId,
|
required String varietyId,
|
||||||
Value<LotType> type,
|
Value<LotType> type,
|
||||||
Value<int?> harvestYear,
|
Value<int?> harvestYear,
|
||||||
|
Value<int?> harvestMonth,
|
||||||
Value<String?> quantityKind,
|
Value<String?> quantityKind,
|
||||||
Value<double?> quantityPrecise,
|
Value<double?> quantityPrecise,
|
||||||
Value<String?> quantityLabel,
|
Value<String?> quantityLabel,
|
||||||
|
|
@ -8207,6 +8262,7 @@ typedef $$LotsTableUpdateCompanionBuilder =
|
||||||
Value<String> varietyId,
|
Value<String> varietyId,
|
||||||
Value<LotType> type,
|
Value<LotType> type,
|
||||||
Value<int?> harvestYear,
|
Value<int?> harvestYear,
|
||||||
|
Value<int?> harvestMonth,
|
||||||
Value<String?> quantityKind,
|
Value<String?> quantityKind,
|
||||||
Value<double?> quantityPrecise,
|
Value<double?> quantityPrecise,
|
||||||
Value<String?> quantityLabel,
|
Value<String?> quantityLabel,
|
||||||
|
|
@ -8270,6 +8326,11 @@ class $$LotsTableFilterComposer extends Composer<_$AppDatabase, $LotsTable> {
|
||||||
builder: (column) => ColumnFilters(column),
|
builder: (column) => ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColumnFilters<int> get harvestMonth => $composableBuilder(
|
||||||
|
column: $table.harvestMonth,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
ColumnFilters<String> get quantityKind => $composableBuilder(
|
ColumnFilters<String> get quantityKind => $composableBuilder(
|
||||||
column: $table.quantityKind,
|
column: $table.quantityKind,
|
||||||
builder: (column) => ColumnFilters(column),
|
builder: (column) => ColumnFilters(column),
|
||||||
|
|
@ -8355,6 +8416,11 @@ class $$LotsTableOrderingComposer extends Composer<_$AppDatabase, $LotsTable> {
|
||||||
builder: (column) => ColumnOrderings(column),
|
builder: (column) => ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<int> get harvestMonth => $composableBuilder(
|
||||||
|
column: $table.harvestMonth,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
ColumnOrderings<String> get quantityKind => $composableBuilder(
|
ColumnOrderings<String> get quantityKind => $composableBuilder(
|
||||||
column: $table.quantityKind,
|
column: $table.quantityKind,
|
||||||
builder: (column) => ColumnOrderings(column),
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
|
@ -8428,6 +8494,11 @@ class $$LotsTableAnnotationComposer
|
||||||
builder: (column) => column,
|
builder: (column) => column,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
GeneratedColumn<int> get harvestMonth => $composableBuilder(
|
||||||
|
column: $table.harvestMonth,
|
||||||
|
builder: (column) => column,
|
||||||
|
);
|
||||||
|
|
||||||
GeneratedColumn<String> get quantityKind => $composableBuilder(
|
GeneratedColumn<String> get quantityKind => $composableBuilder(
|
||||||
column: $table.quantityKind,
|
column: $table.quantityKind,
|
||||||
builder: (column) => column,
|
builder: (column) => column,
|
||||||
|
|
@ -8497,6 +8568,7 @@ class $$LotsTableTableManager
|
||||||
Value<String> varietyId = const Value.absent(),
|
Value<String> varietyId = const Value.absent(),
|
||||||
Value<LotType> type = const Value.absent(),
|
Value<LotType> type = const Value.absent(),
|
||||||
Value<int?> harvestYear = const Value.absent(),
|
Value<int?> harvestYear = const Value.absent(),
|
||||||
|
Value<int?> harvestMonth = const Value.absent(),
|
||||||
Value<String?> quantityKind = const Value.absent(),
|
Value<String?> quantityKind = const Value.absent(),
|
||||||
Value<double?> quantityPrecise = const Value.absent(),
|
Value<double?> quantityPrecise = const Value.absent(),
|
||||||
Value<String?> quantityLabel = const Value.absent(),
|
Value<String?> quantityLabel = const Value.absent(),
|
||||||
|
|
@ -8514,6 +8586,7 @@ class $$LotsTableTableManager
|
||||||
varietyId: varietyId,
|
varietyId: varietyId,
|
||||||
type: type,
|
type: type,
|
||||||
harvestYear: harvestYear,
|
harvestYear: harvestYear,
|
||||||
|
harvestMonth: harvestMonth,
|
||||||
quantityKind: quantityKind,
|
quantityKind: quantityKind,
|
||||||
quantityPrecise: quantityPrecise,
|
quantityPrecise: quantityPrecise,
|
||||||
quantityLabel: quantityLabel,
|
quantityLabel: quantityLabel,
|
||||||
|
|
@ -8533,6 +8606,7 @@ class $$LotsTableTableManager
|
||||||
required String varietyId,
|
required String varietyId,
|
||||||
Value<LotType> type = const Value.absent(),
|
Value<LotType> type = const Value.absent(),
|
||||||
Value<int?> harvestYear = const Value.absent(),
|
Value<int?> harvestYear = const Value.absent(),
|
||||||
|
Value<int?> harvestMonth = const Value.absent(),
|
||||||
Value<String?> quantityKind = const Value.absent(),
|
Value<String?> quantityKind = const Value.absent(),
|
||||||
Value<double?> quantityPrecise = const Value.absent(),
|
Value<double?> quantityPrecise = const Value.absent(),
|
||||||
Value<String?> quantityLabel = const Value.absent(),
|
Value<String?> quantityLabel = const Value.absent(),
|
||||||
|
|
@ -8550,6 +8624,7 @@ class $$LotsTableTableManager
|
||||||
varietyId: varietyId,
|
varietyId: varietyId,
|
||||||
type: type,
|
type: type,
|
||||||
harvestYear: harvestYear,
|
harvestYear: harvestYear,
|
||||||
|
harvestMonth: harvestMonth,
|
||||||
quantityKind: quantityKind,
|
quantityKind: quantityKind,
|
||||||
quantityPrecise: quantityPrecise,
|
quantityPrecise: quantityPrecise,
|
||||||
quantityLabel: quantityLabel,
|
quantityLabel: quantityLabel,
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ class Lots extends Table with SyncColumns {
|
||||||
TextColumn get type =>
|
TextColumn get type =>
|
||||||
textEnum<LotType>().withDefault(const Constant('seed'))();
|
textEnum<LotType>().withDefault(const Constant('seed'))();
|
||||||
IntColumn get harvestYear => integer().nullable()();
|
IntColumn get harvestYear => integer().nullable()();
|
||||||
|
IntColumn get harvestMonth => integer().nullable()(); // 1..12, optional
|
||||||
TextColumn get quantityKind => text().nullable()(); // QuantityKind.name
|
TextColumn get quantityKind => text().nullable()(); // QuantityKind.name
|
||||||
RealColumn get quantityPrecise => real().nullable()();
|
RealColumn get quantityPrecise => real().nullable()();
|
||||||
TextColumn get quantityLabel => text().nullable()();
|
TextColumn get quantityLabel => text().nullable()();
|
||||||
|
|
|
||||||
|
|
@ -56,10 +56,19 @@
|
||||||
},
|
},
|
||||||
"addLot": {
|
"addLot": {
|
||||||
"title": "Add lot",
|
"title": "Add lot",
|
||||||
"year": "Harvest year",
|
"year": "Harvest date",
|
||||||
"quantity": "How much?",
|
"quantity": "How much?",
|
||||||
"amount": "Amount"
|
"amount": "Amount"
|
||||||
},
|
},
|
||||||
|
"harvest": {
|
||||||
|
"pickTitle": "Select month / year",
|
||||||
|
"anyMonth": "Any month",
|
||||||
|
"noDate": "Set harvest date",
|
||||||
|
"monthNames": [
|
||||||
|
"January", "February", "March", "April", "May", "June",
|
||||||
|
"July", "August", "September", "October", "November", "December"
|
||||||
|
]
|
||||||
|
},
|
||||||
"lotType": {
|
"lotType": {
|
||||||
"seed": "Seeds",
|
"seed": "Seeds",
|
||||||
"plant": "Plants"
|
"plant": "Plants"
|
||||||
|
|
|
||||||
|
|
@ -56,10 +56,19 @@
|
||||||
},
|
},
|
||||||
"addLot": {
|
"addLot": {
|
||||||
"title": "Añadir lote",
|
"title": "Añadir lote",
|
||||||
"year": "Año de cosecha",
|
"year": "Fecha de cosecha",
|
||||||
"quantity": "¿Cuánta?",
|
"quantity": "¿Cuánta?",
|
||||||
"amount": "Cantidad"
|
"amount": "Cantidad"
|
||||||
},
|
},
|
||||||
|
"harvest": {
|
||||||
|
"pickTitle": "Seleccionar mes / año",
|
||||||
|
"anyMonth": "Cualquier mes",
|
||||||
|
"noDate": "Indicar fecha de cosecha",
|
||||||
|
"monthNames": [
|
||||||
|
"enero", "febrero", "marzo", "abril", "mayo", "junio",
|
||||||
|
"julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"
|
||||||
|
]
|
||||||
|
},
|
||||||
"lotType": {
|
"lotType": {
|
||||||
"seed": "Semillas",
|
"seed": "Semillas",
|
||||||
"plant": "Plantel"
|
"plant": "Plantel"
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 2
|
/// Locales: 2
|
||||||
/// Strings: 186 (93 per locale)
|
/// Strings: 216 (108 per locale)
|
||||||
///
|
///
|
||||||
/// Built on 2026-07-08 at 09:16 UTC
|
/// Built on 2026-07-08 at 10:02 UTC
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
|
||||||
late final Translations$germination$en germination = Translations$germination$en.internal(_root);
|
late final Translations$germination$en germination = Translations$germination$en.internal(_root);
|
||||||
late final Translations$editVariety$en editVariety = Translations$editVariety$en.internal(_root);
|
late final Translations$editVariety$en editVariety = Translations$editVariety$en.internal(_root);
|
||||||
late final Translations$addLot$en addLot = Translations$addLot$en.internal(_root);
|
late final Translations$addLot$en addLot = Translations$addLot$en.internal(_root);
|
||||||
|
late final Translations$harvest$en harvest = Translations$harvest$en.internal(_root);
|
||||||
late final Translations$lotType$en lotType = Translations$lotType$en.internal(_root);
|
late final Translations$lotType$en lotType = Translations$lotType$en.internal(_root);
|
||||||
late final Translations$unit$en unit = Translations$unit$en.internal(_root);
|
late final Translations$unit$en unit = Translations$unit$en.internal(_root);
|
||||||
}
|
}
|
||||||
|
|
@ -249,8 +250,8 @@ class Translations$addLot$en {
|
||||||
/// en: 'Add lot'
|
/// en: 'Add lot'
|
||||||
String get title => 'Add lot';
|
String get title => 'Add lot';
|
||||||
|
|
||||||
/// en: 'Harvest year'
|
/// en: 'Harvest date'
|
||||||
String get year => 'Harvest year';
|
String get year => 'Harvest date';
|
||||||
|
|
||||||
/// en: 'How much?'
|
/// en: 'How much?'
|
||||||
String get quantity => 'How much?';
|
String get quantity => 'How much?';
|
||||||
|
|
@ -259,6 +260,39 @@ class Translations$addLot$en {
|
||||||
String get amount => 'Amount';
|
String get amount => 'Amount';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path: harvest
|
||||||
|
class Translations$harvest$en {
|
||||||
|
Translations$harvest$en.internal(this._root);
|
||||||
|
|
||||||
|
final Translations _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
|
||||||
|
/// en: 'Select month / year'
|
||||||
|
String get pickTitle => 'Select month / year';
|
||||||
|
|
||||||
|
/// en: 'Any month'
|
||||||
|
String get anyMonth => 'Any month';
|
||||||
|
|
||||||
|
/// en: 'Set harvest date'
|
||||||
|
String get noDate => 'Set harvest date';
|
||||||
|
|
||||||
|
List<String> get monthNames => [
|
||||||
|
'January',
|
||||||
|
'February',
|
||||||
|
'March',
|
||||||
|
'April',
|
||||||
|
'May',
|
||||||
|
'June',
|
||||||
|
'July',
|
||||||
|
'August',
|
||||||
|
'September',
|
||||||
|
'October',
|
||||||
|
'November',
|
||||||
|
'December',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// Path: lotType
|
// Path: lotType
|
||||||
class Translations$lotType$en {
|
class Translations$lotType$en {
|
||||||
Translations$lotType$en.internal(this._root);
|
Translations$lotType$en.internal(this._root);
|
||||||
|
|
@ -682,9 +716,24 @@ extension on Translations {
|
||||||
'editVariety.species' => 'Species (from catalog)',
|
'editVariety.species' => 'Species (from catalog)',
|
||||||
'editVariety.speciesHint' => 'Search a species…',
|
'editVariety.speciesHint' => 'Search a species…',
|
||||||
'addLot.title' => 'Add lot',
|
'addLot.title' => 'Add lot',
|
||||||
'addLot.year' => 'Harvest year',
|
'addLot.year' => 'Harvest date',
|
||||||
'addLot.quantity' => 'How much?',
|
'addLot.quantity' => 'How much?',
|
||||||
'addLot.amount' => 'Amount',
|
'addLot.amount' => 'Amount',
|
||||||
|
'harvest.pickTitle' => 'Select month / year',
|
||||||
|
'harvest.anyMonth' => 'Any month',
|
||||||
|
'harvest.noDate' => 'Set harvest date',
|
||||||
|
'harvest.monthNames.0' => 'January',
|
||||||
|
'harvest.monthNames.1' => 'February',
|
||||||
|
'harvest.monthNames.2' => 'March',
|
||||||
|
'harvest.monthNames.3' => 'April',
|
||||||
|
'harvest.monthNames.4' => 'May',
|
||||||
|
'harvest.monthNames.5' => 'June',
|
||||||
|
'harvest.monthNames.6' => 'July',
|
||||||
|
'harvest.monthNames.7' => 'August',
|
||||||
|
'harvest.monthNames.8' => 'September',
|
||||||
|
'harvest.monthNames.9' => 'October',
|
||||||
|
'harvest.monthNames.10' => 'November',
|
||||||
|
'harvest.monthNames.11' => 'December',
|
||||||
'lotType.seed' => 'Seeds',
|
'lotType.seed' => 'Seeds',
|
||||||
'lotType.plant' => 'Plants',
|
'lotType.plant' => 'Plants',
|
||||||
'unit.aFew' => 'a few',
|
'unit.aFew' => 'a few',
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
|
||||||
@override late final _Translations$germination$es germination = _Translations$germination$es._(_root);
|
@override late final _Translations$germination$es germination = _Translations$germination$es._(_root);
|
||||||
@override late final _Translations$editVariety$es editVariety = _Translations$editVariety$es._(_root);
|
@override late final _Translations$editVariety$es editVariety = _Translations$editVariety$es._(_root);
|
||||||
@override late final _Translations$addLot$es addLot = _Translations$addLot$es._(_root);
|
@override late final _Translations$addLot$es addLot = _Translations$addLot$es._(_root);
|
||||||
|
@override late final _Translations$harvest$es harvest = _Translations$harvest$es._(_root);
|
||||||
@override late final _Translations$lotType$es lotType = _Translations$lotType$es._(_root);
|
@override late final _Translations$lotType$es lotType = _Translations$lotType$es._(_root);
|
||||||
@override late final _Translations$unit$es unit = _Translations$unit$es._(_root);
|
@override late final _Translations$unit$es unit = _Translations$unit$es._(_root);
|
||||||
}
|
}
|
||||||
|
|
@ -163,11 +164,37 @@ class _Translations$addLot$es extends Translations$addLot$en {
|
||||||
|
|
||||||
// Translations
|
// Translations
|
||||||
@override String get title => 'Añadir lote';
|
@override String get title => 'Añadir lote';
|
||||||
@override String get year => 'Año de cosecha';
|
@override String get year => 'Fecha de cosecha';
|
||||||
@override String get quantity => '¿Cuánta?';
|
@override String get quantity => '¿Cuánta?';
|
||||||
@override String get amount => 'Cantidad';
|
@override String get amount => 'Cantidad';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path: harvest
|
||||||
|
class _Translations$harvest$es extends Translations$harvest$en {
|
||||||
|
_Translations$harvest$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||||
|
|
||||||
|
final TranslationsEs _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
@override String get pickTitle => 'Seleccionar mes / año';
|
||||||
|
@override String get anyMonth => 'Cualquier mes';
|
||||||
|
@override String get noDate => 'Indicar fecha de cosecha';
|
||||||
|
@override List<String> get monthNames => [
|
||||||
|
'enero',
|
||||||
|
'febrero',
|
||||||
|
'marzo',
|
||||||
|
'abril',
|
||||||
|
'mayo',
|
||||||
|
'junio',
|
||||||
|
'julio',
|
||||||
|
'agosto',
|
||||||
|
'septiembre',
|
||||||
|
'octubre',
|
||||||
|
'noviembre',
|
||||||
|
'diciembre',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// Path: lotType
|
// Path: lotType
|
||||||
class _Translations$lotType$es extends Translations$lotType$en {
|
class _Translations$lotType$es extends Translations$lotType$en {
|
||||||
_Translations$lotType$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
_Translations$lotType$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||||
|
|
@ -494,9 +521,24 @@ extension on TranslationsEs {
|
||||||
'editVariety.species' => 'Especie (del catálogo)',
|
'editVariety.species' => 'Especie (del catálogo)',
|
||||||
'editVariety.speciesHint' => 'Buscar una especie…',
|
'editVariety.speciesHint' => 'Buscar una especie…',
|
||||||
'addLot.title' => 'Añadir lote',
|
'addLot.title' => 'Añadir lote',
|
||||||
'addLot.year' => 'Año de cosecha',
|
'addLot.year' => 'Fecha de cosecha',
|
||||||
'addLot.quantity' => '¿Cuánta?',
|
'addLot.quantity' => '¿Cuánta?',
|
||||||
'addLot.amount' => 'Cantidad',
|
'addLot.amount' => 'Cantidad',
|
||||||
|
'harvest.pickTitle' => 'Seleccionar mes / año',
|
||||||
|
'harvest.anyMonth' => 'Cualquier mes',
|
||||||
|
'harvest.noDate' => 'Indicar fecha de cosecha',
|
||||||
|
'harvest.monthNames.0' => 'enero',
|
||||||
|
'harvest.monthNames.1' => 'febrero',
|
||||||
|
'harvest.monthNames.2' => 'marzo',
|
||||||
|
'harvest.monthNames.3' => 'abril',
|
||||||
|
'harvest.monthNames.4' => 'mayo',
|
||||||
|
'harvest.monthNames.5' => 'junio',
|
||||||
|
'harvest.monthNames.6' => 'julio',
|
||||||
|
'harvest.monthNames.7' => 'agosto',
|
||||||
|
'harvest.monthNames.8' => 'septiembre',
|
||||||
|
'harvest.monthNames.9' => 'octubre',
|
||||||
|
'harvest.monthNames.10' => 'noviembre',
|
||||||
|
'harvest.monthNames.11' => 'diciembre',
|
||||||
'lotType.seed' => 'Semillas',
|
'lotType.seed' => 'Semillas',
|
||||||
'lotType.plant' => 'Plantel',
|
'lotType.plant' => 'Plantel',
|
||||||
'unit.aFew' => 'unas pocas',
|
'unit.aFew' => 'unas pocas',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:commons_core/commons_core.dart';
|
import 'package:commons_core/commons_core.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
@ -54,11 +55,13 @@ class VarietyDetailCubit extends Cubit<VarietyDetailState> {
|
||||||
Future<void> addLot({
|
Future<void> addLot({
|
||||||
LotType type = LotType.seed,
|
LotType type = LotType.seed,
|
||||||
int? year,
|
int? year,
|
||||||
|
int? month,
|
||||||
Quantity? quantity,
|
Quantity? quantity,
|
||||||
}) => _repo.addLot(
|
}) => _repo.addLot(
|
||||||
varietyId: varietyId,
|
varietyId: varietyId,
|
||||||
type: type,
|
type: type,
|
||||||
harvestYear: year,
|
harvestYear: year,
|
||||||
|
harvestMonth: month,
|
||||||
quantity: quantity,
|
quantity: quantity,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -66,11 +69,13 @@ class VarietyDetailCubit extends Cubit<VarietyDetailState> {
|
||||||
required String lotId,
|
required String lotId,
|
||||||
required LotType type,
|
required LotType type,
|
||||||
int? year,
|
int? year,
|
||||||
|
int? month,
|
||||||
Quantity? quantity,
|
Quantity? quantity,
|
||||||
}) => _repo.updateLot(
|
}) => _repo.updateLot(
|
||||||
lotId: lotId,
|
lotId: lotId,
|
||||||
type: type,
|
type: type,
|
||||||
harvestYear: year,
|
harvestYear: year,
|
||||||
|
harvestMonth: month,
|
||||||
quantity: quantity,
|
quantity: quantity,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -82,6 +87,11 @@ class VarietyDetailCubit extends Cubit<VarietyDetailState> {
|
||||||
Future<void> removeVernacularName(String nameId) =>
|
Future<void> removeVernacularName(String nameId) =>
|
||||||
_repo.removeVernacularName(nameId);
|
_repo.removeVernacularName(nameId);
|
||||||
|
|
||||||
|
Future<void> addPhoto(Uint8List bytes) => _repo.addPhoto(varietyId, bytes);
|
||||||
|
|
||||||
|
Future<void> removePhoto(String attachmentId) =>
|
||||||
|
_repo.removePhoto(attachmentId);
|
||||||
|
|
||||||
Future<void> linkSpecies(String speciesId) =>
|
Future<void> linkSpecies(String speciesId) =>
|
||||||
_repo.linkSpecies(varietyId, speciesId);
|
_repo.linkSpecies(varietyId, speciesId);
|
||||||
|
|
||||||
|
|
|
||||||
396
apps/app_seeds/lib/ui/harvest_date_picker.dart
Normal file
396
apps/app_seeds/lib/ui/harvest_date_picker.dart
Normal file
|
|
@ -0,0 +1,396 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../i18n/strings.g.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
/// How many years back the picker offers. A freshly harvested lot is the common
|
||||||
|
/// case, but old seed still needs a home.
|
||||||
|
const _yearsBack = 40;
|
||||||
|
|
||||||
|
/// A harvest date: a required [year] plus an optional [month] (1..12).
|
||||||
|
class HarvestDate {
|
||||||
|
const HarvestDate(this.year, [this.month]);
|
||||||
|
|
||||||
|
final int year;
|
||||||
|
final int? month;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats a harvest date for display: "September 2021" when a month is set,
|
||||||
|
/// otherwise just the localized "Year 2021".
|
||||||
|
String harvestDateLabel(Translations t, {required int year, int? month}) {
|
||||||
|
if (month == null) return t.detail.year(year: year);
|
||||||
|
return '${t.harvest.monthNames[month - 1]} $year';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An inline, tappable field showing the current harvest date. Tapping opens a
|
||||||
|
/// themed month/year dialog (see [showHarvestDatePicker]). Month is optional, so
|
||||||
|
/// the field can read "September 2021" or just "2021".
|
||||||
|
class HarvestDateField extends StatelessWidget {
|
||||||
|
const HarvestDateField({
|
||||||
|
required this.year,
|
||||||
|
required this.onChanged,
|
||||||
|
this.month,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int year;
|
||||||
|
final int? month;
|
||||||
|
final void Function(int year, int? month) onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = context.t;
|
||||||
|
return OutlinedButton.icon(
|
||||||
|
key: const Key('addLot.year'),
|
||||||
|
onPressed: () async {
|
||||||
|
final picked = await showHarvestDatePicker(
|
||||||
|
context,
|
||||||
|
initial: HarvestDate(year, month),
|
||||||
|
);
|
||||||
|
if (picked != null) onChanged(picked.year, picked.month);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.event_outlined, size: 20),
|
||||||
|
label: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(harvestDateLabel(t, year: year, month: month)),
|
||||||
|
),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: seedGreenDark,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
|
minimumSize: const Size.fromHeight(0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens the themed month/year dialog. Returns the chosen [HarvestDate], or null
|
||||||
|
/// if the user cancels.
|
||||||
|
Future<HarvestDate?> showHarvestDatePicker(
|
||||||
|
BuildContext context, {
|
||||||
|
required HarvestDate initial,
|
||||||
|
}) {
|
||||||
|
return showDialog<HarvestDate>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => _HarvestPickerDialog(initial: initial),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _Mode { months, years }
|
||||||
|
|
||||||
|
class _HarvestPickerDialog extends StatefulWidget {
|
||||||
|
const _HarvestPickerDialog({required this.initial});
|
||||||
|
|
||||||
|
final HarvestDate initial;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_HarvestPickerDialog> createState() => _HarvestPickerDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HarvestPickerDialogState extends State<_HarvestPickerDialog> {
|
||||||
|
late int _year = widget.initial.year;
|
||||||
|
late int? _month = widget.initial.month;
|
||||||
|
var _mode = _Mode.months;
|
||||||
|
|
||||||
|
int get _maxYear => DateTime.now().year;
|
||||||
|
int get _minYear => _maxYear - _yearsBack;
|
||||||
|
|
||||||
|
void _setYear(int year) => setState(() {
|
||||||
|
_year = year.clamp(_minYear, _maxYear);
|
||||||
|
_mode = _Mode.months;
|
||||||
|
});
|
||||||
|
|
||||||
|
void _toggleMonth(int month) =>
|
||||||
|
setState(() => _month = _month == month ? null : month);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = context.t;
|
||||||
|
return Dialog(
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 360,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_Header(
|
||||||
|
title: t.harvest.pickTitle,
|
||||||
|
selection: harvestDateLabel(t, year: _year, month: _month),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
|
||||||
|
child: _NavRow(
|
||||||
|
year: _year,
|
||||||
|
mode: _mode,
|
||||||
|
onToggleMode: () => setState(
|
||||||
|
() =>
|
||||||
|
_mode = _mode == _Mode.years ? _Mode.months : _Mode.years,
|
||||||
|
),
|
||||||
|
onStep: (delta) {
|
||||||
|
if (_mode == _Mode.months) {
|
||||||
|
_setYear(_year + delta);
|
||||||
|
} else {
|
||||||
|
// Page the year grid by a screenful (12 years).
|
||||||
|
_setYearGridPage(delta);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedSize(
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
child: _mode == _Mode.months
|
||||||
|
? _MonthGrid(
|
||||||
|
selected: _month,
|
||||||
|
onSelect: _toggleMonth,
|
||||||
|
onClear: () => setState(() => _month = null),
|
||||||
|
)
|
||||||
|
: _YearGrid(
|
||||||
|
years: _yearPage,
|
||||||
|
selected: _year,
|
||||||
|
onSelect: _setYear,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(8, 4, 8, 8),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
key: const Key('harvestPicker.cancel'),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(t.common.cancel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
key: const Key('harvestPicker.ok'),
|
||||||
|
onPressed: () =>
|
||||||
|
Navigator.of(context).pop(HarvestDate(_year, _month)),
|
||||||
|
child: Text(t.common.save),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Year grid paging -----------------------------------------------------
|
||||||
|
|
||||||
|
// Top-left year of the visible page in years mode. Anchored to the current
|
||||||
|
// year so recent harvests show first; older years are one ‹ page away.
|
||||||
|
late int _yearPageAnchor = _maxYear;
|
||||||
|
|
||||||
|
List<int> get _yearPage {
|
||||||
|
// A page of up to 12 years, newest at the top-left, clamped to the range.
|
||||||
|
final start = _yearPageAnchor;
|
||||||
|
return [for (var y = start; y > start - 12 && y >= _minYear; y--) y];
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setYearGridPage(int delta) => setState(() {
|
||||||
|
// delta -1 (‹) → older page, +1 (›) → newer page.
|
||||||
|
_yearPageAnchor = (_yearPageAnchor + delta * 12).clamp(
|
||||||
|
_minYear + 11,
|
||||||
|
_maxYear,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Header extends StatelessWidget {
|
||||||
|
const _Header({required this.title, required this.selection});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String selection;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
color: seedGreen,
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title.toUpperCase(),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
letterSpacing: 1.2,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
selection,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 30,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NavRow extends StatelessWidget {
|
||||||
|
const _NavRow({
|
||||||
|
required this.year,
|
||||||
|
required this.mode,
|
||||||
|
required this.onToggleMode,
|
||||||
|
required this.onStep,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int year;
|
||||||
|
final _Mode mode;
|
||||||
|
final VoidCallback onToggleMode;
|
||||||
|
final ValueChanged<int> onStep;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
TextButton.icon(
|
||||||
|
key: const Key('harvestPicker.yearToggle'),
|
||||||
|
onPressed: onToggleMode,
|
||||||
|
label: Text('$year'),
|
||||||
|
icon: Icon(
|
||||||
|
mode == _Mode.years ? Icons.arrow_drop_up : Icons.arrow_drop_down,
|
||||||
|
),
|
||||||
|
iconAlignment: IconAlignment.end,
|
||||||
|
style: TextButton.styleFrom(foregroundColor: seedGreenDark),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
key: const Key('harvestPicker.prev'),
|
||||||
|
onPressed: () => onStep(-1),
|
||||||
|
icon: const Icon(Icons.chevron_left),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
key: const Key('harvestPicker.next'),
|
||||||
|
onPressed: () => onStep(1),
|
||||||
|
icon: const Icon(Icons.chevron_right),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MonthGrid extends StatelessWidget {
|
||||||
|
const _MonthGrid({
|
||||||
|
required this.selected,
|
||||||
|
required this.onSelect,
|
||||||
|
required this.onClear,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int? selected;
|
||||||
|
final ValueChanged<int> onSelect;
|
||||||
|
final VoidCallback onClear;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = context.t;
|
||||||
|
final names = t.harvest.monthNames;
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
GridView.count(
|
||||||
|
crossAxisCount: 3,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
childAspectRatio: 2.4,
|
||||||
|
children: [
|
||||||
|
for (var m = 1; m <= 12; m++)
|
||||||
|
_PickCell(
|
||||||
|
key: Key('harvestPicker.month.$m'),
|
||||||
|
label: names[m - 1],
|
||||||
|
selected: selected == m,
|
||||||
|
onTap: () => onSelect(m),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
key: const Key('harvestPicker.anyMonth'),
|
||||||
|
onPressed: selected == null ? null : onClear,
|
||||||
|
child: Text(t.harvest.anyMonth),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _YearGrid extends StatelessWidget {
|
||||||
|
const _YearGrid({
|
||||||
|
required this.years,
|
||||||
|
required this.selected,
|
||||||
|
required this.onSelect,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<int> years;
|
||||||
|
final int selected;
|
||||||
|
final ValueChanged<int> onSelect;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GridView.count(
|
||||||
|
crossAxisCount: 4,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 4, 12, 12),
|
||||||
|
childAspectRatio: 1.8,
|
||||||
|
children: [
|
||||||
|
for (final y in years)
|
||||||
|
_PickCell(
|
||||||
|
key: Key('harvestPicker.year.$y'),
|
||||||
|
label: '$y',
|
||||||
|
selected: y == selected,
|
||||||
|
onTap: () => onSelect(y),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A round-highlight cell used for both months and years, matching the seed
|
||||||
|
/// theme (green pill when selected).
|
||||||
|
class _PickCell extends StatelessWidget {
|
||||||
|
const _PickCell({
|
||||||
|
required this.label,
|
||||||
|
required this.selected,
|
||||||
|
required this.onTap,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final bool selected;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
child: Material(
|
||||||
|
color: selected ? seedGreen : Colors.transparent,
|
||||||
|
shape: const StadiumBorder(),
|
||||||
|
child: InkWell(
|
||||||
|
customBorder: const StadiumBorder(),
|
||||||
|
onTap: onTap,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: selected ? Colors.white : null,
|
||||||
|
fontWeight: selected ? FontWeight.w600 : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
import '../data/species_repository.dart';
|
import '../data/species_repository.dart';
|
||||||
import '../data/variety_repository.dart';
|
import '../data/variety_repository.dart';
|
||||||
import '../db/enums.dart';
|
import '../db/enums.dart';
|
||||||
import '../i18n/strings.g.dart';
|
import '../i18n/strings.g.dart';
|
||||||
import '../state/variety_detail_cubit.dart';
|
import '../state/variety_detail_cubit.dart';
|
||||||
|
import 'harvest_date_picker.dart';
|
||||||
import 'quantity_kind_l10n.dart';
|
import 'quantity_kind_l10n.dart';
|
||||||
import 'quantity_picker.dart';
|
import 'quantity_picker.dart';
|
||||||
import 'seed_glyph.dart';
|
import 'seed_glyph.dart';
|
||||||
|
|
@ -155,7 +157,7 @@ class _DetailView extends StatelessWidget {
|
||||||
String _lotSubtitle(Translations t, VarietyLot lot) {
|
String _lotSubtitle(Translations t, VarietyLot lot) {
|
||||||
final parts = <String>[
|
final parts = <String>[
|
||||||
if (lot.harvestYear != null)
|
if (lot.harvestYear != null)
|
||||||
t.detail.year(year: lot.harvestYear!)
|
harvestDateLabel(t, year: lot.harvestYear!, month: lot.harvestMonth)
|
||||||
else
|
else
|
||||||
t.detail.noYear,
|
t.detail.noYear,
|
||||||
if (lot.quantity != null) quantityDisplay(t, lot.quantity!),
|
if (lot.quantity != null) quantityDisplay(t, lot.quantity!),
|
||||||
|
|
@ -495,11 +497,8 @@ Future<void> _showLotSheet(
|
||||||
VarietyLot? existing,
|
VarietyLot? existing,
|
||||||
}) {
|
}) {
|
||||||
final t = context.t;
|
final t = context.t;
|
||||||
final currentYear = DateTime.now().year;
|
var selectedYear = existing?.harvestYear ?? DateTime.now().year;
|
||||||
// Harvest years: current year down through the last four decades. Newest
|
var selectedMonth = existing?.harvestMonth;
|
||||||
// first because a freshly harvested lot is the common case.
|
|
||||||
final years = [for (var y = currentYear; y >= currentYear - 40; y--) y];
|
|
||||||
var selectedYear = existing?.harvestYear ?? currentYear;
|
|
||||||
var selectedType = existing?.type ?? LotType.seed;
|
var selectedType = existing?.type ?? LotType.seed;
|
||||||
var selectedQuantity = existing?.quantity;
|
var selectedQuantity = existing?.quantity;
|
||||||
final editing = existing != null;
|
final editing = existing != null;
|
||||||
|
|
@ -547,20 +546,18 @@ Future<void> _showLotSheet(
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
DropdownButtonFormField<int>(
|
Text(
|
||||||
key: const Key('addLot.year'),
|
t.addLot.year,
|
||||||
initialValue: selectedYear,
|
style: Theme.of(sheetContext).textTheme.labelLarge,
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: t.addLot.year,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
),
|
),
|
||||||
items: [
|
const SizedBox(height: 8),
|
||||||
for (final y in years)
|
HarvestDateField(
|
||||||
DropdownMenuItem(value: y, child: Text('$y')),
|
year: selectedYear,
|
||||||
],
|
month: selectedMonth,
|
||||||
onChanged: (value) {
|
onChanged: (year, month) => setState(() {
|
||||||
if (value != null) setState(() => selectedYear = value);
|
selectedYear = year;
|
||||||
},
|
selectedMonth = month;
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -589,12 +586,14 @@ Future<void> _showLotSheet(
|
||||||
lotId: existing.id,
|
lotId: existing.id,
|
||||||
type: selectedType,
|
type: selectedType,
|
||||||
year: selectedYear,
|
year: selectedYear,
|
||||||
|
month: selectedMonth,
|
||||||
quantity: selectedQuantity,
|
quantity: selectedQuantity,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
cubit.addLot(
|
cubit.addLot(
|
||||||
type: selectedType,
|
type: selectedType,
|
||||||
year: selectedYear,
|
year: selectedYear,
|
||||||
|
month: selectedMonth,
|
||||||
quantity: selectedQuantity,
|
quantity: selectedQuantity,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -692,18 +691,166 @@ class _DetailHeader extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (detail.photo != null) ...[
|
|
||||||
if (hasText) const SizedBox(width: 12),
|
if (hasText) const SizedBox(width: 12),
|
||||||
|
_PhotoGallery(detail: detail),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A 140×140 photo carousel (mockup 07): swipe between photos, page dots, add a
|
||||||
|
/// photo, and delete the current one. Shows just an "add" button when empty.
|
||||||
|
class _PhotoGallery extends StatefulWidget {
|
||||||
|
const _PhotoGallery({required this.detail});
|
||||||
|
|
||||||
|
final VarietyDetail detail;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_PhotoGallery> createState() => _PhotoGalleryState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PhotoGalleryState extends State<_PhotoGallery> {
|
||||||
|
final _controller = PageController();
|
||||||
|
int _page = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _addPhoto(VarietyDetailCubit cubit) async {
|
||||||
|
final file = await ImagePicker().pickImage(
|
||||||
|
source: ImageSource.gallery,
|
||||||
|
maxWidth: 1280,
|
||||||
|
imageQuality: 80,
|
||||||
|
);
|
||||||
|
final bytes = await file?.readAsBytes();
|
||||||
|
if (bytes != null) await cubit.addPhoto(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cubit = context.read<VarietyDetailCubit>();
|
||||||
|
final photos = widget.detail.photos;
|
||||||
|
|
||||||
|
if (photos.isEmpty) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 140,
|
||||||
|
height: 140,
|
||||||
|
child: OutlinedButton(
|
||||||
|
key: const Key('photo.add'),
|
||||||
|
onPressed: () => _addPhoto(cubit),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.add_a_photo_outlined),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(context.t.quickAdd.addPhoto, textAlign: TextAlign.center),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final page = _page.clamp(0, photos.length - 1);
|
||||||
|
return SizedBox(
|
||||||
|
width: 140,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 140,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Image.memory(
|
child: PageView.builder(
|
||||||
detail.photo!,
|
controller: _controller,
|
||||||
|
itemCount: photos.length,
|
||||||
|
onPageChanged: (i) => setState(() => _page = i),
|
||||||
|
itemBuilder: (_, i) => Image.memory(
|
||||||
|
photos[i].bytes,
|
||||||
width: 140,
|
width: 140,
|
||||||
height: 140,
|
height: 140,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 2,
|
||||||
|
right: 2,
|
||||||
|
child: _CircleIconButton(
|
||||||
|
icon: Icons.close,
|
||||||
|
tooltip: context.t.common.delete,
|
||||||
|
onPressed: () => cubit.removePhoto(photos[page].id),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
if (photos.length > 1) _Dots(count: photos.length, active: page),
|
||||||
|
TextButton.icon(
|
||||||
|
key: const Key('photo.add'),
|
||||||
|
onPressed: () => _addPhoto(cubit),
|
||||||
|
icon: const Icon(Icons.add_a_photo_outlined, size: 18),
|
||||||
|
label: Text(context.t.quickAdd.addPhoto),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CircleIconButton extends StatelessWidget {
|
||||||
|
const _CircleIconButton({
|
||||||
|
required this.icon,
|
||||||
|
required this.onPressed,
|
||||||
|
this.tooltip,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
final String? tooltip;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
color: Colors.black45,
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
child: IconButton(
|
||||||
|
key: const Key('photo.delete'),
|
||||||
|
icon: Icon(icon, size: 18, color: Colors.white),
|
||||||
|
tooltip: tooltip,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onPressed: onPressed,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Dots extends StatelessWidget {
|
||||||
|
const _Dots({required this.count, required this.active});
|
||||||
|
|
||||||
|
final int count;
|
||||||
|
final int active;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
Container(
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: i == active ? seedGreen : Colors.black26,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,19 +11,30 @@ void main() {
|
||||||
verifier = SchemaVerifier(GeneratedHelper());
|
verifier = SchemaVerifier(GeneratedHelper());
|
||||||
});
|
});
|
||||||
|
|
||||||
test('freshly created database matches the exported schema v2', () async {
|
test('freshly created database matches the exported schema v3', () async {
|
||||||
final schema = await verifier.schemaAt(2);
|
final schema = await verifier.schemaAt(3);
|
||||||
final db = AppDatabase(schema.newConnection());
|
final db = AppDatabase(schema.newConnection());
|
||||||
await verifier.migrateAndValidate(db, 2);
|
await verifier.migrateAndValidate(db, 3);
|
||||||
await db.close();
|
await db.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'upgrades v1 → v2 (adds Lot.type) and matches the fresh schema',
|
'upgrades v1 → v3 (adds Lot.type, then harvestMonth) and matches the '
|
||||||
|
'fresh schema',
|
||||||
() async {
|
() async {
|
||||||
final connection = await verifier.startAt(1);
|
final connection = await verifier.startAt(1);
|
||||||
final db = AppDatabase(connection);
|
final db = AppDatabase(connection);
|
||||||
await verifier.migrateAndValidate(db, 2);
|
await verifier.migrateAndValidate(db, 3);
|
||||||
|
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();
|
await db.close();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:drift/drift.dart';
|
||||||
import 'package:drift/internal/migrations.dart';
|
import 'package:drift/internal/migrations.dart';
|
||||||
import 'schema_v1.dart' as v1;
|
import 'schema_v1.dart' as v1;
|
||||||
import 'schema_v2.dart' as v2;
|
import 'schema_v2.dart' as v2;
|
||||||
|
import 'schema_v3.dart' as v3;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
|
|
@ -15,10 +16,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
return v1.DatabaseAtV1(db);
|
return v1.DatabaseAtV1(db);
|
||||||
case 2:
|
case 2:
|
||||||
return v2.DatabaseAtV2(db);
|
return v2.DatabaseAtV2(db);
|
||||||
|
case 3:
|
||||||
|
return v3.DatabaseAtV3(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
throw MissingSchemaException(version, versions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const versions = const [1, 2];
|
static const versions = const [1, 2, 3];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1399
apps/app_seeds/test/db/schema/schema_v3.dart
Normal file
1399
apps/app_seeds/test/db/schema/schema_v3.dart
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -83,9 +83,13 @@ void main() {
|
||||||
await tester.tap(find.byKey(const Key('detail.addLot')));
|
await tester.tap(find.byKey(const Key('detail.addLot')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('addLot.year')));
|
await tester.tap(find.byKey(const Key('addLot.year'))); // open the picker
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.text('2023').last); // pick the year from the dropdown
|
await tester.tap(find.byKey(const Key('harvestPicker.yearToggle')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const Key('harvestPicker.year.2023')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const Key('harvestPicker.ok')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.text('spoon')); // pick the unit from the scale
|
await tester.tap(find.text('spoon')); // pick the unit from the scale
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
@ -222,9 +226,13 @@ void main() {
|
||||||
await tester.tap(find.text('Year 2020 · 1 packet')); // open the lot
|
await tester.tap(find.text('Year 2020 · 1 packet')); // open the lot
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('addLot.year')));
|
await tester.tap(find.byKey(const Key('addLot.year'))); // open the picker
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.text('2024').last);
|
await tester.tap(find.byKey(const Key('harvestPicker.yearToggle')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const Key('harvestPicker.year.2024')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const Key('harvestPicker.ok')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byKey(const Key('addLot.save')));
|
await tester.tap(find.byKey(const Key('addLot.save')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
@ -233,6 +241,44 @@ void main() {
|
||||||
await disposeTree(tester);
|
await disposeTree(tester);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('picking a harvest month shows month + year on the lot', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final repo = newTestRepository(db);
|
||||||
|
final id = await repo.addQuickVariety(label: 'Maize');
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
wrapDetail(
|
||||||
|
repository: repo,
|
||||||
|
varietyId: id,
|
||||||
|
species: newTestSpeciesRepository(db),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('detail.addLot')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('addLot.year'))); // open the picker
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
// Pick year 2022…
|
||||||
|
await tester.tap(find.byKey(const Key('harvestPicker.yearToggle')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const Key('harvestPicker.year.2022')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
// …then September (month 9).
|
||||||
|
await tester.tap(find.byKey(const Key('harvestPicker.month.9')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const Key('harvestPicker.ok')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('addLot.save')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('September 2022'), findsOneWidget);
|
||||||
|
await disposeTree(tester);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('deleting a lot removes it', (tester) async {
|
testWidgets('deleting a lot removes it', (tester) async {
|
||||||
final repo = newTestRepository(db);
|
final repo = newTestRepository(db);
|
||||||
final id = await repo.addQuickVariety(label: 'Maize');
|
final id = await repo.addQuickVariety(label: 'Maize');
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue