import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import '../data/variety_repository.dart'; import '../db/enums.dart'; import '../i18n/strings.g.dart'; import 'quantity_kind_l10n.dart'; /// Opens the batch's story — a read-only timeline of everything already /// recorded about it (movements, germination tests, storage checks, and how it /// entered the collection), plus two one-tap chips to note "sown today" / /// "harvested today". The happy path is literally one tap; there is no form. Future showLotHistorySheet( BuildContext context, { required VarietyRepository repository, required VarietyLot lot, }) { return showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => _LotHistorySheet(repository: repository, lot: lot), ); } class _LotHistorySheet extends StatefulWidget { const _LotHistorySheet({required this.repository, required this.lot}); final VarietyRepository repository; final VarietyLot lot; @override State<_LotHistorySheet> createState() => _LotHistorySheetState(); } class _LotHistorySheetState extends State<_LotHistorySheet> { // One-shot future (data-model rule: a transient sheet must never hold a // live subscription). Re-assigned after a quick record to refresh the list. late Future> _history; bool _saving = false; @override void initState() { super.initState(); _history = widget.repository.lotHistory(widget.lot.id); } Future _record(MovementType type) async { if (_saving) return; setState(() => _saving = true); final t = context.t; final messenger = ScaffoldMessenger.of(context); await widget.repository.recordCultivation( lotId: widget.lot.id, type: type, ); if (!mounted) return; messenger.showSnackBar( SnackBar( content: Text( type == MovementType.sown ? t.history.sownRecorded : t.history.harvestRecorded, ), ), ); setState(() { _saving = false; _history = widget.repository.lotHistory(widget.lot.id); }); } @override Widget build(BuildContext context) { final t = context.t; return SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( t.history.title, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 12), Wrap( spacing: 8, children: [ if (widget.lot.type == LotType.seed) ActionChip( key: const Key('history.sowToday'), avatar: const Icon(Icons.grass_outlined, size: 18), label: Text(t.history.sowToday), onPressed: _saving ? null : () => _record(MovementType.sown), ), ActionChip( key: const Key('history.harvestToday'), avatar: const Icon(Icons.agriculture_outlined, size: 18), label: Text(t.history.harvestToday), onPressed: _saving ? null : () => _record(MovementType.harvested), ), ], ), const SizedBox(height: 8), Flexible( child: FutureBuilder>( future: _history, builder: (context, snapshot) { final entries = snapshot.data; if (entries == null) { return const Padding( padding: EdgeInsets.all(24), child: Center(child: CircularProgressIndicator()), ); } return ListView.builder( shrinkWrap: true, itemCount: entries.length, itemBuilder: (context, i) => _HistoryTile(entry: entries[i]), ); }, ), ), ], ), ), ); } } class _HistoryTile extends StatelessWidget { const _HistoryTile({required this.entry}); final LotHistoryEntry entry; IconData get _icon => switch (entry.kind) { LotHistoryKind.created => Icons.spa_outlined, LotHistoryKind.germinationTest => Icons.science_outlined, LotHistoryKind.conditionCheck => Icons.inventory_2_outlined, LotHistoryKind.movement => switch (entry.movementType!) { MovementType.received => Icons.call_received, MovementType.given => Icons.redeem_outlined, MovementType.sown => Icons.grass_outlined, MovementType.harvested => Icons.agriculture_outlined, MovementType.germinationTest => Icons.science_outlined, MovementType.split => Icons.call_split, MovementType.discarded => Icons.delete_outline, }, }; String _title(Translations t) => switch (entry.kind) { LotHistoryKind.created => t.history.created, LotHistoryKind.conditionCheck => t.conditionCheck.title, LotHistoryKind.germinationTest => entry.germinationRate == null ? t.germination.title : t.history.germinationResult( percent: (entry.germinationRate! * 100).round(), ), LotHistoryKind.movement => switch (entry.movementType!) { MovementType.received => t.history.movementReceived, MovementType.given => t.history.movementGiven, MovementType.sown => t.history.movementSown, MovementType.harvested => t.history.movementHarvested, MovementType.germinationTest => t.history.movementGerminationTest, MovementType.split => t.history.movementSplit, MovementType.discarded => t.history.movementDiscarded, }, }; @override Widget build(BuildContext context) { final t = context.t; final theme = Theme.of(context); final when = entry.when; final date = when == null ? null // Format via the Localizations locale, not the raw app locale (see // backup_section.dart: `ast` has no intl symbols, mapped to `es`). : DateFormat.yMMMd( Localizations.localeOf(context).languageCode, ).format(DateTime.fromMillisecondsSinceEpoch(when)); final origin = [ entry.originName, entry.originPlace, ].whereType().where((s) => s.trim().isNotEmpty).join(' · '); final details = [ if (entry.kind == LotHistoryKind.created && origin.isNotEmpty) t.history.from(origin: origin), if (entry.quantity != null) quantityDisplay(t, entry.quantity!), ?entry.counterpartyName, if (entry.hasParentLink) t.history.linkedEarlier, if (entry.notes case final n? when n.trim().isNotEmpty) n.trim(), ].join('\n'); return ListTile( dense: true, contentPadding: EdgeInsets.zero, leading: Icon(_icon, size: 20, color: theme.colorScheme.primary), title: Text(_title(t)), subtitle: details.isEmpty ? null : Text(details), trailing: date == null ? null : Text(date, style: theme.textTheme.bodySmall), ); } }