export const EXPIRING_WITHIN_DAYS = 3; export function isExpiringSoon(expiresAt: Date | null): boolean { if (!expiresAt) return false; const days = Math.ceil((expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24)); return days >= 0 && days <= EXPIRING_WITHIN_DAYS; } type ScorableRecipe = T & { ingredients: { rawName: string }[] }; export function scoreRecipesAgainstPantry( recipesList: ScorableRecipe[], pantry: { rawName: string; expiresAt: Date | null }[] ) { const pantryKeys = new Set(pantry.map((p) => p.rawName.toLowerCase())); const expiringSoonKeys = new Set( pantry.filter((p) => isExpiringSoon(p.expiresAt)).map((p) => p.rawName.toLowerCase()) ); return recipesList .filter((r) => r.ingredients.length > 0) .map((recipe) => { const matched = recipe.ingredients.filter((ing) => pantryKeys.has(ing.rawName.toLowerCase())).length; const missing = recipe.ingredients .filter((ing) => !pantryKeys.has(ing.rawName.toLowerCase())) .map((ing) => ing.rawName) .slice(0, 5); const usesExpiring = recipe.ingredients .filter((ing) => expiringSoonKeys.has(ing.rawName.toLowerCase())) .map((ing) => ing.rawName); const total = recipe.ingredients.length; return { recipe, matched, total, pct: Math.round((matched / total) * 100), missing, usesExpiring }; }) .sort((a, b) => { if (a.usesExpiring.length > 0 !== b.usesExpiring.length > 0) { return a.usesExpiring.length > 0 ? -1 : 1; } return b.pct - a.pct; }); }