From 3942975dba851ec93188f021d880649f3b9535b6 Mon Sep 17 00:00:00 2001 From: vjrj Date: Wed, 8 Jul 2026 10:51:02 +0200 Subject: [PATCH] feat(inventory): informal quantity scale (number + unit) + seed/plant lots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rework quantities the way seeds are actually described — a number of an informal unit — and let a lot hold seeds or plants (plantel). Quantity model (commons_core): - Quantity is now { kind, count?, label } — "3 cobs", "2 packets", "a few". Count is optional; uncountable vibes (a few, some, pinch) carry no number. - QuantityKind reorganised into an ascending scale: vibes → containers (teaspoon/spoon/cup/jar/sack) → seed & fruit forms → plant units, each tagged countable/not. Stored by name; count reuses the existing numeric column. Lot type (schema v2): - Lots gain a `type` (seed | plant); germination stays meaningful for seeds. schemaVersion 2 with a from1To2 migration (adds the column), drift_schema_v2 exported, migration test covers v1→v2. UI: - New QuantityPicker: a horizontal scale of large seed-glyph icons + a number stepper (shown only for countable units), plus a seed/plant SegmentedButton. Used in quick-add and add-lot. Lot lines render "3 cobs" / "a few". - i18n: unit singular/plural (ES/EN, Weblate-friendly), lot-type labels. Build: slang moved to its CLI (disabled in build_runner to avoid a multi-file collision); CI runs `dart run slang` before build_runner. 38 tests green, Linux migrates the existing v1 DB and runs. --- .gitlab-ci.yml | 3 +- apps/app_seeds/build.yaml | 8 + .../drift_schemas/drift_schema_v2.json | 1471 +++++++++++++++++ .../lib/data/variety_repository.dart | 14 +- apps/app_seeds/lib/db/database.dart | 10 +- apps/app_seeds/lib/db/database.g.dart | 68 + apps/app_seeds/lib/db/enums.dart | 4 + apps/app_seeds/lib/db/tables.dart | 2 + apps/app_seeds/lib/i18n/en.i18n.json | 47 +- apps/app_seeds/lib/i18n/es.i18n.json | 47 +- apps/app_seeds/lib/i18n/strings.g.dart | 4 +- apps/app_seeds/lib/i18n/strings_en.g.dart | 455 ++++- apps/app_seeds/lib/i18n/strings_es.g.dart | 360 +++- apps/app_seeds/lib/state/quick_add_cubit.dart | 31 +- .../lib/state/variety_detail_cubit.dart | 13 +- apps/app_seeds/lib/ui/quantity_kind_l10n.dart | 85 +- apps/app_seeds/lib/ui/quantity_picker.dart | 256 +++ apps/app_seeds/lib/ui/quick_add_sheet.dart | 39 +- apps/app_seeds/lib/ui/seed_glyph.dart | 9 +- .../lib/ui/variety_detail_screen.dart | 55 +- apps/app_seeds/test/db/migration_test.dart | 17 +- apps/app_seeds/test/db/schema/schema.dart | 5 +- apps/app_seeds/test/db/schema/schema_v2.dart | 1390 ++++++++++++++++ .../test/ui/variety_detail_screen_test.dart | 8 +- .../commons_core/lib/src/value/quantity.dart | 102 +- .../test/value/quantity_test.dart | 32 +- 26 files changed, 4220 insertions(+), 315 deletions(-) create mode 100644 apps/app_seeds/build.yaml create mode 100644 apps/app_seeds/drift_schemas/drift_schema_v2.json create mode 100644 apps/app_seeds/lib/ui/quantity_picker.dart create mode 100644 apps/app_seeds/test/db/schema/schema_v2.dart diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6aa6fa6..19e9d79 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -42,7 +42,8 @@ test:app_seeds: stage: test script: - cd apps/app_seeds - # Generated sources (Drift, slang) must exist before analysis/tests. + # slang is generated via its CLI (disabled in build_runner); Drift via build_runner. + - dart run slang - dart run build_runner build --delete-conflicting-outputs - flutter test --coverage coverage: '/lines\.*: \d+\.\d+\%/' diff --git a/apps/app_seeds/build.yaml b/apps/app_seeds/build.yaml new file mode 100644 index 0000000..54783bf --- /dev/null +++ b/apps/app_seeds/build.yaml @@ -0,0 +1,8 @@ +# slang is generated via the CLI (`dart run slang`), which emits multiple files +# (strings.g.dart + per-locale parts). Disable its build_runner integration so +# `build_runner build` (Drift only) doesn't collide with those committed files. +targets: + $default: + builders: + slang_build_runner: + enabled: false diff --git a/apps/app_seeds/drift_schemas/drift_schema_v2.json b/apps/app_seeds/drift_schemas/drift_schema_v2.json new file mode 100644 index 0000000..c373def --- /dev/null +++ b/apps/app_seeds/drift_schemas/drift_schema_v2.json @@ -0,0 +1,1471 @@ +{ + "_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": "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, \"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 34e1d81..187156c 100644 --- a/apps/app_seeds/lib/data/variety_repository.dart +++ b/apps/app_seeds/lib/data/variety_repository.dart @@ -63,6 +63,7 @@ class GerminationEntry extends Equatable { class VarietyLot extends Equatable { const VarietyLot({ required this.id, + this.type = LotType.seed, this.harvestYear, this.quantity, this.storageLocation, @@ -70,6 +71,7 @@ class VarietyLot extends Equatable { }); final String id; + final LotType type; final int? harvestYear; final Quantity? quantity; final String? storageLocation; @@ -82,6 +84,7 @@ class VarietyLot extends Equatable { @override List get props => [ id, + type, harvestYear, quantity, storageLocation, @@ -216,6 +219,7 @@ class VarietyRepository { Future addQuickVariety({ required String label, String? category, + LotType lotType = LotType.seed, Quantity? quantity, Uint8List? photoBytes, }) async { @@ -246,8 +250,9 @@ class VarietyRepository { createdAt: created, updatedAt: updated, lastAuthor: nodeId, + type: Value(lotType), quantityKind: Value(quantity.kind.name), - quantityPrecise: Value(quantity.precise), + quantityPrecise: Value(quantity.count?.toDouble()), quantityLabel: Value(quantity.label), ), ); @@ -423,6 +428,7 @@ class VarietyRepository { /// Adds a lot (a held batch) to a variety. Returns the new lot id. Future addLot({ required String varietyId, + LotType type = LotType.seed, int? harvestYear, Quantity? quantity, String? storageLocation, @@ -438,9 +444,10 @@ class VarietyRepository { createdAt: created, updatedAt: updated, lastAuthor: nodeId, + type: Value(type), harvestYear: Value(harvestYear), quantityKind: Value(quantity?.kind.name), - quantityPrecise: Value(quantity?.precise), + quantityPrecise: Value(quantity?.count?.toDouble()), quantityLabel: Value(quantity?.label), storageLocation: Value(storageLocation), ), @@ -496,13 +503,14 @@ class VarietyRepository { l.quantityLabel != null; return VarietyLot( id: l.id, + type: l.type, harvestYear: l.harvestYear, storageLocation: l.storageLocation, germinationTests: germinationTests, quantity: hasQuantity ? Quantity( kind: _parseKind(l.quantityKind), - precise: l.quantityPrecise, + count: l.quantityPrecise, label: l.quantityLabel, ) : null, diff --git a/apps/app_seeds/lib/db/database.dart b/apps/app_seeds/lib/db/database.dart index 7dbabb6..95dff5f 100644 --- a/apps/app_seeds/lib/db/database.dart +++ b/apps/app_seeds/lib/db/database.dart @@ -27,12 +27,16 @@ class AppDatabase extends _$AppDatabase { AppDatabase(super.e); @override - int get schemaVersion => 1; + int get schemaVersion => 2; @override MigrationStrategy get migration => MigrationStrategy( onCreate: (m) async => m.createAll(), - // Step-by-step upgrades (from1To2, …) are generated into - // schema_versions.dart when schemaVersion is bumped. See data-model §5. + onUpgrade: (m, from, to) async { + // v2: lots can hold seeds or plants/seedlings (plantel). + if (from < 2) { + await m.addColumn(lots, lots.type); + } + }, ); } diff --git a/apps/app_seeds/lib/db/database.g.dart b/apps/app_seeds/lib/db/database.g.dart index dd9bd69..0c4329c 100644 --- a/apps/app_seeds/lib/db/database.g.dart +++ b/apps/app_seeds/lib/db/database.g.dart @@ -2610,6 +2610,16 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> { type: DriftSqlType.string, requiredDuringInsert: true, ); + @override + late final GeneratedColumnWithTypeConverter type = + GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('seed'), + ).withConverter($LotsTable.$convertertype); static const VerificationMeta _harvestYearMeta = const VerificationMeta( 'harvestYear', ); @@ -2695,6 +2705,7 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> { isDeleted, schemaRowVersion, varietyId, + type, harvestYear, quantityKind, quantityPrecise, @@ -2855,6 +2866,12 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> { DriftSqlType.string, data['${effectivePrefix}variety_id'], )!, + type: $LotsTable.$convertertype.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + ), harvestYear: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}harvest_year'], @@ -2893,6 +2910,8 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> { return $LotsTable(attachedDatabase, alias); } + static JsonTypeConverter2 $convertertype = + const EnumNameConverter(LotType.values); static JsonTypeConverter2 $converterofferStatus = const EnumNameConverter(OfferStatus.values); } @@ -2905,6 +2924,7 @@ class Lot extends DataClass implements Insertable { final bool isDeleted; final int schemaRowVersion; final String varietyId; + final LotType type; final int? harvestYear; final String? quantityKind; final double? quantityPrecise; @@ -2920,6 +2940,7 @@ class Lot extends DataClass implements Insertable { required this.isDeleted, required this.schemaRowVersion, required this.varietyId, + required this.type, this.harvestYear, this.quantityKind, this.quantityPrecise, @@ -2938,6 +2959,9 @@ class Lot extends DataClass implements Insertable { map['is_deleted'] = Variable(isDeleted); map['schema_row_version'] = Variable(schemaRowVersion); map['variety_id'] = Variable(varietyId); + { + map['type'] = Variable($LotsTable.$convertertype.toSql(type)); + } if (!nullToAbsent || harvestYear != null) { map['harvest_year'] = Variable(harvestYear); } @@ -2973,6 +2997,7 @@ class Lot extends DataClass implements Insertable { isDeleted: Value(isDeleted), schemaRowVersion: Value(schemaRowVersion), varietyId: Value(varietyId), + type: Value(type), harvestYear: harvestYear == null && nullToAbsent ? const Value.absent() : Value(harvestYear), @@ -3008,6 +3033,9 @@ class Lot extends DataClass implements Insertable { isDeleted: serializer.fromJson(json['isDeleted']), schemaRowVersion: serializer.fromJson(json['schemaRowVersion']), varietyId: serializer.fromJson(json['varietyId']), + type: $LotsTable.$convertertype.fromJson( + serializer.fromJson(json['type']), + ), harvestYear: serializer.fromJson(json['harvestYear']), quantityKind: serializer.fromJson(json['quantityKind']), quantityPrecise: serializer.fromJson(json['quantityPrecise']), @@ -3030,6 +3058,7 @@ class Lot extends DataClass implements Insertable { 'isDeleted': serializer.toJson(isDeleted), 'schemaRowVersion': serializer.toJson(schemaRowVersion), 'varietyId': serializer.toJson(varietyId), + 'type': serializer.toJson($LotsTable.$convertertype.toJson(type)), 'harvestYear': serializer.toJson(harvestYear), 'quantityKind': serializer.toJson(quantityKind), 'quantityPrecise': serializer.toJson(quantityPrecise), @@ -3050,6 +3079,7 @@ class Lot extends DataClass implements Insertable { bool? isDeleted, int? schemaRowVersion, String? varietyId, + LotType? type, Value harvestYear = const Value.absent(), Value quantityKind = const Value.absent(), Value quantityPrecise = const Value.absent(), @@ -3065,6 +3095,7 @@ class Lot extends DataClass implements Insertable { isDeleted: isDeleted ?? this.isDeleted, schemaRowVersion: schemaRowVersion ?? this.schemaRowVersion, varietyId: varietyId ?? this.varietyId, + type: type ?? this.type, harvestYear: harvestYear.present ? harvestYear.value : this.harvestYear, quantityKind: quantityKind.present ? quantityKind.value : this.quantityKind, quantityPrecise: quantityPrecise.present @@ -3092,6 +3123,7 @@ class Lot extends DataClass implements Insertable { ? data.schemaRowVersion.value : this.schemaRowVersion, varietyId: data.varietyId.present ? data.varietyId.value : this.varietyId, + type: data.type.present ? data.type.value : this.type, harvestYear: data.harvestYear.present ? data.harvestYear.value : this.harvestYear, @@ -3126,6 +3158,7 @@ class Lot extends DataClass implements Insertable { ..write('isDeleted: $isDeleted, ') ..write('schemaRowVersion: $schemaRowVersion, ') ..write('varietyId: $varietyId, ') + ..write('type: $type, ') ..write('harvestYear: $harvestYear, ') ..write('quantityKind: $quantityKind, ') ..write('quantityPrecise: $quantityPrecise, ') @@ -3146,6 +3179,7 @@ class Lot extends DataClass implements Insertable { isDeleted, schemaRowVersion, varietyId, + type, harvestYear, quantityKind, quantityPrecise, @@ -3165,6 +3199,7 @@ class Lot extends DataClass implements Insertable { other.isDeleted == this.isDeleted && other.schemaRowVersion == this.schemaRowVersion && other.varietyId == this.varietyId && + other.type == this.type && other.harvestYear == this.harvestYear && other.quantityKind == this.quantityKind && other.quantityPrecise == this.quantityPrecise && @@ -3182,6 +3217,7 @@ class LotsCompanion extends UpdateCompanion { final Value isDeleted; final Value schemaRowVersion; final Value varietyId; + final Value type; final Value harvestYear; final Value quantityKind; final Value quantityPrecise; @@ -3198,6 +3234,7 @@ class LotsCompanion extends UpdateCompanion { this.isDeleted = const Value.absent(), this.schemaRowVersion = const Value.absent(), this.varietyId = const Value.absent(), + this.type = const Value.absent(), this.harvestYear = const Value.absent(), this.quantityKind = const Value.absent(), this.quantityPrecise = const Value.absent(), @@ -3215,6 +3252,7 @@ class LotsCompanion extends UpdateCompanion { this.isDeleted = const Value.absent(), this.schemaRowVersion = const Value.absent(), required String varietyId, + this.type = const Value.absent(), this.harvestYear = const Value.absent(), this.quantityKind = const Value.absent(), this.quantityPrecise = const Value.absent(), @@ -3236,6 +3274,7 @@ class LotsCompanion extends UpdateCompanion { Expression? isDeleted, Expression? schemaRowVersion, Expression? varietyId, + Expression? type, Expression? harvestYear, Expression? quantityKind, Expression? quantityPrecise, @@ -3253,6 +3292,7 @@ class LotsCompanion extends UpdateCompanion { if (isDeleted != null) 'is_deleted': isDeleted, if (schemaRowVersion != null) 'schema_row_version': schemaRowVersion, if (varietyId != null) 'variety_id': varietyId, + if (type != null) 'type': type, if (harvestYear != null) 'harvest_year': harvestYear, if (quantityKind != null) 'quantity_kind': quantityKind, if (quantityPrecise != null) 'quantity_precise': quantityPrecise, @@ -3272,6 +3312,7 @@ class LotsCompanion extends UpdateCompanion { Value? isDeleted, Value? schemaRowVersion, Value? varietyId, + Value? type, Value? harvestYear, Value? quantityKind, Value? quantityPrecise, @@ -3289,6 +3330,7 @@ class LotsCompanion extends UpdateCompanion { isDeleted: isDeleted ?? this.isDeleted, schemaRowVersion: schemaRowVersion ?? this.schemaRowVersion, varietyId: varietyId ?? this.varietyId, + type: type ?? this.type, harvestYear: harvestYear ?? this.harvestYear, quantityKind: quantityKind ?? this.quantityKind, quantityPrecise: quantityPrecise ?? this.quantityPrecise, @@ -3324,6 +3366,11 @@ class LotsCompanion extends UpdateCompanion { if (varietyId.present) { map['variety_id'] = Variable(varietyId.value); } + if (type.present) { + map['type'] = Variable( + $LotsTable.$convertertype.toSql(type.value), + ); + } if (harvestYear.present) { map['harvest_year'] = Variable(harvestYear.value); } @@ -3363,6 +3410,7 @@ class LotsCompanion extends UpdateCompanion { ..write('isDeleted: $isDeleted, ') ..write('schemaRowVersion: $schemaRowVersion, ') ..write('varietyId: $varietyId, ') + ..write('type: $type, ') ..write('harvestYear: $harvestYear, ') ..write('quantityKind: $quantityKind, ') ..write('quantityPrecise: $quantityPrecise, ') @@ -8138,6 +8186,7 @@ typedef $$LotsTableCreateCompanionBuilder = Value isDeleted, Value schemaRowVersion, required String varietyId, + Value type, Value harvestYear, Value quantityKind, Value quantityPrecise, @@ -8156,6 +8205,7 @@ typedef $$LotsTableUpdateCompanionBuilder = Value isDeleted, Value schemaRowVersion, Value varietyId, + Value type, Value harvestYear, Value quantityKind, Value quantityPrecise, @@ -8209,6 +8259,12 @@ class $$LotsTableFilterComposer extends Composer<_$AppDatabase, $LotsTable> { builder: (column) => ColumnFilters(column), ); + ColumnWithTypeConverterFilters get type => + $composableBuilder( + column: $table.type, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); + ColumnFilters get harvestYear => $composableBuilder( column: $table.harvestYear, builder: (column) => ColumnFilters(column), @@ -8289,6 +8345,11 @@ class $$LotsTableOrderingComposer extends Composer<_$AppDatabase, $LotsTable> { builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get harvestYear => $composableBuilder( column: $table.harvestYear, builder: (column) => ColumnOrderings(column), @@ -8359,6 +8420,9 @@ class $$LotsTableAnnotationComposer GeneratedColumn get varietyId => $composableBuilder(column: $table.varietyId, builder: (column) => column); + GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + GeneratedColumn get harvestYear => $composableBuilder( column: $table.harvestYear, builder: (column) => column, @@ -8431,6 +8495,7 @@ class $$LotsTableTableManager Value isDeleted = const Value.absent(), Value schemaRowVersion = const Value.absent(), Value varietyId = const Value.absent(), + Value type = const Value.absent(), Value harvestYear = const Value.absent(), Value quantityKind = const Value.absent(), Value quantityPrecise = const Value.absent(), @@ -8447,6 +8512,7 @@ class $$LotsTableTableManager isDeleted: isDeleted, schemaRowVersion: schemaRowVersion, varietyId: varietyId, + type: type, harvestYear: harvestYear, quantityKind: quantityKind, quantityPrecise: quantityPrecise, @@ -8465,6 +8531,7 @@ class $$LotsTableTableManager Value isDeleted = const Value.absent(), Value schemaRowVersion = const Value.absent(), required String varietyId, + Value type = const Value.absent(), Value harvestYear = const Value.absent(), Value quantityKind = const Value.absent(), Value quantityPrecise = const Value.absent(), @@ -8481,6 +8548,7 @@ class $$LotsTableTableManager isDeleted: isDeleted, schemaRowVersion: schemaRowVersion, varietyId: varietyId, + type: type, harvestYear: harvestYear, quantityKind: quantityKind, quantityPrecise: quantityPrecise, diff --git a/apps/app_seeds/lib/db/enums.dart b/apps/app_seeds/lib/db/enums.dart index 38780a4..d6413f3 100644 --- a/apps/app_seeds/lib/db/enums.dart +++ b/apps/app_seeds/lib/db/enums.dart @@ -3,6 +3,10 @@ // may only be appended, never renumbered or reused; when sync arrives, readers // must tolerate unknown values (map to a safe default). No sync yet in Block 1. +/// Whether a lot holds seeds or living plants/seedlings (plantel). Germination +/// tests apply only to seed lots. +enum LotType { seed, plant } + /// Per-lot visibility (data-model §2.3). Used by the future sharing layer. enum OfferStatus { private, shared, exchange, sell } diff --git a/apps/app_seeds/lib/db/tables.dart b/apps/app_seeds/lib/db/tables.dart index da3c054..9f43296 100644 --- a/apps/app_seeds/lib/db/tables.dart +++ b/apps/app_seeds/lib/db/tables.dart @@ -43,6 +43,8 @@ class SpeciesCommonNames extends Table with SyncColumns { /// Quantity (commons_core value type) is flattened into columns here. class Lots extends Table with SyncColumns { TextColumn get varietyId => text()(); + TextColumn get type => + textEnum().withDefault(const Constant('seed'))(); IntColumn get harvestYear => integer().nullable()(); TextColumn get quantityKind => text().nullable()(); // QuantityKind.name RealColumn get quantityPrecise => real().nullable()(); diff --git a/apps/app_seeds/lib/i18n/en.i18n.json b/apps/app_seeds/lib/i18n/en.i18n.json index 393a00e..d0f99e3 100644 --- a/apps/app_seeds/lib/i18n/en.i18n.json +++ b/apps/app_seeds/lib/i18n/en.i18n.json @@ -6,7 +6,8 @@ "save": "Save", "cancel": "Cancel", "delete": "Delete", - "edit": "Edit" + "edit": "Edit", + "type": "Type" }, "inventory": { "title": "Inventory", @@ -54,26 +55,38 @@ "addLot": { "title": "Add lot", "year": "Harvest year", - "quantity": "Quantity" + "quantity": "How much?", + "amount": "Amount" }, - "quantityKind": { + "lotType": { + "seed": "Seeds", + "plant": "Plants" + }, + "unit": { "aFew": "a few", "some": "some", "plenty": "plenty", - "handful": "a handful", "pinch": "a pinch", - "jar": "a jar", - "packet": "a packet", - "cob": "a cob", - "head": "a head", - "pod": "a pod", - "ear": "an ear", - "fruit": "a fruit", - "bulb": "a bulb", - "tuber": "a tuber", - "seedHead": "a seed head", - "bunch": "a bunch", - "grams": "grams", - "count": "count" + "handful": { "singular": "handful", "plural": "handfuls" }, + "teaspoon": { "singular": "teaspoon", "plural": "teaspoons" }, + "spoon": { "singular": "spoon", "plural": "spoons" }, + "cup": { "singular": "cup", "plural": "cups" }, + "jar": { "singular": "jar", "plural": "jars" }, + "sack": { "singular": "sack", "plural": "sacks" }, + "packet": { "singular": "packet", "plural": "packets" }, + "cob": { "singular": "cob", "plural": "cobs" }, + "pod": { "singular": "pod", "plural": "pods" }, + "ear": { "singular": "ear", "plural": "ears" }, + "head": { "singular": "head", "plural": "heads" }, + "fruit": { "singular": "fruit", "plural": "fruits" }, + "bulb": { "singular": "bulb", "plural": "bulbs" }, + "tuber": { "singular": "tuber", "plural": "tubers" }, + "seedHead": { "singular": "seed head", "plural": "seed heads" }, + "bunch": { "singular": "bunch", "plural": "bunches" }, + "plant": { "singular": "plant", "plural": "plants" }, + "pot": { "singular": "pot", "plural": "pots" }, + "tray": { "singular": "tray", "plural": "trays" }, + "grams": { "singular": "gram", "plural": "grams" }, + "count": { "singular": "seed", "plural": "seeds" } } } diff --git a/apps/app_seeds/lib/i18n/es.i18n.json b/apps/app_seeds/lib/i18n/es.i18n.json index 8abedee..6a95ae4 100644 --- a/apps/app_seeds/lib/i18n/es.i18n.json +++ b/apps/app_seeds/lib/i18n/es.i18n.json @@ -6,7 +6,8 @@ "save": "Guardar", "cancel": "Cancelar", "delete": "Eliminar", - "edit": "Editar" + "edit": "Editar", + "type": "Tipo" }, "inventory": { "title": "Inventario", @@ -54,26 +55,38 @@ "addLot": { "title": "Añadir lote", "year": "Año de cosecha", - "quantity": "Cantidad" + "quantity": "¿Cuánta?", + "amount": "Cantidad" }, - "quantityKind": { + "lotType": { + "seed": "Semillas", + "plant": "Plantel" + }, + "unit": { "aFew": "unas pocas", "some": "algunas", "plenty": "muchas", - "handful": "un puñado", "pinch": "una pizca", - "jar": "un tarro", - "packet": "un sobre", - "cob": "una mazorca", - "head": "una cabezuela", - "pod": "una vaina", - "ear": "una espiga", - "fruit": "un fruto", - "bulb": "un bulbo", - "tuber": "un tubérculo", - "seedHead": "una cabeza de semillas", - "bunch": "un manojo", - "grams": "gramos", - "count": "unidades" + "handful": { "singular": "puñado", "plural": "puñados" }, + "teaspoon": { "singular": "cucharadita", "plural": "cucharaditas" }, + "spoon": { "singular": "cuchara", "plural": "cucharas" }, + "cup": { "singular": "taza", "plural": "tazas" }, + "jar": { "singular": "bote", "plural": "botes" }, + "sack": { "singular": "saco", "plural": "sacos" }, + "packet": { "singular": "sobre", "plural": "sobres" }, + "cob": { "singular": "mazorca", "plural": "mazorcas" }, + "pod": { "singular": "vaina", "plural": "vainas" }, + "ear": { "singular": "espiga", "plural": "espigas" }, + "head": { "singular": "cabezuela", "plural": "cabezuelas" }, + "fruit": { "singular": "fruto", "plural": "frutos" }, + "bulb": { "singular": "bulbo", "plural": "bulbos" }, + "tuber": { "singular": "tubérculo", "plural": "tubérculos" }, + "seedHead": { "singular": "cabeza de semillas", "plural": "cabezas de semillas" }, + "bunch": { "singular": "manojo", "plural": "manojos" }, + "plant": { "singular": "planta", "plural": "plantas" }, + "pot": { "singular": "maceta", "plural": "macetas" }, + "tray": { "singular": "bandeja", "plural": "bandejas" }, + "grams": { "singular": "gramo", "plural": "gramos" }, + "count": { "singular": "semilla", "plural": "semillas" } } } diff --git a/apps/app_seeds/lib/i18n/strings.g.dart b/apps/app_seeds/lib/i18n/strings.g.dart index 894126f..789532d 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: 118 (59 per locale) +/// Strings: 182 (91 per locale) /// -/// Built on 2026-07-07 at 19:58 UTC +/// Built on 2026-07-08 at 08:39 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 888e780..e206959 100644 --- a/apps/app_seeds/lib/i18n/strings_en.g.dart +++ b/apps/app_seeds/lib/i18n/strings_en.g.dart @@ -48,7 +48,8 @@ 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$quantityKind$en quantityKind = Translations$quantityKind$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); } // Path: app @@ -82,6 +83,9 @@ class Translations$common$en { /// en: 'Edit' String get edit => 'Edit'; + + /// en: 'Type' + String get type => 'Type'; } // Path: inventory @@ -242,13 +246,31 @@ class Translations$addLot$en { /// en: 'Harvest year' String get year => 'Harvest year'; - /// en: 'Quantity' - String get quantity => 'Quantity'; + /// en: 'How much?' + String get quantity => 'How much?'; + + /// en: 'Amount' + String get amount => 'Amount'; } -// Path: quantityKind -class Translations$quantityKind$en { - Translations$quantityKind$en.internal(this._root); +// Path: lotType +class Translations$lotType$en { + Translations$lotType$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Seeds' + String get seed => 'Seeds'; + + /// en: 'Plants' + String get plant => 'Plants'; +} + +// Path: unit +class Translations$unit$en { + Translations$unit$en.internal(this._root); final Translations _root; // ignore: unused_field @@ -263,50 +285,345 @@ class Translations$quantityKind$en { /// en: 'plenty' String get plenty => 'plenty'; - /// en: 'a handful' - String get handful => 'a handful'; - /// en: 'a pinch' String get pinch => 'a pinch'; - /// en: 'a jar' - String get jar => 'a jar'; + late final Translations$unit$handful$en handful = Translations$unit$handful$en.internal(_root); + late final Translations$unit$teaspoon$en teaspoon = Translations$unit$teaspoon$en.internal(_root); + late final Translations$unit$spoon$en spoon = Translations$unit$spoon$en.internal(_root); + late final Translations$unit$cup$en cup = Translations$unit$cup$en.internal(_root); + late final Translations$unit$jar$en jar = Translations$unit$jar$en.internal(_root); + late final Translations$unit$sack$en sack = Translations$unit$sack$en.internal(_root); + late final Translations$unit$packet$en packet = Translations$unit$packet$en.internal(_root); + late final Translations$unit$cob$en cob = Translations$unit$cob$en.internal(_root); + late final Translations$unit$pod$en pod = Translations$unit$pod$en.internal(_root); + late final Translations$unit$ear$en ear = Translations$unit$ear$en.internal(_root); + late final Translations$unit$head$en head = Translations$unit$head$en.internal(_root); + late final Translations$unit$fruit$en fruit = Translations$unit$fruit$en.internal(_root); + late final Translations$unit$bulb$en bulb = Translations$unit$bulb$en.internal(_root); + late final Translations$unit$tuber$en tuber = Translations$unit$tuber$en.internal(_root); + late final Translations$unit$seedHead$en seedHead = Translations$unit$seedHead$en.internal(_root); + late final Translations$unit$bunch$en bunch = Translations$unit$bunch$en.internal(_root); + late final Translations$unit$plant$en plant = Translations$unit$plant$en.internal(_root); + late final Translations$unit$pot$en pot = Translations$unit$pot$en.internal(_root); + late final Translations$unit$tray$en tray = Translations$unit$tray$en.internal(_root); + late final Translations$unit$grams$en grams = Translations$unit$grams$en.internal(_root); + late final Translations$unit$count$en count = Translations$unit$count$en.internal(_root); +} - /// en: 'a packet' - String get packet => 'a packet'; +// Path: unit.handful +class Translations$unit$handful$en { + Translations$unit$handful$en.internal(this._root); - /// en: 'a cob' - String get cob => 'a cob'; + final Translations _root; // ignore: unused_field - /// en: 'a head' - String get head => 'a head'; + // Translations - /// en: 'a pod' - String get pod => 'a pod'; + /// en: 'handful' + String get singular => 'handful'; - /// en: 'an ear' - String get ear => 'an ear'; + /// en: 'handfuls' + String get plural => 'handfuls'; +} - /// en: 'a fruit' - String get fruit => 'a fruit'; +// Path: unit.teaspoon +class Translations$unit$teaspoon$en { + Translations$unit$teaspoon$en.internal(this._root); - /// en: 'a bulb' - String get bulb => 'a bulb'; + final Translations _root; // ignore: unused_field - /// en: 'a tuber' - String get tuber => 'a tuber'; + // Translations - /// en: 'a seed head' - String get seedHead => 'a seed head'; + /// en: 'teaspoon' + String get singular => 'teaspoon'; - /// en: 'a bunch' - String get bunch => 'a bunch'; + /// en: 'teaspoons' + String get plural => 'teaspoons'; +} + +// Path: unit.spoon +class Translations$unit$spoon$en { + Translations$unit$spoon$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'spoon' + String get singular => 'spoon'; + + /// en: 'spoons' + String get plural => 'spoons'; +} + +// Path: unit.cup +class Translations$unit$cup$en { + Translations$unit$cup$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'cup' + String get singular => 'cup'; + + /// en: 'cups' + String get plural => 'cups'; +} + +// Path: unit.jar +class Translations$unit$jar$en { + Translations$unit$jar$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'jar' + String get singular => 'jar'; + + /// en: 'jars' + String get plural => 'jars'; +} + +// Path: unit.sack +class Translations$unit$sack$en { + Translations$unit$sack$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'sack' + String get singular => 'sack'; + + /// en: 'sacks' + String get plural => 'sacks'; +} + +// Path: unit.packet +class Translations$unit$packet$en { + Translations$unit$packet$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'packet' + String get singular => 'packet'; + + /// en: 'packets' + String get plural => 'packets'; +} + +// Path: unit.cob +class Translations$unit$cob$en { + Translations$unit$cob$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'cob' + String get singular => 'cob'; + + /// en: 'cobs' + String get plural => 'cobs'; +} + +// Path: unit.pod +class Translations$unit$pod$en { + Translations$unit$pod$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'pod' + String get singular => 'pod'; + + /// en: 'pods' + String get plural => 'pods'; +} + +// Path: unit.ear +class Translations$unit$ear$en { + Translations$unit$ear$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'ear' + String get singular => 'ear'; + + /// en: 'ears' + String get plural => 'ears'; +} + +// Path: unit.head +class Translations$unit$head$en { + Translations$unit$head$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'head' + String get singular => 'head'; + + /// en: 'heads' + String get plural => 'heads'; +} + +// Path: unit.fruit +class Translations$unit$fruit$en { + Translations$unit$fruit$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'fruit' + String get singular => 'fruit'; + + /// en: 'fruits' + String get plural => 'fruits'; +} + +// Path: unit.bulb +class Translations$unit$bulb$en { + Translations$unit$bulb$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'bulb' + String get singular => 'bulb'; + + /// en: 'bulbs' + String get plural => 'bulbs'; +} + +// Path: unit.tuber +class Translations$unit$tuber$en { + Translations$unit$tuber$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'tuber' + String get singular => 'tuber'; + + /// en: 'tubers' + String get plural => 'tubers'; +} + +// Path: unit.seedHead +class Translations$unit$seedHead$en { + Translations$unit$seedHead$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'seed head' + String get singular => 'seed head'; + + /// en: 'seed heads' + String get plural => 'seed heads'; +} + +// Path: unit.bunch +class Translations$unit$bunch$en { + Translations$unit$bunch$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'bunch' + String get singular => 'bunch'; + + /// en: 'bunches' + String get plural => 'bunches'; +} + +// Path: unit.plant +class Translations$unit$plant$en { + Translations$unit$plant$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'plant' + String get singular => 'plant'; + + /// en: 'plants' + String get plural => 'plants'; +} + +// Path: unit.pot +class Translations$unit$pot$en { + Translations$unit$pot$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'pot' + String get singular => 'pot'; + + /// en: 'pots' + String get plural => 'pots'; +} + +// Path: unit.tray +class Translations$unit$tray$en { + Translations$unit$tray$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'tray' + String get singular => 'tray'; + + /// en: 'trays' + String get plural => 'trays'; +} + +// Path: unit.grams +class Translations$unit$grams$en { + Translations$unit$grams$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'gram' + String get singular => 'gram'; /// en: 'grams' - String get grams => 'grams'; + String get plural => 'grams'; +} - /// en: 'count' - String get count => 'count'; +// Path: unit.count +class Translations$unit$count$en { + Translations$unit$count$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'seed' + String get singular => 'seed'; + + /// en: 'seeds' + String get plural => 'seeds'; } /// The flat map containing all translations for locale . @@ -322,6 +639,7 @@ extension on Translations { 'common.cancel' => 'Cancel', 'common.delete' => 'Delete', 'common.edit' => 'Edit', + 'common.type' => 'Type', 'inventory.title' => 'Inventory', 'inventory.searchHint' => 'Search seeds', 'inventory.empty' => 'No seeds yet. Tap + to add your first.', @@ -357,25 +675,56 @@ extension on Translations { 'editVariety.speciesHint' => 'Search a species…', 'addLot.title' => 'Add lot', 'addLot.year' => 'Harvest year', - 'addLot.quantity' => 'Quantity', - 'quantityKind.aFew' => 'a few', - 'quantityKind.some' => 'some', - 'quantityKind.plenty' => 'plenty', - 'quantityKind.handful' => 'a handful', - 'quantityKind.pinch' => 'a pinch', - 'quantityKind.jar' => 'a jar', - 'quantityKind.packet' => 'a packet', - 'quantityKind.cob' => 'a cob', - 'quantityKind.head' => 'a head', - 'quantityKind.pod' => 'a pod', - 'quantityKind.ear' => 'an ear', - 'quantityKind.fruit' => 'a fruit', - 'quantityKind.bulb' => 'a bulb', - 'quantityKind.tuber' => 'a tuber', - 'quantityKind.seedHead' => 'a seed head', - 'quantityKind.bunch' => 'a bunch', - 'quantityKind.grams' => 'grams', - 'quantityKind.count' => 'count', + 'addLot.quantity' => 'How much?', + 'addLot.amount' => 'Amount', + 'lotType.seed' => 'Seeds', + 'lotType.plant' => 'Plants', + 'unit.aFew' => 'a few', + 'unit.some' => 'some', + 'unit.plenty' => 'plenty', + 'unit.pinch' => 'a pinch', + 'unit.handful.singular' => 'handful', + 'unit.handful.plural' => 'handfuls', + 'unit.teaspoon.singular' => 'teaspoon', + 'unit.teaspoon.plural' => 'teaspoons', + 'unit.spoon.singular' => 'spoon', + 'unit.spoon.plural' => 'spoons', + 'unit.cup.singular' => 'cup', + 'unit.cup.plural' => 'cups', + 'unit.jar.singular' => 'jar', + 'unit.jar.plural' => 'jars', + 'unit.sack.singular' => 'sack', + 'unit.sack.plural' => 'sacks', + 'unit.packet.singular' => 'packet', + 'unit.packet.plural' => 'packets', + 'unit.cob.singular' => 'cob', + 'unit.cob.plural' => 'cobs', + 'unit.pod.singular' => 'pod', + 'unit.pod.plural' => 'pods', + 'unit.ear.singular' => 'ear', + 'unit.ear.plural' => 'ears', + 'unit.head.singular' => 'head', + 'unit.head.plural' => 'heads', + 'unit.fruit.singular' => 'fruit', + 'unit.fruit.plural' => 'fruits', + 'unit.bulb.singular' => 'bulb', + 'unit.bulb.plural' => 'bulbs', + 'unit.tuber.singular' => 'tuber', + 'unit.tuber.plural' => 'tubers', + 'unit.seedHead.singular' => 'seed head', + 'unit.seedHead.plural' => 'seed heads', + 'unit.bunch.singular' => 'bunch', + 'unit.bunch.plural' => 'bunches', + 'unit.plant.singular' => 'plant', + 'unit.plant.plural' => 'plants', + 'unit.pot.singular' => 'pot', + 'unit.pot.plural' => 'pots', + 'unit.tray.singular' => 'tray', + 'unit.tray.plural' => 'trays', + 'unit.grams.singular' => 'gram', + 'unit.grams.plural' => 'grams', + 'unit.count.singular' => 'seed', + 'unit.count.plural' => 'seeds', _ => null, }; } diff --git a/apps/app_seeds/lib/i18n/strings_es.g.dart b/apps/app_seeds/lib/i18n/strings_es.g.dart index 2b63a61..79e9728 100644 --- a/apps/app_seeds/lib/i18n/strings_es.g.dart +++ b/apps/app_seeds/lib/i18n/strings_es.g.dart @@ -47,7 +47,8 @@ class TranslationsEs extends Translations with BaseTranslations 'Cancelar'; @override String get delete => 'Eliminar'; @override String get edit => 'Editar'; + @override String get type => 'Tipo'; } // Path: inventory @@ -160,12 +162,24 @@ class _Translations$addLot$es extends Translations$addLot$en { // Translations @override String get title => 'Añadir lote'; @override String get year => 'Año de cosecha'; - @override String get quantity => 'Cantidad'; + @override String get quantity => '¿Cuánta?'; + @override String get amount => 'Cantidad'; } -// Path: quantityKind -class _Translations$quantityKind$es extends Translations$quantityKind$en { - _Translations$quantityKind$es._(TranslationsEs root) : this._root = root, super.internal(root); +// Path: lotType +class _Translations$lotType$es extends Translations$lotType$en { + _Translations$lotType$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get seed => 'Semillas'; + @override String get plant => 'Plantel'; +} + +// Path: unit +class _Translations$unit$es extends Translations$unit$en { + _Translations$unit$es._(TranslationsEs root) : this._root = root, super.internal(root); final TranslationsEs _root; // ignore: unused_field @@ -173,21 +187,259 @@ class _Translations$quantityKind$es extends Translations$quantityKind$en { @override String get aFew => 'unas pocas'; @override String get some => 'algunas'; @override String get plenty => 'muchas'; - @override String get handful => 'un puñado'; @override String get pinch => 'una pizca'; - @override String get jar => 'un tarro'; - @override String get packet => 'un sobre'; - @override String get cob => 'una mazorca'; - @override String get head => 'una cabezuela'; - @override String get pod => 'una vaina'; - @override String get ear => 'una espiga'; - @override String get fruit => 'un fruto'; - @override String get bulb => 'un bulbo'; - @override String get tuber => 'un tubérculo'; - @override String get seedHead => 'una cabeza de semillas'; - @override String get bunch => 'un manojo'; - @override String get grams => 'gramos'; - @override String get count => 'unidades'; + @override late final _Translations$unit$handful$es handful = _Translations$unit$handful$es._(_root); + @override late final _Translations$unit$teaspoon$es teaspoon = _Translations$unit$teaspoon$es._(_root); + @override late final _Translations$unit$spoon$es spoon = _Translations$unit$spoon$es._(_root); + @override late final _Translations$unit$cup$es cup = _Translations$unit$cup$es._(_root); + @override late final _Translations$unit$jar$es jar = _Translations$unit$jar$es._(_root); + @override late final _Translations$unit$sack$es sack = _Translations$unit$sack$es._(_root); + @override late final _Translations$unit$packet$es packet = _Translations$unit$packet$es._(_root); + @override late final _Translations$unit$cob$es cob = _Translations$unit$cob$es._(_root); + @override late final _Translations$unit$pod$es pod = _Translations$unit$pod$es._(_root); + @override late final _Translations$unit$ear$es ear = _Translations$unit$ear$es._(_root); + @override late final _Translations$unit$head$es head = _Translations$unit$head$es._(_root); + @override late final _Translations$unit$fruit$es fruit = _Translations$unit$fruit$es._(_root); + @override late final _Translations$unit$bulb$es bulb = _Translations$unit$bulb$es._(_root); + @override late final _Translations$unit$tuber$es tuber = _Translations$unit$tuber$es._(_root); + @override late final _Translations$unit$seedHead$es seedHead = _Translations$unit$seedHead$es._(_root); + @override late final _Translations$unit$bunch$es bunch = _Translations$unit$bunch$es._(_root); + @override late final _Translations$unit$plant$es plant = _Translations$unit$plant$es._(_root); + @override late final _Translations$unit$pot$es pot = _Translations$unit$pot$es._(_root); + @override late final _Translations$unit$tray$es tray = _Translations$unit$tray$es._(_root); + @override late final _Translations$unit$grams$es grams = _Translations$unit$grams$es._(_root); + @override late final _Translations$unit$count$es count = _Translations$unit$count$es._(_root); +} + +// Path: unit.handful +class _Translations$unit$handful$es extends Translations$unit$handful$en { + _Translations$unit$handful$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'puñado'; + @override String get plural => 'puñados'; +} + +// Path: unit.teaspoon +class _Translations$unit$teaspoon$es extends Translations$unit$teaspoon$en { + _Translations$unit$teaspoon$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'cucharadita'; + @override String get plural => 'cucharaditas'; +} + +// Path: unit.spoon +class _Translations$unit$spoon$es extends Translations$unit$spoon$en { + _Translations$unit$spoon$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'cuchara'; + @override String get plural => 'cucharas'; +} + +// Path: unit.cup +class _Translations$unit$cup$es extends Translations$unit$cup$en { + _Translations$unit$cup$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'taza'; + @override String get plural => 'tazas'; +} + +// Path: unit.jar +class _Translations$unit$jar$es extends Translations$unit$jar$en { + _Translations$unit$jar$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'bote'; + @override String get plural => 'botes'; +} + +// Path: unit.sack +class _Translations$unit$sack$es extends Translations$unit$sack$en { + _Translations$unit$sack$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'saco'; + @override String get plural => 'sacos'; +} + +// Path: unit.packet +class _Translations$unit$packet$es extends Translations$unit$packet$en { + _Translations$unit$packet$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'sobre'; + @override String get plural => 'sobres'; +} + +// Path: unit.cob +class _Translations$unit$cob$es extends Translations$unit$cob$en { + _Translations$unit$cob$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'mazorca'; + @override String get plural => 'mazorcas'; +} + +// Path: unit.pod +class _Translations$unit$pod$es extends Translations$unit$pod$en { + _Translations$unit$pod$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'vaina'; + @override String get plural => 'vainas'; +} + +// Path: unit.ear +class _Translations$unit$ear$es extends Translations$unit$ear$en { + _Translations$unit$ear$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'espiga'; + @override String get plural => 'espigas'; +} + +// Path: unit.head +class _Translations$unit$head$es extends Translations$unit$head$en { + _Translations$unit$head$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'cabezuela'; + @override String get plural => 'cabezuelas'; +} + +// Path: unit.fruit +class _Translations$unit$fruit$es extends Translations$unit$fruit$en { + _Translations$unit$fruit$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'fruto'; + @override String get plural => 'frutos'; +} + +// Path: unit.bulb +class _Translations$unit$bulb$es extends Translations$unit$bulb$en { + _Translations$unit$bulb$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'bulbo'; + @override String get plural => 'bulbos'; +} + +// Path: unit.tuber +class _Translations$unit$tuber$es extends Translations$unit$tuber$en { + _Translations$unit$tuber$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'tubérculo'; + @override String get plural => 'tubérculos'; +} + +// Path: unit.seedHead +class _Translations$unit$seedHead$es extends Translations$unit$seedHead$en { + _Translations$unit$seedHead$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'cabeza de semillas'; + @override String get plural => 'cabezas de semillas'; +} + +// Path: unit.bunch +class _Translations$unit$bunch$es extends Translations$unit$bunch$en { + _Translations$unit$bunch$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'manojo'; + @override String get plural => 'manojos'; +} + +// Path: unit.plant +class _Translations$unit$plant$es extends Translations$unit$plant$en { + _Translations$unit$plant$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'planta'; + @override String get plural => 'plantas'; +} + +// Path: unit.pot +class _Translations$unit$pot$es extends Translations$unit$pot$en { + _Translations$unit$pot$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'maceta'; + @override String get plural => 'macetas'; +} + +// Path: unit.tray +class _Translations$unit$tray$es extends Translations$unit$tray$en { + _Translations$unit$tray$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'bandeja'; + @override String get plural => 'bandejas'; +} + +// Path: unit.grams +class _Translations$unit$grams$es extends Translations$unit$grams$en { + _Translations$unit$grams$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'gramo'; + @override String get plural => 'gramos'; +} + +// Path: unit.count +class _Translations$unit$count$es extends Translations$unit$count$en { + _Translations$unit$count$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get singular => 'semilla'; + @override String get plural => 'semillas'; } /// The flat map containing all translations for locale . @@ -203,6 +455,7 @@ extension on TranslationsEs { 'common.cancel' => 'Cancelar', 'common.delete' => 'Eliminar', 'common.edit' => 'Editar', + 'common.type' => 'Tipo', 'inventory.title' => 'Inventario', 'inventory.searchHint' => 'Buscar semillas', 'inventory.empty' => 'Aún no hay semillas. Toca + para añadir la primera.', @@ -238,25 +491,56 @@ extension on TranslationsEs { 'editVariety.speciesHint' => 'Buscar una especie…', 'addLot.title' => 'Añadir lote', 'addLot.year' => 'Año de cosecha', - 'addLot.quantity' => 'Cantidad', - 'quantityKind.aFew' => 'unas pocas', - 'quantityKind.some' => 'algunas', - 'quantityKind.plenty' => 'muchas', - 'quantityKind.handful' => 'un puñado', - 'quantityKind.pinch' => 'una pizca', - 'quantityKind.jar' => 'un tarro', - 'quantityKind.packet' => 'un sobre', - 'quantityKind.cob' => 'una mazorca', - 'quantityKind.head' => 'una cabezuela', - 'quantityKind.pod' => 'una vaina', - 'quantityKind.ear' => 'una espiga', - 'quantityKind.fruit' => 'un fruto', - 'quantityKind.bulb' => 'un bulbo', - 'quantityKind.tuber' => 'un tubérculo', - 'quantityKind.seedHead' => 'una cabeza de semillas', - 'quantityKind.bunch' => 'un manojo', - 'quantityKind.grams' => 'gramos', - 'quantityKind.count' => 'unidades', + 'addLot.quantity' => '¿Cuánta?', + 'addLot.amount' => 'Cantidad', + 'lotType.seed' => 'Semillas', + 'lotType.plant' => 'Plantel', + 'unit.aFew' => 'unas pocas', + 'unit.some' => 'algunas', + 'unit.plenty' => 'muchas', + 'unit.pinch' => 'una pizca', + 'unit.handful.singular' => 'puñado', + 'unit.handful.plural' => 'puñados', + 'unit.teaspoon.singular' => 'cucharadita', + 'unit.teaspoon.plural' => 'cucharaditas', + 'unit.spoon.singular' => 'cuchara', + 'unit.spoon.plural' => 'cucharas', + 'unit.cup.singular' => 'taza', + 'unit.cup.plural' => 'tazas', + 'unit.jar.singular' => 'bote', + 'unit.jar.plural' => 'botes', + 'unit.sack.singular' => 'saco', + 'unit.sack.plural' => 'sacos', + 'unit.packet.singular' => 'sobre', + 'unit.packet.plural' => 'sobres', + 'unit.cob.singular' => 'mazorca', + 'unit.cob.plural' => 'mazorcas', + 'unit.pod.singular' => 'vaina', + 'unit.pod.plural' => 'vainas', + 'unit.ear.singular' => 'espiga', + 'unit.ear.plural' => 'espigas', + 'unit.head.singular' => 'cabezuela', + 'unit.head.plural' => 'cabezuelas', + 'unit.fruit.singular' => 'fruto', + 'unit.fruit.plural' => 'frutos', + 'unit.bulb.singular' => 'bulbo', + 'unit.bulb.plural' => 'bulbos', + 'unit.tuber.singular' => 'tubérculo', + 'unit.tuber.plural' => 'tubérculos', + 'unit.seedHead.singular' => 'cabeza de semillas', + 'unit.seedHead.plural' => 'cabezas de semillas', + 'unit.bunch.singular' => 'manojo', + 'unit.bunch.plural' => 'manojos', + 'unit.plant.singular' => 'planta', + 'unit.plant.plural' => 'plantas', + 'unit.pot.singular' => 'maceta', + 'unit.pot.plural' => 'macetas', + 'unit.tray.singular' => 'bandeja', + 'unit.tray.plural' => 'bandejas', + 'unit.grams.singular' => 'gramo', + 'unit.grams.plural' => 'gramos', + 'unit.count.singular' => 'semilla', + 'unit.count.plural' => 'semillas', _ => null, }; } diff --git a/apps/app_seeds/lib/state/quick_add_cubit.dart b/apps/app_seeds/lib/state/quick_add_cubit.dart index 2681318..180b404 100644 --- a/apps/app_seeds/lib/state/quick_add_cubit.dart +++ b/apps/app_seeds/lib/state/quick_add_cubit.dart @@ -5,13 +5,17 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../data/variety_repository.dart'; +import '../db/enums.dart'; + +const Object _unset = Object(); /// Form state for the quick-add sheet. Only [label] is required; everything /// else is progressive disclosure behind [expanded]. class QuickAddState extends Equatable { const QuickAddState({ this.label = '', - this.quantityKind, + this.quantity, + this.lotType = LotType.seed, this.photoBytes, this.expanded = false, this.submitting = false, @@ -20,7 +24,8 @@ class QuickAddState extends Equatable { }); final String label; - final QuantityKind? quantityKind; + final Quantity? quantity; + final LotType lotType; final Uint8List? photoBytes; final bool expanded; final bool submitting; @@ -31,7 +36,8 @@ class QuickAddState extends Equatable { QuickAddState copyWith({ String? label, - QuantityKind? quantityKind, + Object? quantity = _unset, + LotType? lotType, Uint8List? photoBytes, bool? expanded, bool? submitting, @@ -40,7 +46,10 @@ class QuickAddState extends Equatable { }) { return QuickAddState( label: label ?? this.label, - quantityKind: quantityKind ?? this.quantityKind, + quantity: identical(quantity, _unset) + ? this.quantity + : quantity as Quantity?, + lotType: lotType ?? this.lotType, photoBytes: photoBytes ?? this.photoBytes, expanded: expanded ?? this.expanded, submitting: submitting ?? this.submitting, @@ -52,7 +61,8 @@ class QuickAddState extends Equatable { @override List get props => [ label, - quantityKind, + quantity, + lotType, photoBytes, expanded, submitting, @@ -70,8 +80,10 @@ class QuickAddCubit extends Cubit { void labelChanged(String value) => emit(state.copyWith(label: value, showLabelError: false)); - void selectQuantity(QuantityKind kind) => - emit(state.copyWith(quantityKind: kind)); + void setQuantity(Quantity? quantity) => + emit(state.copyWith(quantity: quantity)); + + void setLotType(LotType type) => emit(state.copyWith(lotType: type)); void photoPicked(Uint8List bytes) => emit(state.copyWith(photoBytes: bytes)); @@ -87,9 +99,8 @@ class QuickAddCubit extends Cubit { emit(state.copyWith(submitting: true)); await _repo.addQuickVariety( label: state.label.trim(), - quantity: state.quantityKind == null - ? null - : Quantity(kind: state.quantityKind!), + lotType: state.lotType, + quantity: state.quantity, photoBytes: state.photoBytes, ); emit(state.copyWith(submitting: false, submitted: true)); diff --git a/apps/app_seeds/lib/state/variety_detail_cubit.dart b/apps/app_seeds/lib/state/variety_detail_cubit.dart index 0780587..2b0c93b 100644 --- a/apps/app_seeds/lib/state/variety_detail_cubit.dart +++ b/apps/app_seeds/lib/state/variety_detail_cubit.dart @@ -5,6 +5,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../data/variety_repository.dart'; +import '../db/enums.dart'; class VarietyDetailState extends Equatable { const VarietyDetailState({ @@ -50,8 +51,16 @@ class VarietyDetailCubit extends Cubit { notes: notes, ); - Future addLot({int? year, Quantity? quantity}) => - _repo.addLot(varietyId: varietyId, harvestYear: year, quantity: quantity); + Future addLot({ + LotType type = LotType.seed, + int? year, + Quantity? quantity, + }) => _repo.addLot( + varietyId: varietyId, + type: type, + harvestYear: year, + quantity: quantity, + ); Future linkSpecies(String speciesId) => _repo.linkSpecies(varietyId, speciesId); diff --git a/apps/app_seeds/lib/ui/quantity_kind_l10n.dart b/apps/app_seeds/lib/ui/quantity_kind_l10n.dart index 4389297..8eb26db 100644 --- a/apps/app_seeds/lib/ui/quantity_kind_l10n.dart +++ b/apps/app_seeds/lib/ui/quantity_kind_l10n.dart @@ -2,46 +2,49 @@ import 'package:commons_core/commons_core.dart'; import '../i18n/strings.g.dart'; -/// Localized display label for a [QuantityKind]. The enum name is the stable -/// storage key; the label is always resolved through i18n (never hardcoded). -String quantityKindLabel(Translations t, QuantityKind kind) { - final q = t.quantityKind; - switch (kind) { - case QuantityKind.aFew: - return q.aFew; - case QuantityKind.some: - return q.some; - case QuantityKind.plenty: - return q.plenty; - case QuantityKind.handful: - return q.handful; - case QuantityKind.pinch: - return q.pinch; - case QuantityKind.jar: - return q.jar; - case QuantityKind.packet: - return q.packet; - case QuantityKind.cob: - return q.cob; - case QuantityKind.head: - return q.head; - case QuantityKind.pod: - return q.pod; - case QuantityKind.ear: - return q.ear; - case QuantityKind.fruit: - return q.fruit; - case QuantityKind.bulb: - return q.bulb; - case QuantityKind.tuber: - return q.tuber; - case QuantityKind.seedHead: - return q.seedHead; - case QuantityKind.bunch: - return q.bunch; - case QuantityKind.grams: - return q.grams; - case QuantityKind.count: - return q.count; +/// Singular + plural label for a [QuantityKind]. For uncountable *vibe* kinds +/// (a few, some…) both are the same word. +(String, String) _unit(Translations t, QuantityKind kind) => switch (kind) { + QuantityKind.aFew => (t.unit.aFew, t.unit.aFew), + QuantityKind.some => (t.unit.some, t.unit.some), + QuantityKind.plenty => (t.unit.plenty, t.unit.plenty), + QuantityKind.pinch => (t.unit.pinch, t.unit.pinch), + QuantityKind.handful => (t.unit.handful.singular, t.unit.handful.plural), + QuantityKind.teaspoon => (t.unit.teaspoon.singular, t.unit.teaspoon.plural), + QuantityKind.spoon => (t.unit.spoon.singular, t.unit.spoon.plural), + QuantityKind.cup => (t.unit.cup.singular, t.unit.cup.plural), + QuantityKind.jar => (t.unit.jar.singular, t.unit.jar.plural), + QuantityKind.sack => (t.unit.sack.singular, t.unit.sack.plural), + QuantityKind.packet => (t.unit.packet.singular, t.unit.packet.plural), + QuantityKind.cob => (t.unit.cob.singular, t.unit.cob.plural), + QuantityKind.pod => (t.unit.pod.singular, t.unit.pod.plural), + QuantityKind.ear => (t.unit.ear.singular, t.unit.ear.plural), + QuantityKind.head => (t.unit.head.singular, t.unit.head.plural), + QuantityKind.fruit => (t.unit.fruit.singular, t.unit.fruit.plural), + QuantityKind.bulb => (t.unit.bulb.singular, t.unit.bulb.plural), + QuantityKind.tuber => (t.unit.tuber.singular, t.unit.tuber.plural), + QuantityKind.seedHead => (t.unit.seedHead.singular, t.unit.seedHead.plural), + QuantityKind.bunch => (t.unit.bunch.singular, t.unit.bunch.plural), + QuantityKind.plant => (t.unit.plant.singular, t.unit.plant.plural), + QuantityKind.pot => (t.unit.pot.singular, t.unit.pot.plural), + QuantityKind.tray => (t.unit.tray.singular, t.unit.tray.plural), + QuantityKind.grams => (t.unit.grams.singular, t.unit.grams.plural), + QuantityKind.count => (t.unit.count.singular, t.unit.count.plural), +}; + +/// The unit's singular name — for chips/scale items ("cob", "packet"). +String unitLabel(Translations t, QuantityKind kind) => _unit(t, kind).$1; + +/// A full, human quantity: "3 cobs", "2 packets", "a few". +String quantityDisplay(Translations t, Quantity q) { + final n = q.count; + final (one, other) = _unit(t, q.kind); + if (!q.kind.countable || n == null) { + // Vibe amount, or a countable unit with no number → bare unit name. + return (n != null && n != 1) ? other : one; } + return '${_formatCount(n)} ${n == 1 ? one : other}'; } + +String _formatCount(num n) => + n == n.roundToDouble() ? n.toInt().toString() : n.toString(); diff --git a/apps/app_seeds/lib/ui/quantity_picker.dart b/apps/app_seeds/lib/ui/quantity_picker.dart new file mode 100644 index 0000000..c03f0b7 --- /dev/null +++ b/apps/app_seeds/lib/ui/quantity_picker.dart @@ -0,0 +1,256 @@ +import 'package:commons_core/commons_core.dart'; +import 'package:flutter/material.dart'; + +import '../db/enums.dart'; +import '../i18n/strings.g.dart'; +import 'quantity_kind_l10n.dart'; +import 'seed_glyph.dart'; +import 'theme.dart'; + +/// Informal seed amounts, ascending: vibes → containers → seed/fruit forms. +const seedScaleKinds = [ + QuantityKind.aFew, + QuantityKind.handful, + QuantityKind.teaspoon, + QuantityKind.spoon, + QuantityKind.cup, + QuantityKind.jar, + QuantityKind.sack, + QuantityKind.packet, + QuantityKind.cob, + QuantityKind.pod, + QuantityKind.ear, + QuantityKind.head, + QuantityKind.fruit, + QuantityKind.bulb, + QuantityKind.tuber, + QuantityKind.bunch, +]; + +/// Units for a plant/seedling lot. +const plantScaleKinds = [ + QuantityKind.plant, + QuantityKind.pot, + QuantityKind.tray, +]; + +List kindsForType(LotType type) => + type == LotType.seed ? seedScaleKinds : plantScaleKinds; + +/// Seed vs plant/seedling segmented selector. +class LotTypeSelector extends StatelessWidget { + const LotTypeSelector({ + required this.value, + required this.onChanged, + super.key, + }); + + final LotType value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final t = context.t; + return SegmentedButton( + segments: [ + ButtonSegment( + value: LotType.seed, + label: Text(t.lotType.seed), + icon: const SeedGlyph(SeedGlyphs.jars, size: 18), + ), + ButtonSegment( + value: LotType.plant, + label: Text(t.lotType.plant), + icon: const Icon(Icons.local_florist_outlined, size: 18), + ), + ], + selected: {value}, + showSelectedIcon: false, + onSelectionChanged: (s) => onChanged(s.first), + ); + } +} + +/// A horizontal scale of large unit icons + an optional number stepper. Emits a +/// [Quantity] (or null while nothing is picked) via [onChanged]. +class QuantityPicker extends StatefulWidget { + const QuantityPicker({ + required this.type, + required this.onChanged, + this.value, + super.key, + }); + + final LotType type; + final Quantity? value; + final ValueChanged onChanged; + + @override + State createState() => _QuantityPickerState(); +} + +class _QuantityPickerState extends State { + QuantityKind? _kind; + final _countController = TextEditingController(); + + @override + void initState() { + super.initState(); + _kind = widget.value?.kind; + final c = widget.value?.count; + if (c != null) _countController.text = _fmt(c); + } + + @override + void didUpdateWidget(QuantityPicker old) { + super.didUpdateWidget(old); + // Switching seed/plant, or the owner clearing the value, drops a stale + // selection. We only update local fields here (no onChanged during build); + // the owner clears its own value alongside the type change. + if (widget.value == null && _kind != null) { + _kind = null; + _countController.clear(); + } else if (widget.type != old.type && + _kind != null && + !kindsForType(widget.type).contains(_kind)) { + _kind = null; + _countController.clear(); + } + } + + @override + void dispose() { + _countController.dispose(); + super.dispose(); + } + + void _emit() { + final kind = _kind; + if (kind == null) { + widget.onChanged(null); + return; + } + final n = num.tryParse(_countController.text.trim().replaceAll(',', '.')); + widget.onChanged(Quantity(kind: kind, count: kind.countable ? n : null)); + } + + void _select(QuantityKind kind) { + setState(() => _kind = kind); + _emit(); + } + + void _bump(int delta) { + final current = int.tryParse(_countController.text.trim()) ?? 0; + final next = (current + delta).clamp(0, 9999); + _countController.text = next == 0 ? '' : '$next'; + _emit(); + } + + @override + Widget build(BuildContext context) { + final t = context.t; + final kinds = kindsForType(widget.type); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 76, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: kinds.length, + separatorBuilder: (_, _) => const SizedBox(width: 4), + itemBuilder: (context, i) { + final kind = kinds[i]; + return _ScaleItem( + kind: kind, + label: unitLabel(t, kind), + selected: _kind == kind, + onTap: () => _select(kind), + ); + }, + ), + ), + if (_kind?.countable ?? false) ...[ + const SizedBox(height: 8), + Row( + children: [ + IconButton.filledTonal( + onPressed: () => _bump(-1), + icon: const Icon(Icons.remove), + ), + SizedBox( + width: 64, + child: TextField( + key: const Key('quantity.count'), + controller: _countController, + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + decoration: const InputDecoration(isDense: true), + onChanged: (_) => _emit(), + ), + ), + IconButton.filledTonal( + onPressed: () => _bump(1), + icon: const Icon(Icons.add), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + unitLabel(t, _kind!), + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + ], + ), + ], + ], + ); + } +} + +String _fmt(num n) => + n == n.roundToDouble() ? n.toInt().toString() : n.toString(); + +class _ScaleItem extends StatelessWidget { + const _ScaleItem({ + required this.kind, + required this.label, + required this.selected, + required this.onTap, + }); + + final QuantityKind kind; + final String label; + final bool selected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final color = selected ? seedGreenDark : Colors.black54; + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(10), + child: Container( + width: 68, + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 4), + decoration: BoxDecoration( + color: selected ? seedGreen.withValues(alpha: 0.15) : null, + borderRadius: BorderRadius.circular(10), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QuantityKindIcon(kind, size: 30, color: color), + const SizedBox(height: 4), + Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 11, color: color), + ), + ], + ), + ), + ); + } +} diff --git a/apps/app_seeds/lib/ui/quick_add_sheet.dart b/apps/app_seeds/lib/ui/quick_add_sheet.dart index 0921011..922e0f0 100644 --- a/apps/app_seeds/lib/ui/quick_add_sheet.dart +++ b/apps/app_seeds/lib/ui/quick_add_sheet.dart @@ -1,6 +1,5 @@ import 'dart:typed_data'; -import 'package:commons_core/commons_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_picker/image_picker.dart'; @@ -8,24 +7,12 @@ import 'package:image_picker/image_picker.dart'; import '../data/variety_repository.dart'; import '../i18n/strings.g.dart'; import '../state/quick_add_cubit.dart'; -import 'quantity_kind_l10n.dart'; -import 'seed_glyph.dart'; +import 'quantity_picker.dart'; /// Picks a photo and returns its bytes (or null if cancelled). Injected so /// widget tests can supply a fake instead of the camera plugin. typedef PhotoPicker = Future Function(); -/// A short, high-frequency subset of quantity units shown up front to keep the -/// add flow fast; the rest live behind "Add more…". -const _quickKinds = [ - QuantityKind.aFew, - QuantityKind.handful, - QuantityKind.packet, - QuantityKind.pod, - QuantityKind.cob, - QuantityKind.grams, -]; - Future showQuickAddSheet( BuildContext context, { required VarietyRepository repository, @@ -94,22 +81,24 @@ class QuickAddSheet extends StatelessWidget { onSubmitted: (_) => cubit.submit(), ), const SizedBox(height: 16), + LotTypeSelector( + value: state.lotType, + onChanged: (type) { + cubit + ..setLotType(type) + ..setQuantity(null); + }, + ), + const SizedBox(height: 12), Text( t.quickAdd.quantity, style: Theme.of(context).textTheme.labelLarge, ), const SizedBox(height: 8), - Wrap( - spacing: 8, - children: [ - for (final kind in _quickKinds) - ChoiceChip( - avatar: QuantityKindIcon(kind, size: 18), - label: Text(quantityKindLabel(t, kind)), - selected: state.quantityKind == kind, - onSelected: (_) => cubit.selectQuantity(kind), - ), - ], + QuantityPicker( + type: state.lotType, + value: state.quantity, + onChanged: cubit.setQuantity, ), const SizedBox(height: 8), Align( diff --git a/apps/app_seeds/lib/ui/seed_glyph.dart b/apps/app_seeds/lib/ui/seed_glyph.dart index 086b62a..1da185b 100644 --- a/apps/app_seeds/lib/ui/seed_glyph.dart +++ b/apps/app_seeds/lib/ui/seed_glyph.dart @@ -46,9 +46,13 @@ String? seedGlyphForKind(QuantityKind kind) => switch (kind) { QuantityKind.aFew => SeedGlyphs.scattered, QuantityKind.some => SeedGlyphs.smallSpoon, QuantityKind.plenty => SeedGlyphs.jars, - QuantityKind.handful => SeedGlyphs.bigSpoon, QuantityKind.pinch => SeedGlyphs.scattered, + QuantityKind.handful => SeedGlyphs.bigSpoon, + QuantityKind.teaspoon => SeedGlyphs.smallSpoon, + QuantityKind.spoon => SeedGlyphs.bigSpoon, + QuantityKind.cup => SeedGlyphs.mug, QuantityKind.jar => SeedGlyphs.jar, + QuantityKind.sack => SeedGlyphs.sack, QuantityKind.packet => SeedGlyphs.envelope, _ => null, }; @@ -59,6 +63,9 @@ IconData _materialIconForKind(QuantityKind kind) => switch (kind) { QuantityKind.cob || QuantityKind.ear => Icons.grass, QuantityKind.head || QuantityKind.seedHead => Icons.filter_vintage_outlined, QuantityKind.fruit => Icons.local_florist_outlined, + QuantityKind.plant => Icons.local_florist_outlined, + QuantityKind.pot => Icons.yard_outlined, + QuantityKind.tray => Icons.grid_on_outlined, QuantityKind.bulb || QuantityKind.tuber || QuantityKind.bunch => Icons.spa_outlined, diff --git a/apps/app_seeds/lib/ui/variety_detail_screen.dart b/apps/app_seeds/lib/ui/variety_detail_screen.dart index 1ef55a9..d1d6de7 100644 --- a/apps/app_seeds/lib/ui/variety_detail_screen.dart +++ b/apps/app_seeds/lib/ui/variety_detail_screen.dart @@ -4,11 +4,13 @@ import 'package:flutter_bloc/flutter_bloc.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 'quantity_kind_l10n.dart'; +import 'quantity_picker.dart'; import 'seed_glyph.dart'; import 'theme.dart'; -import 'quantity_kind_l10n.dart'; /// Read + edit view of a single variety: its photo, category, notes, other /// names and lots. Edits go through [VarietyDetailCubit]; the view is reactive. @@ -144,20 +146,11 @@ String _lotSubtitle(Translations t, VarietyLot lot) { t.detail.year(year: lot.harvestYear!) else t.detail.noYear, - if (lot.quantity != null) _quantityLabel(t, lot.quantity!), + if (lot.quantity != null) quantityDisplay(t, lot.quantity!), ]; return parts.join(' · '); } -String _quantityLabel(Translations t, Quantity q) { - final kind = quantityKindLabel(t, q.kind); - if (q.precise != null) return '${_trimDouble(q.precise!)} $kind'; - return kind; -} - -String _trimDouble(double v) => - v == v.roundToDouble() ? v.toStringAsFixed(0) : v.toString(); - String _germinationPercent(Translations t, double rate) => t.germination.result(percent: (rate * 100).round()); @@ -485,7 +478,8 @@ class _EditVarietySheetState extends State<_EditVarietySheet> { Future _showAddLotSheet(BuildContext context, VarietyDetailCubit cubit) { final t = context.t; final yearController = TextEditingController(); - QuantityKind? selectedKind; + var selectedType = LotType.seed; + Quantity? selectedQuantity; return showModalBottomSheet( context: context, isScrollControlled: true, @@ -506,6 +500,14 @@ Future _showAddLotSheet(BuildContext context, VarietyDetailCubit cubit) { style: Theme.of(sheetContext).textTheme.titleLarge, ), const SizedBox(height: 12), + LotTypeSelector( + value: selectedType, + onChanged: (type) => setState(() { + selectedType = type; + selectedQuantity = null; + }), + ), + const SizedBox(height: 12), TextField( key: const Key('addLot.year'), controller: yearController, @@ -521,17 +523,10 @@ Future _showAddLotSheet(BuildContext context, VarietyDetailCubit cubit) { style: Theme.of(sheetContext).textTheme.labelLarge, ), const SizedBox(height: 8), - Wrap( - spacing: 8, - children: [ - for (final kind in _addLotKinds) - ChoiceChip( - avatar: QuantityKindIcon(kind, size: 18), - label: Text(quantityKindLabel(t, kind)), - selected: selectedKind == kind, - onSelected: (_) => setState(() => selectedKind = kind), - ), - ], + QuantityPicker( + type: selectedType, + value: selectedQuantity, + onChanged: (q) => selectedQuantity = q, ), const SizedBox(height: 16), Row( @@ -545,10 +540,9 @@ Future _showAddLotSheet(BuildContext context, VarietyDetailCubit cubit) { key: const Key('addLot.save'), onPressed: () { cubit.addLot( + type: selectedType, year: int.tryParse(yearController.text.trim()), - quantity: selectedKind == null - ? null - : Quantity(kind: selectedKind!), + quantity: selectedQuantity, ); Navigator.of(sheetContext).pop(); }, @@ -563,15 +557,6 @@ Future _showAddLotSheet(BuildContext context, VarietyDetailCubit cubit) { ); } -const _addLotKinds = [ - QuantityKind.aFew, - QuantityKind.handful, - QuantityKind.packet, - QuantityKind.pod, - QuantityKind.cob, - QuantityKind.grams, -]; - String? _nullIfBlank(String value) { final trimmed = value.trim(); return trimmed.isEmpty ? null : trimmed; diff --git a/apps/app_seeds/test/db/migration_test.dart b/apps/app_seeds/test/db/migration_test.dart index fec0993..09e1c8f 100644 --- a/apps/app_seeds/test/db/migration_test.dart +++ b/apps/app_seeds/test/db/migration_test.dart @@ -11,11 +11,20 @@ void main() { verifier = SchemaVerifier(GeneratedHelper()); }); - test('freshly created database matches the exported schema v1', () async { - // Guards that `createAll` stays in sync with drift_schemas/drift_schema_v1.json. - final schema = await verifier.schemaAt(1); + test('freshly created database matches the exported schema v2', () async { + final schema = await verifier.schemaAt(2); final db = AppDatabase(schema.newConnection()); - await verifier.migrateAndValidate(db, 1); + await verifier.migrateAndValidate(db, 2); await db.close(); }); + + test( + 'upgrades v1 → v2 (adds Lot.type) and matches the fresh schema', + () async { + final connection = await verifier.startAt(1); + final db = AppDatabase(connection); + await verifier.migrateAndValidate(db, 2); + await db.close(); + }, + ); } diff --git a/apps/app_seeds/test/db/schema/schema.dart b/apps/app_seeds/test/db/schema/schema.dart index 8869e40..f5e870e 100644 --- a/apps/app_seeds/test/db/schema/schema.dart +++ b/apps/app_seeds/test/db/schema/schema.dart @@ -5,6 +5,7 @@ import 'package:drift/drift.dart'; import 'package:drift/internal/migrations.dart'; import 'schema_v1.dart' as v1; +import 'schema_v2.dart' as v2; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -12,10 +13,12 @@ class GeneratedHelper implements SchemaInstantiationHelper { switch (version) { case 1: return v1.DatabaseAtV1(db); + case 2: + return v2.DatabaseAtV2(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [1]; + static const versions = const [1, 2]; } diff --git a/apps/app_seeds/test/db/schema/schema_v2.dart b/apps/app_seeds/test/db/schema/schema_v2.dart new file mode 100644 index 0000000..c9bbaf9 --- /dev/null +++ b/apps/app_seeds/test/db/schema/schema_v2.dart @@ -0,0 +1,1390 @@ +// 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 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, + 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 DatabaseAtV2 extends GeneratedDatabase { + DatabaseAtV2(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 => 2; +} 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 6b49e2c..d0a2160 100644 --- a/apps/app_seeds/test/ui/variety_detail_screen_test.dart +++ b/apps/app_seeds/test/ui/variety_detail_screen_test.dart @@ -34,7 +34,7 @@ void main() { expect(find.text('Maize'), findsOneWidget); // app bar title expect(find.text('Poaceae'), findsOneWidget); // category chip - expect(find.text('Year 2024 · a cob'), findsOneWidget); // lot line + expect(find.text('Year 2024 · cob'), findsOneWidget); // lot line await disposeTree(tester); }); @@ -84,11 +84,13 @@ void main() { await tester.pumpAndSettle(); await tester.enterText(find.byKey(const Key('addLot.year')), '2023'); - await tester.tap(find.text('a handful')); + await tester.tap(find.text('handful')); // pick the unit from the scale + await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key('quantity.count')), '2'); await tester.tap(find.byKey(const Key('addLot.save'))); await tester.pumpAndSettle(); - expect(find.text('Year 2023 · a handful'), findsOneWidget); + expect(find.text('Year 2023 · 2 handfuls'), findsOneWidget); await disposeTree(tester); }); diff --git a/packages/commons_core/lib/src/value/quantity.dart b/packages/commons_core/lib/src/value/quantity.dart index b97eac6..3be1234 100644 --- a/packages/commons_core/lib/src/value/quantity.dart +++ b/packages/commons_core/lib/src/value/quantity.dart @@ -1,67 +1,77 @@ import 'package:equatable/equatable.dart'; -/// Coarse groups used by the UI to suggest relevant [QuantityKind]s for a -/// species/family. Purely presentational — never forces a choice. -enum QuantityGroup { informal, plantForm, precise } +/// Groups used to lay out the quantity picker as an ascending, informal scale. +enum QuantityGroup { vibe, size, seedForm, plant, precise } -/// A stable, extensible, i18n vocabulary for *rough, human* seed amounts. +/// A stable, extensible, i18n vocabulary for *rough, human* amounts. /// -/// Values go deliberately beyond kitchen measures: seeds are counted in the -/// forms the plant gives them (a cob, a pod, a flower head…). The display label -/// is localized from [name] — so the enum name is the stable storage key and -/// must never be renumbered or reused (see data-model §5 migration rules). +/// Amounts are a [count] of one of these units — "3 cobs", "2 packets" — never +/// a weight. Some units are pure *vibes* ("a few", "a handful") that carry no +/// number. The enum name is the stable storage key: values may be appended but +/// never renamed or repurposed (data-model §5). enum QuantityKind { - // Informal / generic - aFew(QuantityGroup.informal), - some(QuantityGroup.informal), - plenty(QuantityGroup.informal), - handful(QuantityGroup.informal), - pinch(QuantityGroup.informal), - jar(QuantityGroup.informal), - packet(QuantityGroup.informal), - // Plant-form natural units - cob(QuantityGroup.plantForm), - head(QuantityGroup.plantForm), - pod(QuantityGroup.plantForm), - ear(QuantityGroup.plantForm), - fruit(QuantityGroup.plantForm), - bulb(QuantityGroup.plantForm), - tuber(QuantityGroup.plantForm), - seedHead(QuantityGroup.plantForm), - bunch(QuantityGroup.plantForm), - // Precise - grams(QuantityGroup.precise), - count(QuantityGroup.precise); + // Vibe — rough amounts, mostly uncountable. + aFew(QuantityGroup.vibe, countable: false), + some(QuantityGroup.vibe, countable: false), + plenty(QuantityGroup.vibe, countable: false), + pinch(QuantityGroup.vibe, countable: false), + handful(QuantityGroup.vibe, countable: true), + // Size scale — informal containers, ascending. + teaspoon(QuantityGroup.size, countable: true), + spoon(QuantityGroup.size, countable: true), + cup(QuantityGroup.size, countable: true), + jar(QuantityGroup.size, countable: true), + sack(QuantityGroup.size, countable: true), + // The forms the plant gives seeds/fruit in. + packet(QuantityGroup.seedForm, countable: true), + cob(QuantityGroup.seedForm, countable: true), + pod(QuantityGroup.seedForm, countable: true), + ear(QuantityGroup.seedForm, countable: true), + head(QuantityGroup.seedForm, countable: true), + fruit(QuantityGroup.seedForm, countable: true), + bulb(QuantityGroup.seedForm, countable: true), + tuber(QuantityGroup.seedForm, countable: true), + seedHead(QuantityGroup.seedForm, countable: true), + bunch(QuantityGroup.seedForm, countable: true), + // Seedlings / plants (for a plant lot). + plant(QuantityGroup.plant, countable: true), + pot(QuantityGroup.plant, countable: true), + tray(QuantityGroup.plant, countable: true), + // Precise (optional). + grams(QuantityGroup.precise, countable: true), + count(QuantityGroup.precise, countable: true); - const QuantityKind(this.group); + const QuantityKind(this.group, {required this.countable}); final QuantityGroup group; + + /// Whether a number reads naturally ("3 cobs"). Vibes like [aFew] don't. + final bool countable; } -/// A quantity held or moved: a qualitative [kind], plus an optional [precise] -/// amount (grams / seed count) and an optional free-text [label] for rough -/// amounts no [kind] captures. Shared value type used by both Lot and Movement. +/// A held or moved amount: a [count] of an informal [kind]. Shared value type +/// used by both Lot and Movement. No weights — deliberately human. class Quantity extends Equatable { - const Quantity({required this.kind, this.precise, this.label}); + const Quantity({required this.kind, this.count, this.label}); final QuantityKind kind; - /// Optional precise amount, meaningful for [QuantityGroup.precise] kinds. - final double? precise; + /// How many of [kind] (e.g. 3 cobs, 2 packets). Null for an uncountable vibe + /// ("a few") or when unspecified. + final num? count; - /// Optional free text ("half a jam jar") when no [kind] fits well. + /// Free text fallback for a rough amount no [kind] captures. final String? label; - bool get isPrecise => precise != null; + bool get hasCount => count != null; - Quantity copyWith({QuantityKind? kind, double? precise, String? label}) { - return Quantity( - kind: kind ?? this.kind, - precise: precise ?? this.precise, - label: label ?? this.label, - ); - } + Quantity copyWith({QuantityKind? kind, num? count, String? label}) => + Quantity( + kind: kind ?? this.kind, + count: count ?? this.count, + label: label ?? this.label, + ); @override - List get props => [kind, precise, label]; + List get props => [kind, count, label]; } diff --git a/packages/commons_core/test/value/quantity_test.dart b/packages/commons_core/test/value/quantity_test.dart index cf261b1..28ac0e8 100644 --- a/packages/commons_core/test/value/quantity_test.dart +++ b/packages/commons_core/test/value/quantity_test.dart @@ -4,25 +4,22 @@ import 'package:test/test.dart'; void main() { group('Quantity', () { test('value equality ignores object identity', () { - const a = Quantity(kind: QuantityKind.pod, precise: 12, label: 'a bag'); - const b = Quantity(kind: QuantityKind.pod, precise: 12, label: 'a bag'); + const a = Quantity(kind: QuantityKind.pod, count: 3, label: 'a bag'); + const b = Quantity(kind: QuantityKind.pod, count: 3, label: 'a bag'); expect(a, b); expect(a.hashCode, b.hashCode); }); - test('isPrecise reflects the presence of a precise amount', () { - expect( - const Quantity(kind: QuantityKind.grams, precise: 5).isPrecise, - isTrue, - ); - expect(const Quantity(kind: QuantityKind.aFew).isPrecise, isFalse); + test('hasCount reflects the presence of a number', () { + expect(const Quantity(kind: QuantityKind.cob, count: 3).hasCount, isTrue); + expect(const Quantity(kind: QuantityKind.aFew).hasCount, isFalse); }); test('copyWith overrides only the given fields', () { - const q = Quantity(kind: QuantityKind.cob, precise: 2); + const q = Quantity(kind: QuantityKind.cob, count: 2); final q2 = q.copyWith(kind: QuantityKind.ear); expect(q2.kind, QuantityKind.ear); - expect(q2.precise, 2); + expect(q2.count, 2); }); }); @@ -31,11 +28,20 @@ void main() { // Guards against accidental renames — these strings live in the DB. expect(QuantityKind.pod.name, 'pod'); expect(QuantityKind.cob.name, 'cob'); - expect(QuantityKind.head.name, 'head'); expect(QuantityKind.packet.name, 'packet'); expect(QuantityKind.handful.name, 'handful'); - expect(QuantityKind.grams.name, 'grams'); - expect(QuantityKind.count.name, 'count'); + expect(QuantityKind.jar.name, 'jar'); + expect(QuantityKind.sack.name, 'sack'); + expect(QuantityKind.plant.name, 'plant'); + }); + + test('vibe amounts are not countable; forms and containers are', () { + expect(QuantityKind.aFew.countable, isFalse); + expect(QuantityKind.some.countable, isFalse); + expect(QuantityKind.cob.countable, isTrue); + expect(QuantityKind.packet.countable, isTrue); + expect(QuantityKind.jar.countable, isTrue); + expect(QuantityKind.plant.countable, isTrue); }); test('every kind belongs to a group', () {