import 'package:flutter/material.dart'; import '../services/database_helper.dart'; import '../models/palm_record.dart'; import '../constants/mpob_colors.dart'; import '../services/app_strings.dart'; import '../theme/locale_controller.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: Text(AppStrings.t('history_reset_title')), content: Text(AppStrings.t('history_reset_message')), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: Text(AppStrings.t('cancel_button'))), TextButton( onPressed: () => Navigator.pop(context, true), child: Text(AppStrings.t('reset_button'), style: TextStyle(color: errorColor)), ), ], ), ); if (confirm == true) { await _dbHelper.clearAllRecords(); _loadHistory(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(AppStrings.t('history_cleared_snackbar'))), ); } } } Future _confirmDelete(String message) async { final errorColor = Theme.of(context).colorScheme.error; final confirm = await showDialog( context: context, builder: (context) => AlertDialog( title: Text(AppStrings.t('delete_button')), content: Text(message), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: Text(AppStrings.t('cancel_button'))), TextButton( onPressed: () => Navigator.pop(context, true), child: Text(AppStrings.t('delete_button'), 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: [ Text(AppStrings.t('filter_sort_title'), style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), TextButton( onPressed: () { _clearFilters(); setSheetState(() {}); }, child: Text(AppStrings.t('reset_button')), ), ], ), const SizedBox(height: 16), Text(AppStrings.t('sort_order_label'), style: const TextStyle(fontWeight: FontWeight.w600)), const SizedBox(height: 8), Row( children: [ Expanded( child: ChoiceChip( label: Text(AppStrings.t('sort_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: Text(AppStrings.t('sort_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), Text(AppStrings.t('date_range_label'), style: const TextStyle(fontWeight: FontWeight.w600)), const SizedBox(height: 8), Row( children: [ Expanded( child: OutlinedButton.icon( icon: const Icon(Icons.date_range), label: Text( _dateRange == null ? AppStrings.t('all_dates_label') : "${_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: AppStrings.t('clear_date_range_tooltip'), 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 ValueListenableBuilder( valueListenable: LocaleController.localeCode, builder: (context, _, _) => _buildScaffold(context), ); } Widget _buildScaffold(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(AppStrings.t('history_vault_title')), actions: [ IconButton( icon: Icon(_hasActiveFilter ? Icons.filter_alt : Icons.filter_list), tooltip: AppStrings.t('filter_sort_title'), onPressed: _openFilterSheet, ), if (_allGroups.isNotEmpty) IconButton( icon: const Icon(Icons.delete_sweep), tooltip: AppStrings.t('reset_history_tooltip'), 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 Center(child: Text(AppStrings.t('history_empty_no_records'))); } return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(AppStrings.t('history_empty_no_matches')), const SizedBox(height: 12), TextButton(onPressed: _clearFilters, child: Text(AppStrings.t('clear_filters_button'))), ], ), ); } 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(AppStrings.t('history_delete_record_confirm')), 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(AppStrings.tp('history_delete_batch_confirm', {'count': '${group.records.length}'})), 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.withValues(alpha: 0.2), child: Icon(Icons.collections_bookmark, color: statusColor), ), title: Text( group.batchId == null ? AppStrings.t('ungrouped_scans_label') : AppStrings.tp('batch_scans_label', {'count': '${group.records.length}'}), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), subtitle: Text( AppStrings.tp('majority_label', {'className': AppStrings.className(group.dominantClass), 'timeRange': 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.withValues(alpha: 0.2), child: Icon(Icons.spa, color: statusColor), ), title: Text( AppStrings.className(record.ripenessClass), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), subtitle: Text( AppStrings.tp('record_conf_timestamp', { 'percent': (record.confidence * 100).toStringAsFixed(1), 'timestamp': 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]; }