diff --git a/apps/app_seeds/drift_schemas/drift_schema_v3.json b/apps/app_seeds/drift_schemas/drift_schema_v3.json new file mode 100644 index 0000000..37142c4 --- /dev/null +++ b/apps/app_seeds/drift_schemas/drift_schema_v3.json @@ -0,0 +1,1481 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.3.0" + }, + "options": { + "store_date_time_values_as_text": false + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "varieties", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "label", + "getter_name": "label", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "species_id", + "getter_name": "speciesId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "cultivar_name", + "getter_name": "cultivarName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "category", + "getter_name": "category", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "notes", + "getter_name": "notes", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 1, + "references": [], + "type": "table", + "data": { + "name": "variety_vernacular_names", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "variety_id", + "getter_name": "varietyId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "language", + "getter_name": "language", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "region", + "getter_name": "region", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 2, + "references": [], + "type": "table", + "data": { + "name": "species", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "scientific_name", + "getter_name": "scientificName", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "wikidata_qid", + "getter_name": "wikidataQid", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "gbif_key", + "getter_name": "gbifKey", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "family", + "getter_name": "family", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_bundled", + "getter_name": "isBundled", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_bundled\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_bundled\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 3, + "references": [], + "type": "table", + "data": { + "name": "species_common_names", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "species_id", + "getter_name": "speciesId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "language", + "getter_name": "language", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 4, + "references": [], + "type": "table", + "data": { + "name": "lots", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "variety_id", + "getter_name": "varietyId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('\\'seed\\'')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(LotType.values)", + "dart_type_name": "LotType" + } + }, + { + "name": "harvest_year", + "getter_name": "harvestYear", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "harvest_month", + "getter_name": "harvestMonth", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_kind", + "getter_name": "quantityKind", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_precise", + "getter_name": "quantityPrecise", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_label", + "getter_name": "quantityLabel", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "storage_location", + "getter_name": "storageLocation", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "offer_status", + "getter_name": "offerStatus", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('\\'private\\'')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(OfferStatus.values)", + "dart_type_name": "OfferStatus" + } + }, + { + "name": "seedbank_id", + "getter_name": "seedbankId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 5, + "references": [], + "type": "table", + "data": { + "name": "germination_tests", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "lot_id", + "getter_name": "lotId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "tested_on", + "getter_name": "testedOn", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sample_size", + "getter_name": "sampleSize", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "germinated_count", + "getter_name": "germinatedCount", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "notes", + "getter_name": "notes", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 6, + "references": [], + "type": "table", + "data": { + "name": "movements", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "lot_id", + "getter_name": "lotId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(MovementType.values)", + "dart_type_name": "MovementType" + } + }, + { + "name": "occurred_on", + "getter_name": "occurredOn", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "counterparty_id", + "getter_name": "counterpartyId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_kind", + "getter_name": "quantityKind", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_precise", + "getter_name": "quantityPrecise", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_label", + "getter_name": "quantityLabel", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "parent_movement_id", + "getter_name": "parentMovementId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "plantare_id", + "getter_name": "plantareId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "notes", + "getter_name": "notes", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 7, + "references": [], + "type": "table", + "data": { + "name": "parties", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "display_name", + "getter_name": "displayName", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "public_key", + "getter_name": "publicKey", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "kind", + "getter_name": "kind", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('\\'person\\'')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(PartyKind.values)", + "dart_type_name": "PartyKind" + } + }, + { + "name": "note", + "getter_name": "note", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 8, + "references": [], + "type": "table", + "data": { + "name": "attachments", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "parent_type", + "getter_name": "parentType", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(ParentType.values)", + "dart_type_name": "ParentType" + } + }, + { + "name": "parent_id", + "getter_name": "parentId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "kind", + "getter_name": "kind", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(AttachmentKind.values)", + "dart_type_name": "AttachmentKind" + } + }, + { + "name": "uri", + "getter_name": "uri", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bytes", + "getter_name": "bytes", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "mime_type", + "getter_name": "mimeType", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 9, + "references": [], + "type": "table", + "data": { + "name": "external_links", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "parent_type", + "getter_name": "parentType", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(ParentType.values)", + "dart_type_name": "ParentType" + } + }, + { + "name": "parent_id", + "getter_name": "parentId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "url", + "getter_name": "url", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "title", + "getter_name": "title", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + } + ], + "fixed_sql": [ + { + "name": "varieties", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"varieties\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"label\" TEXT NOT NULL, \"species_id\" TEXT NULL, \"cultivar_name\" TEXT NULL, \"category\" TEXT NULL, \"notes\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "variety_vernacular_names", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"variety_vernacular_names\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"variety_id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"language\" TEXT NULL, \"region\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "species", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"species\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"scientific_name\" TEXT NOT NULL, \"wikidata_qid\" TEXT NULL, \"gbif_key\" INTEGER NULL, \"family\" TEXT NULL, \"is_bundled\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_bundled\" IN (0, 1)), PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "species_common_names", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"species_common_names\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"species_id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"language\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "lots", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"lots\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"variety_id\" TEXT NOT NULL, \"type\" TEXT NOT NULL DEFAULT 'seed', \"harvest_year\" INTEGER NULL, \"harvest_month\" INTEGER NULL, \"quantity_kind\" TEXT NULL, \"quantity_precise\" REAL NULL, \"quantity_label\" TEXT NULL, \"storage_location\" TEXT NULL, \"offer_status\" TEXT NOT NULL DEFAULT 'private', \"seedbank_id\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "germination_tests", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"germination_tests\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"lot_id\" TEXT NOT NULL, \"tested_on\" INTEGER NULL, \"sample_size\" INTEGER NULL, \"germinated_count\" INTEGER NULL, \"notes\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "movements", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"movements\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"last_author\" TEXT NOT NULL, \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"lot_id\" TEXT NOT NULL, \"type\" TEXT NOT NULL, \"occurred_on\" INTEGER NULL, \"counterparty_id\" TEXT NULL, \"quantity_kind\" TEXT NULL, \"quantity_precise\" REAL NULL, \"quantity_label\" TEXT NULL, \"parent_movement_id\" TEXT NULL, \"plantare_id\" TEXT NULL, \"notes\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "parties", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"parties\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"display_name\" TEXT NOT NULL, \"public_key\" TEXT NULL, \"kind\" TEXT NOT NULL DEFAULT 'person', \"note\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "attachments", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"attachments\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"parent_type\" TEXT NOT NULL, \"parent_id\" TEXT NOT NULL, \"kind\" TEXT NOT NULL, \"uri\" TEXT NULL, \"bytes\" BLOB NULL, \"mime_type\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "external_links", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"external_links\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"parent_type\" TEXT NOT NULL, \"parent_id\" TEXT NOT NULL, \"url\" TEXT NOT NULL, \"title\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + } + ] +} \ No newline at end of file diff --git a/apps/app_seeds/lib/data/variety_repository.dart b/apps/app_seeds/lib/data/variety_repository.dart index d2aaba1..597d109 100644 --- a/apps/app_seeds/lib/data/variety_repository.dart +++ b/apps/app_seeds/lib/data/variety_repository.dart @@ -65,6 +65,7 @@ class VarietyLot extends Equatable { required this.id, this.type = LotType.seed, this.harvestYear, + this.harvestMonth, this.quantity, this.storageLocation, this.germinationTests = const [], @@ -73,6 +74,9 @@ class VarietyLot extends Equatable { final String id; final LotType type; final int? harvestYear; + + /// Optional harvest month (1..12). Only meaningful when [harvestYear] is set. + final int? harvestMonth; final Quantity? quantity; final String? storageLocation; final List germinationTests; @@ -86,6 +90,7 @@ class VarietyLot extends Equatable { id, type, harvestYear, + harvestMonth, quantity, storageLocation, germinationTests, @@ -110,7 +115,18 @@ class VernacularName extends Equatable { List 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 get props => [id, bytes]; +} + +/// The full detail of one variety (identity + its lots, names and photos). class VarietyDetail extends Equatable { const VarietyDetail({ required this.id, @@ -121,7 +137,7 @@ class VarietyDetail extends Equatable { this.scientificName, this.lots = const [], this.vernacularNames = const [], - this.photo, + this.photos = const [], }); final String id; @@ -132,7 +148,7 @@ class VarietyDetail extends Equatable { final String? scientificName; final List lots; final List vernacularNames; - final Uint8List? photo; + final List photos; @override List get props => [ @@ -144,7 +160,7 @@ class VarietyDetail extends Equatable { scientificName, lots, vernacularNames, - photo, + photos, ]; } @@ -380,7 +396,7 @@ class VarietyRepository { a.kind.equalsValue(AttachmentKind.photo) & a.isDeleted.equals(false), ) - ..limit(1)) + ..orderBy([(a) => OrderingTerm(expression: a.createdAt)])) .get(); return VarietyDetail( @@ -401,7 +417,10 @@ class VarietyRepository { ), ) .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, LotType type = LotType.seed, int? harvestYear, + int? harvestMonth, Quantity? quantity, String? storageLocation, }) async { @@ -473,6 +493,7 @@ class VarietyRepository { lastAuthor: nodeId, type: Value(type), harvestYear: Value(harvestYear), + harvestMonth: Value(harvestMonth), quantityKind: Value(quantity?.kind.name), quantityPrecise: Value(quantity?.count?.toDouble()), quantityLabel: Value(quantity?.label), @@ -482,12 +503,13 @@ class VarietyRepository { return id; } - /// Updates an existing lot's fields (LWW). The [quantity] and [harvestYear] - /// are set as given (null clears them). + /// Updates an existing lot's fields (LWW). The [quantity], [harvestYear] and + /// [harvestMonth] are set as given (null clears them). Future updateLot({ required String lotId, required LotType type, int? harvestYear, + int? harvestMonth, Quantity? quantity, String? storageLocation, }) async { @@ -496,6 +518,7 @@ class VarietyRepository { LotsCompanion( type: Value(type), harvestYear: Value(harvestYear), + harvestMonth: Value(harvestMonth), quantityKind: Value(quantity?.kind.name), quantityPrecise: Value(quantity?.count?.toDouble()), quantityLabel: Value(quantity?.label), @@ -558,6 +581,42 @@ class VarietyRepository { ); } + /// Adds a photo (encrypted BLOB) to a variety. Returns the new attachment id. + Future 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 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 /// the row survives for correct CRDT merges later. Future softDeleteVariety(String id) async { @@ -608,6 +667,7 @@ class VarietyRepository { id: l.id, type: l.type, harvestYear: l.harvestYear, + harvestMonth: l.harvestMonth, storageLocation: l.storageLocation, germinationTests: germinationTests, quantity: hasQuantity diff --git a/apps/app_seeds/lib/db/database.dart b/apps/app_seeds/lib/db/database.dart index 95dff5f..29b1815 100644 --- a/apps/app_seeds/lib/db/database.dart +++ b/apps/app_seeds/lib/db/database.dart @@ -27,7 +27,7 @@ class AppDatabase extends _$AppDatabase { AppDatabase(super.e); @override - int get schemaVersion => 2; + int get schemaVersion => 3; @override MigrationStrategy get migration => MigrationStrategy( @@ -37,6 +37,10 @@ class AppDatabase extends _$AppDatabase { if (from < 2) { await m.addColumn(lots, lots.type); } + // v3: optional harvest month alongside the harvest year. + if (from < 3) { + await m.addColumn(lots, lots.harvestMonth); + } }, ); } diff --git a/apps/app_seeds/lib/db/database.g.dart b/apps/app_seeds/lib/db/database.g.dart index 0c4329c..1e6d62c 100644 --- a/apps/app_seeds/lib/db/database.g.dart +++ b/apps/app_seeds/lib/db/database.g.dart @@ -2631,6 +2631,17 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> { type: DriftSqlType.int, requiredDuringInsert: false, ); + static const VerificationMeta _harvestMonthMeta = const VerificationMeta( + 'harvestMonth', + ); + @override + late final GeneratedColumn harvestMonth = GeneratedColumn( + 'harvest_month', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); static const VerificationMeta _quantityKindMeta = const VerificationMeta( 'quantityKind', ); @@ -2707,6 +2718,7 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> { varietyId, type, harvestYear, + harvestMonth, quantityKind, quantityPrecise, 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')) { context.handle( _quantityKindMeta, @@ -2876,6 +2897,10 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> { DriftSqlType.int, data['${effectivePrefix}harvest_year'], ), + harvestMonth: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}harvest_month'], + ), quantityKind: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}quantity_kind'], @@ -2926,6 +2951,7 @@ class Lot extends DataClass implements Insertable { final String varietyId; final LotType type; final int? harvestYear; + final int? harvestMonth; final String? quantityKind; final double? quantityPrecise; final String? quantityLabel; @@ -2942,6 +2968,7 @@ class Lot extends DataClass implements Insertable { required this.varietyId, required this.type, this.harvestYear, + this.harvestMonth, this.quantityKind, this.quantityPrecise, this.quantityLabel, @@ -2965,6 +2992,9 @@ class Lot extends DataClass implements Insertable { if (!nullToAbsent || harvestYear != null) { map['harvest_year'] = Variable(harvestYear); } + if (!nullToAbsent || harvestMonth != null) { + map['harvest_month'] = Variable(harvestMonth); + } if (!nullToAbsent || quantityKind != null) { map['quantity_kind'] = Variable(quantityKind); } @@ -3001,6 +3031,9 @@ class Lot extends DataClass implements Insertable { harvestYear: harvestYear == null && nullToAbsent ? const Value.absent() : Value(harvestYear), + harvestMonth: harvestMonth == null && nullToAbsent + ? const Value.absent() + : Value(harvestMonth), quantityKind: quantityKind == null && nullToAbsent ? const Value.absent() : Value(quantityKind), @@ -3037,6 +3070,7 @@ class Lot extends DataClass implements Insertable { serializer.fromJson(json['type']), ), harvestYear: serializer.fromJson(json['harvestYear']), + harvestMonth: serializer.fromJson(json['harvestMonth']), quantityKind: serializer.fromJson(json['quantityKind']), quantityPrecise: serializer.fromJson(json['quantityPrecise']), quantityLabel: serializer.fromJson(json['quantityLabel']), @@ -3060,6 +3094,7 @@ class Lot extends DataClass implements Insertable { 'varietyId': serializer.toJson(varietyId), 'type': serializer.toJson($LotsTable.$convertertype.toJson(type)), 'harvestYear': serializer.toJson(harvestYear), + 'harvestMonth': serializer.toJson(harvestMonth), 'quantityKind': serializer.toJson(quantityKind), 'quantityPrecise': serializer.toJson(quantityPrecise), 'quantityLabel': serializer.toJson(quantityLabel), @@ -3081,6 +3116,7 @@ class Lot extends DataClass implements Insertable { String? varietyId, LotType? type, Value harvestYear = const Value.absent(), + Value harvestMonth = const Value.absent(), Value quantityKind = const Value.absent(), Value quantityPrecise = const Value.absent(), Value quantityLabel = const Value.absent(), @@ -3097,6 +3133,7 @@ class Lot extends DataClass implements Insertable { varietyId: varietyId ?? this.varietyId, type: type ?? this.type, harvestYear: harvestYear.present ? harvestYear.value : this.harvestYear, + harvestMonth: harvestMonth.present ? harvestMonth.value : this.harvestMonth, quantityKind: quantityKind.present ? quantityKind.value : this.quantityKind, quantityPrecise: quantityPrecise.present ? quantityPrecise.value @@ -3127,6 +3164,9 @@ class Lot extends DataClass implements Insertable { harvestYear: data.harvestYear.present ? data.harvestYear.value : this.harvestYear, + harvestMonth: data.harvestMonth.present + ? data.harvestMonth.value + : this.harvestMonth, quantityKind: data.quantityKind.present ? data.quantityKind.value : this.quantityKind, @@ -3160,6 +3200,7 @@ class Lot extends DataClass implements Insertable { ..write('varietyId: $varietyId, ') ..write('type: $type, ') ..write('harvestYear: $harvestYear, ') + ..write('harvestMonth: $harvestMonth, ') ..write('quantityKind: $quantityKind, ') ..write('quantityPrecise: $quantityPrecise, ') ..write('quantityLabel: $quantityLabel, ') @@ -3181,6 +3222,7 @@ class Lot extends DataClass implements Insertable { varietyId, type, harvestYear, + harvestMonth, quantityKind, quantityPrecise, quantityLabel, @@ -3201,6 +3243,7 @@ class Lot extends DataClass implements Insertable { other.varietyId == this.varietyId && other.type == this.type && other.harvestYear == this.harvestYear && + other.harvestMonth == this.harvestMonth && other.quantityKind == this.quantityKind && other.quantityPrecise == this.quantityPrecise && other.quantityLabel == this.quantityLabel && @@ -3219,6 +3262,7 @@ class LotsCompanion extends UpdateCompanion { final Value varietyId; final Value type; final Value harvestYear; + final Value harvestMonth; final Value quantityKind; final Value quantityPrecise; final Value quantityLabel; @@ -3236,6 +3280,7 @@ class LotsCompanion extends UpdateCompanion { this.varietyId = const Value.absent(), this.type = const Value.absent(), this.harvestYear = const Value.absent(), + this.harvestMonth = const Value.absent(), this.quantityKind = const Value.absent(), this.quantityPrecise = const Value.absent(), this.quantityLabel = const Value.absent(), @@ -3254,6 +3299,7 @@ class LotsCompanion extends UpdateCompanion { required String varietyId, this.type = const Value.absent(), this.harvestYear = const Value.absent(), + this.harvestMonth = const Value.absent(), this.quantityKind = const Value.absent(), this.quantityPrecise = const Value.absent(), this.quantityLabel = const Value.absent(), @@ -3276,6 +3322,7 @@ class LotsCompanion extends UpdateCompanion { Expression? varietyId, Expression? type, Expression? harvestYear, + Expression? harvestMonth, Expression? quantityKind, Expression? quantityPrecise, Expression? quantityLabel, @@ -3294,6 +3341,7 @@ class LotsCompanion extends UpdateCompanion { if (varietyId != null) 'variety_id': varietyId, if (type != null) 'type': type, if (harvestYear != null) 'harvest_year': harvestYear, + if (harvestMonth != null) 'harvest_month': harvestMonth, if (quantityKind != null) 'quantity_kind': quantityKind, if (quantityPrecise != null) 'quantity_precise': quantityPrecise, if (quantityLabel != null) 'quantity_label': quantityLabel, @@ -3314,6 +3362,7 @@ class LotsCompanion extends UpdateCompanion { Value? varietyId, Value? type, Value? harvestYear, + Value? harvestMonth, Value? quantityKind, Value? quantityPrecise, Value? quantityLabel, @@ -3332,6 +3381,7 @@ class LotsCompanion extends UpdateCompanion { varietyId: varietyId ?? this.varietyId, type: type ?? this.type, harvestYear: harvestYear ?? this.harvestYear, + harvestMonth: harvestMonth ?? this.harvestMonth, quantityKind: quantityKind ?? this.quantityKind, quantityPrecise: quantityPrecise ?? this.quantityPrecise, quantityLabel: quantityLabel ?? this.quantityLabel, @@ -3374,6 +3424,9 @@ class LotsCompanion extends UpdateCompanion { if (harvestYear.present) { map['harvest_year'] = Variable(harvestYear.value); } + if (harvestMonth.present) { + map['harvest_month'] = Variable(harvestMonth.value); + } if (quantityKind.present) { map['quantity_kind'] = Variable(quantityKind.value); } @@ -3412,6 +3465,7 @@ class LotsCompanion extends UpdateCompanion { ..write('varietyId: $varietyId, ') ..write('type: $type, ') ..write('harvestYear: $harvestYear, ') + ..write('harvestMonth: $harvestMonth, ') ..write('quantityKind: $quantityKind, ') ..write('quantityPrecise: $quantityPrecise, ') ..write('quantityLabel: $quantityLabel, ') @@ -8188,6 +8242,7 @@ typedef $$LotsTableCreateCompanionBuilder = required String varietyId, Value type, Value harvestYear, + Value harvestMonth, Value quantityKind, Value quantityPrecise, Value quantityLabel, @@ -8207,6 +8262,7 @@ typedef $$LotsTableUpdateCompanionBuilder = Value varietyId, Value type, Value harvestYear, + Value harvestMonth, Value quantityKind, Value quantityPrecise, Value quantityLabel, @@ -8270,6 +8326,11 @@ class $$LotsTableFilterComposer extends Composer<_$AppDatabase, $LotsTable> { builder: (column) => ColumnFilters(column), ); + ColumnFilters get harvestMonth => $composableBuilder( + column: $table.harvestMonth, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get quantityKind => $composableBuilder( column: $table.quantityKind, builder: (column) => ColumnFilters(column), @@ -8355,6 +8416,11 @@ class $$LotsTableOrderingComposer extends Composer<_$AppDatabase, $LotsTable> { builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get harvestMonth => $composableBuilder( + column: $table.harvestMonth, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get quantityKind => $composableBuilder( column: $table.quantityKind, builder: (column) => ColumnOrderings(column), @@ -8428,6 +8494,11 @@ class $$LotsTableAnnotationComposer builder: (column) => column, ); + GeneratedColumn get harvestMonth => $composableBuilder( + column: $table.harvestMonth, + builder: (column) => column, + ); + GeneratedColumn get quantityKind => $composableBuilder( column: $table.quantityKind, builder: (column) => column, @@ -8497,6 +8568,7 @@ class $$LotsTableTableManager Value varietyId = const Value.absent(), Value type = const Value.absent(), Value harvestYear = const Value.absent(), + Value harvestMonth = const Value.absent(), Value quantityKind = const Value.absent(), Value quantityPrecise = const Value.absent(), Value quantityLabel = const Value.absent(), @@ -8514,6 +8586,7 @@ class $$LotsTableTableManager varietyId: varietyId, type: type, harvestYear: harvestYear, + harvestMonth: harvestMonth, quantityKind: quantityKind, quantityPrecise: quantityPrecise, quantityLabel: quantityLabel, @@ -8533,6 +8606,7 @@ class $$LotsTableTableManager required String varietyId, Value type = const Value.absent(), Value harvestYear = const Value.absent(), + Value harvestMonth = const Value.absent(), Value quantityKind = const Value.absent(), Value quantityPrecise = const Value.absent(), Value quantityLabel = const Value.absent(), @@ -8550,6 +8624,7 @@ class $$LotsTableTableManager varietyId: varietyId, type: type, harvestYear: harvestYear, + harvestMonth: harvestMonth, quantityKind: quantityKind, quantityPrecise: quantityPrecise, quantityLabel: quantityLabel, diff --git a/apps/app_seeds/lib/db/tables.dart b/apps/app_seeds/lib/db/tables.dart index 9f43296..3890992 100644 --- a/apps/app_seeds/lib/db/tables.dart +++ b/apps/app_seeds/lib/db/tables.dart @@ -46,6 +46,7 @@ class Lots extends Table with SyncColumns { TextColumn get type => textEnum().withDefault(const Constant('seed'))(); IntColumn get harvestYear => integer().nullable()(); + IntColumn get harvestMonth => integer().nullable()(); // 1..12, optional TextColumn get quantityKind => text().nullable()(); // QuantityKind.name RealColumn get quantityPrecise => real().nullable()(); TextColumn get quantityLabel => text().nullable()(); diff --git a/apps/app_seeds/lib/i18n/en.i18n.json b/apps/app_seeds/lib/i18n/en.i18n.json index 11265d8..b5cb8f8 100644 --- a/apps/app_seeds/lib/i18n/en.i18n.json +++ b/apps/app_seeds/lib/i18n/en.i18n.json @@ -56,10 +56,19 @@ }, "addLot": { "title": "Add lot", - "year": "Harvest year", + "year": "Harvest date", "quantity": "How much?", "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": { "seed": "Seeds", "plant": "Plants" diff --git a/apps/app_seeds/lib/i18n/es.i18n.json b/apps/app_seeds/lib/i18n/es.i18n.json index e5bc1a6..3254e46 100644 --- a/apps/app_seeds/lib/i18n/es.i18n.json +++ b/apps/app_seeds/lib/i18n/es.i18n.json @@ -56,10 +56,19 @@ }, "addLot": { "title": "Añadir lote", - "year": "Año de cosecha", + "year": "Fecha de cosecha", "quantity": "¿Cuánta?", "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": { "seed": "Semillas", "plant": "Plantel" diff --git a/apps/app_seeds/lib/i18n/strings.g.dart b/apps/app_seeds/lib/i18n/strings.g.dart index 5adc61f..af449a3 100644 --- a/apps/app_seeds/lib/i18n/strings.g.dart +++ b/apps/app_seeds/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// 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 // ignore_for_file: type=lint, unused_import diff --git a/apps/app_seeds/lib/i18n/strings_en.g.dart b/apps/app_seeds/lib/i18n/strings_en.g.dart index 4cbd619..1baccc0 100644 --- a/apps/app_seeds/lib/i18n/strings_en.g.dart +++ b/apps/app_seeds/lib/i18n/strings_en.g.dart @@ -48,6 +48,7 @@ class Translations with BaseTranslations { 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$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$unit$en unit = Translations$unit$en.internal(_root); } @@ -249,8 +250,8 @@ class Translations$addLot$en { /// en: 'Add lot' String get title => 'Add lot'; - /// en: 'Harvest year' - String get year => 'Harvest year'; + /// en: 'Harvest date' + String get year => 'Harvest date'; /// en: 'How much?' String get quantity => 'How much?'; @@ -259,6 +260,39 @@ class Translations$addLot$en { 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 get monthNames => [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; +} + // Path: lotType class Translations$lotType$en { Translations$lotType$en.internal(this._root); @@ -682,9 +716,24 @@ extension on Translations { 'editVariety.species' => 'Species (from catalog)', 'editVariety.speciesHint' => 'Search a species…', 'addLot.title' => 'Add lot', - 'addLot.year' => 'Harvest year', + 'addLot.year' => 'Harvest date', 'addLot.quantity' => 'How much?', '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.plant' => 'Plants', 'unit.aFew' => 'a few', diff --git a/apps/app_seeds/lib/i18n/strings_es.g.dart b/apps/app_seeds/lib/i18n/strings_es.g.dart index 8f101a5..a2a64c2 100644 --- a/apps/app_seeds/lib/i18n/strings_es.g.dart +++ b/apps/app_seeds/lib/i18n/strings_es.g.dart @@ -47,6 +47,7 @@ class TranslationsEs extends Translations with BaseTranslations '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 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 get monthNames => [ + 'enero', + 'febrero', + 'marzo', + 'abril', + 'mayo', + 'junio', + 'julio', + 'agosto', + 'septiembre', + 'octubre', + 'noviembre', + 'diciembre', + ]; +} + // Path: lotType class _Translations$lotType$es extends Translations$lotType$en { _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.speciesHint' => 'Buscar una especie…', 'addLot.title' => 'Añadir lote', - 'addLot.year' => 'Año de cosecha', + 'addLot.year' => 'Fecha de cosecha', 'addLot.quantity' => '¿Cuánta?', '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.plant' => 'Plantel', 'unit.aFew' => 'unas pocas', diff --git a/apps/app_seeds/lib/state/variety_detail_cubit.dart b/apps/app_seeds/lib/state/variety_detail_cubit.dart index 7c43c9a..078118c 100644 --- a/apps/app_seeds/lib/state/variety_detail_cubit.dart +++ b/apps/app_seeds/lib/state/variety_detail_cubit.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:typed_data'; import 'package:commons_core/commons_core.dart'; import 'package:equatable/equatable.dart'; @@ -54,11 +55,13 @@ class VarietyDetailCubit extends Cubit { Future addLot({ LotType type = LotType.seed, int? year, + int? month, Quantity? quantity, }) => _repo.addLot( varietyId: varietyId, type: type, harvestYear: year, + harvestMonth: month, quantity: quantity, ); @@ -66,11 +69,13 @@ class VarietyDetailCubit extends Cubit { required String lotId, required LotType type, int? year, + int? month, Quantity? quantity, }) => _repo.updateLot( lotId: lotId, type: type, harvestYear: year, + harvestMonth: month, quantity: quantity, ); @@ -82,6 +87,11 @@ class VarietyDetailCubit extends Cubit { Future removeVernacularName(String nameId) => _repo.removeVernacularName(nameId); + Future addPhoto(Uint8List bytes) => _repo.addPhoto(varietyId, bytes); + + Future removePhoto(String attachmentId) => + _repo.removePhoto(attachmentId); + Future linkSpecies(String speciesId) => _repo.linkSpecies(varietyId, speciesId); diff --git a/apps/app_seeds/lib/ui/harvest_date_picker.dart b/apps/app_seeds/lib/ui/harvest_date_picker.dart new file mode 100644 index 0000000..122013b --- /dev/null +++ b/apps/app_seeds/lib/ui/harvest_date_picker.dart @@ -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 showHarvestDatePicker( + BuildContext context, { + required HarvestDate initial, +}) { + return showDialog( + 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 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 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 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 years; + final int selected; + final ValueChanged 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, + ), + ), + ), + ), + ), + ); + } +} diff --git a/apps/app_seeds/lib/ui/variety_detail_screen.dart b/apps/app_seeds/lib/ui/variety_detail_screen.dart index f732527..533534a 100644 --- a/apps/app_seeds/lib/ui/variety_detail_screen.dart +++ b/apps/app_seeds/lib/ui/variety_detail_screen.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:image_picker/image_picker.dart'; import '../data/species_repository.dart'; import '../data/variety_repository.dart'; import '../db/enums.dart'; import '../i18n/strings.g.dart'; import '../state/variety_detail_cubit.dart'; +import 'harvest_date_picker.dart'; import 'quantity_kind_l10n.dart'; import 'quantity_picker.dart'; import 'seed_glyph.dart'; @@ -155,7 +157,7 @@ class _DetailView extends StatelessWidget { String _lotSubtitle(Translations t, VarietyLot lot) { final parts = [ if (lot.harvestYear != null) - t.detail.year(year: lot.harvestYear!) + harvestDateLabel(t, year: lot.harvestYear!, month: lot.harvestMonth) else t.detail.noYear, if (lot.quantity != null) quantityDisplay(t, lot.quantity!), @@ -495,11 +497,8 @@ Future _showLotSheet( VarietyLot? existing, }) { final t = context.t; - final currentYear = DateTime.now().year; - // Harvest years: current year down through the last four decades. Newest - // 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 selectedYear = existing?.harvestYear ?? DateTime.now().year; + var selectedMonth = existing?.harvestMonth; var selectedType = existing?.type ?? LotType.seed; var selectedQuantity = existing?.quantity; final editing = existing != null; @@ -547,20 +546,18 @@ Future _showLotSheet( }), ), const SizedBox(height: 12), - DropdownButtonFormField( - key: const Key('addLot.year'), - initialValue: selectedYear, - decoration: InputDecoration( - labelText: t.addLot.year, - border: const OutlineInputBorder(), - ), - items: [ - for (final y in years) - DropdownMenuItem(value: y, child: Text('$y')), - ], - onChanged: (value) { - if (value != null) setState(() => selectedYear = value); - }, + Text( + t.addLot.year, + style: Theme.of(sheetContext).textTheme.labelLarge, + ), + const SizedBox(height: 8), + HarvestDateField( + year: selectedYear, + month: selectedMonth, + onChanged: (year, month) => setState(() { + selectedYear = year; + selectedMonth = month; + }), ), const SizedBox(height: 16), Text( @@ -589,12 +586,14 @@ Future _showLotSheet( lotId: existing.id, type: selectedType, year: selectedYear, + month: selectedMonth, quantity: selectedQuantity, ); } else { cubit.addLot( type: selectedType, year: selectedYear, + month: selectedMonth, quantity: selectedQuantity, ); } @@ -692,18 +691,166 @@ class _DetailHeader extends StatelessWidget { ], ), ), - if (detail.photo != null) ...[ - if (hasText) const SizedBox(width: 12), - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image.memory( - detail.photo!, - width: 140, - height: 140, - fit: BoxFit.cover, + 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 _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(); + 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( + borderRadius: BorderRadius.circular(12), + child: PageView.builder( + controller: _controller, + itemCount: photos.length, + onPageChanged: (i) => setState(() => _page = i), + itemBuilder: (_, i) => Image.memory( + photos[i].bytes, + width: 140, + height: 140, + 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, + ), + ), ], ); } diff --git a/apps/app_seeds/test/db/migration_test.dart b/apps/app_seeds/test/db/migration_test.dart index 09e1c8f..d1740ed 100644 --- a/apps/app_seeds/test/db/migration_test.dart +++ b/apps/app_seeds/test/db/migration_test.dart @@ -11,19 +11,30 @@ void main() { verifier = SchemaVerifier(GeneratedHelper()); }); - test('freshly created database matches the exported schema v2', () async { - final schema = await verifier.schemaAt(2); + test('freshly created database matches the exported schema v3', () async { + final schema = await verifier.schemaAt(3); final db = AppDatabase(schema.newConnection()); - await verifier.migrateAndValidate(db, 2); + await verifier.migrateAndValidate(db, 3); await db.close(); }); 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 { final connection = await verifier.startAt(1); 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(); }, ); diff --git a/apps/app_seeds/test/db/schema/schema.dart b/apps/app_seeds/test/db/schema/schema.dart index f5e870e..f7feae4 100644 --- a/apps/app_seeds/test/db/schema/schema.dart +++ b/apps/app_seeds/test/db/schema/schema.dart @@ -6,6 +6,7 @@ import 'package:drift/drift.dart'; import 'package:drift/internal/migrations.dart'; import 'schema_v1.dart' as v1; import 'schema_v2.dart' as v2; +import 'schema_v3.dart' as v3; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -15,10 +16,12 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v1.DatabaseAtV1(db); case 2: return v2.DatabaseAtV2(db); + case 3: + return v3.DatabaseAtV3(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [1, 2]; + static const versions = const [1, 2, 3]; } diff --git a/apps/app_seeds/test/db/schema/schema_v3.dart b/apps/app_seeds/test/db/schema/schema_v3.dart new file mode 100644 index 0000000..82aec0e --- /dev/null +++ b/apps/app_seeds/test/db/schema/schema_v3.dart @@ -0,0 +1,1399 @@ +// dart format width=80 +import 'dart:typed_data' as i2; +// GENERATED BY drift_dev, DO NOT MODIFY. +// ignore_for_file: type=lint,unused_import +// +import 'package:drift/drift.dart'; + +class Varieties extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Varieties(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn label = GeneratedColumn( + 'label', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn speciesId = GeneratedColumn( + 'species_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn cultivarName = GeneratedColumn( + 'cultivar_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn category = GeneratedColumn( + 'category', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn notes = GeneratedColumn( + 'notes', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + label, + speciesId, + cultivarName, + category, + notes, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'varieties'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Varieties createAlias(String alias) { + return Varieties(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class VarietyVernacularNames extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + VarietyVernacularNames(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn varietyId = GeneratedColumn( + 'variety_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn language = GeneratedColumn( + 'language', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn region = GeneratedColumn( + 'region', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + varietyId, + name, + language, + region, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'variety_vernacular_names'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + VarietyVernacularNames createAlias(String alias) { + return VarietyVernacularNames(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Species extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Species(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn scientificName = GeneratedColumn( + 'scientific_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn wikidataQid = GeneratedColumn( + 'wikidata_qid', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn gbifKey = GeneratedColumn( + 'gbif_key', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn family = GeneratedColumn( + 'family', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn isBundled = GeneratedColumn( + 'is_bundled', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_bundled IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + scientificName, + wikidataQid, + gbifKey, + family, + isBundled, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'species'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Species createAlias(String alias) { + return Species(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class SpeciesCommonNames extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SpeciesCommonNames(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn speciesId = GeneratedColumn( + 'species_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn language = GeneratedColumn( + 'language', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + speciesId, + name, + language, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'species_common_names'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + SpeciesCommonNames createAlias(String alias) { + return SpeciesCommonNames(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Lots extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Lots(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn varietyId = GeneratedColumn( + 'variety_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT \'seed\'', + defaultValue: const CustomExpression('\'seed\''), + ); + late final GeneratedColumn harvestYear = GeneratedColumn( + 'harvest_year', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn harvestMonth = GeneratedColumn( + 'harvest_month', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityKind = GeneratedColumn( + 'quantity_kind', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityPrecise = GeneratedColumn( + 'quantity_precise', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityLabel = GeneratedColumn( + 'quantity_label', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn storageLocation = GeneratedColumn( + 'storage_location', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn offerStatus = GeneratedColumn( + 'offer_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT \'private\'', + defaultValue: const CustomExpression('\'private\''), + ); + late final GeneratedColumn seedbankId = GeneratedColumn( + 'seedbank_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + varietyId, + type, + harvestYear, + harvestMonth, + quantityKind, + quantityPrecise, + quantityLabel, + storageLocation, + offerStatus, + seedbankId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'lots'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Lots createAlias(String alias) { + return Lots(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class GerminationTests extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + GerminationTests(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn lotId = GeneratedColumn( + 'lot_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn testedOn = GeneratedColumn( + 'tested_on', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn sampleSize = GeneratedColumn( + 'sample_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn germinatedCount = GeneratedColumn( + 'germinated_count', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn notes = GeneratedColumn( + 'notes', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + lotId, + testedOn, + sampleSize, + germinatedCount, + notes, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'germination_tests'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + GerminationTests createAlias(String alias) { + return GerminationTests(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Movements extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Movements(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn lotId = GeneratedColumn( + 'lot_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn occurredOn = GeneratedColumn( + 'occurred_on', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn counterpartyId = GeneratedColumn( + 'counterparty_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityKind = GeneratedColumn( + 'quantity_kind', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityPrecise = GeneratedColumn( + 'quantity_precise', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityLabel = GeneratedColumn( + 'quantity_label', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn parentMovementId = GeneratedColumn( + 'parent_movement_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn plantareId = GeneratedColumn( + 'plantare_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn notes = GeneratedColumn( + 'notes', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + lastAuthor, + schemaRowVersion, + lotId, + type, + occurredOn, + counterpartyId, + quantityKind, + quantityPrecise, + quantityLabel, + parentMovementId, + plantareId, + notes, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'movements'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Movements createAlias(String alias) { + return Movements(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Parties extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Parties(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn publicKey = GeneratedColumn( + 'public_key', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn kind = GeneratedColumn( + 'kind', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT \'person\'', + defaultValue: const CustomExpression('\'person\''), + ); + late final GeneratedColumn note = GeneratedColumn( + 'note', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + displayName, + publicKey, + kind, + note, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'parties'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Parties createAlias(String alias) { + return Parties(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Attachments extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Attachments(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn parentType = GeneratedColumn( + 'parent_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn parentId = GeneratedColumn( + 'parent_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn kind = GeneratedColumn( + 'kind', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn uri = GeneratedColumn( + 'uri', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn bytes = + GeneratedColumn( + 'bytes', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn mimeType = GeneratedColumn( + 'mime_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + parentType, + parentId, + kind, + uri, + bytes, + mimeType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'attachments'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Attachments createAlias(String alias) { + return Attachments(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class ExternalLinks extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ExternalLinks(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn parentType = GeneratedColumn( + 'parent_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn parentId = GeneratedColumn( + 'parent_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn url = GeneratedColumn( + 'url', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn title = GeneratedColumn( + 'title', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + parentType, + parentId, + url, + title, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'external_links'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + ExternalLinks createAlias(String alias) { + return ExternalLinks(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class DatabaseAtV3 extends GeneratedDatabase { + DatabaseAtV3(QueryExecutor e) : super(e); + late final Varieties varieties = Varieties(this); + late final VarietyVernacularNames varietyVernacularNames = + VarietyVernacularNames(this); + late final Species species = Species(this); + late final SpeciesCommonNames speciesCommonNames = SpeciesCommonNames(this); + late final Lots lots = Lots(this); + late final GerminationTests germinationTests = GerminationTests(this); + late final Movements movements = Movements(this); + late final Parties parties = Parties(this); + late final Attachments attachments = Attachments(this); + late final ExternalLinks externalLinks = ExternalLinks(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + varieties, + varietyVernacularNames, + species, + speciesCommonNames, + lots, + germinationTests, + movements, + parties, + attachments, + externalLinks, + ]; + @override + int get schemaVersion => 3; +} diff --git a/apps/app_seeds/test/ui/variety_detail_screen_test.dart b/apps/app_seeds/test/ui/variety_detail_screen_test.dart index 8d60539..20ecaec 100644 --- a/apps/app_seeds/test/ui/variety_detail_screen_test.dart +++ b/apps/app_seeds/test/ui/variety_detail_screen_test.dart @@ -83,9 +83,13 @@ void main() { await tester.tap(find.byKey(const Key('detail.addLot'))); 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.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.tap(find.text('spoon')); // pick the unit from the scale await tester.pumpAndSettle(); @@ -222,9 +226,13 @@ void main() { await tester.tap(find.text('Year 2020 · 1 packet')); // open the lot 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.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.tap(find.byKey(const Key('addLot.save'))); await tester.pumpAndSettle(); @@ -233,6 +241,44 @@ void main() { 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 { final repo = newTestRepository(db); final id = await repo.addQuickVariety(label: 'Maize');