tane/apps/app_seeds/lib/state/quick_add_cubit.dart
vjrj 3942975dba 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.
2026-07-08 10:51:02 +02:00

108 lines
2.9 KiB
Dart

import 'dart:typed_data';
import 'package:commons_core/commons_core.dart';
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.quantity,
this.lotType = LotType.seed,
this.photoBytes,
this.expanded = false,
this.submitting = false,
this.submitted = false,
this.showLabelError = false,
});
final String label;
final Quantity? quantity;
final LotType lotType;
final Uint8List? photoBytes;
final bool expanded;
final bool submitting;
final bool submitted;
final bool showLabelError;
bool get hasValidLabel => label.trim().isNotEmpty;
QuickAddState copyWith({
String? label,
Object? quantity = _unset,
LotType? lotType,
Uint8List? photoBytes,
bool? expanded,
bool? submitting,
bool? submitted,
bool? showLabelError,
}) {
return QuickAddState(
label: label ?? this.label,
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,
submitted: submitted ?? this.submitted,
showLabelError: showLabelError ?? this.showLabelError,
);
}
@override
List<Object?> get props => [
label,
quantity,
lotType,
photoBytes,
expanded,
submitting,
submitted,
showLabelError,
];
}
/// Drives the 20-second quick-add flow and persists via [VarietyRepository].
class QuickAddCubit extends Cubit<QuickAddState> {
QuickAddCubit(this._repo) : super(const QuickAddState());
final VarietyRepository _repo;
void labelChanged(String value) =>
emit(state.copyWith(label: value, showLabelError: false));
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));
void toggleExpanded() => emit(state.copyWith(expanded: !state.expanded));
/// Validates and saves. No-op (sets [showLabelError]) if the label is empty.
Future<void> submit() async {
if (!state.hasValidLabel) {
emit(state.copyWith(showLabelError: true));
return;
}
if (state.submitting) return;
emit(state.copyWith(submitting: true));
await _repo.addQuickVariety(
label: state.label.trim(),
lotType: state.lotType,
quantity: state.quantity,
photoBytes: state.photoBytes,
);
emit(state.copyWith(submitting: false, submitted: true));
}
}