feat(block1): germination tests per lot with derived rate
Record germination tests on a lot and surface the result. - GerminationEntry model with a derived rate (germinated / sample). VarietyLot carries its tests (most-recent first) and exposes latestGerminationRate. - VarietyRepository.addGerminationTest; watchVariety now also re-emits on germination-test changes, so the detail refreshes live. - Detail UI: each lot shows a germination % badge and a grass action that opens a sheet listing past tests and adding a new one (germinated + sample size). i18n strings (ES/EN). Tests: derived-rate + reactivity at the repository, and a widget test for the record-test → badge flow. Full suite: 35 passing, 0 skipped.
This commit is contained in:
parent
4e8b8293e0
commit
37427fa738
10 changed files with 405 additions and 6 deletions
|
|
@ -139,6 +139,20 @@ class _DetailView extends StatelessWidget {
|
|||
subtitle: lot.storageLocation == null
|
||||
? null
|
||||
: Text(lot.storageLocation!),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (lot.latestGerminationRate != null)
|
||||
_GerminationBadge(rate: lot.latestGerminationRate!),
|
||||
IconButton(
|
||||
key: Key('lot.germination.${lot.id}'),
|
||||
icon: const Icon(Icons.grass_outlined),
|
||||
tooltip: t.germination.title,
|
||||
onPressed: () =>
|
||||
_showGerminationSheet(context, cubit, lot),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -166,6 +180,135 @@ String _quantityLabel(Translations t, Quantity q) {
|
|||
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());
|
||||
|
||||
/// A compact pill showing the latest germination rate on a lot tile.
|
||||
class _GerminationBadge extends StatelessWidget {
|
||||
const _GerminationBadge({required this.rate});
|
||||
|
||||
final double rate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
_germinationPercent(context.t, rate),
|
||||
style: TextStyle(
|
||||
color: scheme.onSecondaryContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showGerminationSheet(
|
||||
BuildContext context,
|
||||
VarietyDetailCubit cubit,
|
||||
VarietyLot lot,
|
||||
) {
|
||||
final t = context.t;
|
||||
final sample = TextEditingController();
|
||||
final germinated = TextEditingController();
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (sheetContext) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: MediaQuery.of(sheetContext).viewInsets.bottom + 16,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
t.germination.title,
|
||||
style: Theme.of(sheetContext).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (lot.germinationTests.isEmpty)
|
||||
Text(t.germination.none)
|
||||
else
|
||||
for (final test in lot.germinationTests)
|
||||
ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.grass_outlined),
|
||||
title: Text(
|
||||
test.rate == null ? '—' : _germinationPercent(t, test.rate!),
|
||||
),
|
||||
subtitle:
|
||||
(test.sampleSize != null && test.germinatedCount != null)
|
||||
? Text('${test.germinatedCount}/${test.sampleSize}')
|
||||
: null,
|
||||
),
|
||||
const Divider(),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
key: const Key('germination.germinated'),
|
||||
controller: germinated,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.germination.germinated,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
key: const Key('germination.sampleSize'),
|
||||
controller: sample,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.germination.sampleSize,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(sheetContext).pop(),
|
||||
child: Text(t.common.cancel),
|
||||
),
|
||||
const Spacer(),
|
||||
FilledButton(
|
||||
key: const Key('germination.save'),
|
||||
onPressed: () {
|
||||
cubit.addGerminationTest(
|
||||
lotId: lot.id,
|
||||
testedOn: DateTime.now().millisecondsSinceEpoch,
|
||||
sampleSize: int.tryParse(sample.text.trim()),
|
||||
germinatedCount: int.tryParse(germinated.text.trim()),
|
||||
);
|
||||
Navigator.of(sheetContext).pop();
|
||||
},
|
||||
child: Text(t.germination.add),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmDelete(
|
||||
BuildContext context,
|
||||
VarietyDetailCubit cubit,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue