import 'package:flutter/material.dart'; import '../services/database_helper.dart'; import '../models/palm_record.dart'; import '../constants/mpob_colors.dart'; import 'batch_report_screen.dart'; import 'record_detail_screen.dart'; /// A group of [PalmRecord]s that share a batchId (or the fallback /// "ungrouped" bucket for pre-migration records with no batchId). class _BatchGroup { final String? batchId; final List records; _BatchGroup({required this.batchId, required this.records}); DateTime get latestTimestamp => records.first.timestamp; String get dominantClass { final tally = {}; for (final r in records) { tally[r.ripenessClass] = (tally[r.ripenessClass] ?? 0) + 1; } return tally.entries.reduce((a, b) => a.value >= b.value ? a : b).key; } } class HistoryScreen extends StatefulWidget { const HistoryScreen({super.key}); @override State createState() => _HistoryScreenState(); } class _HistoryScreenState extends State { final DatabaseHelper _dbHelper = DatabaseHelper(); List<_BatchGroup> _allGroups = []; List<_BatchGroup> _groups = []; bool _isLoading = true; bool _sortAscending = false; // false = latest first (default) DateTimeRange? _dateRange; @override void initState() { super.initState(); _loadHistory(); } Future _loadHistory() async { final records = await _dbHelper.getAllRecords(); // already ordered timestamp DESC final Map> byBatch = {}; for (final record in records) { final key = record.batchId ?? '__ungrouped__'; byBatch.putIfAbsent(key, () => []).add(record); } final groups = byBatch.entries .map((e) => _BatchGroup(batchId: e.key == '__ungrouped__' ? null : e.key, records: e.value)) .toList(); setState(() { _allGroups = groups; _isLoading = false; }); _applyFilters(); } void _applyFilters() { var filtered = _allGroups; if (_dateRange != null) { filtered = filtered.where((g) { final d = g.latestTimestamp; return !d.isBefore(_dateRange!.start) && d.isBefore(_dateRange!.end.add(const Duration(days: 1))); }).toList(); } filtered = [...filtered] ..sort((a, b) => _sortAscending ? a.latestTimestamp.compareTo(b.latestTimestamp) : b.latestTimestamp.compareTo(a.latestTimestamp)); setState(() => _groups = filtered); } void _clearFilters() { setState(() { _sortAscending = false; _dateRange = null; }); _applyFilters(); } Future _resetHistory() async { final errorColor = Theme.of(context).colorScheme.error; final confirm = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text("Reset History"), content: const Text("Are you sure you want to clear all analysis records? This action is irreversible."), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("Cancel")), TextButton( onPressed: () => Navigator.pop(context, true), child: Text("Reset", style: TextStyle(color: errorColor)), ), ], ), ); if (confirm == true) { await _dbHelper.clearAllRecords(); _loadHistory(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("History cleared successfully.")), ); } } } Future _confirmDelete(String message) async { final errorColor = Theme.of(context).colorScheme.error; final confirm = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text("Delete"), content: Text(message), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("Cancel")), TextButton( onPressed: () => Navigator.pop(context, true), child: Text("Delete", style: TextStyle(color: errorColor)), ), ], ), ); return confirm ?? false; } Future _deleteGroup(_BatchGroup group) async { if (group.batchId != null) { await _dbHelper.deleteBatch(group.batchId!); } else { await _dbHelper.deleteRecord(group.records.first); } _loadHistory(); } bool get _hasActiveFilter => _dateRange != null || _sortAscending; Future _openFilterSheet() async { await showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (sheetContext) { return StatefulBuilder( builder: (sheetContext, setSheetState) { final colorScheme = Theme.of(sheetContext).colorScheme; return Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text("Filter & Sort", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), TextButton( onPressed: () { _clearFilters(); setSheetState(() {}); }, child: const Text("Reset"), ), ], ), const SizedBox(height: 16), const Text("Sort Order", style: TextStyle(fontWeight: FontWeight.w600)), const SizedBox(height: 8), Row( children: [ Expanded( child: ChoiceChip( label: const Text("Latest First"), selected: !_sortAscending, selectedColor: colorScheme.primary, labelStyle: TextStyle(color: !_sortAscending ? colorScheme.onPrimary : colorScheme.onSurface), onSelected: (_) { setState(() => _sortAscending = false); setSheetState(() {}); _applyFilters(); }, ), ), const SizedBox(width: 12), Expanded( child: ChoiceChip( label: const Text("Oldest First"), selected: _sortAscending, selectedColor: colorScheme.primary, labelStyle: TextStyle(color: _sortAscending ? colorScheme.onPrimary : colorScheme.onSurface), onSelected: (_) { setState(() => _sortAscending = true); setSheetState(() {}); _applyFilters(); }, ), ), ], ), const SizedBox(height: 24), const Text("Date Range", style: TextStyle(fontWeight: FontWeight.w600)), const SizedBox(height: 8), Row( children: [ Expanded( child: OutlinedButton.icon( icon: const Icon(Icons.date_range), label: Text( _dateRange == null ? "All Dates" : "${_formatDate(_dateRange!.start)} – ${_formatDate(_dateRange!.end)}", ), onPressed: () async { final now = DateTime.now(); final picked = await showDateRangePicker( context: sheetContext, firstDate: DateTime(now.year - 5), lastDate: now, initialDateRange: _dateRange, ); if (picked != null) { setState(() => _dateRange = picked); setSheetState(() {}); _applyFilters(); } }, ), ), if (_dateRange != null) IconButton( icon: const Icon(Icons.clear), tooltip: 'Clear date range', onPressed: () { setState(() => _dateRange = null); setSheetState(() {}); _applyFilters(); }, ), ], ), const SizedBox(height: 8), ], ), ); }, ); }, ); } String _formatDate(DateTime d) => "${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}"; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('History Vault'), actions: [ IconButton( icon: Icon(_hasActiveFilter ? Icons.filter_alt : Icons.filter_list), tooltip: 'Filter & Sort', onPressed: _openFilterSheet, ), if (_allGroups.isNotEmpty) IconButton( icon: const Icon(Icons.delete_sweep), tooltip: 'Reset History', onPressed: _resetHistory, ), ], ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : _groups.isEmpty ? _buildEmptyState() : ListView.builder( itemCount: _groups.length, padding: const EdgeInsets.all(12), itemBuilder: (context, index) => _buildGroupCard(_groups[index]), ), ); } Widget _buildEmptyState() { if (_allGroups.isEmpty) { return const Center(child: Text("No records found yet.")); } return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text("No records match the selected filters."), const SizedBox(height: 12), TextButton(onPressed: _clearFilters, child: const Text("Clear Filters")), ], ), ); } Widget _buildDeleteBackground() { final colorScheme = Theme.of(context).colorScheme; return Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: colorScheme.error, borderRadius: BorderRadius.circular(15), ), alignment: Alignment.centerRight, padding: const EdgeInsets.symmetric(horizontal: 24), child: Icon(Icons.delete, color: colorScheme.onError, size: 28), ); } Widget _buildGroupCard(_BatchGroup group) { final key = ValueKey(group.batchId ?? 'record_${group.records.first.id}'); // A single ungrouped legacy record: render exactly as before, no batch chrome. if (group.batchId == null && group.records.length == 1) { return Dismissible( key: key, direction: DismissDirection.endToStart, background: _buildDeleteBackground(), confirmDismiss: (_) => _confirmDelete("Delete this record? This action is irreversible."), onDismissed: (_) => _deleteGroup(group), child: _buildRecordTile(group.records.first), ); } final statusColor = getStatusColor(group.dominantClass); final start = group.records.last.timestamp; final end = group.records.first.timestamp; final timeRange = start.difference(end).inSeconds.abs() < 1 ? _formatTime(end) : "${_formatTime(start)} – ${_formatTime(end)}"; return Dismissible( key: key, direction: DismissDirection.endToStart, background: _buildDeleteBackground(), confirmDismiss: (_) => _confirmDelete("Delete this batch of ${group.records.length} scans? This action is irreversible."), onDismissed: (_) => _deleteGroup(group), child: Card( margin: const EdgeInsets.only(bottom: 12), elevation: 3, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), child: ListTile( leading: CircleAvatar( backgroundColor: statusColor.withOpacity(0.2), child: Icon(Icons.collections_bookmark, color: statusColor), ), title: Text( group.batchId == null ? "Ungrouped Scans" : "Batch • ${group.records.length} scans", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), subtitle: Text( "Majority: ${group.dominantClass}\n$timeRange", style: const TextStyle(fontSize: 12), ), trailing: const Icon(Icons.chevron_right), isThreeLine: true, onTap: group.batchId == null ? null : () => Navigator.push( context, MaterialPageRoute(builder: (context) => BatchReportScreen(batchId: group.batchId!)), ), ), ), ); } Widget _buildRecordTile(PalmRecord record) { final statusColor = getStatusColor(record.ripenessClass); return Card( margin: const EdgeInsets.only(bottom: 12), elevation: 3, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), child: ListTile( leading: CircleAvatar( backgroundColor: statusColor.withOpacity(0.2), child: Icon(Icons.spa, color: statusColor), ), title: Text( record.ripenessClass, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), subtitle: Text( "Conf: ${(record.confidence * 100).toStringAsFixed(1)}% | ${record.timestamp.toString().split('.')[0]}", style: const TextStyle(fontSize: 12), ), trailing: const Icon(Icons.chevron_right), isThreeLine: true, onTap: () => Navigator.push( context, MaterialPageRoute(builder: (context) => RecordDetailScreen(frames: [record], initialIndex: 0)), ), ), ); } String _formatTime(DateTime t) => t.toString().split('.')[0]; }