Merge branch 'claude/zealous-nobel-7dabeb'
This commit is contained in:
commit
332f52f470
29 changed files with 5874 additions and 201 deletions
44
apps/app_seeds/lib/ui/currency_quick_picks.dart
Normal file
44
apps/app_seeds/lib/ui/currency_quick_picks.dart
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../i18n/strings.g.dart';
|
||||
|
||||
/// One-tap currency chips bound to a free-text [controller]. Ğ1 sits quietly
|
||||
/// among the familiar ones — offered, never imposed — and free text still
|
||||
/// takes anything else (sharing-model §6). Shared by the sale sheet, the lot
|
||||
/// price field and the hand-over sheet so every money-ish input feels the same.
|
||||
class CurrencyQuickPicks extends StatelessWidget {
|
||||
const CurrencyQuickPicks({
|
||||
required this.controller,
|
||||
required this.keyPrefix,
|
||||
required this.onChanged,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
|
||||
/// Namespaces the chip [Key]s (e.g. `sale` → `sale.currencyChip.€`).
|
||||
final String keyPrefix;
|
||||
|
||||
/// Fires after a chip toggles the controller text (for `setState`).
|
||||
final VoidCallback onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (final c in ['€', 'Ğ1', t.sale.hours])
|
||||
ChoiceChip(
|
||||
key: Key('$keyPrefix.currencyChip.$c'),
|
||||
label: Text(c),
|
||||
selected: controller.text.trim() == c,
|
||||
onSelected: (sel) {
|
||||
controller.text = sel ? c : '';
|
||||
onChanged();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
346
apps/app_seeds/lib/ui/handover_sheet.dart
Normal file
346
apps/app_seeds/lib/ui/handover_sheet.dart
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../data/variety_repository.dart';
|
||||
import '../db/enums.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import 'currency_quick_picks.dart';
|
||||
import 'harvest_date_picker.dart';
|
||||
import 'quantity_kind_l10n.dart';
|
||||
import 'quantity_picker.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// Opens the hand-over sheet — the ONE place to note that seeds changed hands
|
||||
/// (a gift, a swap, a sale…), optionally with money and/or a return promise.
|
||||
/// It records the Movement and spawns the Sale/Plantare rows in one save
|
||||
/// (data-model §2.8), so the person never juggles three separate forms.
|
||||
///
|
||||
/// [lots] are the variety's live batches: one → preselected; several → a
|
||||
/// chip picker; none → the quantity section hides and only the optional
|
||||
/// records are saved. [initialLot] preselects a batch (entry from a lot row).
|
||||
Future<void> showHandoverSheet(
|
||||
BuildContext context, {
|
||||
required VarietyRepository repository,
|
||||
required String varietyId,
|
||||
List<VarietyLot> lots = const [],
|
||||
VarietyLot? initialLot,
|
||||
}) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (_) => _HandoverSheet(
|
||||
repository: repository,
|
||||
varietyId: varietyId,
|
||||
lots: lots,
|
||||
initialLot: initialLot ?? (lots.length == 1 ? lots.single : null),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _HandoverSheet extends StatefulWidget {
|
||||
const _HandoverSheet({
|
||||
required this.repository,
|
||||
required this.varietyId,
|
||||
required this.lots,
|
||||
this.initialLot,
|
||||
});
|
||||
|
||||
final VarietyRepository repository;
|
||||
final String varietyId;
|
||||
final List<VarietyLot> lots;
|
||||
final VarietyLot? initialLot;
|
||||
|
||||
@override
|
||||
State<_HandoverSheet> createState() => _HandoverSheetState();
|
||||
}
|
||||
|
||||
class _HandoverSheetState extends State<_HandoverSheet> {
|
||||
HandoverDirection _direction = HandoverDirection.iGave;
|
||||
VarietyLot? _lot;
|
||||
bool _gaveAll = true;
|
||||
Quantity? _partialQuantity;
|
||||
final _counterparty = TextEditingController();
|
||||
bool _withPayment = false;
|
||||
final _amount = TextEditingController();
|
||||
final _currency = TextEditingController();
|
||||
bool _withPromise = false;
|
||||
final _owed = TextEditingController();
|
||||
bool _saving = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_lot = widget.initialLot;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_counterparty.dispose();
|
||||
_amount.dispose();
|
||||
_currency.dispose();
|
||||
_owed.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String? _nullIfBlank(String s) => s.trim().isEmpty ? null : s.trim();
|
||||
|
||||
String _lotLabel(Translations t, VarietyLot lot) {
|
||||
final parts = <String>[
|
||||
if (lot.harvestYear != null)
|
||||
harvestDateLabel(t, year: lot.harvestYear!, month: lot.harvestMonth)
|
||||
else
|
||||
t.detail.noYear,
|
||||
if (lot.quantity != null) quantityDisplay(t, lot.quantity!),
|
||||
];
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
setState(() => _saving = true);
|
||||
final counterparty = _nullIfBlank(_counterparty.text);
|
||||
final amount = double.tryParse(_amount.text.trim().replaceAll(',', '.'));
|
||||
final currency = _nullIfBlank(_currency.text);
|
||||
final owed = _nullIfBlank(_owed.text);
|
||||
final lot = _lot;
|
||||
if (lot != null) {
|
||||
await widget.repository.recordHandover(
|
||||
lotId: lot.id,
|
||||
direction: _direction,
|
||||
gaveAll: _gaveAll,
|
||||
quantity: _gaveAll ? null : _partialQuantity,
|
||||
counterparty: counterparty,
|
||||
withPayment: _withPayment,
|
||||
paymentAmount: amount,
|
||||
paymentCurrency: currency,
|
||||
withPromise: _withPromise,
|
||||
promiseOwedDescription: owed,
|
||||
);
|
||||
} else {
|
||||
// No batch to move — record just the ledgers the person asked for.
|
||||
if (_withPayment) {
|
||||
await widget.repository.createSale(
|
||||
direction: _direction == HandoverDirection.iGave
|
||||
? SaleDirection.iSold
|
||||
: SaleDirection.iBought,
|
||||
varietyId: widget.varietyId,
|
||||
counterparty: counterparty,
|
||||
amount: amount,
|
||||
currency: currency,
|
||||
);
|
||||
}
|
||||
if (_withPromise) {
|
||||
await widget.repository.createPlantare(
|
||||
direction: _direction == HandoverDirection.iGave
|
||||
? PlantareDirection.owedToMe
|
||||
: PlantareDirection.iReturn,
|
||||
varietyId: widget.varietyId,
|
||||
counterparty: counterparty,
|
||||
owedDescription: owed,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
final gave = _direction == HandoverDirection.iGave;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + 16,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
t.handover.title,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
t.handover.help,
|
||||
style: const TextStyle(color: seedMuted, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Two big, human choices — same idiom as the plantare sheet.
|
||||
for (final d in HandoverDirection.values)
|
||||
ListTile(
|
||||
key: Key('handover.direction.${d.name}'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(
|
||||
_direction == d
|
||||
? Icons.radio_button_checked
|
||||
: Icons.radio_button_unchecked,
|
||||
color: _direction == d ? seedGreen : seedMuted,
|
||||
),
|
||||
title: Text(
|
||||
d == HandoverDirection.iGave
|
||||
? t.handover.iGave
|
||||
: t.handover.iReceived,
|
||||
),
|
||||
onTap: () => setState(() => _direction = d),
|
||||
),
|
||||
if (widget.lots.length > 1) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
t.handover.whichLot,
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
for (final lot in widget.lots)
|
||||
ChoiceChip(
|
||||
key: Key('handover.lot.${lot.id}'),
|
||||
label: Text(_lotLabel(t, lot)),
|
||||
selected: _lot?.id == lot.id,
|
||||
onSelected: (sel) =>
|
||||
setState(() => _lot = sel ? lot : null),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
if (_lot != null && gave) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
t.handover.howMuch,
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
ChoiceChip(
|
||||
key: const Key('handover.allOfIt'),
|
||||
label: Text(t.handover.allOfIt),
|
||||
selected: _gaveAll,
|
||||
onSelected: (sel) => setState(() => _gaveAll = true),
|
||||
),
|
||||
ChoiceChip(
|
||||
key: const Key('handover.partOfIt'),
|
||||
label: Text(t.handover.partOfIt),
|
||||
selected: !_gaveAll,
|
||||
onSelected: (sel) => setState(() => _gaveAll = false),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!_gaveAll) ...[
|
||||
const SizedBox(height: 8),
|
||||
QuantityPicker(
|
||||
type: _lot!.type,
|
||||
value: _partialQuantity,
|
||||
onChanged: (q) => _partialQuantity = q,
|
||||
),
|
||||
],
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
key: const Key('handover.counterparty'),
|
||||
controller: _counterparty,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.sale.counterparty,
|
||||
helperText: t.sale.counterpartyHint,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Money and promise stay one tap away — a plain gift needs neither.
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
if (!_withPayment)
|
||||
ActionChip(
|
||||
key: const Key('handover.addPayment'),
|
||||
avatar: const Icon(Icons.sell_outlined, size: 18),
|
||||
label: Text(t.handover.paymentChip),
|
||||
onPressed: () => setState(() => _withPayment = true),
|
||||
),
|
||||
if (!_withPromise)
|
||||
ActionChip(
|
||||
key: const Key('handover.addPromise'),
|
||||
avatar: const Icon(
|
||||
Icons.volunteer_activism_outlined,
|
||||
size: 18,
|
||||
),
|
||||
label: Text(
|
||||
gave
|
||||
? t.handover.promiseGave
|
||||
: t.handover.promiseReceived,
|
||||
),
|
||||
onPressed: () => setState(() => _withPromise = true),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_withPayment) ...[
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
key: const Key('handover.amount'),
|
||||
controller: _amount,
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: t.sale.amount,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
key: const Key('handover.currency'),
|
||||
controller: _currency,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.sale.currency,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CurrencyQuickPicks(
|
||||
controller: _currency,
|
||||
keyPrefix: 'handover',
|
||||
onChanged: () => setState(() {}),
|
||||
),
|
||||
],
|
||||
if (_withPromise) ...[
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
key: const Key('handover.owed'),
|
||||
controller: _owed,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.plantare.owed,
|
||||
helperText: t.plantare.owedHint,
|
||||
helperMaxLines: 2,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
FilledButton(
|
||||
key: const Key('handover.save'),
|
||||
onPressed: _saving ? null : _save,
|
||||
child: Text(t.common.save),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import '../data/variety_repository.dart';
|
||||
import '../db/enums.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import 'currency_quick_picks.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// Opens the "record a sale" sheet. Optionally pre-attached to [varietyId]
|
||||
|
|
@ -140,22 +141,10 @@ class _SaleSheetState extends State<_SaleSheet> {
|
|||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Quick picks so a currency is one tap. Ğ1 sits quietly among the
|
||||
// familiar ones — offered, never imposed — and free text still takes
|
||||
// anything else.
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (final c in ['€', 'Ğ1', t.sale.hours])
|
||||
ChoiceChip(
|
||||
key: Key('sale.currencyChip.$c'),
|
||||
label: Text(c),
|
||||
selected: _currency.text.trim() == c,
|
||||
onSelected: (sel) => setState(() {
|
||||
_currency.text = sel ? c : '';
|
||||
}),
|
||||
),
|
||||
],
|
||||
CurrencyQuickPicks(
|
||||
controller: _currency,
|
||||
keyPrefix: 'sale',
|
||||
onChanged: () => setState(() {}),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ import '../domain/seed_viability.dart';
|
|||
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';
|
||||
|
|
@ -117,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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -322,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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -496,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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -1375,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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -1464,6 +1471,18 @@ Future<void> _showLotSheet(
|
|||
existing?.originName != null || existing?.originPlace != null;
|
||||
var showAbundance = existing?.abundance != null;
|
||||
var showShare = selectedShare != OfferStatus.private;
|
||||
final existingPrice = existing?.priceAmount;
|
||||
final priceAmountCtrl = TextEditingController(
|
||||
// Drop a trailing .0 so the field shows "5", not "5.0".
|
||||
text: existingPrice == null
|
||||
? ''
|
||||
: (existingPrice == existingPrice.roundToDouble()
|
||||
? existingPrice.toInt().toString()
|
||||
: existingPrice.toString()),
|
||||
);
|
||||
final priceCurrencyCtrl = TextEditingController(
|
||||
text: existing?.priceCurrency ?? '',
|
||||
);
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
|
|
@ -1660,6 +1679,49 @@ Future<void> _showLotSheet(
|
|||
style: Theme.of(sheetContext).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
// Price only when selling — informational, agreed and
|
||||
// paid off-app. Empty amount = "to be agreed".
|
||||
if (selectedShare == OfferStatus.sell) ...[
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
key: const Key('lot.priceAmount'),
|
||||
controller: priceAmountCtrl,
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: t.share.price,
|
||||
helperText: t.share.priceHint,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
key: const Key('lot.priceCurrency'),
|
||||
controller: priceCurrencyCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.sale.currency,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CurrencyQuickPicks(
|
||||
controller: priceCurrencyCtrl,
|
||||
keyPrefix: 'lot',
|
||||
onChanged: () => setState(() {}),
|
||||
),
|
||||
],
|
||||
],
|
||||
// Layer 2: advanced seed-bank details, collapsed by default.
|
||||
// Only for seed lots, and (condition checks) an existing lot.
|
||||
|
|
@ -1710,6 +1772,10 @@ Future<void> _showLotSheet(
|
|||
onPressed: () {
|
||||
final originName = _nullIfBlank(originNameCtrl.text);
|
||||
final originPlace = _nullIfBlank(originPlaceCtrl.text);
|
||||
final priceAmount = double.tryParse(
|
||||
priceAmountCtrl.text.replaceAll(',', '.').trim(),
|
||||
);
|
||||
final priceCurrency = _nullIfBlank(priceCurrencyCtrl.text);
|
||||
if (editing) {
|
||||
cubit.updateLot(
|
||||
lotId: existing.id,
|
||||
|
|
@ -1723,6 +1789,8 @@ Future<void> _showLotSheet(
|
|||
abundance: selectedAbundance,
|
||||
preservationFormat: selectedPreservation,
|
||||
offerStatus: selectedShare,
|
||||
priceAmount: priceAmount,
|
||||
priceCurrency: priceCurrency,
|
||||
);
|
||||
} else {
|
||||
cubit.addLot(
|
||||
|
|
@ -1736,6 +1804,8 @@ Future<void> _showLotSheet(
|
|||
abundance: selectedAbundance,
|
||||
preservationFormat: selectedPreservation,
|
||||
offerStatus: selectedShare,
|
||||
priceAmount: priceAmount,
|
||||
priceCurrency: priceCurrency,
|
||||
);
|
||||
}
|
||||
Navigator.of(sheetContext).pop();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue