/// Log of saved shopping trips. library; import 'dart:io'; import 'package:flutter/material.dart'; import 'models/shop.dart'; import 'services/app_settings.dart'; import 'services/product_repository.dart'; import 'shop_detail_screen.dart'; import 'utils/dates.dart'; import 'utils/money.dart'; import 'widgets/confirm_dialog.dart'; import 'widgets/settings_button.dart'; import 'widgets/store_tag.dart'; class ShopLogScreen extends StatefulWidget { const ShopLogScreen({super.key, required this.repository}); final ProductRepository repository; @override State createState() => _ShopLogScreenState(); } class _ShopLogScreenState extends State { late Future> _future; bool _editing = false; @override void initState() { super.initState(); _future = widget.repository.getShops(); } void _reload() { setState(() => _future = widget.repository.getShops()); } Future _openShop(Shop shop) async { await Navigator.of(context).push( MaterialPageRoute( builder: (_) => ShopDetailScreen(repository: widget.repository, shopId: shop.id), ), ); if (mounted) _reload(); } Future _removeShop(Shop shop) async { final confirmed = await confirmAction( context, title: 'Remove shopping trip?', message: 'This trip will be removed from the shop log. Products from this trip stay in your catalog.', ); if (!confirmed || !mounted) return; await widget.repository.deleteShop(shop.id); if (mounted) _reload(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final stores = SettingsScope.maybeOf(context)?.supermarkets ?? const []; return Scaffold( appBar: AppBar( title: const Text('Shop log'), actions: [ IconButton( tooltip: _editing ? 'Done' : 'Edit', onPressed: () => setState(() => _editing = !_editing), icon: Icon(_editing ? Icons.check : Icons.edit_outlined), ), const SettingsButton(), ], ), body: FutureBuilder>( future: _future, builder: (context, snapshot) { if (snapshot.hasError) { return Center( child: Text( 'Could not load the shop log.', style: theme.textTheme.bodyLarge, ), ); } if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator()); } final shops = snapshot.data!; if (shops.isEmpty) { return Center( child: Text( 'Scan a receipt or enter a trip to log it.', textAlign: TextAlign.center, style: theme.textTheme.bodyLarge, ), ); } return ListView.separated( padding: const EdgeInsets.fromLTRB(8, 8, 8, 16), itemCount: shops.length, separatorBuilder: (_, _) => const SizedBox(height: 4), itemBuilder: (context, index) { final shop = shops[index]; final count = shop.itemCount; return ListTile( leading: _ReceiptThumb(path: shop.receiptImagePath), title: Align( alignment: Alignment.centerLeft, child: StoreTag(name: shop.supermarket, stores: stores), ), subtitle: Text( [ formatDate(shop.shoppedAt), count == 1 ? '1 product' : '$count products', ].join(' ยท '), ), trailing: _editing ? IconButton( tooltip: 'Remove', onPressed: () => _removeShop(shop), icon: const Icon(Icons.delete_outline), ) : Text( formatEuro(shop.total), style: theme.textTheme.titleMedium, ), onTap: _editing ? null : () => _openShop(shop), ); }, ); }, ), ); } } class _ReceiptThumb extends StatelessWidget { const _ReceiptThumb({this.path}); final String? path; @override Widget build(BuildContext context) { final file = path == null ? null : File(path!); final hasFile = file != null && file.existsSync(); final scheme = Theme.of(context).colorScheme; return ClipRRect( borderRadius: BorderRadius.circular(12), child: SizedBox( width: 48, height: 48, child: hasFile ? Image.file( file, fit: BoxFit.cover, cacheWidth: 144, errorBuilder: (_, _, _) => ColoredBox( color: scheme.secondaryContainer, child: Icon( Icons.receipt_long_outlined, color: scheme.onSecondaryContainer, ), ), ) : ColoredBox( color: scheme.secondaryContainer, child: Icon( Icons.receipt_long_outlined, color: scheme.onSecondaryContainer, ), ), ), ); } }