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:
parent
de6938d5d7
commit
6de039d518
27 changed files with 6156 additions and 23 deletions
2151
apps/app_seeds/drift_schemas/drift_schema_v10.json
Normal file
2151
apps/app_seeds/drift_schemas/drift_schema_v10.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -33,6 +33,7 @@ import 'ui/home_screen.dart';
|
||||||
import 'ui/intro_screen.dart';
|
import 'ui/intro_screen.dart';
|
||||||
import 'ui/inventory_list_screen.dart';
|
import 'ui/inventory_list_screen.dart';
|
||||||
import 'ui/plantares_screen.dart';
|
import 'ui/plantares_screen.dart';
|
||||||
|
import 'ui/sales_screen.dart';
|
||||||
import 'ui/market_offer_detail_screen.dart';
|
import 'ui/market_offer_detail_screen.dart';
|
||||||
import 'ui/market_screen.dart';
|
import 'ui/market_screen.dart';
|
||||||
import 'ui/offline_banner.dart';
|
import 'ui/offline_banner.dart';
|
||||||
|
|
@ -260,6 +261,11 @@ class TaneApp extends StatelessWidget {
|
||||||
path: '/plantares',
|
path: '/plantares',
|
||||||
builder: (context, state) => const PlantaresScreen(),
|
builder: (context, state) => const PlantaresScreen(),
|
||||||
),
|
),
|
||||||
|
// Local sales ledger — also inventory-adjacent, no network.
|
||||||
|
GoRoute(
|
||||||
|
path: '/sales',
|
||||||
|
builder: (context, state) => const SalesScreen(),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/variety/:id',
|
path: '/variety/:id',
|
||||||
builder: (context, state) => BlocProvider(
|
builder: (context, state) => BlocProvider(
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,26 @@ class InventoryJsonCodec {
|
||||||
'note': p.note,
|
'note': p.note,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
'sales': [
|
||||||
|
for (final s in snapshot.sales)
|
||||||
|
{
|
||||||
|
..._syncMeta(
|
||||||
|
id: s.id,
|
||||||
|
createdAt: s.createdAt,
|
||||||
|
updatedAt: s.updatedAt,
|
||||||
|
lastAuthor: s.lastAuthor,
|
||||||
|
schemaRowVersion: s.schemaRowVersion,
|
||||||
|
isDeleted: s.isDeleted,
|
||||||
|
),
|
||||||
|
'varietyId': s.varietyId,
|
||||||
|
'direction': s.direction.name,
|
||||||
|
'counterparty': s.counterparty,
|
||||||
|
'amount': s.amount,
|
||||||
|
'currency': s.currency,
|
||||||
|
'soldOn': s.soldOn,
|
||||||
|
'note': s.note,
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -462,6 +482,24 @@ class InventoryJsonCodec {
|
||||||
note: m['note'] as String?,
|
note: m['note'] as String?,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
sales: _rows(root, 'sales', (m) {
|
||||||
|
return Sale(
|
||||||
|
id: _string(m, 'id'),
|
||||||
|
createdAt: _int(m, 'createdAt'),
|
||||||
|
updatedAt: _string(m, 'updatedAt'),
|
||||||
|
lastAuthor: _string(m, 'lastAuthor'),
|
||||||
|
isDeleted: _bool(m, 'isDeleted'),
|
||||||
|
schemaRowVersion: _int(m, 'schemaRowVersion', fallback: 1),
|
||||||
|
varietyId: m['varietyId'] as String?,
|
||||||
|
direction:
|
||||||
|
_enumOr(SaleDirection.values, m['direction'], SaleDirection.iSold),
|
||||||
|
counterparty: m['counterparty'] as String?,
|
||||||
|
amount: (m['amount'] as num?)?.toDouble(),
|
||||||
|
currency: m['currency'] as String?,
|
||||||
|
soldOn: _int(m, 'soldOn'),
|
||||||
|
note: m['note'] as String?,
|
||||||
|
);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ class InventorySnapshot {
|
||||||
this.parties = const [],
|
this.parties = const [],
|
||||||
this.attachments = const [],
|
this.attachments = const [],
|
||||||
this.plantares = const [],
|
this.plantares = const [],
|
||||||
|
this.sales = const [],
|
||||||
this.speciesNamesById = const {},
|
this.speciesNamesById = const {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -44,6 +45,7 @@ class InventorySnapshot {
|
||||||
final List<Party> parties;
|
final List<Party> parties;
|
||||||
final List<Attachment> attachments;
|
final List<Attachment> attachments;
|
||||||
final List<Plantare> plantares;
|
final List<Plantare> plantares;
|
||||||
|
final List<Sale> sales;
|
||||||
|
|
||||||
/// speciesId → scientificName, for the species referenced by [varieties].
|
/// speciesId → scientificName, for the species referenced by [varieties].
|
||||||
final Map<String, String> speciesNamesById;
|
final Map<String, String> speciesNamesById;
|
||||||
|
|
|
||||||
|
|
@ -1781,6 +1781,64 @@ class VarietyRepository {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Sales: recorded seed sales (separate from gift/Plantare) --------------
|
||||||
|
|
||||||
|
/// Records a seed sale (or purchase) — price in ANY currency. Returns its id.
|
||||||
|
Future<String> createSale({
|
||||||
|
required SaleDirection direction,
|
||||||
|
String? varietyId,
|
||||||
|
String? counterparty,
|
||||||
|
double? amount,
|
||||||
|
String? currency,
|
||||||
|
String? note,
|
||||||
|
}) async {
|
||||||
|
final (created, updated) = await _stamp();
|
||||||
|
final id = idGen.newId();
|
||||||
|
await _db.into(_db.sales).insert(
|
||||||
|
SalesCompanion.insert(
|
||||||
|
id: id,
|
||||||
|
createdAt: created,
|
||||||
|
updatedAt: updated,
|
||||||
|
lastAuthor: nodeId,
|
||||||
|
direction: direction,
|
||||||
|
varietyId: Value(varietyId),
|
||||||
|
counterparty: Value(counterparty),
|
||||||
|
amount: Value(amount),
|
||||||
|
currency: Value(currency),
|
||||||
|
soldOn: created,
|
||||||
|
note: Value(note),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All live sales, newest first (for the Sales screen).
|
||||||
|
Stream<List<Sale>> watchSales() => (_db.select(_db.sales)
|
||||||
|
..where((s) => s.isDeleted.equals(false))
|
||||||
|
..orderBy([(s) => OrderingTerm.desc(s.soldOn)]))
|
||||||
|
.watch();
|
||||||
|
|
||||||
|
/// The sales recorded against one variety.
|
||||||
|
Stream<List<Sale>> watchSalesForVariety(String varietyId) =>
|
||||||
|
(_db.select(_db.sales)
|
||||||
|
..where(
|
||||||
|
(s) => s.varietyId.equals(varietyId) & s.isDeleted.equals(false),
|
||||||
|
)
|
||||||
|
..orderBy([(s) => OrderingTerm.desc(s.soldOn)]))
|
||||||
|
.watch();
|
||||||
|
|
||||||
|
/// Soft-deletes a sale (tombstone, so it syncs as a removal).
|
||||||
|
Future<void> deleteSale(String id) async {
|
||||||
|
final (_, updated) = await _stamp();
|
||||||
|
await (_db.update(_db.sales)..where((s) => s.id.equals(id))).write(
|
||||||
|
SalesCompanion(
|
||||||
|
isDeleted: const Value(true),
|
||||||
|
updatedAt: Value(updated),
|
||||||
|
lastAuthor: Value(nodeId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Snapshots the live inventory (tombstones excluded) for the interchange
|
/// Snapshots the live inventory (tombstones excluded) for the interchange
|
||||||
/// export — data-model §7. Includes photo bytes; the JSON codec embeds them
|
/// export — data-model §7. Includes photo bytes; the JSON codec embeds them
|
||||||
/// as base64 and the CSV codec ignores them.
|
/// as base64 and the CSV codec ignores them.
|
||||||
|
|
@ -1819,6 +1877,9 @@ class VarietyRepository {
|
||||||
plantares: await (_db.select(
|
plantares: await (_db.select(
|
||||||
_db.plantares,
|
_db.plantares,
|
||||||
)..where((p) => p.isDeleted.equals(false))).get(),
|
)..where((p) => p.isDeleted.equals(false))).get(),
|
||||||
|
sales: await (_db.select(
|
||||||
|
_db.sales,
|
||||||
|
)..where((s) => s.isDeleted.equals(false))).get(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1842,6 +1903,7 @@ class VarietyRepository {
|
||||||
movements: await _db.select(_db.movements).get(),
|
movements: await _db.select(_db.movements).get(),
|
||||||
parties: await _db.select(_db.parties).get(),
|
parties: await _db.select(_db.parties).get(),
|
||||||
plantares: await _db.select(_db.plantares).get(),
|
plantares: await _db.select(_db.plantares).get(),
|
||||||
|
sales: await _db.select(_db.sales).get(),
|
||||||
// attachments intentionally omitted — photos don't ride the sync wire.
|
// attachments intentionally omitted — photos don't ride the sync wire.
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1942,6 +2004,14 @@ class VarietyRepository {
|
||||||
updatedAtOf: (r) => r.updatedAt,
|
updatedAtOf: (r) => r.updatedAt,
|
||||||
reconciler: reconciler,
|
reconciler: reconciler,
|
||||||
);
|
);
|
||||||
|
summary += await _importMutableRows(
|
||||||
|
table: _db.sales,
|
||||||
|
idColumn: _db.sales.id,
|
||||||
|
rows: snapshot.sales,
|
||||||
|
idOf: (r) => r.id,
|
||||||
|
updatedAtOf: (r) => r.updatedAt,
|
||||||
|
reconciler: reconciler,
|
||||||
|
);
|
||||||
summary += await _importMovements(snapshot.movements, reconciler);
|
summary += await _importMovements(snapshot.movements, reconciler);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1955,6 +2025,7 @@ class VarietyRepository {
|
||||||
for (final p in snapshot.parties) p.updatedAt,
|
for (final p in snapshot.parties) p.updatedAt,
|
||||||
for (final a in snapshot.attachments) a.updatedAt,
|
for (final a in snapshot.attachments) a.updatedAt,
|
||||||
for (final pl in snapshot.plantares) pl.updatedAt,
|
for (final pl in snapshot.plantares) pl.updatedAt,
|
||||||
|
for (final s in snapshot.sales) s.updatedAt,
|
||||||
]);
|
]);
|
||||||
if (maxIncoming != null) {
|
if (maxIncoming != null) {
|
||||||
_clock = _clock.receiveEvent(maxIncoming, _now());
|
_clock = _clock.receiveEvent(maxIncoming, _now());
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ part 'database.g.dart';
|
||||||
Attachments,
|
Attachments,
|
||||||
ExternalLinks,
|
ExternalLinks,
|
||||||
Plantares,
|
Plantares,
|
||||||
|
Sales,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
|
|
@ -30,7 +31,7 @@ class AppDatabase extends _$AppDatabase {
|
||||||
|
|
||||||
/// Current schema version; also stamped into interchange exports so an
|
/// Current schema version; also stamped into interchange exports so an
|
||||||
/// importer knows which app generation wrote the file (data-model §7).
|
/// importer knows which app generation wrote the file (data-model §7).
|
||||||
static const int currentSchemaVersion = 9;
|
static const int currentSchemaVersion = 10;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => currentSchemaVersion;
|
int get schemaVersion => currentSchemaVersion;
|
||||||
|
|
@ -146,6 +147,12 @@ class AppDatabase extends _$AppDatabase {
|
||||||
await m.createTable(plantares);
|
await m.createTable(plantares);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// v10: Sales — recorded seed sales (separate from gift/Plantare). Guarded.
|
||||||
|
if (from < 10) {
|
||||||
|
if (!await _hasTable('sales')) {
|
||||||
|
await m.createTable(sales);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -101,3 +101,8 @@ enum PlantareDirection {
|
||||||
/// Lifecycle of a Plantare. Framed as a promise, not a debt (data-model §2.7):
|
/// Lifecycle of a Plantare. Framed as a promise, not a debt (data-model §2.7):
|
||||||
/// `forgiven` (not "defaulted") when it's let go.
|
/// `forgiven` (not "defaulted") when it's let go.
|
||||||
enum PlantareStatus { open, returned, forgiven }
|
enum PlantareStatus { open, returned, forgiven }
|
||||||
|
|
||||||
|
/// Direction of a recorded seed Sale, seen from this app's owner. A sale is a
|
||||||
|
/// SEPARATE model from a gift or a Plantare — seed for money (any currency:
|
||||||
|
/// €, Ğ1, a local/time currency…). No commission is ever taken on seeds.
|
||||||
|
enum SaleDirection { iSold, iBought }
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,34 @@ class Plantares extends Table with SyncColumns {
|
||||||
TextColumn get note => text().nullable()();
|
TextColumn get note => text().nullable()();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A recorded seed Sale — seed for money (data-model §2.8/sharing-model §6). A
|
||||||
|
/// SEPARATE model from a gift or a Plantare: a completed exchange with a price
|
||||||
|
/// in ANY currency (€, Ğ1, a local/time currency). Local-first ledger; the
|
||||||
|
/// cross-party/receipt form is a later social-layer concern.
|
||||||
|
class Sales extends Table with SyncColumns {
|
||||||
|
/// The seed sold/bought. Nullable so a sale can be jotted before it's linked
|
||||||
|
/// to a catalogued variety.
|
||||||
|
TextColumn get varietyId => text().nullable()(); // → Variety
|
||||||
|
|
||||||
|
/// Whether I sold or I bought.
|
||||||
|
TextColumn get direction => textEnum<SaleDirection>()();
|
||||||
|
|
||||||
|
/// The other party as a human name (v1).
|
||||||
|
TextColumn get counterparty => text().nullable()();
|
||||||
|
|
||||||
|
/// Price paid. Nullable — a barter-ish "sale" recorded before a figure is set.
|
||||||
|
RealColumn get amount => real().nullable()();
|
||||||
|
|
||||||
|
/// The currency, free text: "€", "Ğ1", "horas", a local currency name. Never
|
||||||
|
/// assumed — the seed world uses many (sharing-model §6).
|
||||||
|
TextColumn get currency => text().nullable()();
|
||||||
|
|
||||||
|
/// When the sale happened (ms since epoch).
|
||||||
|
IntColumn get soldOn => integer()();
|
||||||
|
|
||||||
|
TextColumn get note => text().nullable()();
|
||||||
|
}
|
||||||
|
|
||||||
/// Any pasted URL (Wikipedia, forum…). Polymorphic parent.
|
/// Any pasted URL (Wikipedia, forum…). Polymorphic parent.
|
||||||
class ExternalLinks extends Table with SyncColumns {
|
class ExternalLinks extends Table with SyncColumns {
|
||||||
TextColumn get parentType => textEnum<ParentType>()();
|
TextColumn get parentType => textEnum<ParentType>()();
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
"wishlist": "Llista de deseos",
|
"wishlist": "Llista de deseos",
|
||||||
"following": "Siguiendo",
|
"following": "Siguiendo",
|
||||||
"plantares": "Plantares",
|
"plantares": "Plantares",
|
||||||
|
"sales": "Ventes",
|
||||||
"settings": "Axustes"
|
"settings": "Axustes"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|
@ -577,5 +578,23 @@
|
||||||
"openSection": "Pendientes",
|
"openSection": "Pendientes",
|
||||||
"settledSection": "Fechos",
|
"settledSection": "Fechos",
|
||||||
"removeConfirm": "¿Quitar esti compromisu?"
|
"removeConfirm": "¿Quitar esti compromisu?"
|
||||||
|
},
|
||||||
|
"sale": {
|
||||||
|
"title": "Ventes",
|
||||||
|
"help": "Rexistra semilla vendida o mercada — dineru, Ğ1 o cualquier moneda. Un modelu aparte del regalu y del Plantare. Enxamás se cobra comisión poles semilles.",
|
||||||
|
"add": "Rexistrar venta",
|
||||||
|
"empty": "Entá nun hai ventes. Anota equí lo que vendes o merques.",
|
||||||
|
"iSold": "Vendí",
|
||||||
|
"iBought": "Mercué",
|
||||||
|
"direction": "¿Vendes o merques?",
|
||||||
|
"counterparty": "¿Con quién?",
|
||||||
|
"counterpartyHint": "Una persona o un coleutivu (opcional)",
|
||||||
|
"amount": "Importe",
|
||||||
|
"currency": "Moneda",
|
||||||
|
"currencyHint": "€, Ğ1, hores… (opcional)",
|
||||||
|
"note": "Nota (opcional)",
|
||||||
|
"save": "Guardar",
|
||||||
|
"delete": "Quitar",
|
||||||
|
"removeConfirm": "¿Quitar esta venta?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
"wishlist": "Wishlist",
|
"wishlist": "Wishlist",
|
||||||
"following": "Following",
|
"following": "Following",
|
||||||
"plantares": "Plantares",
|
"plantares": "Plantares",
|
||||||
|
"sales": "Sales",
|
||||||
"settings": "Settings"
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|
@ -580,5 +581,23 @@
|
||||||
"openSection": "Open",
|
"openSection": "Open",
|
||||||
"settledSection": "Done",
|
"settledSection": "Done",
|
||||||
"removeConfirm": "Remove this commitment?"
|
"removeConfirm": "Remove this commitment?"
|
||||||
|
},
|
||||||
|
"sale": {
|
||||||
|
"title": "Sales",
|
||||||
|
"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.",
|
||||||
|
"add": "Record a sale",
|
||||||
|
"empty": "No sales yet. Note here what you sell or buy.",
|
||||||
|
"iSold": "I sold",
|
||||||
|
"iBought": "I bought",
|
||||||
|
"direction": "Sold or bought?",
|
||||||
|
"counterparty": "With whom?",
|
||||||
|
"counterpartyHint": "A person or a collective (optional)",
|
||||||
|
"amount": "Amount",
|
||||||
|
"currency": "Currency",
|
||||||
|
"currencyHint": "€, Ğ1, hours… (optional)",
|
||||||
|
"note": "Note (optional)",
|
||||||
|
"save": "Save",
|
||||||
|
"delete": "Remove",
|
||||||
|
"removeConfirm": "Remove this sale?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
"wishlist": "Lista de deseos",
|
"wishlist": "Lista de deseos",
|
||||||
"following": "Siguiendo",
|
"following": "Siguiendo",
|
||||||
"plantares": "Plantares",
|
"plantares": "Plantares",
|
||||||
|
"sales": "Ventas",
|
||||||
"settings": "Ajustes"
|
"settings": "Ajustes"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|
@ -579,5 +580,23 @@
|
||||||
"openSection": "Pendientes",
|
"openSection": "Pendientes",
|
||||||
"settledSection": "Hechos",
|
"settledSection": "Hechos",
|
||||||
"removeConfirm": "¿Quitar este compromiso?"
|
"removeConfirm": "¿Quitar este compromiso?"
|
||||||
|
},
|
||||||
|
"sale": {
|
||||||
|
"title": "Ventas",
|
||||||
|
"help": "Registra semilla vendida o comprada — dinero, Ğ1 o cualquier moneda. Un modelo aparte del regalo y del Plantare. Nunca se cobra comisión por las semillas.",
|
||||||
|
"add": "Registrar venta",
|
||||||
|
"empty": "Aún no hay ventas. Anota aquí lo que vendes o compras.",
|
||||||
|
"iSold": "Vendí",
|
||||||
|
"iBought": "Compré",
|
||||||
|
"direction": "¿Vendes o compras?",
|
||||||
|
"counterparty": "¿Con quién?",
|
||||||
|
"counterpartyHint": "Una persona o un colectivo (opcional)",
|
||||||
|
"amount": "Importe",
|
||||||
|
"currency": "Moneda",
|
||||||
|
"currencyHint": "€, Ğ1, horas… (opcional)",
|
||||||
|
"note": "Nota (opcional)",
|
||||||
|
"save": "Guardar",
|
||||||
|
"delete": "Quitar",
|
||||||
|
"removeConfirm": "¿Quitar esta venta?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
"wishlist": "Lista de desejos",
|
"wishlist": "Lista de desejos",
|
||||||
"following": "A seguir",
|
"following": "A seguir",
|
||||||
"plantares": "Plantares",
|
"plantares": "Plantares",
|
||||||
|
"sales": "Vendas",
|
||||||
"settings": "Definições"
|
"settings": "Definições"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|
@ -576,5 +577,23 @@
|
||||||
"openSection": "Pendentes",
|
"openSection": "Pendentes",
|
||||||
"settledSection": "Feitos",
|
"settledSection": "Feitos",
|
||||||
"removeConfirm": "Remover este compromisso?"
|
"removeConfirm": "Remover este compromisso?"
|
||||||
|
},
|
||||||
|
"sale": {
|
||||||
|
"title": "Vendas",
|
||||||
|
"help": "Regista semente vendida ou comprada — dinheiro, Ğ1 ou qualquer moeda. Um modelo separado do presente e do Plantare. Nunca se cobra comissão pelas sementes.",
|
||||||
|
"add": "Registar venda",
|
||||||
|
"empty": "Ainda não há vendas. Anota aqui o que vendes ou compras.",
|
||||||
|
"iSold": "Vendi",
|
||||||
|
"iBought": "Comprei",
|
||||||
|
"direction": "Vendes ou compras?",
|
||||||
|
"counterparty": "Com quem?",
|
||||||
|
"counterpartyHint": "Uma pessoa ou um coletivo (opcional)",
|
||||||
|
"amount": "Montante",
|
||||||
|
"currency": "Moeda",
|
||||||
|
"currencyHint": "€, Ğ1, horas… (opcional)",
|
||||||
|
"note": "Nota (opcional)",
|
||||||
|
"save": "Guardar",
|
||||||
|
"delete": "Remover",
|
||||||
|
"removeConfirm": "Remover esta venda?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 4
|
/// Locales: 4
|
||||||
/// Strings: 1760 (440 per locale)
|
/// Strings: 1828 (457 per locale)
|
||||||
///
|
///
|
||||||
/// Built on 2026-07-11 at 00:04 UTC
|
/// Built on 2026-07-11 at 00:20 UTC
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ class TranslationsAst extends Translations with BaseTranslations<AppLocale, Tran
|
||||||
@override late final _Translations$wot$ast wot = _Translations$wot$ast._(_root);
|
@override late final _Translations$wot$ast wot = _Translations$wot$ast._(_root);
|
||||||
@override late final _Translations$notifications$ast notifications = _Translations$notifications$ast._(_root);
|
@override late final _Translations$notifications$ast notifications = _Translations$notifications$ast._(_root);
|
||||||
@override late final _Translations$plantare$ast plantare = _Translations$plantare$ast._(_root);
|
@override late final _Translations$plantare$ast plantare = _Translations$plantare$ast._(_root);
|
||||||
|
@override late final _Translations$sale$ast sale = _Translations$sale$ast._(_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: app
|
// Path: app
|
||||||
|
|
@ -159,6 +160,7 @@ class _Translations$menu$ast extends Translations$menu$en {
|
||||||
@override String get wishlist => 'Llista de deseos';
|
@override String get wishlist => 'Llista de deseos';
|
||||||
@override String get following => 'Siguiendo';
|
@override String get following => 'Siguiendo';
|
||||||
@override String get plantares => 'Plantares';
|
@override String get plantares => 'Plantares';
|
||||||
|
@override String get sales => 'Ventes';
|
||||||
@override String get settings => 'Axustes';
|
@override String get settings => 'Axustes';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -839,6 +841,31 @@ class _Translations$plantare$ast extends Translations$plantare$en {
|
||||||
@override String get removeConfirm => '¿Quitar esti compromisu?';
|
@override String get removeConfirm => '¿Quitar esti compromisu?';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path: sale
|
||||||
|
class _Translations$sale$ast extends Translations$sale$en {
|
||||||
|
_Translations$sale$ast._(TranslationsAst root) : this._root = root, super.internal(root);
|
||||||
|
|
||||||
|
final TranslationsAst _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
@override String get title => 'Ventes';
|
||||||
|
@override String get help => 'Rexistra semilla vendida o mercada — dineru, Ğ1 o cualquier moneda. Un modelu aparte del regalu y del Plantare. Enxamás se cobra comisión poles semilles.';
|
||||||
|
@override String get add => 'Rexistrar venta';
|
||||||
|
@override String get empty => 'Entá nun hai ventes. Anota equí lo que vendes o merques.';
|
||||||
|
@override String get iSold => 'Vendí';
|
||||||
|
@override String get iBought => 'Mercué';
|
||||||
|
@override String get direction => '¿Vendes o merques?';
|
||||||
|
@override String get counterparty => '¿Con quién?';
|
||||||
|
@override String get counterpartyHint => 'Una persona o un coleutivu (opcional)';
|
||||||
|
@override String get amount => 'Importe';
|
||||||
|
@override String get currency => 'Moneda';
|
||||||
|
@override String get currencyHint => '€, Ğ1, hores… (opcional)';
|
||||||
|
@override String get note => 'Nota (opcional)';
|
||||||
|
@override String get save => 'Guardar';
|
||||||
|
@override String get delete => 'Quitar';
|
||||||
|
@override String get removeConfirm => '¿Quitar esta venta?';
|
||||||
|
}
|
||||||
|
|
||||||
// Path: intro.slides
|
// Path: intro.slides
|
||||||
class _Translations$intro$slides$ast extends Translations$intro$slides$en {
|
class _Translations$intro$slides$ast extends Translations$intro$slides$en {
|
||||||
_Translations$intro$slides$ast._(TranslationsAst root) : this._root = root, super.internal(root);
|
_Translations$intro$slides$ast._(TranslationsAst root) : this._root = root, super.internal(root);
|
||||||
|
|
@ -1207,6 +1234,7 @@ extension on TranslationsAst {
|
||||||
'menu.wishlist' => 'Llista de deseos',
|
'menu.wishlist' => 'Llista de deseos',
|
||||||
'menu.following' => 'Siguiendo',
|
'menu.following' => 'Siguiendo',
|
||||||
'menu.plantares' => 'Plantares',
|
'menu.plantares' => 'Plantares',
|
||||||
|
'menu.sales' => 'Ventes',
|
||||||
'menu.settings' => 'Axustes',
|
'menu.settings' => 'Axustes',
|
||||||
'settings.language' => 'Llingua',
|
'settings.language' => 'Llingua',
|
||||||
'settings.systemLanguage' => 'Llingua del sistema',
|
'settings.systemLanguage' => 'Llingua del sistema',
|
||||||
|
|
@ -1619,6 +1647,22 @@ extension on TranslationsAst {
|
||||||
'plantare.openSection' => 'Pendientes',
|
'plantare.openSection' => 'Pendientes',
|
||||||
'plantare.settledSection' => 'Fechos',
|
'plantare.settledSection' => 'Fechos',
|
||||||
'plantare.removeConfirm' => '¿Quitar esti compromisu?',
|
'plantare.removeConfirm' => '¿Quitar esti compromisu?',
|
||||||
|
'sale.title' => 'Ventes',
|
||||||
|
'sale.help' => 'Rexistra semilla vendida o mercada — dineru, Ğ1 o cualquier moneda. Un modelu aparte del regalu y del Plantare. Enxamás se cobra comisión poles semilles.',
|
||||||
|
'sale.add' => 'Rexistrar venta',
|
||||||
|
'sale.empty' => 'Entá nun hai ventes. Anota equí lo que vendes o merques.',
|
||||||
|
'sale.iSold' => 'Vendí',
|
||||||
|
'sale.iBought' => 'Mercué',
|
||||||
|
'sale.direction' => '¿Vendes o merques?',
|
||||||
|
'sale.counterparty' => '¿Con quién?',
|
||||||
|
'sale.counterpartyHint' => 'Una persona o un coleutivu (opcional)',
|
||||||
|
'sale.amount' => 'Importe',
|
||||||
|
'sale.currency' => 'Moneda',
|
||||||
|
'sale.currencyHint' => '€, Ğ1, hores… (opcional)',
|
||||||
|
'sale.note' => 'Nota (opcional)',
|
||||||
|
'sale.save' => 'Guardar',
|
||||||
|
'sale.delete' => 'Quitar',
|
||||||
|
'sale.removeConfirm' => '¿Quitar esta venta?',
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
|
||||||
late final Translations$wot$en wot = Translations$wot$en.internal(_root);
|
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$notifications$en notifications = Translations$notifications$en.internal(_root);
|
||||||
late final Translations$plantare$en plantare = Translations$plantare$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
|
// Path: app
|
||||||
|
|
@ -218,6 +219,9 @@ class Translations$menu$en {
|
||||||
/// en: 'Plantares'
|
/// en: 'Plantares'
|
||||||
String get plantares => 'Plantares';
|
String get plantares => 'Plantares';
|
||||||
|
|
||||||
|
/// en: 'Sales'
|
||||||
|
String get sales => 'Sales';
|
||||||
|
|
||||||
/// en: 'Settings'
|
/// en: 'Settings'
|
||||||
String get settings => 'Settings';
|
String get settings => 'Settings';
|
||||||
}
|
}
|
||||||
|
|
@ -1590,6 +1594,63 @@ class Translations$plantare$en {
|
||||||
String get removeConfirm => 'Remove this commitment?';
|
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
|
// Path: intro.slides
|
||||||
class Translations$intro$slides$en {
|
class Translations$intro$slides$en {
|
||||||
Translations$intro$slides$en.internal(this._root);
|
Translations$intro$slides$en.internal(this._root);
|
||||||
|
|
@ -2075,6 +2136,7 @@ extension on Translations {
|
||||||
'menu.wishlist' => 'Wishlist',
|
'menu.wishlist' => 'Wishlist',
|
||||||
'menu.following' => 'Following',
|
'menu.following' => 'Following',
|
||||||
'menu.plantares' => 'Plantares',
|
'menu.plantares' => 'Plantares',
|
||||||
|
'menu.sales' => 'Sales',
|
||||||
'menu.settings' => 'Settings',
|
'menu.settings' => 'Settings',
|
||||||
'settings.language' => 'Language',
|
'settings.language' => 'Language',
|
||||||
'settings.systemLanguage' => 'System language',
|
'settings.systemLanguage' => 'System language',
|
||||||
|
|
@ -2489,6 +2551,22 @@ extension on Translations {
|
||||||
'plantare.openSection' => 'Open',
|
'plantare.openSection' => 'Open',
|
||||||
'plantare.settledSection' => 'Done',
|
'plantare.settledSection' => 'Done',
|
||||||
'plantare.removeConfirm' => 'Remove this commitment?',
|
'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,
|
_ => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
|
||||||
@override late final _Translations$wot$es wot = _Translations$wot$es._(_root);
|
@override late final _Translations$wot$es wot = _Translations$wot$es._(_root);
|
||||||
@override late final _Translations$notifications$es notifications = _Translations$notifications$es._(_root);
|
@override late final _Translations$notifications$es notifications = _Translations$notifications$es._(_root);
|
||||||
@override late final _Translations$plantare$es plantare = _Translations$plantare$es._(_root);
|
@override late final _Translations$plantare$es plantare = _Translations$plantare$es._(_root);
|
||||||
|
@override late final _Translations$sale$es sale = _Translations$sale$es._(_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: app
|
// Path: app
|
||||||
|
|
@ -160,6 +161,7 @@ class _Translations$menu$es extends Translations$menu$en {
|
||||||
@override String get wishlist => 'Lista de deseos';
|
@override String get wishlist => 'Lista de deseos';
|
||||||
@override String get following => 'Siguiendo';
|
@override String get following => 'Siguiendo';
|
||||||
@override String get plantares => 'Plantares';
|
@override String get plantares => 'Plantares';
|
||||||
|
@override String get sales => 'Ventas';
|
||||||
@override String get settings => 'Ajustes';
|
@override String get settings => 'Ajustes';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -841,6 +843,31 @@ class _Translations$plantare$es extends Translations$plantare$en {
|
||||||
@override String get removeConfirm => '¿Quitar este compromiso?';
|
@override String get removeConfirm => '¿Quitar este compromiso?';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path: sale
|
||||||
|
class _Translations$sale$es extends Translations$sale$en {
|
||||||
|
_Translations$sale$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||||
|
|
||||||
|
final TranslationsEs _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
@override String get title => 'Ventas';
|
||||||
|
@override String get help => 'Registra semilla vendida o comprada — dinero, Ğ1 o cualquier moneda. Un modelo aparte del regalo y del Plantare. Nunca se cobra comisión por las semillas.';
|
||||||
|
@override String get add => 'Registrar venta';
|
||||||
|
@override String get empty => 'Aún no hay ventas. Anota aquí lo que vendes o compras.';
|
||||||
|
@override String get iSold => 'Vendí';
|
||||||
|
@override String get iBought => 'Compré';
|
||||||
|
@override String get direction => '¿Vendes o compras?';
|
||||||
|
@override String get counterparty => '¿Con quién?';
|
||||||
|
@override String get counterpartyHint => 'Una persona o un colectivo (opcional)';
|
||||||
|
@override String get amount => 'Importe';
|
||||||
|
@override String get currency => 'Moneda';
|
||||||
|
@override String get currencyHint => '€, Ğ1, horas… (opcional)';
|
||||||
|
@override String get note => 'Nota (opcional)';
|
||||||
|
@override String get save => 'Guardar';
|
||||||
|
@override String get delete => 'Quitar';
|
||||||
|
@override String get removeConfirm => '¿Quitar esta venta?';
|
||||||
|
}
|
||||||
|
|
||||||
// Path: intro.slides
|
// Path: intro.slides
|
||||||
class _Translations$intro$slides$es extends Translations$intro$slides$en {
|
class _Translations$intro$slides$es extends Translations$intro$slides$en {
|
||||||
_Translations$intro$slides$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
_Translations$intro$slides$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||||
|
|
@ -1210,6 +1237,7 @@ extension on TranslationsEs {
|
||||||
'menu.wishlist' => 'Lista de deseos',
|
'menu.wishlist' => 'Lista de deseos',
|
||||||
'menu.following' => 'Siguiendo',
|
'menu.following' => 'Siguiendo',
|
||||||
'menu.plantares' => 'Plantares',
|
'menu.plantares' => 'Plantares',
|
||||||
|
'menu.sales' => 'Ventas',
|
||||||
'menu.settings' => 'Ajustes',
|
'menu.settings' => 'Ajustes',
|
||||||
'settings.language' => 'Idioma',
|
'settings.language' => 'Idioma',
|
||||||
'settings.systemLanguage' => 'Idioma del sistema',
|
'settings.systemLanguage' => 'Idioma del sistema',
|
||||||
|
|
@ -1623,6 +1651,22 @@ extension on TranslationsEs {
|
||||||
'plantare.openSection' => 'Pendientes',
|
'plantare.openSection' => 'Pendientes',
|
||||||
'plantare.settledSection' => 'Hechos',
|
'plantare.settledSection' => 'Hechos',
|
||||||
'plantare.removeConfirm' => '¿Quitar este compromiso?',
|
'plantare.removeConfirm' => '¿Quitar este compromiso?',
|
||||||
|
'sale.title' => 'Ventas',
|
||||||
|
'sale.help' => 'Registra semilla vendida o comprada — dinero, Ğ1 o cualquier moneda. Un modelo aparte del regalo y del Plantare. Nunca se cobra comisión por las semillas.',
|
||||||
|
'sale.add' => 'Registrar venta',
|
||||||
|
'sale.empty' => 'Aún no hay ventas. Anota aquí lo que vendes o compras.',
|
||||||
|
'sale.iSold' => 'Vendí',
|
||||||
|
'sale.iBought' => 'Compré',
|
||||||
|
'sale.direction' => '¿Vendes o compras?',
|
||||||
|
'sale.counterparty' => '¿Con quién?',
|
||||||
|
'sale.counterpartyHint' => 'Una persona o un colectivo (opcional)',
|
||||||
|
'sale.amount' => 'Importe',
|
||||||
|
'sale.currency' => 'Moneda',
|
||||||
|
'sale.currencyHint' => '€, Ğ1, horas… (opcional)',
|
||||||
|
'sale.note' => 'Nota (opcional)',
|
||||||
|
'sale.save' => 'Guardar',
|
||||||
|
'sale.delete' => 'Quitar',
|
||||||
|
'sale.removeConfirm' => '¿Quitar esta venta?',
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ class TranslationsPt extends Translations with BaseTranslations<AppLocale, Trans
|
||||||
@override late final _Translations$wot$pt wot = _Translations$wot$pt._(_root);
|
@override late final _Translations$wot$pt wot = _Translations$wot$pt._(_root);
|
||||||
@override late final _Translations$notifications$pt notifications = _Translations$notifications$pt._(_root);
|
@override late final _Translations$notifications$pt notifications = _Translations$notifications$pt._(_root);
|
||||||
@override late final _Translations$plantare$pt plantare = _Translations$plantare$pt._(_root);
|
@override late final _Translations$plantare$pt plantare = _Translations$plantare$pt._(_root);
|
||||||
|
@override late final _Translations$sale$pt sale = _Translations$sale$pt._(_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: app
|
// Path: app
|
||||||
|
|
@ -160,6 +161,7 @@ class _Translations$menu$pt extends Translations$menu$en {
|
||||||
@override String get wishlist => 'Lista de desejos';
|
@override String get wishlist => 'Lista de desejos';
|
||||||
@override String get following => 'A seguir';
|
@override String get following => 'A seguir';
|
||||||
@override String get plantares => 'Plantares';
|
@override String get plantares => 'Plantares';
|
||||||
|
@override String get sales => 'Vendas';
|
||||||
@override String get settings => 'Definições';
|
@override String get settings => 'Definições';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -838,6 +840,31 @@ class _Translations$plantare$pt extends Translations$plantare$en {
|
||||||
@override String get removeConfirm => 'Remover este compromisso?';
|
@override String get removeConfirm => 'Remover este compromisso?';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path: sale
|
||||||
|
class _Translations$sale$pt extends Translations$sale$en {
|
||||||
|
_Translations$sale$pt._(TranslationsPt root) : this._root = root, super.internal(root);
|
||||||
|
|
||||||
|
final TranslationsPt _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
@override String get title => 'Vendas';
|
||||||
|
@override String get help => 'Regista semente vendida ou comprada — dinheiro, Ğ1 ou qualquer moeda. Um modelo separado do presente e do Plantare. Nunca se cobra comissão pelas sementes.';
|
||||||
|
@override String get add => 'Registar venda';
|
||||||
|
@override String get empty => 'Ainda não há vendas. Anota aqui o que vendes ou compras.';
|
||||||
|
@override String get iSold => 'Vendi';
|
||||||
|
@override String get iBought => 'Comprei';
|
||||||
|
@override String get direction => 'Vendes ou compras?';
|
||||||
|
@override String get counterparty => 'Com quem?';
|
||||||
|
@override String get counterpartyHint => 'Uma pessoa ou um coletivo (opcional)';
|
||||||
|
@override String get amount => 'Montante';
|
||||||
|
@override String get currency => 'Moeda';
|
||||||
|
@override String get currencyHint => '€, Ğ1, horas… (opcional)';
|
||||||
|
@override String get note => 'Nota (opcional)';
|
||||||
|
@override String get save => 'Guardar';
|
||||||
|
@override String get delete => 'Remover';
|
||||||
|
@override String get removeConfirm => 'Remover esta venda?';
|
||||||
|
}
|
||||||
|
|
||||||
// Path: intro.slides
|
// Path: intro.slides
|
||||||
class _Translations$intro$slides$pt extends Translations$intro$slides$en {
|
class _Translations$intro$slides$pt extends Translations$intro$slides$en {
|
||||||
_Translations$intro$slides$pt._(TranslationsPt root) : this._root = root, super.internal(root);
|
_Translations$intro$slides$pt._(TranslationsPt root) : this._root = root, super.internal(root);
|
||||||
|
|
@ -1207,6 +1234,7 @@ extension on TranslationsPt {
|
||||||
'menu.wishlist' => 'Lista de desejos',
|
'menu.wishlist' => 'Lista de desejos',
|
||||||
'menu.following' => 'A seguir',
|
'menu.following' => 'A seguir',
|
||||||
'menu.plantares' => 'Plantares',
|
'menu.plantares' => 'Plantares',
|
||||||
|
'menu.sales' => 'Vendas',
|
||||||
'menu.settings' => 'Definições',
|
'menu.settings' => 'Definições',
|
||||||
'settings.language' => 'Idioma',
|
'settings.language' => 'Idioma',
|
||||||
'settings.systemLanguage' => 'Idioma do sistema',
|
'settings.systemLanguage' => 'Idioma do sistema',
|
||||||
|
|
@ -1617,6 +1645,22 @@ extension on TranslationsPt {
|
||||||
'plantare.openSection' => 'Pendentes',
|
'plantare.openSection' => 'Pendentes',
|
||||||
'plantare.settledSection' => 'Feitos',
|
'plantare.settledSection' => 'Feitos',
|
||||||
'plantare.removeConfirm' => 'Remover este compromisso?',
|
'plantare.removeConfirm' => 'Remover este compromisso?',
|
||||||
|
'sale.title' => 'Vendas',
|
||||||
|
'sale.help' => 'Regista semente vendida ou comprada — dinheiro, Ğ1 ou qualquer moeda. Um modelo separado do presente e do Plantare. Nunca se cobra comissão pelas sementes.',
|
||||||
|
'sale.add' => 'Registar venda',
|
||||||
|
'sale.empty' => 'Ainda não há vendas. Anota aqui o que vendes ou compras.',
|
||||||
|
'sale.iSold' => 'Vendi',
|
||||||
|
'sale.iBought' => 'Comprei',
|
||||||
|
'sale.direction' => 'Vendes ou compras?',
|
||||||
|
'sale.counterparty' => 'Com quem?',
|
||||||
|
'sale.counterpartyHint' => 'Uma pessoa ou um coletivo (opcional)',
|
||||||
|
'sale.amount' => 'Montante',
|
||||||
|
'sale.currency' => 'Moeda',
|
||||||
|
'sale.currencyHint' => '€, Ğ1, horas… (opcional)',
|
||||||
|
'sale.note' => 'Nota (opcional)',
|
||||||
|
'sale.save' => 'Guardar',
|
||||||
|
'sale.delete' => 'Remover',
|
||||||
|
'sale.removeConfirm' => 'Remover esta venda?',
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,15 @@ class AppDrawer extends StatelessWidget {
|
||||||
context.push('/plantares');
|
context.push('/plantares');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
// Local sales ledger — also offline.
|
||||||
|
_DrawerItem(
|
||||||
|
icon: const Icon(Icons.sell_outlined),
|
||||||
|
label: t.menu.sales,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
context.push('/sales');
|
||||||
|
},
|
||||||
|
),
|
||||||
_DrawerItem(
|
_DrawerItem(
|
||||||
icon: const Icon(Symbols.storefront),
|
icon: const Icon(Symbols.storefront),
|
||||||
label: t.menu.market,
|
label: t.menu.market,
|
||||||
|
|
|
||||||
165
apps/app_seeds/lib/ui/sale_sheet.dart
Normal file
165
apps/app_seeds/lib/ui/sale_sheet.dart
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../data/variety_repository.dart';
|
||||||
|
import '../db/enums.dart';
|
||||||
|
import '../i18n/strings.g.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
/// Opens the "record a sale" sheet. Optionally pre-attached to [varietyId]
|
||||||
|
/// (when opened from a variety's detail).
|
||||||
|
Future<void> showSaleSheet(
|
||||||
|
BuildContext context, {
|
||||||
|
required VarietyRepository repository,
|
||||||
|
String? varietyId,
|
||||||
|
}) {
|
||||||
|
return showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (_) => _SaleSheet(repository: repository, varietyId: varietyId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SaleSheet extends StatefulWidget {
|
||||||
|
const _SaleSheet({required this.repository, this.varietyId});
|
||||||
|
|
||||||
|
final VarietyRepository repository;
|
||||||
|
final String? varietyId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_SaleSheet> createState() => _SaleSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SaleSheetState extends State<_SaleSheet> {
|
||||||
|
SaleDirection _direction = SaleDirection.iSold;
|
||||||
|
final _counterparty = TextEditingController();
|
||||||
|
final _amount = TextEditingController();
|
||||||
|
final _currency = TextEditingController();
|
||||||
|
final _note = TextEditingController();
|
||||||
|
bool _saving = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_counterparty.dispose();
|
||||||
|
_amount.dispose();
|
||||||
|
_currency.dispose();
|
||||||
|
_note.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _nullIfBlank(String s) => s.trim().isEmpty ? null : s.trim();
|
||||||
|
|
||||||
|
Future<void> _save() async {
|
||||||
|
setState(() => _saving = true);
|
||||||
|
await widget.repository.createSale(
|
||||||
|
direction: _direction,
|
||||||
|
varietyId: widget.varietyId,
|
||||||
|
counterparty: _nullIfBlank(_counterparty.text),
|
||||||
|
amount: double.tryParse(_amount.text.trim().replaceAll(',', '.')),
|
||||||
|
currency: _nullIfBlank(_currency.text),
|
||||||
|
note: _nullIfBlank(_note.text),
|
||||||
|
);
|
||||||
|
if (mounted) Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = context.t;
|
||||||
|
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.sale.add, style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(t.sale.help,
|
||||||
|
style: const TextStyle(color: seedMuted, fontSize: 13)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(t.sale.direction,
|
||||||
|
style: Theme.of(context).textTheme.labelLarge),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
for (final d in SaleDirection.values)
|
||||||
|
ListTile(
|
||||||
|
key: Key('sale.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 == SaleDirection.iSold ? t.sale.iSold : t.sale.iBought),
|
||||||
|
onTap: () => setState(() => _direction = d),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextField(
|
||||||
|
key: const Key('sale.counterparty'),
|
||||||
|
controller: _counterparty,
|
||||||
|
textCapitalization: TextCapitalization.words,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: t.sale.counterparty,
|
||||||
|
helperText: t.sale.counterpartyHint,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
key: const Key('sale.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('sale.currency'),
|
||||||
|
controller: _currency,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: t.sale.currency,
|
||||||
|
helperText: t.sale.currencyHint,
|
||||||
|
helperMaxLines: 2,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextField(
|
||||||
|
key: const Key('sale.note'),
|
||||||
|
controller: _note,
|
||||||
|
minLines: 1,
|
||||||
|
maxLines: 3,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: t.sale.note,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
FilledButton(
|
||||||
|
key: const Key('sale.save'),
|
||||||
|
onPressed: _saving ? null : _save,
|
||||||
|
child: Text(t.sale.save),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
111
apps/app_seeds/lib/ui/sales_screen.dart
Normal file
111
apps/app_seeds/lib/ui/sales_screen.dart
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
import '../data/variety_repository.dart';
|
||||||
|
import '../db/database.dart';
|
||||||
|
import '../db/enums.dart';
|
||||||
|
import '../i18n/strings.g.dart';
|
||||||
|
import 'sale_sheet.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
/// The Sales screen — a local ledger of seed sold or bought, in any currency.
|
||||||
|
/// A model separate from a gift or a Plantare. Reads the encrypted inventory DB.
|
||||||
|
class SalesScreen extends StatelessWidget {
|
||||||
|
const SalesScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = context.t;
|
||||||
|
final repo = context.read<VarietyRepository>();
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text(t.sale.title)),
|
||||||
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
|
key: const Key('sales.add'),
|
||||||
|
onPressed: () => showSaleSheet(context, repository: repo),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: Text(t.sale.add),
|
||||||
|
),
|
||||||
|
body: StreamBuilder<List<Sale>>(
|
||||||
|
stream: repo.watchSales(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final sales = snapshot.data;
|
||||||
|
if (sales == null) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (sales.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Text(
|
||||||
|
t.sale.empty,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(color: seedMuted, fontSize: 15),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ListView.separated(
|
||||||
|
padding: const EdgeInsets.only(bottom: 88),
|
||||||
|
itemCount: sales.length,
|
||||||
|
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||||
|
itemBuilder: (context, i) => _SaleTile(sales[i], repo),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SaleTile extends StatelessWidget {
|
||||||
|
const _SaleTile(this.s, this.repo);
|
||||||
|
|
||||||
|
final Sale s;
|
||||||
|
final VarietyRepository repo;
|
||||||
|
|
||||||
|
String _money() {
|
||||||
|
final amount = s.amount;
|
||||||
|
if (amount == null) return s.currency?.trim() ?? '';
|
||||||
|
// Drop a trailing .0 so "5.0 €" reads "5 €".
|
||||||
|
final n = amount == amount.roundToDouble()
|
||||||
|
? amount.toInt().toString()
|
||||||
|
: amount.toString();
|
||||||
|
return [n, s.currency?.trim()].where((e) => e != null && e.isNotEmpty).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = context.t;
|
||||||
|
final iSold = s.direction == SaleDirection.iSold;
|
||||||
|
final money = _money();
|
||||||
|
final date = MaterialLocalizations.of(context)
|
||||||
|
.formatShortDate(DateTime.fromMillisecondsSinceEpoch(s.soldOn));
|
||||||
|
final subtitle = [
|
||||||
|
iSold ? t.sale.iSold : t.sale.iBought,
|
||||||
|
if (s.counterparty?.trim().isNotEmpty ?? false) s.counterparty!.trim(),
|
||||||
|
date,
|
||||||
|
].join(' · ');
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
iSold ? Icons.sell_outlined : Icons.shopping_bag_outlined,
|
||||||
|
color: seedGreen,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
money.isEmpty
|
||||||
|
? (s.note?.trim().isNotEmpty ?? false
|
||||||
|
? s.note!.trim()
|
||||||
|
: (iSold ? t.sale.iSold : t.sale.iBought))
|
||||||
|
: money,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
subtitle: Text(subtitle, maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||||
|
trailing: PopupMenuButton<String>(
|
||||||
|
key: Key('sale.menu.${s.id}'),
|
||||||
|
onSelected: (v) {
|
||||||
|
if (v == 'delete') repo.deleteSale(s.id);
|
||||||
|
},
|
||||||
|
itemBuilder: (context) =>
|
||||||
|
[PopupMenuItem(value: 'delete', child: Text(t.sale.delete))],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,7 @@ import 'harvest_date_picker.dart';
|
||||||
import 'photo_pick.dart';
|
import 'photo_pick.dart';
|
||||||
import 'plantare_sheet.dart';
|
import 'plantare_sheet.dart';
|
||||||
import 'quantity_kind_l10n.dart';
|
import 'quantity_kind_l10n.dart';
|
||||||
|
import 'sale_sheet.dart';
|
||||||
import 'quantity_picker.dart';
|
import 'quantity_picker.dart';
|
||||||
import 'seed_glyph.dart';
|
import 'seed_glyph.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
|
@ -110,19 +111,31 @@ class _DetailView extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_SectionTitle(t.plantare.title),
|
Wrap(
|
||||||
Align(
|
spacing: 8,
|
||||||
alignment: AlignmentDirectional.centerStart,
|
runSpacing: 8,
|
||||||
child: OutlinedButton.icon(
|
children: [
|
||||||
key: const Key('detail.addPlantare'),
|
OutlinedButton.icon(
|
||||||
icon: const Icon(Icons.volunteer_activism_outlined, size: 18),
|
key: const Key('detail.addPlantare'),
|
||||||
onPressed: () => showPlantareSheet(
|
icon: const Icon(Icons.volunteer_activism_outlined, size: 18),
|
||||||
context,
|
onPressed: () => showPlantareSheet(
|
||||||
repository: context.read<VarietyRepository>(),
|
context,
|
||||||
varietyId: detail.id,
|
repository: context.read<VarietyRepository>(),
|
||||||
|
varietyId: detail.id,
|
||||||
|
),
|
||||||
|
label: Text(t.plantare.add),
|
||||||
),
|
),
|
||||||
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
if (detail.notes != null && detail.notes!.trim().isNotEmpty) ...[
|
if (detail.notes != null && detail.notes!.trim().isNotEmpty) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
|
||||||
71
apps/app_seeds/test/data/sales_test.dart
Normal file
71
apps/app_seeds/test/data/sales_test.dart
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:tane/db/database.dart';
|
||||||
|
import 'package:tane/db/enums.dart';
|
||||||
|
|
||||||
|
import '../support/test_support.dart';
|
||||||
|
|
||||||
|
/// A Sale is a recorded seed sale/purchase — a model separate from a gift or a
|
||||||
|
/// Plantare, with a price in any currency.
|
||||||
|
void main() {
|
||||||
|
late AppDatabase db;
|
||||||
|
setUp(() => db = newTestDatabase());
|
||||||
|
tearDown(() => db.close());
|
||||||
|
|
||||||
|
test('records a sale with amount + currency and lists it', () async {
|
||||||
|
final repo = newTestRepository(db);
|
||||||
|
final vid = await repo.addQuickVariety(label: 'Tomate rosa');
|
||||||
|
await repo.createSale(
|
||||||
|
direction: SaleDirection.iSold,
|
||||||
|
varietyId: vid,
|
||||||
|
counterparty: 'Ana',
|
||||||
|
amount: 5,
|
||||||
|
currency: '€',
|
||||||
|
);
|
||||||
|
|
||||||
|
final all = await repo.watchSales().first;
|
||||||
|
expect(all.single.direction, SaleDirection.iSold);
|
||||||
|
expect(all.single.amount, 5);
|
||||||
|
expect(all.single.currency, '€');
|
||||||
|
expect(all.single.counterparty, 'Ana');
|
||||||
|
expect(await repo.watchSalesForVariety(vid).first, hasLength(1));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports any currency (Ğ1, time…) and a price-less record', () async {
|
||||||
|
final repo = newTestRepository(db);
|
||||||
|
await repo.createSale(direction: SaleDirection.iBought, amount: 12, currency: 'Ğ1');
|
||||||
|
await repo.createSale(direction: SaleDirection.iSold); // no figure yet
|
||||||
|
final all = await repo.watchSales().first;
|
||||||
|
expect(all, hasLength(2));
|
||||||
|
expect(all.any((s) => s.currency == 'Ğ1' && s.amount == 12), isTrue);
|
||||||
|
expect(all.any((s) => s.amount == null), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleting a sale tombstones it', () async {
|
||||||
|
final repo = newTestRepository(db);
|
||||||
|
final id = await repo.createSale(direction: SaleDirection.iSold, amount: 3);
|
||||||
|
expect(await repo.watchSales().first, hasLength(1));
|
||||||
|
await repo.deleteSale(id);
|
||||||
|
expect(await repo.watchSales().first, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sales survive a backup round-trip', () async {
|
||||||
|
final repoA = newTestRepository(db);
|
||||||
|
await repoA.createSale(
|
||||||
|
direction: SaleDirection.iSold,
|
||||||
|
counterparty: 'Feria de la comarca',
|
||||||
|
amount: 2.5,
|
||||||
|
currency: '€',
|
||||||
|
);
|
||||||
|
final snapshot = await repoA.exportInventory();
|
||||||
|
expect(snapshot.sales, hasLength(1));
|
||||||
|
|
||||||
|
final dbB = newTestDatabase();
|
||||||
|
addTearDown(dbB.close);
|
||||||
|
final repoB = newTestRepository(dbB);
|
||||||
|
await repoB.importInventory(snapshot);
|
||||||
|
final onB = await repoB.watchSales().first;
|
||||||
|
expect(onB.single.amount, 2.5);
|
||||||
|
expect(onB.single.currency, '€');
|
||||||
|
expect(onB.single.counterparty, 'Feria de la comarca');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -11,19 +11,19 @@ void main() {
|
||||||
verifier = SchemaVerifier(GeneratedHelper());
|
verifier = SchemaVerifier(GeneratedHelper());
|
||||||
});
|
});
|
||||||
|
|
||||||
test('freshly created database matches the exported schema v9', () async {
|
test('freshly created database matches the exported schema v10', () async {
|
||||||
final schema = await verifier.schemaAt(9);
|
final schema = await verifier.schemaAt(10);
|
||||||
final db = AppDatabase(schema.newConnection());
|
final db = AppDatabase(schema.newConnection());
|
||||||
await verifier.migrateAndValidate(db, 9);
|
await verifier.migrateAndValidate(db, 10);
|
||||||
await db.close();
|
await db.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Every historical version upgrades cleanly to the current schema (v9).
|
// Every historical version upgrades cleanly to the current schema (v10).
|
||||||
for (var from = 1; from <= 8; from++) {
|
for (var from = 1; from <= 9; from++) {
|
||||||
test('upgrades v$from → v9 and matches the fresh schema', () async {
|
test('upgrades v$from → v10 and matches the fresh schema', () async {
|
||||||
final connection = await verifier.startAt(from);
|
final connection = await verifier.startAt(from);
|
||||||
final db = AppDatabase(connection);
|
final db = AppDatabase(connection);
|
||||||
await verifier.migrateAndValidate(db, 9);
|
await verifier.migrateAndValidate(db, 10);
|
||||||
await db.close();
|
await db.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import 'schema_v6.dart' as v6;
|
||||||
import 'schema_v7.dart' as v7;
|
import 'schema_v7.dart' as v7;
|
||||||
import 'schema_v8.dart' as v8;
|
import 'schema_v8.dart' as v8;
|
||||||
import 'schema_v9.dart' as v9;
|
import 'schema_v9.dart' as v9;
|
||||||
|
import 'schema_v10.dart' as v10;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
|
|
@ -36,10 +37,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
return v8.DatabaseAtV8(db);
|
return v8.DatabaseAtV8(db);
|
||||||
case 9:
|
case 9:
|
||||||
return v9.DatabaseAtV9(db);
|
return v9.DatabaseAtV9(db);
|
||||||
|
case 10:
|
||||||
|
return v10.DatabaseAtV10(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
throw MissingSchemaException(version, versions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1997
apps/app_seeds/test/db/schema/schema_v10.dart
Normal file
1997
apps/app_seeds/test/db/schema/schema_v10.dart
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -17,6 +17,7 @@ import 'package:tane/ui/inventory_list_screen.dart';
|
||||||
import 'package:tane/ui/market_screen.dart';
|
import 'package:tane/ui/market_screen.dart';
|
||||||
import 'package:tane/ui/plantares_screen.dart';
|
import 'package:tane/ui/plantares_screen.dart';
|
||||||
import 'package:tane/ui/profile_screen.dart';
|
import 'package:tane/ui/profile_screen.dart';
|
||||||
|
import 'package:tane/ui/sales_screen.dart';
|
||||||
import 'package:tane/ui/settings_screen.dart';
|
import 'package:tane/ui/settings_screen.dart';
|
||||||
|
|
||||||
import '../support/test_support.dart';
|
import '../support/test_support.dart';
|
||||||
|
|
@ -51,6 +52,19 @@ void main() {
|
||||||
tester.view.devicePixelRatio = 1.0;
|
tester.view.devicePixelRatio = 1.0;
|
||||||
addTearDown(tester.view.resetPhysicalSize);
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
addTearDown(tester.view.resetDevicePixelRatio);
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
|
// flutter_test's headless engine can't decode our real PNG assets (the
|
||||||
|
// logo), so `Image.asset` throws from the "image resource service". That's
|
||||||
|
// unrelated to layout — swallow ONLY those, and let every other FlutterError
|
||||||
|
// (crucially the "rendering library" RenderFlex overflow this guard exists
|
||||||
|
// to catch) still fail the test. Installed here, inside the running test,
|
||||||
|
// because testWidgets reinstalls its own onError at test start (a setUp
|
||||||
|
// override would be clobbered).
|
||||||
|
final previous = FlutterError.onError;
|
||||||
|
FlutterError.onError = (details) {
|
||||||
|
if (details.library == 'image resource service') return;
|
||||||
|
previous?.call(details);
|
||||||
|
};
|
||||||
|
addTearDown(() => FlutterError.onError = previous);
|
||||||
await tester.pumpWidget(widget);
|
await tester.pumpWidget(widget);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
@ -258,6 +272,28 @@ void main() {
|
||||||
await disposeTree(tester);
|
await disposeTree(tester);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('the Sales screen (with a sale) fits a small screen',
|
||||||
|
(tester) async {
|
||||||
|
final db = newTestDatabase();
|
||||||
|
addTearDown(db.close);
|
||||||
|
final repo = newTestRepository(db);
|
||||||
|
await repo.createSale(
|
||||||
|
direction: SaleDirection.iSold,
|
||||||
|
counterparty: 'Feria de intercambio de la comarca',
|
||||||
|
amount: 12.5,
|
||||||
|
currency: '€',
|
||||||
|
);
|
||||||
|
await pumpSmall(
|
||||||
|
tester,
|
||||||
|
wrapScreen(
|
||||||
|
repository: repo,
|
||||||
|
locale: AppLocale.es,
|
||||||
|
child: const SalesScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await disposeTree(tester);
|
||||||
|
});
|
||||||
|
|
||||||
// NOTE: the variety-detail screen is deliberately NOT overflow-checked here.
|
// NOTE: the variety-detail screen is deliberately NOT overflow-checked here.
|
||||||
// It reports a ~2px overflow on a RenderFlex that is already DISPOSED/DEFUNCT
|
// It reports a ~2px overflow on a RenderFlex that is already DISPOSED/DEFUNCT
|
||||||
// — a transient stale frame while its cubit's Drift stream rebuilds during
|
// — a transient stale frame while its cubit's Drift stream rebuilds during
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue