import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/locale/unit_provider.dart'; import '../../l10n/app_localizations.dart'; import '../../shared/models/product.dart'; import '../products/user_product_provider.dart'; import 'recognition_service.dart'; /// Editable confirmation screen shown after receipt/products recognition. /// The user can adjust quantities, units, remove items, then batch-add to pantry. class RecognitionConfirmScreen extends ConsumerStatefulWidget { const RecognitionConfirmScreen({super.key, required this.items}); final List items; @override ConsumerState createState() => _RecognitionConfirmScreenState(); } class _RecognitionConfirmScreenState extends ConsumerState { late final List<_EditableItem> _items; bool _saving = false; @override void initState() { super.initState(); _items = widget.items .map((item) => _EditableItem( name: item.name, quantity: item.quantity, unit: item.unit, category: item.category, primaryProductId: item.primaryProductId, storageDays: item.storageDays, confidence: item.confidence, quantityConfidence: item.quantityConfidence, )) .toList(); // Sort: low-confidence items first so the user notices them. _items.sort((firstItem, secondItem) => firstItem.confidence.compareTo(secondItem.confidence)); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Scaffold( appBar: AppBar( title: Text(l10n.recognitionFoundProducts(_items.length)), actions: [ if (_items.isNotEmpty) TextButton( onPressed: _saving ? null : _addAll, child: Text(l10n.recognitionAddAll), ), ], ), body: _items.isEmpty ? _EmptyState(onBack: () => Navigator.pop(context)) : ListView.builder( padding: const EdgeInsets.only(bottom: 80), itemCount: _items.length, itemBuilder: (_, index) => _ItemTile( item: _items[index], units: ref.watch(unitsProvider).valueOrNull ?? {}, onDelete: () => setState(() => _items.removeAt(index)), onChanged: () => setState(() {}), ), ), floatingActionButton: _items.isEmpty ? null : FloatingActionButton.extended( onPressed: _saving ? null : _addAll, icon: _saving ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.add_shopping_cart), label: Text(l10n.recognitionAddToStock), ), ); } Future _addAll() async { final l10n = AppLocalizations.of(context)!; final messenger = ScaffoldMessenger.of(context); setState(() => _saving = true); try { final payloads = _items .map((item) => { 'name': item.name, 'quantity': item.quantity, 'unit': item.unit, 'category': item.category, 'storage_days': item.storageDays, if (item.primaryProductId != null) 'primary_product_id': item.primaryProductId, }) .toList(); await ref.read(userProductsProvider.notifier).batchCreate(payloads); if (mounted) { messenger.showSnackBar( SnackBar(content: Text(l10n.recognitionAdded(_items.length))), ); // Pop back to products screen. int count = 0; Navigator.popUntil(context, (_) => count++ >= 2); } } catch (_) { if (mounted) { messenger.showSnackBar( SnackBar(content: Text(l10n.recognitionProductsFailed)), ); } } finally { if (mounted) setState(() => _saving = false); } } } // --------------------------------------------------------------------------- // Editable item model // --------------------------------------------------------------------------- class _EditableItem { String name; double quantity; String unit; final String category; String? primaryProductId; final int storageDays; final double confidence; final double quantityConfidence; _EditableItem({ required this.name, required this.quantity, required this.unit, required this.category, this.primaryProductId, required this.storageDays, required this.confidence, required this.quantityConfidence, }); } // --------------------------------------------------------------------------- // Item tile with inline editing // --------------------------------------------------------------------------- class _ItemTile extends ConsumerStatefulWidget { const _ItemTile({ required this.item, required this.units, required this.onDelete, required this.onChanged, }); final _EditableItem item; final Map units; final VoidCallback onDelete; final VoidCallback onChanged; @override ConsumerState<_ItemTile> createState() => _ItemTileState(); } class _ItemTileState extends ConsumerState<_ItemTile> { late final _qtyController = TextEditingController(text: _formatQty(widget.item.quantity)); @override void dispose() { _qtyController.dispose(); super.dispose(); } String _formatQty(double value) => value == value.roundToDouble() ? value.toInt().toString() : value.toStringAsFixed(1); void _openProductPicker() async { final picked = await showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => _ProductPickerSheet(currentName: widget.item.name), ); if (picked != null && mounted) { setState(() { widget.item.name = picked.displayName; widget.item.primaryProductId = picked.id; if (picked.defaultUnit != null) { widget.item.unit = picked.defaultUnit!; } }); widget.onChanged(); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final l10n = AppLocalizations.of(context)!; final conf = widget.item.confidence; final confColor = conf >= 0.8 ? Colors.green : conf >= 0.5 ? Colors.orange : Colors.red; final qtyUncertain = widget.item.quantityConfidence < 0.7; final tileColor = conf < 0.7 ? theme.colorScheme.errorContainer.withValues(alpha: 0.15) : null; return Dismissible( key: ValueKey(widget.item.name), direction: DismissDirection.endToStart, background: Container( color: theme.colorScheme.error, alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: 20), child: Icon(Icons.delete_outline, color: theme.colorScheme.onError), ), onDismissed: (_) => widget.onDelete(), child: Container( color: tileColor, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( onTap: _openProductPicker, child: Row( children: [ Expanded( child: Text(widget.item.name, style: theme.textTheme.bodyLarge), ), Icon( Icons.edit_outlined, size: 16, color: theme.colorScheme.onSurfaceVariant, ), ], ), ), Row( children: [ Container( width: 8, height: 8, decoration: BoxDecoration( color: confColor, shape: BoxShape.circle, ), ), const SizedBox(width: 4), Text( l10n.recognitionConfidence((conf * 100).toInt()), style: theme.textTheme.labelSmall ?.copyWith(color: confColor), ), ], ), ], ), ), const SizedBox(width: 8), SizedBox( width: 72, child: TextField( controller: _qtyController, keyboardType: const TextInputType.numberWithOptions(decimal: true), textAlign: TextAlign.center, decoration: InputDecoration( isDense: true, contentPadding: const EdgeInsets.symmetric(vertical: 8), border: const OutlineInputBorder(), enabledBorder: qtyUncertain ? const OutlineInputBorder( borderSide: BorderSide(color: Colors.orange, width: 2), ) : null, ), onChanged: (value) { final parsed = double.tryParse(value); if (parsed != null) { widget.item.quantity = parsed; widget.onChanged(); } }, ), ), const SizedBox(width: 8), widget.units.isEmpty ? const SizedBox(width: 48) : Builder(builder: (builderContext) { if (!widget.units.containsKey(widget.item.unit)) { widget.item.unit = widget.units.keys.first; } return DropdownButton( value: widget.item.unit, underline: const SizedBox(), items: widget.units.entries .map((entry) => DropdownMenuItem( value: entry.key, child: Text(entry.value))) .toList(), onChanged: (value) { if (value != null) { setState(() => widget.item.unit = value); widget.onChanged(); } }, ); }), IconButton( icon: const Icon(Icons.close), onPressed: widget.onDelete, ), ], ), ), ); } } // --------------------------------------------------------------------------- // Product picker bottom sheet // --------------------------------------------------------------------------- class _ProductPickerSheet extends ConsumerStatefulWidget { const _ProductPickerSheet({required this.currentName}); final String currentName; @override ConsumerState<_ProductPickerSheet> createState() => _ProductPickerSheetState(); } class _ProductPickerSheetState extends ConsumerState<_ProductPickerSheet> { late final _searchController = TextEditingController(text: widget.currentName); List _results = []; bool _loading = false; @override void initState() { super.initState(); _search(widget.currentName); } @override void dispose() { _searchController.dispose(); super.dispose(); } Future _search(String query) async { if (query.isEmpty) { setState(() => _results = []); return; } setState(() => _loading = true); try { final service = ref.read(userProductServiceProvider); final searchResults = await service.searchProducts(query); if (mounted) setState(() => _results = searchResults); } catch (_) { // Silently ignore search errors. } finally { if (mounted) setState(() => _loading = false); } } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( l10n.recognitionReplaceProduct, style: Theme.of(context).textTheme.titleMedium, ), ), const SizedBox(height: 8), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: TextField( controller: _searchController, autofocus: true, decoration: InputDecoration( hintText: l10n.searchProductsHint, prefixIcon: const Icon(Icons.search), border: const OutlineInputBorder(), isDense: true, ), onChanged: _search, ), ), const SizedBox(height: 8), if (_loading) const Padding( padding: EdgeInsets.all(16), child: CircularProgressIndicator(), ) else ConstrainedBox( constraints: const BoxConstraints(maxHeight: 300), child: ListView.builder( shrinkWrap: true, itemCount: _results.length, itemBuilder: (_, index) { final product = _results[index]; return ListTile( title: Text(product.displayName), onTap: () => Navigator.pop(context, product), ); }, ), ), const SizedBox(height: 8), ], ), ); } } // --------------------------------------------------------------------------- // Empty state // --------------------------------------------------------------------------- class _EmptyState extends StatelessWidget { const _EmptyState({required this.onBack}); final VoidCallback onBack; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.search_off, size: 64), const SizedBox(height: 12), Text(l10n.recognitionEmpty), const SizedBox(height: 16), FilledButton(onPressed: onBack, child: Text(l10n.cancel)), ], ), ); } }