feat: improved receipt recognition, batch product add, and scan UX
- Rewrite receipt OCR prompt: completes truncated names, preserves fat% and flavour attributes, extracts weight/volume from line, infers typical package sizes for solid goods with quantity_confidence field - Add quantity_confidence to RecognizedItem, EnrichedItem, and ProductJobResultItem; propagate through item enricher and worker - Replace per-item create loop with single POST /user-products/batch call from RecognitionConfirmScreen - Rebuild RecognitionConfirmScreen: amber qty border for low quantity_confidence, tappable product name → catalog picker, sort items by confidence, full L10n (no hardcoded strings) - Add timestamps (HH:mm / d MMM HH:mm) to recent scan chips - Show close-app hint on ProductJobWatchScreen (queued + processing) - Refresh recentProductJobsProvider on watch screen init so new job appears without a manual pull-to-refresh - App-level WidgetsBindingObserver refreshes product and dish job lists on resume, fixing stale lists after background/foreground transitions - Add 9 new L10n keys across all 12 locales Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../core/locale/unit_provider.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
@@ -120,7 +121,7 @@ class _RecentScansSection extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 72,
|
||||
height: 84,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
@@ -187,6 +188,12 @@ class _ScanJobChip extends ConsumerWidget {
|
||||
isFailed: isFailed,
|
||||
isActive: isActive,
|
||||
),
|
||||
Text(
|
||||
_formatChipDate(job.createdAt),
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -197,6 +204,17 @@ class _ScanJobChip extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
String _formatChipDate(DateTime dateTime) {
|
||||
final local = dateTime.toLocal();
|
||||
final now = DateTime.now();
|
||||
final isToday = local.year == now.year &&
|
||||
local.month == now.month &&
|
||||
local.day == now.day;
|
||||
return isToday
|
||||
? DateFormat.Hm().format(local)
|
||||
: DateFormat('d MMM HH:mm').format(local);
|
||||
}
|
||||
|
||||
class _StatusBadge extends StatelessWidget {
|
||||
const _StatusBadge({
|
||||
required this.status,
|
||||
|
||||
@@ -72,6 +72,17 @@ class UserProductsNotifier
|
||||
});
|
||||
}
|
||||
|
||||
/// Adds multiple products in a single request and appends them to the list.
|
||||
Future<void> batchCreate(List<Map<String, dynamic>> payloads) async {
|
||||
final created = await _service.batchCreateProducts(payloads);
|
||||
state.whenData((products) {
|
||||
final updated = [...products, ...created]
|
||||
..sort((firstProduct, secondProduct) =>
|
||||
firstProduct.expiresAt.compareTo(secondProduct.expiresAt));
|
||||
state = AsyncValue.data(updated);
|
||||
});
|
||||
}
|
||||
|
||||
/// Updates a product in-place, keeping list sort order.
|
||||
Future<void> update(
|
||||
String id, {
|
||||
|
||||
@@ -61,6 +61,14 @@ class UserProductService {
|
||||
return UserProduct.fromJson(data);
|
||||
}
|
||||
|
||||
Future<List<UserProduct>> batchCreateProducts(
|
||||
List<Map<String, dynamic>> payloads) async {
|
||||
final list = await _client.postList('/user-products/batch', data: payloads);
|
||||
return list
|
||||
.map((element) => UserProduct.fromJson(element as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<void> deleteProduct(String id) =>
|
||||
_client.deleteVoid('/user-products/$id');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user