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:
dbastrikin
2026-03-26 23:09:57 +02:00
parent b2bdcbae6f
commit 5c5ed25e5b
38 changed files with 1221 additions and 115 deletions

View File

@@ -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,

View File

@@ -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, {

View File

@@ -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');