diff --git a/Controllers/HelloFresh/HelloFreshController.cs b/Controllers/HelloFresh/HelloFreshController.cs index b129adf..8165858 100644 --- a/Controllers/HelloFresh/HelloFreshController.cs +++ b/Controllers/HelloFresh/HelloFreshController.cs @@ -84,6 +84,7 @@ public class HelloFreshController : AppController } } + // ❌ Supprime la limite de 3 portions [HttpPost("SaveRecipe")] public IActionResult SaveRecipe([FromBody] string idSavingRecette) { @@ -94,7 +95,6 @@ public class HelloFreshController : AppController if (string.IsNullOrWhiteSpace(idSavingRecette)) return BadRequest("Id invalide."); var current = _context.SavingRecettes.Count(s => s.UserId == userId && s.IdSavingRecette == idSavingRecette); - if (current >= 3) return Ok(new { message = "cap", qty = 3 }); _context.SavingRecettes.Add(new SavingRecette { IdSavingRecette = idSavingRecette, UserId = userId.Value }); _context.SaveChanges(); @@ -115,12 +115,10 @@ public class HelloFreshController : AppController var sessionUserId = HttpContext.Session.GetInt32("UserId"); if (sessionUserId == null) return Unauthorized("Utilisateur non connecté."); - // petite fonction pour normaliser les clés de regroupement par nom static string Key(string? s) => (s ?? "").Trim().ToLowerInvariant(); if (isUserImportant) { - // Mes recettes : on inner-join sur Recettes, on matérialise puis on regroupe par NOM var rows = ( from s in _context.SavingRecettes.AsNoTracking() where s.UserId == sessionUserId @@ -141,7 +139,6 @@ public class HelloFreshController : AppController .GroupBy(x => Key(x.Name)) .Select(g => { - // on choisit un représentant qui a un PDF si possible var withPdf = g.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.Pdf)); var pick = withPdf ?? g.OrderByDescending(x => x.Id).First(); @@ -163,7 +160,6 @@ public class HelloFreshController : AppController } else { - // Tous les utilisateurs : on joint aussi sur Users, puis on regroupe par NOM var rows = ( from s in _context.SavingRecettes.AsNoTracking() join r in _context.Recettes.AsNoTracking() on s.IdSavingRecette equals r.Id @@ -212,7 +208,6 @@ public class HelloFreshController : AppController } } - [HttpPost("DeleteRecipesOwned")] public IActionResult DeleteRecipesOwned([FromBody] string idSavingRecette) { @@ -287,7 +282,7 @@ public class HelloFreshController : AppController client.DefaultRequestHeaders.Accept.ParseAdd("application/pdf"); client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("fr-FR,fr;q=0.9,en;q=0.8"); - using var resp = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead); + using var resp = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, HttpContext.RequestAborted); if (!resp.IsSuccessStatusCode) { var preview = await resp.Content.ReadAsStringAsync(); @@ -301,17 +296,16 @@ public class HelloFreshController : AppController || uri.AbsolutePath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase); if (!isPdf) return BadRequest($"Le document récupéré n'est pas un PDF. Content-Type: {mediaType}"); + await using var upstream = await resp.Content.ReadAsStreamAsync(HttpContext.RequestAborted); var ms = new MemoryStream(); - await resp.Content.CopyToAsync(ms); + await upstream.CopyToAsync(ms, HttpContext.RequestAborted); ms.Position = 0; - if (resp.Content.Headers.ContentLength is long len) - Response.ContentLength = len; - Response.Headers["Cache-Control"] = "public, max-age=3600"; Response.Headers["Content-Disposition"] = "inline; filename=\"recette.pdf\""; - return File(ms, "application/pdf"); + // ✅ Range activé → scroll multi-pages iOS/iframe/pdf.js OK + return new FileStreamResult(ms, "application/pdf") { EnableRangeProcessing = true }; } catch (TaskCanceledException) { @@ -417,17 +411,21 @@ public class HelloFreshController : AppController .Select(r => new { r.Id, r.Ingredients }) .ToList(); - var portionById = perRecipe.ToDictionary(x => x.RecetteId, x => Math.Min(4, Math.Max(1, x.Portions))); + // ✅ on ne borne plus à 4 pour le calcul; si >4, on prend la table "4" et on scale + var portionsById = perRecipe.ToDictionary(x => x.RecetteId, x => Math.Max(1, x.Portions)); var rawItems = new List(); foreach (var r in recettes) { - if (!portionById.TryGetValue(r.Id, out var portions)) portions = 1; + if (!portionsById.TryGetValue(r.Id, out var portions)) portions = 1; + + int basePortions = Math.Min(4, Math.Max(1, portions)); + double scale = portions <= 4 ? 1.0 : (double)portions / 4.0; try { using var doc = JsonDocument.Parse(r.Ingredients ?? "{}"); - if (!doc.RootElement.TryGetProperty(portions.ToString(), out var arr) || arr.ValueKind != JsonValueKind.Array) + if (!doc.RootElement.TryGetProperty(basePortions.ToString(), out var arr) || arr.ValueKind != JsonValueKind.Array) continue; foreach (var el in arr.EnumerateArray()) @@ -435,9 +433,22 @@ public class HelloFreshController : AppController var name = el.TryGetProperty("Name", out var n) ? n.GetString() ?? "" : ""; var qty = el.TryGetProperty("Quantity", out var q) ? q.GetString() : null; var img = el.TryGetProperty("Image", out var im) ? im.GetString() : null; - if (string.IsNullOrWhiteSpace(name)) continue; + // ✅ scale si >4 et quantité numérique + if (scale != 1.0 && !string.IsNullOrWhiteSpace(qty)) + { + var parsed = ParseQuantity(qty); + if (parsed.Value is double v) + { + var scaled = v * scale; + var qtyStr = ((scaled % 1.0) == 0.0) + ? scaled.ToString("0", System.Globalization.CultureInfo.InvariantCulture) + : scaled.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture); + qty = string.IsNullOrWhiteSpace(parsed.Unit) ? qtyStr : $"{qtyStr} {parsed.Unit}"; + } + } + rawItems.Add(new IngredientItemDto { Name = name.Trim(), @@ -483,7 +494,6 @@ public class HelloFreshController : AppController var userId = HttpContext.Session.GetInt32("UserId"); if (userId == null) return Unauthorized("Utilisateur non connecté."); - // 1) Reconstituer la liste agrégée comme dans AggregatedIngredients var ownedNames = _context.Ingredients.Select(i => i.NameOwnedIngredients) .ToList() .Select(s => (s ?? "").Trim().ToLowerInvariant()) @@ -503,16 +513,20 @@ public class HelloFreshController : AppController .Select(r => new { r.Id, r.Ingredients }) .ToList(); - var portionById = perRecipe.ToDictionary(x => x.RecetteId, x => Math.Min(4, Math.Max(1, x.Portions))); + var portionsById = perRecipe.ToDictionary(x => x.RecetteId, x => Math.Max(1, x.Portions)); var rawItems = new List(); foreach (var r in recettes) { - if (!portionById.TryGetValue(r.Id, out var portions)) portions = 1; + if (!portionsById.TryGetValue(r.Id, out var portions)) portions = 1; + + int basePortions = Math.Min(4, Math.Max(1, portions)); + double scale = portions <= 4 ? 1.0 : (double)portions / 4.0; + try { using var doc = JsonDocument.Parse(r.Ingredients ?? "{}"); - if (!doc.RootElement.TryGetProperty(portions.ToString(), out var arr) || arr.ValueKind != JsonValueKind.Array) + if (!doc.RootElement.TryGetProperty(basePortions.ToString(), out var arr) || arr.ValueKind != JsonValueKind.Array) continue; foreach (var el in arr.EnumerateArray()) @@ -520,8 +534,21 @@ public class HelloFreshController : AppController var name = el.TryGetProperty("Name", out var n) ? n.GetString() ?? "" : ""; var qty = el.TryGetProperty("Quantity", out var q) ? q.GetString() : null; var img = el.TryGetProperty("Image", out var im) ? im.GetString() : null; - if (string.IsNullOrWhiteSpace(name)) continue; + + if (scale != 1.0 && !string.IsNullOrWhiteSpace(qty)) + { + var parsed = ParseQuantity(qty); + if (parsed.Value is double v) + { + var scaled = v * scale; + var qtyStr = ((scaled % 1.0) == 0.0) + ? scaled.ToString("0", System.Globalization.CultureInfo.InvariantCulture) + : scaled.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture); + qty = string.IsNullOrWhiteSpace(parsed.Unit) ? qtyStr : $"{qtyStr} {parsed.Unit}"; + } + } + rawItems.Add(new IngredientItemDto { Name = name.Trim(), @@ -537,9 +564,8 @@ public class HelloFreshController : AppController var aggregated = AggregateIngredients(rawItems, ownedNames); var allowedNames = aggregated.Select(a => a.Name).Distinct().ToList(); - // 2) Appel interne à notre IA selector /api/ai/select (même host) var client = httpClientFactory.CreateClient(); - client.Timeout = TimeSpan.FromSeconds(60); // on laisse confortable + client.Timeout = TimeSpan.FromSeconds(60); var baseUrl = $"{Request.Scheme}://{Request.Host}"; var reqBody = new @@ -559,7 +585,6 @@ public class HelloFreshController : AppController var raw = await resp.Content.ReadAsStringAsync(); if (!resp.IsSuccessStatusCode) { - // fallback live (contient) var nq = (body?.Query ?? "").Trim().ToLowerInvariant(); var fallback = string.IsNullOrEmpty(nq) ? aggregated @@ -577,7 +602,6 @@ public class HelloFreshController : AppController ? nEl.EnumerateArray().Select(x => x.GetString() ?? "").Where(s => !string.IsNullOrWhiteSpace(s)).ToList() : new List(); - // 3) Filtrer/ordonner selon la réponse IA var order = names.Select((n, i) => new { n = n.Trim().ToLowerInvariant(), i }) .ToDictionary(x => x.n, x => x.i); var selected = aggregated @@ -585,7 +609,6 @@ public class HelloFreshController : AppController .OrderBy(a => order[(a.Name ?? "").Trim().ToLowerInvariant()]) .ToList(); - // Fallback si l’IA ne renvoie rien if (selected.Count == 0) { var nq = (body?.Query ?? "").Trim().ToLowerInvariant(); @@ -625,7 +648,7 @@ public class HelloFreshController : AppController image = r.Image, difficulte = r.Difficulte, ingredientsJson = r.Ingredients, - pdf = r.Pdf // 🟢 important pour le fallback + pdf = r.Pdf }) .ToList(); @@ -676,10 +699,10 @@ public class HelloFreshController : AppController return StatusCode(500, new { message = "Erreur serveur", error = ex.Message }); } } + [HttpGet("GetAllRecipes")] public IActionResult GetAllRecipes() { - // 1) Comptages par recette et utilisateur var counts = _context.SavingRecettes .AsNoTracking() .GroupBy(s => new { s.IdSavingRecette, s.UserId }) @@ -691,8 +714,6 @@ public class HelloFreshController : AppController }) .ToList(); - // 2) Recettes ✅ AJOUTE le champ JSON "IngredientsToHad" - // ⚠️ adapte "r.IngredientsToHad" si ton modèle a un autre nom (ex: IngredientsToHaveAtHome / Pantry / etc.) var recettes = _context.Recettes .AsNoTracking() .Select(r => new @@ -702,11 +723,10 @@ public class HelloFreshController : AppController r.Image, r.TempsDePreparation, r.Tags, - IngredientsToHad = r.IngredientsToHad // <— ICI + IngredientsToHad = r.IngredientsToHad }) .ToList(); - // 3) Users var neededUserIds = counts.Select(x => x.UserId).Distinct().ToList(); var users = _layout.Users @@ -715,7 +735,6 @@ public class HelloFreshController : AppController .Select(u => new { u.Id, u.Username }) .ToList(); - // 4) Join en mémoire ✅ propage "IngredientsToHad" var baseRows = from c in counts join r in recettes on c.RecetteId equals r.Id @@ -729,11 +748,10 @@ public class HelloFreshController : AppController r.TempsDePreparation, c.Portions, r.Tags, - r.IngredientsToHad, // <— ICI + r.IngredientsToHad, User = u?.Username }; - // 5) Regroupement final par RecetteId var result = baseRows .GroupBy(x => x.RecetteId) .Select(g => new @@ -748,8 +766,6 @@ public class HelloFreshController : AppController .Select(x => x.User) .Distinct() .ToList(), - - // ✅ expose le JSON côté API en camelCase pour le front ingredientsToHad = g.First().IngredientsToHad }) .ToList(); @@ -757,9 +773,6 @@ public class HelloFreshController : AppController return Ok(result); } - - - [HttpGet("/HelloFresh/GetAllRecipesWithUsers")] public IActionResult GetAllRecipesWithUsers() { @@ -787,8 +800,6 @@ public class HelloFreshController : AppController return Ok(data); } - - [HttpGet("GetHistory")] public IActionResult GetHistory([FromQuery] bool isUserImportant = true, [FromQuery] string? search = null) { @@ -798,7 +809,6 @@ public class HelloFreshController : AppController if (isUserImportant && sessionUserId == null) return Unauthorized("Utilisateur non connecté."); - // 1) Historique (matérialisé) var histQ = _context.HistoriqueRecettes.AsNoTracking(); if (isUserImportant) histQ = histQ.Where(h => h.UserId == sessionUserId); @@ -806,7 +816,6 @@ public class HelloFreshController : AppController .Select(h => new { h.DateHistorique, h.RecetteId, h.UserId }) .ToList(); - // 2) Dictionnaire Recettes avec clé normalisée (trim+lower) pour éviter tout souci de casse/espaces string K(string? s) => (s ?? "").Trim().ToLowerInvariant(); var recDict = _context.Recettes.AsNoTracking() .Select(r => new { r.Id, r.Name, r.Image, r.TempsDePreparation }) @@ -814,7 +823,6 @@ public class HelloFreshController : AppController .GroupBy(r => K(r.Id)) .ToDictionary(g => g.Key, g => g.First()); - // 3) “Join” en mémoire (pas de ?. dans l’expression EF) var joined = hist.Select(h => { recDict.TryGetValue(K(h.RecetteId), out var r); @@ -829,14 +837,12 @@ public class HelloFreshController : AppController }; }).ToList(); - // 4) Filtre recherche serveur (optionnel) if (!string.IsNullOrWhiteSpace(search)) { var s = search.Trim().ToLowerInvariant(); joined = joined.Where(x => (x.Name ?? "").ToLowerInvariant().Contains(s)).ToList(); } - // 5) Noms d’utilisateurs si “tous” var userNames = new Dictionary(); if (!isUserImportant) { @@ -849,7 +855,6 @@ public class HelloFreshController : AppController .ToDictionary(g => g.Key, g => g.First().Username ?? $"User#{g.Key}"); } - // 6) Regroupement par Date + Recette var rows = joined .GroupBy(x => new { x.Date, x.RecetteId }) .Select(g => new @@ -868,7 +873,6 @@ public class HelloFreshController : AppController }) .ToList(); - // 7) Mise en forme par date var culture = new System.Globalization.CultureInfo("fr-FR"); var result = rows .GroupBy(x => x.Date) @@ -889,8 +893,6 @@ public class HelloFreshController : AppController } } - - [HttpPost("ArchiveAll")] public IActionResult ArchiveAll() { @@ -899,7 +901,6 @@ public class HelloFreshController : AppController try { - // 1) Récup selections de l'utilisateur var selections = _context.SavingRecettes .Where(s => s.UserId == userId) .AsNoTracking() @@ -908,16 +909,14 @@ public class HelloFreshController : AppController if (selections.Count == 0) return Ok(new { message = "empty", archivedPortions = 0, archivedRecipes = 0, skippedIds = Array.Empty() }); - // 2) Groupes par recette (id string) = nb de portions var groups = selections .GroupBy(s => s.IdSavingRecette) .Select(g => new { RecetteId = g.Key, Portions = g.Count() }) .ToList(); - // 3) Filtrer sur ids valides (évite FK/erreurs) var validIds = _context.Recettes .AsNoTracking() - .Select(r => r.Id) // string + .Select(r => r.Id) .ToHashSet(StringComparer.Ordinal); var validGroups = groups.Where(g => validIds.Contains(g.RecetteId)).ToList(); @@ -926,7 +925,6 @@ public class HelloFreshController : AppController if (validGroups.Count == 0) return Ok(new { message = "nothing_valid", archivedPortions = 0, archivedRecipes = 0, skippedIds }); - // 4) Date Paris (DateOnly) DateOnly todayParis; try { @@ -937,7 +935,6 @@ public class HelloFreshController : AppController using var tx = _context.Database.BeginTransaction(); - // 5) Préparer les insertions (1 ligne par portion) var toInsert = new List(capacity: validGroups.Sum(x => x.Portions)); foreach (var g in validGroups) { @@ -945,14 +942,13 @@ public class HelloFreshController : AppController { toInsert.Add(new HistoriqueRecette { - RecetteId = g.RecetteId, // string + RecetteId = g.RecetteId, UserId = userId.Value, DateHistorique = todayParis }); } } - // 6) Insert + Delete (seulement les rows correspondants aux ids valides) if (toInsert.Count > 0) _context.HistoriqueRecettes.AddRange(toInsert); @@ -962,11 +958,9 @@ public class HelloFreshController : AppController _context.SavingRecettes.RemoveRange(rowsToDelete); - var archivedPortions = _context.SaveChanges(); // compte total opérations (insert + delete) + _context.SaveChanges(); tx.Commit(); - // NB: archivedPortions = insertions + suppressions (EF renvoie le nb total de rows affectées) - // on renvoie plutôt nos compteurs fiables : var archivedRecipes = validGroups.Count; var archivedPieces = validGroups.Sum(g => g.Portions); @@ -980,7 +974,6 @@ public class HelloFreshController : AppController } catch (DbUpdateException dbex) { - // remonte l’essentiel pour debug front/logs return StatusCode(500, new { message = "archive_failed", @@ -1001,17 +994,15 @@ public class HelloFreshController : AppController // POST /HelloFresh/HasHistory [HttpPost("HasHistory")] - [IgnoreAntiforgeryToken] // garde si tu postes depuis du JS sans token + [IgnoreAntiforgeryToken] public async Task HasHistory([FromBody] string recipeId) { - // même logique que le reste de ton contrôleur : user via Session var sessionUserId = HttpContext.Session.GetInt32("UserId"); if (sessionUserId == null) return Unauthorized("Utilisateur non connecté."); if (string.IsNullOrWhiteSpace(recipeId)) return Ok(new { exists = false }); - // ⚠️ compare sur la bonne colonne (RecetteId) et type string bool exists = await _context.HistoriqueRecettes .AsNoTracking() .AnyAsync(h => h.RecetteId == recipeId && h.UserId == sessionUserId.Value); @@ -1064,6 +1055,7 @@ public class HelloFreshController : AppController var list = tags.OrderBy(s => s, StringComparer.Create(new System.Globalization.CultureInfo("fr-FR"), true)).ToList(); return Ok(list); } + // API: recettes choisies distinctes par nom, avec PDF [HttpGet("GetOwnedForReader")] public IActionResult GetOwnedForReader() @@ -1071,7 +1063,6 @@ public class HelloFreshController : AppController var userId = HttpContext.Session.GetInt32("UserId"); if (userId == null) return Unauthorized("Utilisateur non connecté."); - // 1) portions par recette de l'utilisateur var owned = _context.SavingRecettes .Where(s => s.UserId == userId) .GroupBy(s => s.IdSavingRecette) @@ -1080,7 +1071,6 @@ public class HelloFreshController : AppController if (owned.Count == 0) return Ok(Array.Empty()); - // 2) join recettes (on ramène le PDF) var joined = owned .Join(_context.Recettes, g => g.RecetteId, @@ -1096,7 +1086,6 @@ public class HelloFreshController : AppController }) .ToList(); - // 3) dédoublonnage par nom (insensible à la casse/espaces) — on garde celui avec + de portions puis nom ASC string Key(string s) => (s ?? "").Trim().ToLowerInvariant(); var distinct = joined .GroupBy(x => Key(x.name)) @@ -1106,4 +1095,4 @@ public class HelloFreshController : AppController return Ok(distinct); } -} \ No newline at end of file +} diff --git a/Views/Connections/3n2yu2pg.uts~ b/Views/Connections/3n2yu2pg.uts~ new file mode 100644 index 0000000..e69de29 diff --git a/Views/Connections/psr2sgj0.jxy~ b/Views/Connections/psr2sgj0.jxy~ new file mode 100644 index 0000000..e69de29 diff --git a/wwwroot/css/HelloFresh/cuisine.css b/wwwroot/css/HelloFresh/cuisine.css index 75e15c5..501d93a 100644 --- a/wwwroot/css/HelloFresh/cuisine.css +++ b/wwwroot/css/HelloFresh/cuisine.css @@ -82,7 +82,7 @@ body { } .cui-cards { - padding: 10px; + padding: 5px; overflow: auto; height: 100%; } @@ -185,19 +185,16 @@ body { } .cui-view { - position: relative; - flex: 1; - min-height: 0; - background: #0a0d13; + height: 100%; + overflow: auto; + -webkit-overflow-scrolling: touch; } -.cui-iframe { - position: absolute; - inset: 0; +.cui-iframe, .viewer-iframe { width: 100%; height: 100%; border: 0; - display: none; + display: block; } .cui-placeholder { @@ -295,6 +292,8 @@ body { /* quand la sidebar est réduite, même anneau interne */ .cui-root.collapsed .cui-card.active { box-shadow: 0 0 0 2px var(--accent) inset; + width: max-content; + grid-template-columns: 29px 1fr; } /* Titre de la barre du haut : ellipsis propre si le nom est long */ diff --git a/wwwroot/js/HelloFresh/cuisine.js b/wwwroot/js/HelloFresh/cuisine.js index 4b40065..70c72ec 100644 --- a/wwwroot/js/HelloFresh/cuisine.js +++ b/wwwroot/js/HelloFresh/cuisine.js @@ -33,6 +33,15 @@ 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(); + }); + + function buildPdfSrc(rawUrl) { if (!rawUrl) return ""; diff --git a/wwwroot/js/HelloFresh/index.js b/wwwroot/js/HelloFresh/index.js index 13bf94c..b3cfcf8 100644 --- a/wwwroot/js/HelloFresh/index.js +++ b/wwwroot/js/HelloFresh/index.js @@ -5,7 +5,6 @@ let currentPage = 1; let lastPageGlobal = 1; const countPerPage = 12; let currentSearchTerm = ''; -const MAX_PORTIONS = 14; let CURRENT_CLIENT_LIST = null; // liste filtrée paginée côté client let ALL_RECIPES_CACHE = null; // toutes les recettes (toutes pages) @@ -19,10 +18,19 @@ const labelMaps = { ingredients: {}, tags: {} }; // idRecette -> portions (0..3) const ownedMap = new Map(); + +const MAX_PER_RECIPE = null; // null = illimité par recette +const MAX_TOTAL = null; // null = illimité au total +const clampQty = (n) => { + const v = Number(n) || 0; + if (v < 0) return 0; + // si bornes définies, on clamp ; sinon on laisse passer + if (Number.isFinite(MAX_PER_RECIPE)) return Math.min(MAX_PER_RECIPE, v); + return v; +}; /*********************** * UTILS ***********************/ -const clampQty = n => Math.max(0, Math.min(3, Number(n) || 0)); const truncate = (s, m) => (s && s.length > m ? s.slice(0, m) + '…' : (s ?? '')); const normalize = (str) => (str ?? '').toString().normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim(); const splitTags = (str) => (str || '').split(/[,•]/).map(t => t.trim()).filter(Boolean).filter(t => t !== '•'); @@ -232,7 +240,12 @@ function buildRecipeCardList(recipe) { } const current = clampQty(ownedMap.get(String(recipe.id)) || 0); - if (current === 0 && !canAddPortion()) { alert(`Limite atteinte (${MAX_PORTIONS}).`); return; } + if (current === 0 && !canAddPortion(recipe.id)) { + // message seulement s'il y a vraiment une limite + if (Number.isFinite(MAX_TOTAL)) alert(`Limite atteinte (${MAX_TOTAL}).`); + else if (Number.isFinite(MAX_PER_RECIPE)) alert(`Limite par recette atteinte (${MAX_PER_RECIPE}).`); + return; + }; const selectedNow = current > 0; if (!selectedNow) await saveRecipe(recipe.id); else await clearRecipe(recipe.id); applySelectionUIById(recipe.id); @@ -273,8 +286,15 @@ function buildRecipeCardOwned(recipe) { // plus/moins existants card.querySelector('.btn-minus')?.addEventListener('click', (e) => { e.stopPropagation(); removeRecette(recipe.id); updateOwnedCountUI(); if (getTotalPortionsFromMap() === 0) closeOwnedIfOpen(); }); - card.querySelector('.btn-plus')?.addEventListener('click', (e) => { e.stopPropagation(); if (!canAddPortion()) { alert(`Limite atteinte (${MAX_PORTIONS}).`); return; } saveRecipe(recipe.id); updateOwnedCountUI(); }); - + card.querySelector('.btn-plus')?.addEventListener('click', (e) => { + e.stopPropagation(); + if (!canAddPortion(recipe.id)) { + if (Number.isFinite(MAX_TOTAL)) alert(`Limite atteinte (${MAX_TOTAL}).`); + else if (Number.isFinite(MAX_PER_RECIPE)) alert(`Limite par recette atteinte (${MAX_PER_RECIPE}).`); + return; + } + saveRecipe(recipe.id); updateOwnedCountUI(); + }); // 🟢 Ctrl+clic = ouvrir le PDF (avec fallback GetRecipesDetails) card.addEventListener('click', async (e) => { if (!e.ctrlKey) return; @@ -743,12 +763,28 @@ function wireFooterButtons() { ***********************/ function getTotalPortionsFromMap() { let total = 0; ownedMap.forEach(v => { total += (Number(v) || 0); }); return total; } function updateOwnedCountUI() { - const el = document.getElementById('ownedCount'); if (!el) return; - const total = getTotalPortionsFromMap(); el.textContent = `(${total}/${MAX_PORTIONS})`; - el.style.color = (total >= MAX_PORTIONS) ? '#c62828' : ''; + const el = document.getElementById('ownedCount'); if (!el) return; + const total = getTotalPortionsFromMap(); + if (Number.isFinite(MAX_TOTAL)) { + el.textContent = `(${total}/${MAX_TOTAL})`; + el.style.color = (total >= MAX_TOTAL) ? '#c62828' : ''; + } else { + // affichage sans borne + el.textContent = `(${total})`; + el.style.color = ''; + } +} +function canAddPortion(forRecipeId) { + // limite globale + if (Number.isFinite(MAX_TOTAL) && getTotalPortionsFromMap() >= MAX_TOTAL) return false; + // limite par recette + if (Number.isFinite(MAX_PER_RECIPE)) { + const curr = Number(ownedMap.get(String(forRecipeId)) || 0); + return curr < MAX_PER_RECIPE; + } + // illimité + return true; } -function canAddPortion() { return getTotalPortionsFromMap() < MAX_PORTIONS; } - /*********************** * Ingredients meta attach (for page) ***********************/