import 'package:commons_core/commons_core.dart'; import 'package:drift/drift.dart'; import 'package:equatable/equatable.dart'; import '../db/database.dart'; import '../db/enums.dart'; /// A lightweight row for the inventory list (only what the list renders). class VarietyListItem extends Equatable { const VarietyListItem({required this.id, required this.label, this.category}); final String id; final String label; final String? category; @override List get props => [id, label, category]; } /// Reads and writes the inventory. The encrypted Drift DB is the single source /// of truth; [watchInventory] exposes a reactive stream the UI subscribes to. /// /// Every write stamps the row with the local [Hlc] and [nodeId] so the CRDT /// metadata is correct from day one, even though sync does not exist yet. class VarietyRepository { VarietyRepository( this._db, { required this.idGen, required this.nodeId, int Function()? nowMillis, }) : _now = nowMillis ?? (() => DateTime.now().millisecondsSinceEpoch), _clock = Hlc.zero(nodeId); final AppDatabase _db; final IdGen idGen; final String nodeId; final int Function() _now; Hlc _clock; /// Emits the non-deleted inventory, ordered by category then label. Stream> watchInventory() { final query = _db.select(_db.varieties) ..where((v) => v.isDeleted.equals(false)) ..orderBy([ (v) => OrderingTerm(expression: v.category), (v) => OrderingTerm(expression: v.label), ]); return query.watch().map( (rows) => rows .map( (v) => VarietyListItem(id: v.id, label: v.label, category: v.category), ) .toList(), ); } /// The 20-second quick-add: a [label] (required) plus an optional [category], /// an optional [quantity] (creates a Lot) and an optional [photoBytes] /// (stored as an encrypted BLOB Attachment). Returns the new Variety id. Future addQuickVariety({ required String label, String? category, Quantity? quantity, Uint8List? photoBytes, }) async { final varietyId = idGen.newId(); await _db.transaction(() async { final (created, updated) = _stamp(); await _db .into(_db.varieties) .insert( VarietiesCompanion.insert( id: varietyId, label: label, createdAt: created, updatedAt: updated, lastAuthor: nodeId, category: Value(category), ), ); if (quantity != null) { final (created, updated) = _stamp(); await _db .into(_db.lots) .insert( LotsCompanion.insert( id: idGen.newId(), varietyId: varietyId, createdAt: created, updatedAt: updated, lastAuthor: nodeId, quantityKind: Value(quantity.kind.name), quantityPrecise: Value(quantity.precise), quantityLabel: Value(quantity.label), ), ); } if (photoBytes != null) { final (created, updated) = _stamp(); await _db .into(_db.attachments) .insert( AttachmentsCompanion.insert( id: idGen.newId(), createdAt: created, updatedAt: updated, lastAuthor: nodeId, parentType: ParentType.variety, parentId: varietyId, kind: AttachmentKind.photo, bytes: Value(photoBytes), mimeType: const Value('image/jpeg'), ), ); } }); return varietyId; } /// Advances the local clock and returns `(createdAtMillis, packedHlc)`. (int, String) _stamp() { final now = _now(); _clock = _clock.localEvent(now); return (now, _clock.pack()); } }