Correction PDF
This commit is contained in:
@@ -33,13 +33,6 @@
|
||||
const normalize = (s) => (s ?? "").toString().trim().toLowerCase();
|
||||
const proxify = (url) => url ? `/HelloFresh/ProxyPdf?url=${encodeURIComponent(url)}` : "";
|
||||
|
||||
// Détection iOS (iPhone/iPad et iPadOS mode desktop)
|
||||
const IS_IOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.userAgent.includes("Mac") && "ontouchend" in document);
|
||||
els.btnOpen?.addEventListener("click", () => {
|
||||
const r = RECIPES[CUR]; if (!r?.pdf) return;
|
||||
const u = `/HelloFresh/ProxyPdf?url=${encodeURIComponent(r.pdf)}`;
|
||||
if (IS_IOS) window.open(u, "_blank", "noopener"); else openViewer();
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -37,6 +37,19 @@ const splitTags = (str) => (str || '').split(/[,•]/).map(t => t.trim()).filter
|
||||
const esc = (s) => { const d = document.createElement('div'); d.textContent = (s ?? ''); return d.innerHTML; };
|
||||
function capFirst(s) { if (s == null) return ''; s = s.toString().trim(); if (!s) return ''; const f = s[0].toLocaleUpperCase('fr-FR'); return f + s.slice(1); }
|
||||
|
||||
const DEBOUNCE_MS = 250;
|
||||
function debounce(fn, wait = DEBOUNCE_MS) {
|
||||
let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); };
|
||||
}
|
||||
function onlySearchActive() {
|
||||
return normalize(currentSearchTerm) !== '' &&
|
||||
appliedFilters.ingredients.length === 0 &&
|
||||
appliedFilters.tags.length === 0 &&
|
||||
appliedFilters.timeMin == null &&
|
||||
appliedFilters.timeMax == null;
|
||||
}
|
||||
|
||||
|
||||
/***********************
|
||||
* COLORS for tags (persistées localStorage)
|
||||
***********************/
|
||||
@@ -326,28 +339,44 @@ function buildRecipeCardOwned(recipe) {
|
||||
async function loadRecipes(page = 1) {
|
||||
try {
|
||||
const url = new URL('/HelloFresh/GetRecipesByPage', window.location.origin);
|
||||
url.searchParams.set('page', page); url.searchParams.set('count', countPerPage);
|
||||
const res = await fetch(url.toString()); if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
url.searchParams.set('page', page);
|
||||
url.searchParams.set('count', countPerPage);
|
||||
|
||||
// 👉 On laisse le serveur faire la recherche quand il n'y a QUE la recherche active
|
||||
if (onlySearchActive()) {
|
||||
url.searchParams.set('search', currentSearchTerm || '');
|
||||
}
|
||||
|
||||
const res = await fetch(url.toString());
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
|
||||
const { recipes, currentPage: serverPage, lastPage } = data;
|
||||
currentPage = serverPage; lastPageGlobal = lastPage;
|
||||
|
||||
const grid = document.getElementById('recipeGrid'); grid.innerHTML = '';
|
||||
if (!recipes || recipes.length === 0) {
|
||||
grid.innerHTML = '<p class="placeholder">Aucune recette disponible pour le moment.</p>';
|
||||
renderPagination(currentPage, lastPageGlobal); return;
|
||||
renderPagination(currentPage, lastPageGlobal);
|
||||
return;
|
||||
}
|
||||
|
||||
recipes.forEach(r => grid.appendChild(buildRecipeCardList(r)));
|
||||
if (window.tippy) {
|
||||
tippy('[data-tippy-content]', { placement: 'top', arrow: true, delay: [100, 0], theme: 'light-border', maxWidth: 250, allowHTML: true });
|
||||
}
|
||||
await hydrateIngredientsForPage(recipes);
|
||||
refreshSelectedBorders();
|
||||
if ((currentSearchTerm ?? '').trim() !== '') applySearchFilter();
|
||||
|
||||
// ⚠️ si tu avais une ancienne fonction applySearchFilter(), on ne l'appelle plus ici.
|
||||
// if ((currentSearchTerm ?? '').trim() !== '' && typeof applySearchFilter === 'function') applySearchFilter();
|
||||
|
||||
refreshDoneFlagsOnVisibleCards();
|
||||
renderPagination(currentPage, lastPageGlobal);
|
||||
} catch (e) { console.error('loadRecipes error', e); }
|
||||
}
|
||||
|
||||
|
||||
async function loadRecipesOwned({ onEmpty = 'placeholder' } = {}) {
|
||||
const gridOwned = document.getElementById('recipeGridOwned'); if (!gridOwned) return false;
|
||||
const wasOpen = document.getElementById('recipeOwnedWrap')?.classList.contains('open');
|
||||
@@ -450,19 +479,40 @@ function anyFilterActive() {
|
||||
appliedFilters.timeMin != null ||
|
||||
appliedFilters.timeMax != null;
|
||||
}
|
||||
async function fetchAllRecipes() {
|
||||
if (ALL_RECIPES_CACHE) return ALL_RECIPES_CACHE;
|
||||
|
||||
async function fetchAllRecipes(opts = {}) {
|
||||
const search = (opts.search || '').trim();
|
||||
if (!search && ALL_RECIPES_CACHE) return ALL_RECIPES_CACHE;
|
||||
|
||||
let page = 1, last = 1, all = [];
|
||||
do {
|
||||
const url = new URL('/HelloFresh/GetRecipesByPage', window.location.origin);
|
||||
url.searchParams.set('page', page); url.searchParams.set('count', countPerPage);
|
||||
const res = await fetch(url.toString()); if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
url.searchParams.set('page', page);
|
||||
url.searchParams.set('count', countPerPage);
|
||||
if (search) url.searchParams.set('search', search);
|
||||
|
||||
const res = await fetch(url.toString());
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
all.push(...(data.recipes || []));
|
||||
last = data.lastPage || 1; page++;
|
||||
last = data.lastPage || 1;
|
||||
page++;
|
||||
} while (page <= last);
|
||||
ALL_RECIPES_CACHE = all; return all;
|
||||
|
||||
if (!search) ALL_RECIPES_CACHE = all;
|
||||
return all;
|
||||
}
|
||||
|
||||
async function fetchDetailsBatched(ids, batchSize = 150) {
|
||||
const out = [];
|
||||
for (let i = 0; i < ids.length; i += batchSize) {
|
||||
const part = ids.slice(i, i + batchSize);
|
||||
const res = await fetch(`/HelloFresh/GetRecipesDetails?ids=${encodeURIComponent(part.join(','))}`);
|
||||
if (res.ok) out.push(...(await res.json()));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function recipeMatchesFilters(cardMeta) {
|
||||
const term = normalize(currentSearchTerm);
|
||||
if (term && !cardMeta.name.includes(term)) return false;
|
||||
@@ -487,14 +537,16 @@ function extractIngredientNames(ingredientsJson) {
|
||||
} catch { }
|
||||
return [...out];
|
||||
}
|
||||
|
||||
async function buildMetaFor(recipes) {
|
||||
const ids = recipes.map(r => r.id).join(',');
|
||||
const ids = recipes.map(r => r.id);
|
||||
const metaById = {};
|
||||
|
||||
try {
|
||||
const res = await fetch(`/HelloFresh/GetRecipesDetails?ids=${encodeURIComponent(ids)}`);
|
||||
const details = await res.json(); // [{id, ingredientsJson}]
|
||||
const details = await fetchDetailsBatched(ids, 150); // ← chunks pour éviter les URLs géantes
|
||||
details.forEach(d => { metaById[d.id] = { ing: extractIngredientNames(d.ingredientsJson).map(normalize) }; });
|
||||
} catch (e) { console.warn('details fail', e); }
|
||||
|
||||
recipes.forEach(r => {
|
||||
const tagsVal = (r.tags ?? r.Tags ?? '').trim();
|
||||
const tags = splitTags(tagsVal).map(normalize);
|
||||
@@ -502,24 +554,45 @@ async function buildMetaFor(recipes) {
|
||||
const name = normalize(r.name || '');
|
||||
metaById[r.id] = Object.assign({ tags, time, name, ing: [] }, metaById[r.id] || {});
|
||||
});
|
||||
|
||||
return metaById;
|
||||
}
|
||||
|
||||
async function applyAllFilters() {
|
||||
if (!anyFilterActive()) {
|
||||
const hasSearch = normalize(currentSearchTerm) !== '';
|
||||
const hasOtherFilters = appliedFilters.ingredients.length || appliedFilters.tags.length ||
|
||||
appliedFilters.timeMin != null || appliedFilters.timeMax != null;
|
||||
|
||||
// A) Rien d'actif → liste classique
|
||||
if (!hasSearch && !hasOtherFilters) {
|
||||
CURRENT_CLIENT_LIST = null;
|
||||
await loadRecipes(1);
|
||||
updateActiveFilterBadge(); // peut masquer le badge si rien d’actif
|
||||
updateActiveFilterBadge();
|
||||
return;
|
||||
}
|
||||
const all = await fetchAllRecipes();
|
||||
|
||||
// B) Uniquement la recherche → serveur (rapide, paginé)
|
||||
if (hasSearch && !hasOtherFilters) {
|
||||
CURRENT_CLIENT_LIST = null; // on revient au flux serveur paginé
|
||||
currentPage = 1;
|
||||
await loadRecipes(1); // loadRecipes enverra ?search=...
|
||||
updateActiveFilterBadge();
|
||||
return;
|
||||
}
|
||||
|
||||
// C) Recherche + (tags/ingrédients/temps) OU filtres sans recherche
|
||||
// → on réduit d'abord côté serveur avec ?search=..., puis filtre côté client
|
||||
const all = await fetchAllRecipes({ search: hasSearch ? currentSearchTerm : '' });
|
||||
const meta = await buildMetaFor(all);
|
||||
const filtered = all.filter(r => recipeMatchesFilters(meta[r.id] || {}));
|
||||
|
||||
CURRENT_CLIENT_LIST = filtered;
|
||||
currentPage = 1;
|
||||
lastPageGlobal = Math.max(1, Math.ceil(filtered.length / countPerPage));
|
||||
await renderClientPage();
|
||||
updateActiveFilterBadge(); // badge = basé sur appliedFilters UNIQUEMENT
|
||||
updateActiveFilterBadge();
|
||||
}
|
||||
|
||||
async function renderClientPage() {
|
||||
const list = CURRENT_CLIENT_LIST || [];
|
||||
const start = (currentPage - 1) * countPerPage;
|
||||
@@ -809,9 +882,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Live search
|
||||
const searchInput = document.querySelector('#divSearch input');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', async (e) => {
|
||||
const debounced = debounce(async () => { await applyAllFilters(); }, 250);
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
currentSearchTerm = e.target.value;
|
||||
await applyAllFilters(); // search est immédiat
|
||||
debounced();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,18 @@ const els = { list: null, search: null };
|
||||
// --- Exclusions par recette ---
|
||||
const RECIPE_ING_CACHE = new Map(); // id -> { '1': [noms], '2': [...], ... }
|
||||
const EXCLUDED_BY_RECIPE = new Map(); // id -> Set(noms normalisés)
|
||||
|
||||
const GET_ALL_RECIPES_URL = "/HelloFresh/GetAllRecipes";
|
||||
const GET_MY_OWNED_URL = "/HelloFresh/GetRecipesOwned?isUserImportant=true";
|
||||
|
||||
const CORE_USERS = Array.isArray(window.CORE_USERS) ? window.CORE_USERS : ["Mae", "Byakuya"];
|
||||
|
||||
// État “Ingrédients à avoir”
|
||||
let LAST_ING_TO_HAD = null;
|
||||
let LAST_PORTIONS = 1;
|
||||
let lastNeededSignature = "";
|
||||
let prevNotShippedCount = -1;
|
||||
const IS_CORE_USER = (window.IS_CORE_USER === true || window.IS_CORE_USER === 'true');
|
||||
|
||||
const ACCENT_RX = /[\u0300-\u036f]/g;
|
||||
const normalizeName = (s) => (s || "").trim().toLowerCase();
|
||||
@@ -295,21 +302,40 @@ function reapplyCurrentSearch() {
|
||||
async function loadAndRenderIngredients() {
|
||||
if (!els.list) return;
|
||||
els.list.innerHTML = "Chargement…";
|
||||
|
||||
try {
|
||||
const res = await fetch("/HelloFresh/AggregatedIngredients");
|
||||
if (!res.ok) { const txt = await res.text().catch(() => ""); console.error("[AggregatedIngredients] HTTP", res.status, txt); els.list.innerHTML = `Erreur chargement ingrédients (${res.status})`; return; }
|
||||
const data = await res.json();
|
||||
ALL_AGG_INGS = Array.isArray(data) ? data : [];
|
||||
OWNED_SET = new Set(ALL_AGG_INGS.filter(x => x?.Name && x.Owned === true).map(x => x.Name.trim().toLowerCase()));
|
||||
if (IS_CORE_USER) {
|
||||
// 🔒 Vue partagée: ingrédients = uniquement Mae/Byakuya
|
||||
const url = `/HelloFresh/GetRecipesOwnedByUsers?names=${encodeURIComponent(CORE_USERS.join(","))}`;
|
||||
const res = await fetch(url, { credentials: "same-origin" });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const rows = await res.json();
|
||||
window.allRecipes = Array.isArray(rows) ? rows : [];
|
||||
|
||||
const items = await buildAggregatedIngredientsFromRows(window.allRecipes);
|
||||
ALL_AGG_INGS = items;
|
||||
|
||||
const list = sortIngredientsForDisplay(applyExclusionsTo(items));
|
||||
els.list.innerHTML = "";
|
||||
renderItems(list.length ? list : []);
|
||||
if (!list.length) els.list.innerHTML = "<em>Aucun ingrédient</em>";
|
||||
return;
|
||||
}
|
||||
|
||||
// Utilisateur "tiers" : ingrédients = SES recettes (déjà en place)
|
||||
const mineAgg = await buildMyAggregatedIngredients();
|
||||
ALL_AGG_INGS = mineAgg;
|
||||
const list = sortIngredientsForDisplay(applyExclusionsTo(mineAgg));
|
||||
els.list.innerHTML = "";
|
||||
const initialMerged = mergeDuplicates([...ALL_AGG_INGS]);
|
||||
const initial = sortIngredientsForDisplay(applyExclusionsTo(initialMerged));
|
||||
els.list.innerHTML = "";
|
||||
renderItems(initial.length ? initial : []);
|
||||
if (!initial.length) els.list.innerHTML = "<em>Aucun ingrédient</em>";
|
||||
} catch (e) { console.error("[AggregatedIngredients] exception", e); els.list.innerHTML = "Erreur chargement ingrédients"; }
|
||||
renderItems(list.length ? list : []);
|
||||
if (!list.length) els.list.innerHTML = "<em>Aucun ingrédient</em>";
|
||||
} catch (e) {
|
||||
console.error("[ingredients] loadAndRenderIngredients exception", e);
|
||||
els.list.innerHTML = "Erreur chargement ingrédients";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ========== Not shipped (DOM) + BDD ==========
|
||||
function getNotShippedIngredientsFromDOM(root = document) {
|
||||
const nodes = Array.from(root.querySelectorAll(".ingredient-item-not-shipped"));
|
||||
@@ -536,13 +562,15 @@ function buildRecipeCardList(recipe) {
|
||||
});
|
||||
|
||||
|
||||
const usersVal = recipe.Users ?? recipe.users ?? recipe.User ?? recipe.user ?? [];
|
||||
const usersArr = Array.isArray(usersVal) ? usersVal.filter(Boolean) : String(usersVal || "").split(/[,•]/).map(s => s.trim()).filter(Boolean);
|
||||
if (usersArr.length) {
|
||||
const img = card.querySelector(".image-container");
|
||||
const wrap = document.createElement("div"); wrap.className = "label-container";
|
||||
usersArr.forEach(u => { const span = document.createElement("span"); span.className = "label-badge user-badge"; span.textContent = u; wrap.appendChild(span); });
|
||||
img?.appendChild(wrap);
|
||||
if (IS_CORE_USER) {
|
||||
const usersVal = recipe.Users ?? recipe.users ?? recipe.User ?? recipe.user ?? [];
|
||||
const usersArr = Array.isArray(usersVal) ? usersVal.filter(Boolean) : String(usersVal || "").split(/[,•]/).map(s => s.trim()).filter(Boolean);
|
||||
if (usersArr.length) {
|
||||
const img = card.querySelector(".image-container");
|
||||
const wrap = document.createElement("div"); wrap.className = "label-container";
|
||||
usersArr.forEach(u => { const span = document.createElement("span"); span.className = "label-badge user-badge"; span.textContent = u; wrap.appendChild(span); });
|
||||
img?.appendChild(wrap);
|
||||
}
|
||||
}
|
||||
|
||||
// Mémorise la dernière recette pour un fallback, via détection auto
|
||||
@@ -558,42 +586,227 @@ function buildRecipeCardList(recipe) {
|
||||
return card;
|
||||
}
|
||||
|
||||
const GET_ALL_RECIPES_URL = "/HelloFresh/GetAllRecipes";
|
||||
async function loadAllRecipesHorizontal() {
|
||||
const strip = document.getElementById("recipeStrip");
|
||||
if (!strip) return;
|
||||
|
||||
try {
|
||||
const r = await fetch("/HelloFresh/GetRecipesOwned?isUserImportant=false", { credentials: "same-origin" });
|
||||
if (r.ok) { const data = await r.json(); ownedMap.clear(); (data || []).forEach(x => ownedMap.set(String(x.id), clampQty(x.portions || 0))); }
|
||||
} catch { }
|
||||
// === Vue "partagée" (Mae/Byakuya) : ne montrer QUE leurs recettes ===
|
||||
if (IS_CORE_USER) {
|
||||
try {
|
||||
const url = `/HelloFresh/GetRecipesOwnedByUsers?names=${encodeURIComponent(CORE_USERS.join(","))}`;
|
||||
const res = await fetch(url, { credentials: "same-origin" });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const onlyCore = await res.json();
|
||||
window.allRecipes = Array.isArray(onlyCore) ? onlyCore : [];
|
||||
|
||||
strip.innerHTML = "";
|
||||
if (!window.allRecipes.length) {
|
||||
strip.innerHTML = '<p class="placeholder">Aucune recette (Mae/Byakuya).</p>';
|
||||
updateNeededList();
|
||||
return;
|
||||
}
|
||||
|
||||
window.allRecipes.sort((a, b) => String(a.name ?? "").localeCompare(String(b.name ?? ""), "fr", { sensitivity: "base" }));
|
||||
window.allRecipes.forEach(r => strip.appendChild(buildRecipeCardList(r)));
|
||||
updateNeededList("afterCoreRecipes");
|
||||
} catch (e) {
|
||||
console.error("loadAllRecipesHorizontal (core) error", e);
|
||||
strip.innerHTML = '<p class="placeholder">Erreur de chargement.</p>';
|
||||
updateNeededList();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// === Utilisateur "tiers" : uniquement SES recettes ===
|
||||
try {
|
||||
const res = await fetch(GET_ALL_RECIPES_URL, { credentials: "same-origin" });
|
||||
const res = await fetch(GET_MY_OWNED_URL, { credentials: "same-origin" });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const all = await res.json();
|
||||
window.allRecipes = all;
|
||||
const mine = await res.json();
|
||||
window.allRecipes = Array.isArray(mine) ? mine : [];
|
||||
|
||||
strip.innerHTML = "";
|
||||
if (!Array.isArray(all) || all.length === 0) { strip.innerHTML = '<p class="placeholder">Aucune recette disponible.</p>'; updateNeededList(); return; }
|
||||
|
||||
all.sort((a, b) => {
|
||||
const ua = String(a.userId ?? ""), ub = String(b.userId ?? "");
|
||||
if (ua !== ub) return ua.localeCompare(ub);
|
||||
return String(a.name ?? "").localeCompare(String(b.name ?? ""), "fr", { sensitivity: "base" });
|
||||
});
|
||||
|
||||
all.forEach(r => strip.appendChild(buildRecipeCardList(r)));
|
||||
updateNeededList("afterGetAllRecipes");
|
||||
console.debug("[needed] recipes loaded:", Array.isArray(window.allRecipes) ? window.allRecipes.length : 0);
|
||||
if (!window.allRecipes.length) {
|
||||
strip.innerHTML = '<p class="placeholder">Vous n’avez pas encore de recettes sélectionnées.</p>';
|
||||
updateNeededList();
|
||||
return;
|
||||
}
|
||||
|
||||
window.allRecipes.sort((a, b) => String(a.name ?? "").localeCompare(String(b.name ?? ""), "fr", { sensitivity: "base" }));
|
||||
window.allRecipes.forEach(r => strip.appendChild(buildRecipeCardList(r)));
|
||||
updateNeededList("afterGetMyRecipes");
|
||||
} catch (e) {
|
||||
console.error("loadAllRecipesHorizontal error", e);
|
||||
console.error("loadAllRecipesHorizontal (mine) error", e);
|
||||
strip.innerHTML = '<p class="placeholder">Erreur de chargement.</p>';
|
||||
updateNeededList();
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshOwnedSet() {
|
||||
try {
|
||||
const r = await fetch("/HelloFresh/GetOwnedIngredients");
|
||||
if (r.ok) {
|
||||
const names = await r.json();
|
||||
OWNED_SET = new Set((names || []).map(normalizeName));
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
async function buildAggregatedIngredientsFromRows(rows) {
|
||||
const ids = (rows || []).map(r => r.id);
|
||||
const details = await fetchDetailsBatched(ids);
|
||||
const byId = new Map(details.map(d => [String(d.id), d]));
|
||||
await refreshOwnedSet();
|
||||
|
||||
const rawItems = [];
|
||||
for (const r of (rows || [])) {
|
||||
const det = byId.get(String(r.id));
|
||||
if (!det || !det.ingredientsJson) continue;
|
||||
|
||||
let portions = Number(r.portions || 1);
|
||||
if (!Number.isFinite(portions) || portions < 1) portions = 1;
|
||||
|
||||
const base = Math.min(4, Math.max(1, portions));
|
||||
const scale = (portions <= 4) ? 1 : (portions / 4);
|
||||
|
||||
let obj; try { obj = JSON.parse(det.ingredientsJson); } catch { obj = null; }
|
||||
const arr = (obj && Array.isArray(obj[String(base)])) ? obj[String(base)] : [];
|
||||
for (const el of arr) {
|
||||
const it = normalizeItem(el);
|
||||
if (!it.Name) continue;
|
||||
|
||||
const { value, unit } = parseQuantity(it.Quantity);
|
||||
if (!Number.isNaN(value) && scale !== 1) {
|
||||
const scaled = value * scale;
|
||||
it.Quantity = unit ? `${fmtNumber(scaled)} ${unit}` : fmtNumber(scaled);
|
||||
}
|
||||
it.Owned = isOwned(it.Name);
|
||||
rawItems.push(it);
|
||||
}
|
||||
}
|
||||
return aggregateRawItems(rawItems);
|
||||
}
|
||||
|
||||
|
||||
function fmtNumber(n) {
|
||||
const i = Math.round(n);
|
||||
return (Math.abs(n - i) < 1e-9) ? String(i) : String(Number(n.toFixed(2))).replace(/\.?0+$/, '');
|
||||
}
|
||||
|
||||
function aggregateRawItems(rawItems) {
|
||||
// Somme numérique par (name, unit) + “meilleure” quantité textuelle sinon
|
||||
const numMap = new Map(); // key: name##unit -> {sum, unit, item}
|
||||
const textMap = new Map(); // key: name -> {set:Set(qty), item}
|
||||
|
||||
for (const raw of rawItems) {
|
||||
const it = normalizeItem(raw);
|
||||
if (!it.Name) continue;
|
||||
const keyName = normalizeName(it.Name);
|
||||
const q = it.Quantity || "";
|
||||
const { value, unit } = parseQuantity(q);
|
||||
|
||||
if (!Number.isNaN(value)) {
|
||||
const k = `${keyName}##${unit || ""}`;
|
||||
const prev = numMap.get(k);
|
||||
if (!prev) {
|
||||
numMap.set(k, { sum: value, unit: unit || "", item: { ...it } });
|
||||
} else {
|
||||
prev.sum += value;
|
||||
// garder une image si absente
|
||||
if (!prev.item.Image && it.Image) prev.item.Image = it.Image;
|
||||
prev.item.Owned = prev.item.Owned || it.Owned;
|
||||
}
|
||||
} else {
|
||||
const p = textMap.get(keyName);
|
||||
if (!p) {
|
||||
const s = new Set(); if (q) s.add(q);
|
||||
textMap.set(keyName, { set: s, item: { ...it } });
|
||||
} else {
|
||||
if (q) p.set.add(q);
|
||||
if (!p.item.Image && it.Image) p.item.Image = it.Image;
|
||||
p.item.Owned = p.item.Owned || it.Owned;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const out = [];
|
||||
|
||||
// numérique → format “val unit”
|
||||
for (const { sum, unit, item } of numMap.values()) {
|
||||
const qty = unit ? `${fmtNumber(sum)} ${unit}` : fmtNumber(sum);
|
||||
out.push({ ...item, Quantity: qty });
|
||||
}
|
||||
|
||||
// textuel → pickBestQuantity
|
||||
for (const { set, item } of textMap.values()) {
|
||||
const qty = pickBestQuantity(Array.from(set));
|
||||
out.push({ ...item, Quantity: qty });
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
async function fetchDetailsBatched(ids, batchSize = 120) {
|
||||
const all = [];
|
||||
for (let i = 0; i < ids.length; i += batchSize) {
|
||||
const part = ids.slice(i, i + batchSize);
|
||||
const res = await fetch(`/HelloFresh/GetRecipesDetails?ids=${encodeURIComponent(part.join(','))}`);
|
||||
if (res.ok) all.push(...await res.json());
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
async function buildMyAggregatedIngredients() {
|
||||
// 1) mes recettes + portions
|
||||
const r = await fetch(GET_MY_OWNED_URL, { credentials: "same-origin" });
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
const mine = await r.json(); // [{id, portions, ...}]
|
||||
|
||||
if (!Array.isArray(mine) || mine.length === 0) return [];
|
||||
|
||||
// 2) détails (ingrédients par 1..4)
|
||||
const ids = mine.map(x => x.id);
|
||||
const details = await fetchDetailsBatched(ids);
|
||||
const byId = new Map(details.map(d => [String(d.id), d]));
|
||||
|
||||
// 3) Owned flags (facultatif, pour tri visuel)
|
||||
await refreshOwnedSet();
|
||||
|
||||
// 4) construire la liste brute + mise à l’échelle des quantités (>4 portions)
|
||||
const rawItems = [];
|
||||
for (const rcp of mine) {
|
||||
const id = String(rcp.id);
|
||||
const det = byId.get(id);
|
||||
if (!det || !det.ingredientsJson) continue;
|
||||
|
||||
let portions = Number(rcp.portions || 1);
|
||||
if (!Number.isFinite(portions) || portions < 1) portions = 1;
|
||||
|
||||
const base = Math.min(4, Math.max(1, portions));
|
||||
const scale = (portions <= 4) ? 1 : (portions / 4);
|
||||
|
||||
let obj;
|
||||
try { obj = JSON.parse(det.ingredientsJson); } catch { obj = null; }
|
||||
const arr = (obj && Array.isArray(obj[String(base)])) ? obj[String(base)] : [];
|
||||
for (const el of arr) {
|
||||
const it = normalizeItem(el);
|
||||
if (!it.Name) continue;
|
||||
|
||||
// scale quantité si numérique
|
||||
const { value, unit } = parseQuantity(it.Quantity);
|
||||
if (!Number.isNaN(value) && scale !== 1) {
|
||||
const scaled = value * scale;
|
||||
it.Quantity = unit ? `${fmtNumber(scaled)} ${unit}` : fmtNumber(scaled);
|
||||
}
|
||||
|
||||
it.Owned = isOwned(it.Name);
|
||||
rawItems.push(it);
|
||||
}
|
||||
}
|
||||
|
||||
return aggregateRawItems(rawItems);
|
||||
}
|
||||
|
||||
|
||||
// ======================
|
||||
// Init
|
||||
// ======================
|
||||
|
||||
Reference in New Issue
Block a user