feat(flyer-import): add detailed product signals and display names
Test Suite / backend-pr-quick (push) Has been skipped
Test Suite / quick-import-pr-quick (push) Has been skipped
Test Suite / backend-full (push) Successful in 5m12s
Test Suite / flutter-quality (push) Failing after 2m8s

- Added `signals` and `displayNameDetailed` fields to FlyerItem model in Prisma schema
- Introduced `FlyerImportSignals` type with origin countries, labels, quality flags, variant, and packaging
- Added `displayNameDetailed` field to FlyerImportItem DTO and Flutter model
- Implemented utility functions for signal extraction and display name building
- Updated flyer import service to persist and return signals/category data
- Enhanced Flutter UI to display detailed product information including badges for signals
- Added new test coverage for signals persistence and display name generation
- Added new import-common module for shared import utilities
- Created database migration for new fields
- Added Kilo plan for feature development
This commit is contained in:
Nils-Johan Gynther
2026-05-24 19:32:13 +02:00
parent d9f992ca9a
commit b04d157915
16 changed files with 1124 additions and 107 deletions
@@ -501,6 +501,40 @@ class _FlyerImportTabState extends ConsumerState<FlyerImportTab> {
);
}
Widget _buildMetadataBadges(FlyerImportItem item, ThemeData theme) {
final values = <String>[
...(item.signals?.originCountries ?? const <String>[]),
...(item.signals?.labels ?? const <String>[]),
];
if (values.isEmpty) return const SizedBox.shrink();
return Wrap(
spacing: 6,
runSpacing: 6,
children: values
.map(
(value) => Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(999),
border: Border.all(
color: theme.colorScheme.primary.withValues(alpha: 0.25),
),
),
child: Text(
value,
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
),
)
.toList(),
);
}
Future<void> _copyText(String value, String label) async {
await Clipboard.setData(ClipboardData(text: value));
if (!mounted) return;
@@ -744,7 +778,7 @@ class _FlyerImportTabState extends ConsumerState<FlyerImportTab> {
},
title: Row(
children: [
Expanded(child: Text(item.rawName)),
Expanded(child: Text(item.displayNameDetailed ?? item.rawName)),
IconButton(
tooltip: 'Redigera',
visualDensity: VisualDensity.compact,
@@ -765,6 +799,10 @@ class _FlyerImportTabState extends ConsumerState<FlyerImportTab> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (priceText.isNotEmpty) Text('Pris: $priceText'),
if (item.isBundle && item.bundleItems.isNotEmpty)
Text('Paketinnehåll: ${item.bundleItems.join(' + ')}'),
if (item.brand != null && item.brand!.trim().isNotEmpty)
Text('Varumärke: ${item.brand}'),
if ((item.category ?? '').trim().isNotEmpty)
Text('Kategori: ${item.category}'),
if (comparisonText.isNotEmpty)
@@ -778,6 +816,11 @@ class _FlyerImportTabState extends ConsumerState<FlyerImportTab> {
),
),
if (sanitizedOfferText.isNotEmpty) Text(sanitizedOfferText),
if ((item.signals?.originCountries.isNotEmpty ?? false) ||
(item.signals?.labels.isNotEmpty ?? false)) ...[
const SizedBox(height: 6),
_buildMetadataBadges(item, theme),
],
if (item.matchedProductName != null)
Text('Match: ${item.matchedProductName}'),
if (detailedReasons.isNotEmpty) ...[