Correction PDF
This commit is contained in:
@@ -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