160 lines
4.9 KiB
JavaScript
160 lines
4.9 KiB
JavaScript
const scoreMap = {
|
|
good: 25,
|
|
medium: 14,
|
|
critical: 0
|
|
};
|
|
|
|
function priorityWeight(priority) {
|
|
if (priority === 'high') return 1.2;
|
|
if (priority === 'low') return 0.8;
|
|
return 1;
|
|
}
|
|
|
|
function statusFromScore(score) {
|
|
if (score >= 78) return 'good';
|
|
if (score >= 48) return 'medium';
|
|
return 'critical';
|
|
}
|
|
|
|
function severityFromStatus(status) {
|
|
if (status === 'critical') return 'critical';
|
|
if (status === 'medium') return 'warning';
|
|
return 'info';
|
|
}
|
|
|
|
export function calculateProductRating(product, preferences) {
|
|
const avoidAllergens = preferences.avoidAllergens || [];
|
|
const matchingAllergens = (product.allergens || []).filter((allergen) =>
|
|
avoidAllergens.some((blockedAllergen) => blockedAllergen.toLowerCase() === allergen.toLowerCase())
|
|
);
|
|
|
|
const allergies = matchingAllergens.length > 0
|
|
? {
|
|
status: 'critical',
|
|
message: `Enthält: ${matchingAllergens.join(', ')}.`
|
|
}
|
|
: {
|
|
status: 'good',
|
|
message: 'Keine hinterlegten Allergene gefunden.'
|
|
};
|
|
|
|
const nutrition = rateNutrition(product, preferences);
|
|
const sustainability = rateSustainability(product, preferences);
|
|
const budget = rateBudget(product, preferences);
|
|
|
|
const categoryScores = {
|
|
allergies,
|
|
budget,
|
|
nutrition,
|
|
sustainability
|
|
};
|
|
|
|
const weightedScores = [
|
|
scoreMap[allergies.status] * 1.35,
|
|
scoreMap[budget.status] * 1,
|
|
scoreMap[nutrition.status] * priorityWeight(preferences.nutritionPriority),
|
|
scoreMap[sustainability.status] * priorityWeight(preferences.sustainabilityPriority)
|
|
];
|
|
const maxScore = 25 * (1.35 + 1 + priorityWeight(preferences.nutritionPriority) + priorityWeight(preferences.sustainabilityPriority));
|
|
const overallScore = Math.round((weightedScores.reduce((sum, value) => sum + value, 0) / maxScore) * 100);
|
|
const overallStatus = statusFromScore(overallScore);
|
|
|
|
const reasons = Object.entries(categoryScores).map(([category, value]) => ({
|
|
category,
|
|
message: value.message,
|
|
severity: severityFromStatus(value.status)
|
|
}));
|
|
|
|
return {
|
|
productId: product.id,
|
|
overallStatus,
|
|
overallScore,
|
|
reasons,
|
|
categoryScores
|
|
};
|
|
}
|
|
|
|
function rateNutrition(product, preferences) {
|
|
const nutrition = product.nutrition || {};
|
|
|
|
if (preferences.preferredDiet === 'lowSugar') {
|
|
if ((nutrition.sugarPer100g || 0) > 22) {
|
|
return { status: 'critical', message: 'Sehr hoher Zuckergehalt für dein Ziel.' };
|
|
}
|
|
|
|
if ((nutrition.sugarPer100g || 0) > 8) {
|
|
return { status: 'medium', message: 'Zuckergehalt prüfen, liegt über deinem Ziel.' };
|
|
}
|
|
}
|
|
|
|
if (preferences.preferredDiet === 'highProtein') {
|
|
if ((nutrition.proteinPer100g || 0) >= 10) {
|
|
return { status: 'good', message: 'Passt gut zu deinem Protein-Ziel.' };
|
|
}
|
|
|
|
return { status: 'medium', message: 'Proteinanteil ist eher niedrig.' };
|
|
}
|
|
|
|
if (preferences.preferredDiet === 'vegan' && (product.allergens || []).includes('Milch')) {
|
|
return { status: 'critical', message: 'Enthält Milch und passt nicht zu vegan.' };
|
|
}
|
|
|
|
if (['A', 'B'].includes(nutrition.nutriScore)) {
|
|
return { status: 'good', message: `Nutri-Score ${nutrition.nutriScore} ist positiv.` };
|
|
}
|
|
|
|
if (nutrition.nutriScore === 'C') {
|
|
return { status: 'medium', message: 'Nutri-Score C: in Ordnung, aber nicht optimal.' };
|
|
}
|
|
|
|
return { status: 'critical', message: `Nutri-Score ${nutrition.nutriScore || 'unbekannt'} ist kritisch.` };
|
|
}
|
|
|
|
function rateSustainability(product, preferences) {
|
|
const sustainability = product.sustainability || {};
|
|
|
|
if (preferences.preferBio && !sustainability.isBio) {
|
|
return { status: 'medium', message: 'Nicht Bio, obwohl Bio bevorzugt wird.' };
|
|
}
|
|
|
|
if (['A', 'B'].includes(sustainability.ecoScore)) {
|
|
return { status: 'good', message: `Eco-Score ${sustainability.ecoScore} ist stark.` };
|
|
}
|
|
|
|
if (sustainability.ecoScore === 'C') {
|
|
return { status: 'medium', message: 'Eco-Score C: akzeptabel, aber verbesserbar.' };
|
|
}
|
|
|
|
return { status: 'critical', message: `Eco-Score ${sustainability.ecoScore || 'unbekannt'} ist schwach.` };
|
|
}
|
|
|
|
function rateBudget(product, preferences) {
|
|
if (typeof product.price !== 'number' || typeof preferences.maxBudgetPerShoppingTrip !== 'number') {
|
|
return { status: 'medium', message: 'Preis oder Budget fehlt für eine genaue Prüfung.' };
|
|
}
|
|
|
|
const share = product.price / preferences.maxBudgetPerShoppingTrip;
|
|
const periodLabel = budgetPeriodLabel(preferences.budgetPeriod);
|
|
|
|
if (share <= 0.08) {
|
|
return { status: 'good', message: `Preis passt gut zu deinem Budget pro ${periodLabel}.` };
|
|
}
|
|
|
|
if (share <= 0.18) {
|
|
return { status: 'medium', message: `Preis ist für dein Budget pro ${periodLabel} spürbar, aber noch im Rahmen.` };
|
|
}
|
|
|
|
return { status: 'critical', message: `Preis belastet dein Budget pro ${periodLabel} stark.` };
|
|
}
|
|
|
|
function budgetPeriodLabel(period) {
|
|
const labels = {
|
|
day: 'Tag',
|
|
week: 'Woche',
|
|
month: 'Monat',
|
|
shoppingTrip: 'Einkauf'
|
|
};
|
|
|
|
return labels[period] || labels.week;
|
|
}
|