feat(inventory): edit and delete existing lots
Lots could only be created; now they can be edited and removed: - Repository: updateLot (LWW) + softDeleteLot (tombstone). - Cubit: updateLot / deleteLot. - The add-lot sheet is now a shared add/edit sheet — tapping a lot opens it prefilled (type, year, quantity), with a trash action to delete. The reactive detail view refreshes on save/delete. Tests: repo updateLot/softDeleteLot + widget edit-lot and delete-lot. 42 green.
This commit is contained in:
parent
7e92410728
commit
f8d4d9a778
10 changed files with 192 additions and 16 deletions
|
|
@ -455,6 +455,42 @@ class VarietyRepository {
|
|||
return id;
|
||||
}
|
||||
|
||||
/// Updates an existing lot's fields (LWW). The [quantity] and [harvestYear]
|
||||
/// are set as given (null clears them).
|
||||
Future<void> updateLot({
|
||||
required String lotId,
|
||||
required LotType type,
|
||||
int? harvestYear,
|
||||
Quantity? quantity,
|
||||
String? storageLocation,
|
||||
}) async {
|
||||
final (_, updated) = _stamp();
|
||||
await (_db.update(_db.lots)..where((l) => l.id.equals(lotId))).write(
|
||||
LotsCompanion(
|
||||
type: Value(type),
|
||||
harvestYear: Value(harvestYear),
|
||||
quantityKind: Value(quantity?.kind.name),
|
||||
quantityPrecise: Value(quantity?.count?.toDouble()),
|
||||
quantityLabel: Value(quantity?.label),
|
||||
storageLocation: Value(storageLocation),
|
||||
updatedAt: Value(updated),
|
||||
lastAuthor: Value(nodeId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Soft-deletes a lot (tombstone); it leaves the variety's lot list.
|
||||
Future<void> softDeleteLot(String lotId) async {
|
||||
final (_, updated) = _stamp();
|
||||
await (_db.update(_db.lots)..where((l) => l.id.equals(lotId))).write(
|
||||
LotsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
updatedAt: Value(updated),
|
||||
lastAuthor: Value(nodeId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Soft-deletes a variety (tombstone); it disappears from the inventory but
|
||||
/// the row survives for correct CRDT merges later.
|
||||
Future<void> softDeleteVariety(String id) async {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
"names": "Also known as",
|
||||
"notes": "Notes",
|
||||
"addLot": "Add lot",
|
||||
"editLot": "Edit lot",
|
||||
"deleteConfirm": "Delete this seed?",
|
||||
"year": "Year {year}",
|
||||
"noYear": "Year unknown"
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
"names": "También conocida como",
|
||||
"notes": "Notas",
|
||||
"addLot": "Añadir lote",
|
||||
"editLot": "Editar lote",
|
||||
"deleteConfirm": "¿Eliminar esta semilla?",
|
||||
"year": "Año {year}",
|
||||
"noYear": "Año desconocido"
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 2
|
||||
/// Strings: 182 (91 per locale)
|
||||
/// Strings: 184 (92 per locale)
|
||||
///
|
||||
/// Built on 2026-07-08 at 08:39 UTC
|
||||
/// Built on 2026-07-08 at 09:07 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
|
|
|||
|
|
@ -168,6 +168,9 @@ class Translations$detail$en {
|
|||
/// en: 'Add lot'
|
||||
String get addLot => 'Add lot';
|
||||
|
||||
/// en: 'Edit lot'
|
||||
String get editLot => 'Edit lot';
|
||||
|
||||
/// en: 'Delete this seed?'
|
||||
String get deleteConfirm => 'Delete this seed?';
|
||||
|
||||
|
|
@ -658,6 +661,7 @@ extension on Translations {
|
|||
'detail.names' => 'Also known as',
|
||||
'detail.notes' => 'Notes',
|
||||
'detail.addLot' => 'Add lot',
|
||||
'detail.editLot' => 'Edit lot',
|
||||
'detail.deleteConfirm' => 'Delete this seed?',
|
||||
'detail.year' => ({required Object year}) => 'Year ${year}',
|
||||
'detail.noYear' => 'Year unknown',
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ class _Translations$detail$es extends Translations$detail$en {
|
|||
@override String get names => 'También conocida como';
|
||||
@override String get notes => 'Notas';
|
||||
@override String get addLot => 'Añadir lote';
|
||||
@override String get editLot => 'Editar lote';
|
||||
@override String get deleteConfirm => '¿Eliminar esta semilla?';
|
||||
@override String year({required Object year}) => 'Año ${year}';
|
||||
@override String get noYear => 'Año desconocido';
|
||||
|
|
@ -474,6 +475,7 @@ extension on TranslationsEs {
|
|||
'detail.names' => 'También conocida como',
|
||||
'detail.notes' => 'Notas',
|
||||
'detail.addLot' => 'Añadir lote',
|
||||
'detail.editLot' => 'Editar lote',
|
||||
'detail.deleteConfirm' => '¿Eliminar esta semilla?',
|
||||
'detail.year' => ({required Object year}) => 'Año ${year}',
|
||||
'detail.noYear' => 'Año desconocido',
|
||||
|
|
|
|||
|
|
@ -62,6 +62,20 @@ class VarietyDetailCubit extends Cubit<VarietyDetailState> {
|
|||
quantity: quantity,
|
||||
);
|
||||
|
||||
Future<void> updateLot({
|
||||
required String lotId,
|
||||
required LotType type,
|
||||
int? year,
|
||||
Quantity? quantity,
|
||||
}) => _repo.updateLot(
|
||||
lotId: lotId,
|
||||
type: type,
|
||||
harvestYear: year,
|
||||
quantity: quantity,
|
||||
);
|
||||
|
||||
Future<void> deleteLot(String lotId) => _repo.softDeleteLot(lotId);
|
||||
|
||||
Future<void> linkSpecies(String speciesId) =>
|
||||
_repo.linkSpecies(varietyId, speciesId);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
|
|
@ -100,7 +99,7 @@ class _DetailView extends StatelessWidget {
|
|||
_SectionTitle(t.detail.lots),
|
||||
TextButton.icon(
|
||||
key: const Key('detail.addLot'),
|
||||
onPressed: () => _showAddLotSheet(context, cubit),
|
||||
onPressed: () => _showLotSheet(context, cubit),
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(t.detail.addLot),
|
||||
),
|
||||
|
|
@ -111,7 +110,9 @@ class _DetailView extends StatelessWidget {
|
|||
else
|
||||
for (final lot in detail.lots)
|
||||
ListTile(
|
||||
key: Key('lot.${lot.id}'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
onTap: () => _showLotSheet(context, cubit, existing: lot),
|
||||
leading: lot.quantity == null
|
||||
? const Icon(Icons.inventory_2_outlined)
|
||||
: QuantityKindIcon(lot.quantity!.kind, size: 28),
|
||||
|
|
@ -475,15 +476,20 @@ class _EditVarietySheetState extends State<_EditVarietySheet> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _showAddLotSheet(BuildContext context, VarietyDetailCubit cubit) {
|
||||
Future<void> _showLotSheet(
|
||||
BuildContext context,
|
||||
VarietyDetailCubit cubit, {
|
||||
VarietyLot? existing,
|
||||
}) {
|
||||
final t = context.t;
|
||||
final currentYear = DateTime.now().year;
|
||||
// Harvest years: current year down through the last four decades. Newest
|
||||
// first because a freshly harvested lot is the common case.
|
||||
final years = [for (var y = currentYear; y >= currentYear - 40; y--) y];
|
||||
var selectedYear = currentYear;
|
||||
var selectedType = LotType.seed;
|
||||
Quantity? selectedQuantity;
|
||||
var selectedYear = existing?.harvestYear ?? currentYear;
|
||||
var selectedType = existing?.type ?? LotType.seed;
|
||||
var selectedQuantity = existing?.quantity;
|
||||
final editing = existing != null;
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
|
|
@ -499,9 +505,25 @@ Future<void> _showAddLotSheet(BuildContext context, VarietyDetailCubit cubit) {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
t.addLot.title,
|
||||
style: Theme.of(sheetContext).textTheme.titleLarge,
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
editing ? t.detail.editLot : t.addLot.title,
|
||||
style: Theme.of(sheetContext).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
if (editing)
|
||||
IconButton(
|
||||
key: const Key('lot.delete'),
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
tooltip: t.common.delete,
|
||||
onPressed: () {
|
||||
cubit.deleteLot(existing.id);
|
||||
Navigator.of(sheetContext).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
LotTypeSelector(
|
||||
|
|
@ -549,11 +571,20 @@ Future<void> _showAddLotSheet(BuildContext context, VarietyDetailCubit cubit) {
|
|||
FilledButton(
|
||||
key: const Key('addLot.save'),
|
||||
onPressed: () {
|
||||
cubit.addLot(
|
||||
type: selectedType,
|
||||
year: selectedYear,
|
||||
quantity: selectedQuantity,
|
||||
);
|
||||
if (editing) {
|
||||
cubit.updateLot(
|
||||
lotId: existing.id,
|
||||
type: selectedType,
|
||||
year: selectedYear,
|
||||
quantity: selectedQuantity,
|
||||
);
|
||||
} else {
|
||||
cubit.addLot(
|
||||
type: selectedType,
|
||||
year: selectedYear,
|
||||
quantity: selectedQuantity,
|
||||
);
|
||||
}
|
||||
Navigator.of(sheetContext).pop();
|
||||
},
|
||||
child: Text(t.common.save),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'package:tane/data/species_repository.dart';
|
||||
import 'package:tane/data/variety_repository.dart';
|
||||
import 'package:tane/db/database.dart';
|
||||
import 'package:tane/db/enums.dart';
|
||||
|
||||
import '../support/test_support.dart';
|
||||
|
||||
|
|
@ -139,4 +140,35 @@ void main() {
|
|||
|
||||
await queue.cancel();
|
||||
});
|
||||
|
||||
test('updateLot changes a lot\'s year, type and quantity', () async {
|
||||
final id = await repo.addQuickVariety(label: 'Maize');
|
||||
final lotId = await repo.addLot(
|
||||
varietyId: id,
|
||||
harvestYear: 2020,
|
||||
quantity: const Quantity(kind: QuantityKind.packet, count: 1),
|
||||
);
|
||||
|
||||
await repo.updateLot(
|
||||
lotId: lotId,
|
||||
type: LotType.plant,
|
||||
harvestYear: 2024,
|
||||
quantity: const Quantity(kind: QuantityKind.plant, count: 3),
|
||||
);
|
||||
|
||||
final lot = (await repo.watchVariety(id).first)!.lots.single;
|
||||
expect(lot.type, LotType.plant);
|
||||
expect(lot.harvestYear, 2024);
|
||||
expect(lot.quantity?.kind, QuantityKind.plant);
|
||||
expect(lot.quantity?.count, 3);
|
||||
});
|
||||
|
||||
test('softDeleteLot removes the lot from the variety', () async {
|
||||
final id = await repo.addQuickVariety(label: 'Maize');
|
||||
final lotId = await repo.addLot(varietyId: id, harvestYear: 2024);
|
||||
|
||||
await repo.softDeleteLot(lotId);
|
||||
|
||||
expect((await repo.watchVariety(id).first)!.lots, isEmpty);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,4 +200,59 @@ void main() {
|
|||
expect(find.text('90%'), findsOneWidget); // germination badge on the lot
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('editing a lot updates its line', (tester) async {
|
||||
final repo = newTestRepository(db);
|
||||
final id = await repo.addQuickVariety(label: 'Maize');
|
||||
await repo.addLot(
|
||||
varietyId: id,
|
||||
harvestYear: 2020,
|
||||
quantity: const Quantity(kind: QuantityKind.packet, count: 1),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapDetail(
|
||||
repository: repo,
|
||||
varietyId: id,
|
||||
species: newTestSpeciesRepository(db),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Year 2020 · 1 packet')); // open the lot
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(const Key('addLot.year')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('2024').last);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('addLot.save')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Year 2024 · 1 packet'), findsOneWidget);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('deleting a lot removes it', (tester) async {
|
||||
final repo = newTestRepository(db);
|
||||
final id = await repo.addQuickVariety(label: 'Maize');
|
||||
await repo.addLot(varietyId: id, harvestYear: 2020);
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapDetail(
|
||||
repository: repo,
|
||||
varietyId: id,
|
||||
species: newTestSpeciesRepository(db),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.textContaining('Year 2020')); // open the lot
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('lot.delete')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('No lots yet.'), findsOneWidget);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue