import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import '../data/variety_repository.dart'; import '../db/enums.dart'; import '../domain/seed_saving.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, SeedSavingGuide? guide, }) { return showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => _LotHistorySheet(repository: repository, lot: lot, guide: guide), ); } class _LotHistorySheet extends StatefulWidget { const _LotHistorySheet({ required this.repository, required this.lot, this.guide, }); final VarietyRepository repository; final VarietyLot lot; /// Bundled species guidance, or null. Only read to drop the one-line /// isolation hint when a cross-pollinating crop is sown — selfers get /// nothing (the fields people record religiously where they don't matter). final SeedSavingGuide? guide; @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; // The optional, skippable "how did it do?" asked right after a harvest is // noted — the only moment it appears. Dismissed, it never nags again. bool _askOutcome = false; final _outcomeNote = TextEditingController(); // One passive line after sowing a crosser ("this species crosses — isolate // ~400 m"). Never shown for selfers, never an input. bool _showSowingHint = false; @override void initState() { super.initState(); _history = widget.repository.lotHistory(widget.lot.id); } @override void dispose() { _outcomeNote.dispose(); super.dispose(); } 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, ), ), ); final guide = widget.guide; setState(() { _saving = false; _askOutcome = type == MovementType.harvested; _showSowingHint = type == MovementType.sown && guide?.pollination == Pollination.cross && guide?.isolationMinM != null; _history = widget.repository.lotHistory(widget.lot.id); }); } Future _saveOutcome(GardenOutcomeRating rating) async { if (_saving) return; setState(() => _saving = true); final t = context.t; final messenger = ScaffoldMessenger.of(context); final note = _outcomeNote.text.trim(); await widget.repository.addGardenOutcome( lotId: widget.lot.id, year: DateTime.now().year, rating: rating, notes: note.isEmpty ? null : note, ); if (!mounted) return; messenger.showSnackBar(SnackBar(content: Text(t.history.outcomeSaved))); _outcomeNote.clear(); setState(() { _saving = false; _askOutcome = false; _history = widget.repository.lotHistory(widget.lot.id); }); } @override Widget build(BuildContext context) { final t = context.t; return SafeArea( child: Padding( // Bottom inset keeps the optional note field above the keyboard. padding: EdgeInsets.fromLTRB( 16, 16, 16, 24 + MediaQuery.of(context).viewInsets.bottom, ), 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), ), ], ), if (_showSowingHint) Padding( key: const Key('history.isolationHint'), padding: const EdgeInsetsDirectional.only(top: 8, start: 4), child: Row( children: [ const Icon(Icons.lightbulb_outline, size: 16), const SizedBox(width: 6), Expanded( child: Text( context.t.history.isolationHint( meters: widget.guide!.isolationMinM!, ), style: Theme.of(context).textTheme.bodySmall, ), ), ], ), ), if (_askOutcome) ...[ const SizedBox(height: 8), _OutcomeQuestion( note: _outcomeNote, saving: _saving, onRate: _saveOutcome, onDismiss: () => setState(() => _askOutcome = false), ), ], 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]), ); }, ), ), ], ), ), ); } } /// The one skippable question of the whole flow: three faces and an optional /// note. Tapping a face saves; the X dismisses without a trace. class _OutcomeQuestion extends StatelessWidget { const _OutcomeQuestion({ required this.note, required this.saving, required this.onRate, required this.onDismiss, }); final TextEditingController note; final bool saving; final ValueChanged onRate; final VoidCallback onDismiss; @override Widget build(BuildContext context) { final t = context.t; return Card( key: const Key('history.outcomeQuestion'), child: Padding( padding: const EdgeInsets.fromLTRB(12, 4, 4, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( t.history.outcomeQuestion, style: Theme.of(context).textTheme.titleSmall, ), ), IconButton( key: const Key('history.outcomeDismiss'), icon: const Icon(Icons.close, size: 18), onPressed: onDismiss, ), ], ), Wrap( spacing: 8, children: [ ActionChip( key: const Key('history.outcomeGood'), avatar: const Icon(Icons.sentiment_satisfied_alt, size: 18), label: Text(t.history.outcomeGood), onPressed: saving ? null : () => onRate(GardenOutcomeRating.good), ), ActionChip( key: const Key('history.outcomeMixed'), avatar: const Icon(Icons.sentiment_neutral, size: 18), label: Text(t.history.outcomeMixed), onPressed: saving ? null : () => onRate(GardenOutcomeRating.mixed), ), ActionChip( key: const Key('history.outcomePoor'), avatar: const Icon(Icons.sentiment_dissatisfied, size: 18), label: Text(t.history.outcomePoor), onPressed: saving ? null : () => onRate(GardenOutcomeRating.poor), ), ], ), TextField( key: const Key('history.outcomeNote'), controller: note, decoration: InputDecoration( hintText: t.history.outcomeNoteHint, isDense: true, border: InputBorder.none, ), ), ], ), ), ); } } 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.gardenOutcome => switch (entry.rating) { GardenOutcomeRating.good => Icons.sentiment_satisfied_alt, GardenOutcomeRating.mixed => Icons.sentiment_neutral, GardenOutcomeRating.poor => Icons.sentiment_dissatisfied, null => Icons.edit_note, }, 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.gardenOutcome => switch (entry.rating) { GardenOutcomeRating.good => t.history.ratedGood, GardenOutcomeRating.mixed => t.history.ratedMixed, GardenOutcomeRating.poor => t.history.ratedPoor, null => t.history.outcomeTitle, }, 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.year case final year?) t.history.outcomeYear(year: year), 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), ); } }