feat(offers): asking price on lots, published with sale offers (schema v11)
The data model always meant sale offers to carry an informational price (data-model §2.8, sharing-model §4.4); with offer state collapsed onto the Lot, the price lives there. Null amount on a sell lot = 'to be agreed' — the NIP-99 event simply omits the price tag. - schema v11: Lots.price_amount/price_currency, guarded migration, schema dump + generated schema_v11 for the round-trip test - ShareableLot/VarietyLot carry the price (sell-only in shareableLots); addLot/updateLot persist it and clear it when the lot leaves sale - OffersCubit.publishLots forwards it (outbox flush picks it up free) - lot sheet: price + currency inputs revealed only on 'For sale', with the €/Ğ1/hours quick-picks extracted to a shared CurrencyQuickPicks widget (reused by the sale sheet) - backup JSON round-trips the new columns - i18n share.price/priceHint in en/es/pt/ast
This commit is contained in:
parent
89addb1ed7
commit
cfa1053842
25 changed files with 4690 additions and 31 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();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,6 +16,7 @@ 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 'harvest_date_picker.dart';
|
||||
import 'photo_pick.dart';
|
||||
import 'plantare_sheet.dart';
|
||||
|
|
@ -1464,6 +1465,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 +1673,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 +1766,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 +1783,8 @@ Future<void> _showLotSheet(
|
|||
abundance: selectedAbundance,
|
||||
preservationFormat: selectedPreservation,
|
||||
offerStatus: selectedShare,
|
||||
priceAmount: priceAmount,
|
||||
priceCurrency: priceCurrency,
|
||||
);
|
||||
} else {
|
||||
cubit.addLot(
|
||||
|
|
@ -1736,6 +1798,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