feat(inventory): informal quantity scale (number + unit) + seed/plant lots

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.
This commit is contained in:
vjrj 2026-07-08 10:51:02 +02:00
parent 48e9d15772
commit 3942975dba
26 changed files with 4220 additions and 315 deletions

View file

@ -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<Object?> get props => [
label,
quantityKind,
quantity,
lotType,
photoBytes,
expanded,
submitting,
@ -70,8 +80,10 @@ class QuickAddCubit extends Cubit<QuickAddState> {
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<QuickAddState> {
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));