feat(handover): one hand-over sheet replaces the separate sale/plantare doors
'Seeds changed hands' is now the single entry point on the variety detail (and per lot row): direction (gave/received), one-tap 'all of it' (default — the whole-plant/shrub case), optional partial quantity, counterparty, and reveal-on-tap money and return-promise sections that spawn the Sale/Plantare rows through recordHandover in one save. Zero-lot varieties still work (ledger rows only); several lots get a chip picker. The Sales/Plantares screens keep their own add buttons for off-app records — the sheets stay, demoted to secondary. i18n handover block in en/es/pt/ast; widget tests for the flow.
This commit is contained in:
parent
665e3e9452
commit
269fb59070
12 changed files with 849 additions and 152 deletions
|
|
@ -17,11 +17,10 @@ import '../domain/species_reference_links.dart';
|
|||
import '../i18n/strings.g.dart';
|
||||
import '../state/variety_detail_cubit.dart';
|
||||
import 'currency_quick_picks.dart';
|
||||
import 'handover_sheet.dart';
|
||||
import 'harvest_date_picker.dart';
|
||||
import 'photo_pick.dart';
|
||||
import 'plantare_sheet.dart';
|
||||
import 'quantity_kind_l10n.dart';
|
||||
import 'sale_sheet.dart';
|
||||
import 'quantity_picker.dart';
|
||||
import 'seed_glyph.dart';
|
||||
import 'theme.dart';
|
||||
|
|
@ -118,25 +117,19 @@ class _DetailView extends StatelessWidget {
|
|||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
// ONE door for "seeds changed hands": gift, swap or sale, with
|
||||
// or without money or a return promise. The sales/plantares
|
||||
// screens keep their own add buttons for off-app records.
|
||||
OutlinedButton.icon(
|
||||
key: const Key('detail.addPlantare'),
|
||||
icon: const Icon(Icons.volunteer_activism_outlined, size: 18),
|
||||
onPressed: () => showPlantareSheet(
|
||||
key: const Key('detail.handover'),
|
||||
icon: const Icon(Icons.handshake_outlined, size: 18),
|
||||
onPressed: () => showHandoverSheet(
|
||||
context,
|
||||
repository: context.read<VarietyRepository>(),
|
||||
varietyId: detail.id,
|
||||
lots: detail.lots,
|
||||
),
|
||||
label: Text(t.plantare.add),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
key: const Key('detail.addSale'),
|
||||
icon: const Icon(Icons.sell_outlined, size: 18),
|
||||
onPressed: () => showSaleSheet(
|
||||
context,
|
||||
repository: context.read<VarietyRepository>(),
|
||||
varietyId: detail.id,
|
||||
),
|
||||
label: Text(t.sale.add),
|
||||
label: Text(t.handover.title),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -323,22 +316,32 @@ class _LotTile extends StatelessWidget {
|
|||
?warning,
|
||||
],
|
||||
),
|
||||
// Germination only applies to seed lots, not to plantel.
|
||||
trailing: lot.type != LotType.seed
|
||||
? null
|
||||
: 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),
|
||||
),
|
||||
],
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (lot.type == LotType.seed && lot.latestGerminationRate != null)
|
||||
_GerminationBadge(rate: lot.latestGerminationRate!),
|
||||
IconButton(
|
||||
key: Key('lot.handover.${lot.id}'),
|
||||
icon: const Icon(Icons.handshake_outlined),
|
||||
tooltip: t.handover.title,
|
||||
onPressed: () => showHandoverSheet(
|
||||
context,
|
||||
repository: context.read<VarietyRepository>(),
|
||||
varietyId: cubit.varietyId,
|
||||
lots: [lot],
|
||||
),
|
||||
),
|
||||
// Germination only applies to seed lots, not to plantel.
|
||||
if (lot.type == LotType.seed)
|
||||
IconButton(
|
||||
key: Key('lot.germination.${lot.id}'),
|
||||
icon: const Icon(Icons.grass_outlined),
|
||||
tooltip: t.germination.title,
|
||||
onPressed: () => _showGerminationSheet(context, cubit, lot),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -497,80 +500,82 @@ Future<void> _showGerminationSheet(
|
|||
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!),
|
||||
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,
|
||||
),
|
||||
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(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
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(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
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(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -1376,62 +1381,63 @@ Future<void> _showConditionSheet(
|
|||
t.conditionCheck.title,
|
||||
style: Theme.of(sheetContext).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
key: const Key('conditionCheck.containers'),
|
||||
controller: containers,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.conditionCheck.containers,
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
key: const Key('conditionCheck.containers'),
|
||||
controller: containers,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.conditionCheck.containers,
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
t.conditionCheck.desiccant,
|
||||
style: Theme.of(sheetContext).textTheme.labelLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
for (final d in DesiccantState.values)
|
||||
ChoiceChip(
|
||||
key: Key('desiccant.${d.name}'),
|
||||
label: Text(desiccantLabel(t, d)),
|
||||
selected: state == d,
|
||||
onSelected: (sel) => setState(() => state = sel ? d : null),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
t.conditionCheck.desiccant,
|
||||
style: Theme.of(sheetContext).textTheme.labelLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
for (final d in DesiccantState.values)
|
||||
ChoiceChip(
|
||||
key: Key('desiccant.${d.name}'),
|
||||
label: Text(desiccantLabel(t, d)),
|
||||
selected: state == d,
|
||||
onSelected: (sel) =>
|
||||
setState(() => state = sel ? d : null),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(sheetContext).pop(),
|
||||
child: Text(t.common.cancel),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(sheetContext).pop(),
|
||||
child: Text(t.common.cancel),
|
||||
),
|
||||
const Spacer(),
|
||||
FilledButton(
|
||||
key: const Key('conditionCheck.save'),
|
||||
onPressed: () {
|
||||
cubit.addConditionCheck(
|
||||
lotId: lot.id,
|
||||
checkedOn: DateTime.now().millisecondsSinceEpoch,
|
||||
containerCount: int.tryParse(containers.text.trim()),
|
||||
desiccantState: state,
|
||||
);
|
||||
Navigator.of(sheetContext).pop();
|
||||
},
|
||||
child: Text(t.conditionCheck.add),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
FilledButton(
|
||||
key: const Key('conditionCheck.save'),
|
||||
onPressed: () {
|
||||
cubit.addConditionCheck(
|
||||
lotId: lot.id,
|
||||
checkedOn: DateTime.now().millisecondsSinceEpoch,
|
||||
containerCount: int.tryParse(containers.text.trim()),
|
||||
desiccantState: state,
|
||||
);
|
||||
Navigator.of(sheetContext).pop();
|
||||
},
|
||||
child: Text(t.conditionCheck.add),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue