Correction PDF

This commit is contained in:
2025-09-16 22:20:21 +02:00
parent d9fca86145
commit 41bd8254e7
17 changed files with 591 additions and 110 deletions

View File

@@ -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 navez 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
// ======================