feat: Enhance apple categorization logic and improve bulk category update feedback
Test Suite / test (24.15.0) (push) Has been cancelled
Test Suite / test (24.15.0) (push) Has been cancelled
This commit is contained in:
@@ -144,6 +144,13 @@ Regler:
|
|||||||
throw new ServiceUnavailableException('MISTRAL_API_KEY är inte konfigurerad i miljövariabler');
|
throw new ServiceUnavailableException('MISTRAL_API_KEY är inte konfigurerad i miljövariabler');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Snabb deterministic guard för välkända äppelsorter.
|
||||||
|
// Detta minskar felklassning när "äpple" saknas i namnet (t.ex. Granny Smith).
|
||||||
|
const appleCategory = this.matchAppleVarietyCategory(productName, categories);
|
||||||
|
if (appleCategory) {
|
||||||
|
return appleCategory;
|
||||||
|
}
|
||||||
|
|
||||||
const categoryList = categories
|
const categoryList = categories
|
||||||
.map((c) => `[${c.id}] ${c.path}`)
|
.map((c) => `[${c.id}] ${c.path}`)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
@@ -156,6 +163,7 @@ ${categoryList}
|
|||||||
KWICK-MATCHNING (gör detta först):
|
KWICK-MATCHNING (gör detta först):
|
||||||
- Kött/fläsk: Sök efter ord som "fläsk", "flaskytterfile", "bacon", "kotlett", "karré" → Kött, chark & fågel > Fläsk
|
- Kött/fläsk: Sök efter ord som "fläsk", "flaskytterfile", "bacon", "kotlett", "karré" → Kött, chark & fågel > Fläsk
|
||||||
- Fågel: Sök efter "kyckling", "kalkon", "drumstick", "filé" → Kött, chark & fågel > Fågel
|
- Fågel: Sök efter "kyckling", "kalkon", "drumstick", "filé" → Kött, chark & fågel > Fågel
|
||||||
|
- Äpplen/fruktsorter: Sök efter "äpple", "apple", "granny smith", "pink lady", "royal gala", "golden delicious", "jonagold", "fuji" → Frukt & Grönt > Frukt > Äpplen (eller Frukt om Äpplen saknas)
|
||||||
- Choklad/spreads: Sök efter "nutella", "choklad", "kakao", "spreads" → Sötsaker & snacks > Choklad & spreads
|
- Choklad/spreads: Sök efter "nutella", "choklad", "kakao", "spreads" → Sötsaker & snacks > Choklad & spreads
|
||||||
- Bröd: Sök efter "bröd", "toast", "brödrost", "limpa" → Bröd & bakvaror > Bröd
|
- Bröd: Sök efter "bröd", "toast", "brödrost", "limpa" → Bröd & bakvaror > Bröd
|
||||||
|
|
||||||
@@ -288,4 +296,45 @@ Regler:
|
|||||||
usedFallback: true,
|
usedFallback: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private matchAppleVarietyCategory(
|
||||||
|
productName: string,
|
||||||
|
categories: FlatCategory[],
|
||||||
|
): CategorySuggestion | null {
|
||||||
|
const normalized = productName.trim().toLowerCase();
|
||||||
|
const looksLikeApple = /\b(äpple|apple|granny\s*smith|pink\s*lady|royal\s*gala|golden\s*delicious|jonagold|fuji|braeburn|aroma)\b/i
|
||||||
|
.test(normalized);
|
||||||
|
|
||||||
|
if (!looksLikeApple) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appleLeaf = categories.find(
|
||||||
|
(c) => c.path.toLowerCase() === 'frukt & grönt > frukt > äpplen',
|
||||||
|
);
|
||||||
|
if (appleLeaf) {
|
||||||
|
return {
|
||||||
|
categoryId: appleLeaf.id,
|
||||||
|
categoryName: appleLeaf.name,
|
||||||
|
path: appleLeaf.path,
|
||||||
|
confidence: 'high',
|
||||||
|
usedFallback: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const fruitFallback = categories.find(
|
||||||
|
(c) => c.path.toLowerCase() === 'frukt & grönt > frukt',
|
||||||
|
);
|
||||||
|
if (fruitFallback) {
|
||||||
|
return {
|
||||||
|
categoryId: fruitFallback.id,
|
||||||
|
categoryName: fruitFallback.name,
|
||||||
|
path: fruitFallback.path,
|
||||||
|
confidence: 'high',
|
||||||
|
usedFallback: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -269,6 +269,15 @@ INSERT INTO `Category` (`name`, `parentId`)
|
|||||||
SELECT 'Citrusfrukt', c2.id FROM `Category` c1
|
SELECT 'Citrusfrukt', c2.id FROM `Category` c1
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Frukt'
|
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Frukt'
|
||||||
WHERE c1.name = 'Frukt & Grönt' AND c1.parentId IS NULL;
|
WHERE c1.name = 'Frukt & Grönt' AND c1.parentId IS NULL;
|
||||||
|
INSERT INTO `Category` (`name`, `parentId`)
|
||||||
|
SELECT 'Äpplen', c2.id FROM `Category` c1
|
||||||
|
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Frukt'
|
||||||
|
WHERE c1.name = 'Frukt & Grönt'
|
||||||
|
AND c1.parentId IS NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `Category` existing
|
||||||
|
WHERE existing.parentId = c2.id AND existing.name = 'Äpplen'
|
||||||
|
);
|
||||||
|
|
||||||
-- ── NIVÅ 3: under Glass > Chips, snacks & dip ───────────────
|
-- ── NIVÅ 3: under Glass > Chips, snacks & dip ───────────────
|
||||||
INSERT INTO `Category` (`name`, `parentId`)
|
INSERT INTO `Category` (`name`, `parentId`)
|
||||||
|
|||||||
@@ -173,9 +173,6 @@ class AdminRepository {
|
|||||||
// ── Produkter ──────────────────────────────────────────────────────────────
|
// ── Produkter ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
Future<List<AdminProduct>> listProducts() =>
|
Future<List<AdminProduct>> listProducts() =>
|
||||||
_getList(ProductApiPaths.mine, AdminProduct.fromJson);
|
|
||||||
|
|
||||||
Future<List<AdminProduct>> listGlobalProducts() =>
|
|
||||||
_getList(ProductApiPaths.list, AdminProduct.fromJson, requiresAuth: false);
|
_getList(ProductApiPaths.list, AdminProduct.fromJson, requiresAuth: false);
|
||||||
|
|
||||||
Future<List<PendingProduct>> listPrivateProducts() =>
|
Future<List<PendingProduct>> listPrivateProducts() =>
|
||||||
@@ -244,8 +241,15 @@ class AdminRepository {
|
|||||||
parse: (d) => d as Map<String, dynamic>,
|
parse: (d) => d as Map<String, dynamic>,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> bulkSetCategory(List<int> ids, {required int? categoryId}) =>
|
Future<int> bulkSetCategory(List<int> ids, {required int? categoryId}) =>
|
||||||
_postVoid(ProductApiPaths.bulkUpdate, {'ids': ids, 'categoryId': categoryId});
|
_post<int>(
|
||||||
|
ProductApiPaths.bulkUpdate,
|
||||||
|
body: {'ids': ids, 'categoryId': categoryId},
|
||||||
|
parse: (d) {
|
||||||
|
final map = Map<String, dynamic>.from(d as Map);
|
||||||
|
return (map['updated'] as num?)?.toInt() ?? 0;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
Future<void> mergeProducts({
|
Future<void> mergeProducts({
|
||||||
required int sourceProductId,
|
required int sourceProductId,
|
||||||
|
|||||||
@@ -116,10 +116,15 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
|
|
||||||
setState(() => _isApplying = true);
|
setState(() => _isApplying = true);
|
||||||
try {
|
try {
|
||||||
await ref.read(adminRepositoryProvider).bulkSetCategory(
|
final updated = await ref.read(adminRepositoryProvider).bulkSetCategory(
|
||||||
_selectedIds.toList(),
|
_selectedIds.toList(),
|
||||||
categoryId: categoryId,
|
categoryId: categoryId,
|
||||||
);
|
);
|
||||||
|
if (updated == 0) {
|
||||||
|
if (!mounted) return;
|
||||||
|
_showError('Inga produkter uppdaterades. Kontrollera att valda produkter fortfarande finns och försök igen.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedIds.clear();
|
_selectedIds.clear();
|
||||||
@@ -164,11 +169,18 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
if (!selectedProductIds.contains(row.productId)) continue;
|
if (!selectedProductIds.contains(row.productId)) continue;
|
||||||
grouped.putIfAbsent(row.categoryId, () => <int>[]).add(row.productId);
|
grouped.putIfAbsent(row.categoryId, () => <int>[]).add(row.productId);
|
||||||
}
|
}
|
||||||
|
var totalUpdated = 0;
|
||||||
for (final entry in grouped.entries) {
|
for (final entry in grouped.entries) {
|
||||||
await ref.read(adminRepositoryProvider).bulkSetCategory(
|
final updated = await ref.read(adminRepositoryProvider).bulkSetCategory(
|
||||||
entry.value,
|
entry.value,
|
||||||
categoryId: entry.key,
|
categoryId: entry.key,
|
||||||
);
|
);
|
||||||
|
totalUpdated += updated;
|
||||||
|
}
|
||||||
|
if (totalUpdated == 0) {
|
||||||
|
if (!mounted) return;
|
||||||
|
_showError('AI-förslag kunde inte sparas. Prova att uppdatera listan och kör igen.');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _selectedIds.clear());
|
setState(() => _selectedIds.clear());
|
||||||
|
|||||||
Reference in New Issue
Block a user