feat(sales): local sales/purchase ledger (any currency)

A sale is a distinct model from a gift or a Plantare (reproduction
commitment): a recorded seed sale or purchase with an optional price in
ANY currency — €, Ğ1, time, or none yet. Mirrors the Plantare feature.

- schema v10: Sales table (SyncColumns), guarded createTable migration,
  schema dump + generated schema_v10 for the migration round-trip test
- enum SaleDirection { iSold, iBought }
- VarietyRepository: create/watch/watchForVariety/delete + backup
  export/exportForSync/import (LWW-by-HLC, tombstones)
- InventorySnapshot.sales + JSON codec encode/decode (round-trips amount,
  currency, counterparty)
- UI: SalesScreen (/sales), sale sheet, drawer entry, variety-detail
  action beside 'add Plantare'; money hides a trailing .0
- i18n sale block + menu.sales in en/es/pt/ast
- tests: sales repo test (5) incl. Ğ1/price-less/backup round-trip;
  Sales screen added to the small-screen overflow guard

Also harden the overflow guard: swallow the headless engine's image
resource service PNG-decode errors (unrelated to layout) while still
failing on real RenderFlex overflows, so home/about stop reporting
false failures.
This commit is contained in:
vjrj 2026-07-11 02:33:42 +02:00
parent de6938d5d7
commit 6de039d518
27 changed files with 6156 additions and 23 deletions

View file

@ -79,6 +79,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
late final Translations$wot$en wot = Translations$wot$en.internal(_root);
late final Translations$notifications$en notifications = Translations$notifications$en.internal(_root);
late final Translations$plantare$en plantare = Translations$plantare$en.internal(_root);
late final Translations$sale$en sale = Translations$sale$en.internal(_root);
}
// Path: app
@ -218,6 +219,9 @@ class Translations$menu$en {
/// en: 'Plantares'
String get plantares => 'Plantares';
/// en: 'Sales'
String get sales => 'Sales';
/// en: 'Settings'
String get settings => 'Settings';
}
@ -1590,6 +1594,63 @@ class Translations$plantare$en {
String get removeConfirm => 'Remove this commitment?';
}
// Path: sale
class Translations$sale$en {
Translations$sale$en.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Sales'
String get title => 'Sales';
/// en: 'Record seed you sold or bought — money, Ğ1, or any currency. A separate model from a gift or a Plantare. No commission is ever taken on seeds.'
String get help => 'Record seed you sold or bought — money, Ğ1, or any currency. A separate model from a gift or a Plantare. No commission is ever taken on seeds.';
/// en: 'Record a sale'
String get add => 'Record a sale';
/// en: 'No sales yet. Note here what you sell or buy.'
String get empty => 'No sales yet. Note here what you sell or buy.';
/// en: 'I sold'
String get iSold => 'I sold';
/// en: 'I bought'
String get iBought => 'I bought';
/// en: 'Sold or bought?'
String get direction => 'Sold or bought?';
/// en: 'With whom?'
String get counterparty => 'With whom?';
/// en: 'A person or a collective (optional)'
String get counterpartyHint => 'A person or a collective (optional)';
/// en: 'Amount'
String get amount => 'Amount';
/// en: 'Currency'
String get currency => 'Currency';
/// en: '€, Ğ1, hours… (optional)'
String get currencyHint => '€, Ğ1, hours… (optional)';
/// en: 'Note (optional)'
String get note => 'Note (optional)';
/// en: 'Save'
String get save => 'Save';
/// en: 'Remove'
String get delete => 'Remove';
/// en: 'Remove this sale?'
String get removeConfirm => 'Remove this sale?';
}
// Path: intro.slides
class Translations$intro$slides$en {
Translations$intro$slides$en.internal(this._root);
@ -2075,6 +2136,7 @@ extension on Translations {
'menu.wishlist' => 'Wishlist',
'menu.following' => 'Following',
'menu.plantares' => 'Plantares',
'menu.sales' => 'Sales',
'menu.settings' => 'Settings',
'settings.language' => 'Language',
'settings.systemLanguage' => 'System language',
@ -2489,6 +2551,22 @@ extension on Translations {
'plantare.openSection' => 'Open',
'plantare.settledSection' => 'Done',
'plantare.removeConfirm' => 'Remove this commitment?',
'sale.title' => 'Sales',
'sale.help' => 'Record seed you sold or bought — money, Ğ1, or any currency. A separate model from a gift or a Plantare. No commission is ever taken on seeds.',
'sale.add' => 'Record a sale',
'sale.empty' => 'No sales yet. Note here what you sell or buy.',
'sale.iSold' => 'I sold',
'sale.iBought' => 'I bought',
'sale.direction' => 'Sold or bought?',
'sale.counterparty' => 'With whom?',
'sale.counterpartyHint' => 'A person or a collective (optional)',
'sale.amount' => 'Amount',
'sale.currency' => 'Currency',
'sale.currencyHint' => '€, Ğ1, hours… (optional)',
'sale.note' => 'Note (optional)',
'sale.save' => 'Save',
'sale.delete' => 'Remove',
'sale.removeConfirm' => 'Remove this sale?',
_ => null,
};
}