feat(ux): usable amounts, colour-coded category/form chips, currency quick-picks
Four usability passes across the everyday flows: - Quantity picker: a countable unit (cob/pod/ear…) now defaults to 1 and the stepper clamps at a minimum of 1 — you can never store 'a cob' with a blank/zero figure. Vibe units (a few) still carry no number. - Category & form chips are colour-coded (new category_palette.dart): a small muted earthy palette maps each botanical family to a STABLE tonality (fill + readable ink), and each lot form gets its own tone. Applied to the inventory filter bar (now grouped attributes | forms | families, separated by a hairline), the list's category headers (a matching colour dot), and the lot-form selector. - Sale currency: quick-pick chips (€ · Ğ1 · hours) fill the field in one tap — Ğ1 offered quietly among familiar options, never imposed — while free text still takes anything else. New i18n key sale.hours (en/es/pt/ast). Tests: quantity_picker_test (default-1, clamp, vibe, clear-still-1), category_palette_test (stable/distinct swatches); overflow guard + the inventory widget tests stay green.
This commit is contained in:
parent
6de039d518
commit
28e8318026
15 changed files with 364 additions and 53 deletions
37
apps/app_seeds/test/ui/category_palette_test.dart
Normal file
37
apps/app_seeds/test/ui/category_palette_test.dart
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/db/enums.dart';
|
||||
import 'package:tane/ui/category_palette.dart';
|
||||
|
||||
/// Category/form colours must be STABLE (same name → same tonality every
|
||||
/// launch) so a family's chip and list header always match.
|
||||
void main() {
|
||||
test('a category always maps to the same swatch', () {
|
||||
final a = categorySwatch('Solanáceas');
|
||||
final b = categorySwatch('Solanáceas');
|
||||
expect(a.fill, b.fill);
|
||||
expect(a.ink, b.ink);
|
||||
});
|
||||
|
||||
test('different families generally get different tonalities', () {
|
||||
final names = [
|
||||
'Solanáceas',
|
||||
'Leguminosas',
|
||||
'Cucurbitáceas',
|
||||
'Brasicáceas',
|
||||
'Asteráceas',
|
||||
'Amarantáceas',
|
||||
];
|
||||
final inks = {for (final n in names) categorySwatch(n).ink};
|
||||
// Not required to be all-distinct (palette wraps), but a fixed input set
|
||||
// must not collapse to a single colour.
|
||||
expect(inks.length, greaterThan(1));
|
||||
});
|
||||
|
||||
test('every lot form has its own swatch', () {
|
||||
for (final type in LotType.values) {
|
||||
final s = lotTypeSwatch(type);
|
||||
expect(s.fill, isNotNull);
|
||||
expect(s.ink, isNotNull);
|
||||
}
|
||||
});
|
||||
}
|
||||
83
apps/app_seeds/test/ui/quantity_picker_test.dart
Normal file
83
apps/app_seeds/test/ui/quantity_picker_test.dart
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/db/enums.dart';
|
||||
import 'package:tane/ui/quantity_picker.dart';
|
||||
|
||||
import '../support/test_support.dart';
|
||||
|
||||
/// A countable amount must never be blank: picking cob/pod/… defaults to 1 and
|
||||
/// the stepper can't drop below 1 — you can't save "a cob" with no figure.
|
||||
void main() {
|
||||
Quantity? emitted;
|
||||
|
||||
Future<void> pumpPicker(WidgetTester tester,
|
||||
{LotType type = LotType.seed}) async {
|
||||
emitted = null;
|
||||
final db = newTestDatabase();
|
||||
addTearDown(db.close);
|
||||
await tester.pumpWidget(wrapScreen(
|
||||
repository: newTestRepository(db),
|
||||
child: Scaffold(
|
||||
body: QuantityPicker(
|
||||
type: type,
|
||||
onChanged: (q) => emitted = q,
|
||||
),
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
testWidgets('selecting a countable unit defaults the count to 1',
|
||||
(tester) async {
|
||||
await pumpPicker(tester);
|
||||
await tester.tap(find.byKey(const Key('quantity.kind.cob')));
|
||||
await tester.pump();
|
||||
|
||||
expect(emitted, isNotNull);
|
||||
expect(emitted!.kind, QuantityKind.cob);
|
||||
expect(emitted!.count, 1);
|
||||
expect(
|
||||
find.widgetWithText(TextField, '1'),
|
||||
findsOneWidget,
|
||||
reason: 'the count field shows 1, not an empty box',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('the minus button never takes a countable amount below 1',
|
||||
(tester) async {
|
||||
await pumpPicker(tester);
|
||||
await tester.tap(find.byKey(const Key('quantity.kind.cob')));
|
||||
await tester.pump();
|
||||
// Two decrements from 1 must clamp at 1, never 0 or blank.
|
||||
await tester.tap(find.byIcon(Icons.remove));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byIcon(Icons.remove));
|
||||
await tester.pump();
|
||||
|
||||
expect(emitted!.count, 1);
|
||||
});
|
||||
|
||||
testWidgets('a vibe unit (a few) carries no number and shows no stepper',
|
||||
(tester) async {
|
||||
await pumpPicker(tester);
|
||||
await tester.tap(find.byKey(const Key('quantity.kind.aFew')));
|
||||
await tester.pump();
|
||||
|
||||
expect(emitted!.kind, QuantityKind.aFew);
|
||||
expect(emitted!.count, isNull);
|
||||
expect(find.byKey(const Key('quantity.count')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('typing then clearing the box still stores 1, never empty',
|
||||
(tester) async {
|
||||
await pumpPicker(tester);
|
||||
await tester.tap(find.byKey(const Key('quantity.kind.pod')));
|
||||
await tester.pump();
|
||||
await tester.enterText(find.byKey(const Key('quantity.count')), '');
|
||||
await tester.pump();
|
||||
|
||||
expect(emitted!.kind, QuantityKind.pod);
|
||||
expect(emitted!.count, 1);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue