This commit is contained in:
2025-09-11 00:02:38 +02:00
parent e78ea9da35
commit 902c8c52a9
6 changed files with 122 additions and 89 deletions

View File

@@ -84,6 +84,7 @@ public class HelloFreshController : AppController
} }
} }
// ❌ Supprime la limite de 3 portions
[HttpPost("SaveRecipe")] [HttpPost("SaveRecipe")]
public IActionResult SaveRecipe([FromBody] string idSavingRecette) public IActionResult SaveRecipe([FromBody] string idSavingRecette)
{ {
@@ -94,7 +95,6 @@ public class HelloFreshController : AppController
if (string.IsNullOrWhiteSpace(idSavingRecette)) return BadRequest("Id invalide."); if (string.IsNullOrWhiteSpace(idSavingRecette)) return BadRequest("Id invalide.");
var current = _context.SavingRecettes.Count(s => s.UserId == userId && s.IdSavingRecette == idSavingRecette); 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.SavingRecettes.Add(new SavingRecette { IdSavingRecette = idSavingRecette, UserId = userId.Value });
_context.SaveChanges(); _context.SaveChanges();
@@ -115,12 +115,10 @@ public class HelloFreshController : AppController
var sessionUserId = HttpContext.Session.GetInt32("UserId"); var sessionUserId = HttpContext.Session.GetInt32("UserId");
if (sessionUserId == null) return Unauthorized("Utilisateur non connecté."); 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(); static string Key(string? s) => (s ?? "").Trim().ToLowerInvariant();
if (isUserImportant) if (isUserImportant)
{ {
// Mes recettes : on inner-join sur Recettes, on matérialise puis on regroupe par NOM
var rows = ( var rows = (
from s in _context.SavingRecettes.AsNoTracking() from s in _context.SavingRecettes.AsNoTracking()
where s.UserId == sessionUserId where s.UserId == sessionUserId
@@ -141,7 +139,6 @@ public class HelloFreshController : AppController
.GroupBy(x => Key(x.Name)) .GroupBy(x => Key(x.Name))
.Select(g => .Select(g =>
{ {
// on choisit un représentant qui a un PDF si possible
var withPdf = g.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.Pdf)); var withPdf = g.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.Pdf));
var pick = withPdf ?? g.OrderByDescending(x => x.Id).First(); var pick = withPdf ?? g.OrderByDescending(x => x.Id).First();
@@ -163,7 +160,6 @@ public class HelloFreshController : AppController
} }
else else
{ {
// Tous les utilisateurs : on joint aussi sur Users, puis on regroupe par NOM
var rows = ( var rows = (
from s in _context.SavingRecettes.AsNoTracking() from s in _context.SavingRecettes.AsNoTracking()
join r in _context.Recettes.AsNoTracking() on s.IdSavingRecette equals r.Id join r in _context.Recettes.AsNoTracking() on s.IdSavingRecette equals r.Id
@@ -212,7 +208,6 @@ public class HelloFreshController : AppController
} }
} }
[HttpPost("DeleteRecipesOwned")] [HttpPost("DeleteRecipesOwned")]
public IActionResult DeleteRecipesOwned([FromBody] string idSavingRecette) public IActionResult DeleteRecipesOwned([FromBody] string idSavingRecette)
{ {
@@ -287,7 +282,7 @@ public class HelloFreshController : AppController
client.DefaultRequestHeaders.Accept.ParseAdd("application/pdf"); client.DefaultRequestHeaders.Accept.ParseAdd("application/pdf");
client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("fr-FR,fr;q=0.9,en;q=0.8"); 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) if (!resp.IsSuccessStatusCode)
{ {
var preview = await resp.Content.ReadAsStringAsync(); var preview = await resp.Content.ReadAsStringAsync();
@@ -301,17 +296,16 @@ public class HelloFreshController : AppController
|| uri.AbsolutePath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase); || uri.AbsolutePath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase);
if (!isPdf) return BadRequest($"Le document récupéré n'est pas un PDF. Content-Type: {mediaType}"); 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(); var ms = new MemoryStream();
await resp.Content.CopyToAsync(ms); await upstream.CopyToAsync(ms, HttpContext.RequestAborted);
ms.Position = 0; ms.Position = 0;
if (resp.Content.Headers.ContentLength is long len)
Response.ContentLength = len;
Response.Headers["Cache-Control"] = "public, max-age=3600"; Response.Headers["Cache-Control"] = "public, max-age=3600";
Response.Headers["Content-Disposition"] = "inline; filename=\"recette.pdf\""; 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) catch (TaskCanceledException)
{ {
@@ -417,17 +411,21 @@ public class HelloFreshController : AppController
.Select(r => new { r.Id, r.Ingredients }) .Select(r => new { r.Id, r.Ingredients })
.ToList(); .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<IngredientItemDto>(); var rawItems = new List<IngredientItemDto>();
foreach (var r in recettes) 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 try
{ {
using var doc = JsonDocument.Parse(r.Ingredients ?? "{}"); 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; continue;
foreach (var el in arr.EnumerateArray()) 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 name = el.TryGetProperty("Name", out var n) ? n.GetString() ?? "" : "";
var qty = el.TryGetProperty("Quantity", out var q) ? q.GetString() : null; var qty = el.TryGetProperty("Quantity", out var q) ? q.GetString() : null;
var img = el.TryGetProperty("Image", out var im) ? im.GetString() : null; var img = el.TryGetProperty("Image", out var im) ? im.GetString() : null;
if (string.IsNullOrWhiteSpace(name)) continue; 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 rawItems.Add(new IngredientItemDto
{ {
Name = name.Trim(), Name = name.Trim(),
@@ -483,7 +494,6 @@ public class HelloFreshController : AppController
var userId = HttpContext.Session.GetInt32("UserId"); var userId = HttpContext.Session.GetInt32("UserId");
if (userId == null) return Unauthorized("Utilisateur non connecté."); 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) var ownedNames = _context.Ingredients.Select(i => i.NameOwnedIngredients)
.ToList() .ToList()
.Select(s => (s ?? "").Trim().ToLowerInvariant()) .Select(s => (s ?? "").Trim().ToLowerInvariant())
@@ -503,16 +513,20 @@ public class HelloFreshController : AppController
.Select(r => new { r.Id, r.Ingredients }) .Select(r => new { r.Id, r.Ingredients })
.ToList(); .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<IngredientItemDto>(); var rawItems = new List<IngredientItemDto>();
foreach (var r in recettes) 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 try
{ {
using var doc = JsonDocument.Parse(r.Ingredients ?? "{}"); 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; continue;
foreach (var el in arr.EnumerateArray()) 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 name = el.TryGetProperty("Name", out var n) ? n.GetString() ?? "" : "";
var qty = el.TryGetProperty("Quantity", out var q) ? q.GetString() : null; var qty = el.TryGetProperty("Quantity", out var q) ? q.GetString() : null;
var img = el.TryGetProperty("Image", out var im) ? im.GetString() : null; var img = el.TryGetProperty("Image", out var im) ? im.GetString() : null;
if (string.IsNullOrWhiteSpace(name)) continue; 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 rawItems.Add(new IngredientItemDto
{ {
Name = name.Trim(), Name = name.Trim(),
@@ -537,9 +564,8 @@ public class HelloFreshController : AppController
var aggregated = AggregateIngredients(rawItems, ownedNames); var aggregated = AggregateIngredients(rawItems, ownedNames);
var allowedNames = aggregated.Select(a => a.Name).Distinct().ToList(); 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(); var client = httpClientFactory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(60); // on laisse confortable client.Timeout = TimeSpan.FromSeconds(60);
var baseUrl = $"{Request.Scheme}://{Request.Host}"; var baseUrl = $"{Request.Scheme}://{Request.Host}";
var reqBody = new var reqBody = new
@@ -559,7 +585,6 @@ public class HelloFreshController : AppController
var raw = await resp.Content.ReadAsStringAsync(); var raw = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode) if (!resp.IsSuccessStatusCode)
{ {
// fallback live (contient)
var nq = (body?.Query ?? "").Trim().ToLowerInvariant(); var nq = (body?.Query ?? "").Trim().ToLowerInvariant();
var fallback = string.IsNullOrEmpty(nq) var fallback = string.IsNullOrEmpty(nq)
? aggregated ? aggregated
@@ -577,7 +602,6 @@ public class HelloFreshController : AppController
? nEl.EnumerateArray().Select(x => x.GetString() ?? "").Where(s => !string.IsNullOrWhiteSpace(s)).ToList() ? nEl.EnumerateArray().Select(x => x.GetString() ?? "").Where(s => !string.IsNullOrWhiteSpace(s)).ToList()
: new List<string>(); : new List<string>();
// 3) Filtrer/ordonner selon la réponse IA
var order = names.Select((n, i) => new { n = n.Trim().ToLowerInvariant(), i }) var order = names.Select((n, i) => new { n = n.Trim().ToLowerInvariant(), i })
.ToDictionary(x => x.n, x => x.i); .ToDictionary(x => x.n, x => x.i);
var selected = aggregated var selected = aggregated
@@ -585,7 +609,6 @@ public class HelloFreshController : AppController
.OrderBy(a => order[(a.Name ?? "").Trim().ToLowerInvariant()]) .OrderBy(a => order[(a.Name ?? "").Trim().ToLowerInvariant()])
.ToList(); .ToList();
// Fallback si lIA ne renvoie rien
if (selected.Count == 0) if (selected.Count == 0)
{ {
var nq = (body?.Query ?? "").Trim().ToLowerInvariant(); var nq = (body?.Query ?? "").Trim().ToLowerInvariant();
@@ -625,7 +648,7 @@ public class HelloFreshController : AppController
image = r.Image, image = r.Image,
difficulte = r.Difficulte, difficulte = r.Difficulte,
ingredientsJson = r.Ingredients, ingredientsJson = r.Ingredients,
pdf = r.Pdf // 🟢 important pour le fallback pdf = r.Pdf
}) })
.ToList(); .ToList();
@@ -676,10 +699,10 @@ public class HelloFreshController : AppController
return StatusCode(500, new { message = "Erreur serveur", error = ex.Message }); return StatusCode(500, new { message = "Erreur serveur", error = ex.Message });
} }
} }
[HttpGet("GetAllRecipes")] [HttpGet("GetAllRecipes")]
public IActionResult GetAllRecipes() public IActionResult GetAllRecipes()
{ {
// 1) Comptages par recette et utilisateur
var counts = _context.SavingRecettes var counts = _context.SavingRecettes
.AsNoTracking() .AsNoTracking()
.GroupBy(s => new { s.IdSavingRecette, s.UserId }) .GroupBy(s => new { s.IdSavingRecette, s.UserId })
@@ -691,8 +714,6 @@ public class HelloFreshController : AppController
}) })
.ToList(); .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 var recettes = _context.Recettes
.AsNoTracking() .AsNoTracking()
.Select(r => new .Select(r => new
@@ -702,11 +723,10 @@ public class HelloFreshController : AppController
r.Image, r.Image,
r.TempsDePreparation, r.TempsDePreparation,
r.Tags, r.Tags,
IngredientsToHad = r.IngredientsToHad // <— ICI IngredientsToHad = r.IngredientsToHad
}) })
.ToList(); .ToList();
// 3) Users
var neededUserIds = counts.Select(x => x.UserId).Distinct().ToList(); var neededUserIds = counts.Select(x => x.UserId).Distinct().ToList();
var users = _layout.Users var users = _layout.Users
@@ -715,7 +735,6 @@ public class HelloFreshController : AppController
.Select(u => new { u.Id, u.Username }) .Select(u => new { u.Id, u.Username })
.ToList(); .ToList();
// 4) Join en mémoire ✅ propage "IngredientsToHad"
var baseRows = var baseRows =
from c in counts from c in counts
join r in recettes on c.RecetteId equals r.Id join r in recettes on c.RecetteId equals r.Id
@@ -729,11 +748,10 @@ public class HelloFreshController : AppController
r.TempsDePreparation, r.TempsDePreparation,
c.Portions, c.Portions,
r.Tags, r.Tags,
r.IngredientsToHad, // <— ICI r.IngredientsToHad,
User = u?.Username User = u?.Username
}; };
// 5) Regroupement final par RecetteId
var result = baseRows var result = baseRows
.GroupBy(x => x.RecetteId) .GroupBy(x => x.RecetteId)
.Select(g => new .Select(g => new
@@ -748,8 +766,6 @@ public class HelloFreshController : AppController
.Select(x => x.User) .Select(x => x.User)
.Distinct() .Distinct()
.ToList(), .ToList(),
// ✅ expose le JSON côté API en camelCase pour le front
ingredientsToHad = g.First().IngredientsToHad ingredientsToHad = g.First().IngredientsToHad
}) })
.ToList(); .ToList();
@@ -757,9 +773,6 @@ public class HelloFreshController : AppController
return Ok(result); return Ok(result);
} }
[HttpGet("/HelloFresh/GetAllRecipesWithUsers")] [HttpGet("/HelloFresh/GetAllRecipesWithUsers")]
public IActionResult GetAllRecipesWithUsers() public IActionResult GetAllRecipesWithUsers()
{ {
@@ -787,8 +800,6 @@ public class HelloFreshController : AppController
return Ok(data); return Ok(data);
} }
[HttpGet("GetHistory")] [HttpGet("GetHistory")]
public IActionResult GetHistory([FromQuery] bool isUserImportant = true, [FromQuery] string? search = null) public IActionResult GetHistory([FromQuery] bool isUserImportant = true, [FromQuery] string? search = null)
{ {
@@ -798,7 +809,6 @@ public class HelloFreshController : AppController
if (isUserImportant && sessionUserId == null) if (isUserImportant && sessionUserId == null)
return Unauthorized("Utilisateur non connecté."); return Unauthorized("Utilisateur non connecté.");
// 1) Historique (matérialisé)
var histQ = _context.HistoriqueRecettes.AsNoTracking(); var histQ = _context.HistoriqueRecettes.AsNoTracking();
if (isUserImportant) histQ = histQ.Where(h => h.UserId == sessionUserId); 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 }) .Select(h => new { h.DateHistorique, h.RecetteId, h.UserId })
.ToList(); .ToList();
// 2) Dictionnaire Recettes avec clé normalisée (trim+lower) pour éviter tout souci de casse/espaces
string K(string? s) => (s ?? "").Trim().ToLowerInvariant(); string K(string? s) => (s ?? "").Trim().ToLowerInvariant();
var recDict = _context.Recettes.AsNoTracking() var recDict = _context.Recettes.AsNoTracking()
.Select(r => new { r.Id, r.Name, r.Image, r.TempsDePreparation }) .Select(r => new { r.Id, r.Name, r.Image, r.TempsDePreparation })
@@ -814,7 +823,6 @@ public class HelloFreshController : AppController
.GroupBy(r => K(r.Id)) .GroupBy(r => K(r.Id))
.ToDictionary(g => g.Key, g => g.First()); .ToDictionary(g => g.Key, g => g.First());
// 3) “Join” en mémoire (pas de ?. dans lexpression EF)
var joined = hist.Select(h => var joined = hist.Select(h =>
{ {
recDict.TryGetValue(K(h.RecetteId), out var r); recDict.TryGetValue(K(h.RecetteId), out var r);
@@ -829,14 +837,12 @@ public class HelloFreshController : AppController
}; };
}).ToList(); }).ToList();
// 4) Filtre recherche serveur (optionnel)
if (!string.IsNullOrWhiteSpace(search)) if (!string.IsNullOrWhiteSpace(search))
{ {
var s = search.Trim().ToLowerInvariant(); var s = search.Trim().ToLowerInvariant();
joined = joined.Where(x => (x.Name ?? "").ToLowerInvariant().Contains(s)).ToList(); joined = joined.Where(x => (x.Name ?? "").ToLowerInvariant().Contains(s)).ToList();
} }
// 5) Noms dutilisateurs si “tous”
var userNames = new Dictionary<int, string>(); var userNames = new Dictionary<int, string>();
if (!isUserImportant) if (!isUserImportant)
{ {
@@ -849,7 +855,6 @@ public class HelloFreshController : AppController
.ToDictionary(g => g.Key, g => g.First().Username ?? $"User#{g.Key}"); .ToDictionary(g => g.Key, g => g.First().Username ?? $"User#{g.Key}");
} }
// 6) Regroupement par Date + Recette
var rows = joined var rows = joined
.GroupBy(x => new { x.Date, x.RecetteId }) .GroupBy(x => new { x.Date, x.RecetteId })
.Select(g => new .Select(g => new
@@ -868,7 +873,6 @@ public class HelloFreshController : AppController
}) })
.ToList(); .ToList();
// 7) Mise en forme par date
var culture = new System.Globalization.CultureInfo("fr-FR"); var culture = new System.Globalization.CultureInfo("fr-FR");
var result = rows var result = rows
.GroupBy(x => x.Date) .GroupBy(x => x.Date)
@@ -889,8 +893,6 @@ public class HelloFreshController : AppController
} }
} }
[HttpPost("ArchiveAll")] [HttpPost("ArchiveAll")]
public IActionResult ArchiveAll() public IActionResult ArchiveAll()
{ {
@@ -899,7 +901,6 @@ public class HelloFreshController : AppController
try try
{ {
// 1) Récup selections de l'utilisateur
var selections = _context.SavingRecettes var selections = _context.SavingRecettes
.Where(s => s.UserId == userId) .Where(s => s.UserId == userId)
.AsNoTracking() .AsNoTracking()
@@ -908,16 +909,14 @@ public class HelloFreshController : AppController
if (selections.Count == 0) if (selections.Count == 0)
return Ok(new { message = "empty", archivedPortions = 0, archivedRecipes = 0, skippedIds = Array.Empty<string>() }); return Ok(new { message = "empty", archivedPortions = 0, archivedRecipes = 0, skippedIds = Array.Empty<string>() });
// 2) Groupes par recette (id string) = nb de portions
var groups = selections var groups = selections
.GroupBy(s => s.IdSavingRecette) .GroupBy(s => s.IdSavingRecette)
.Select(g => new { RecetteId = g.Key, Portions = g.Count() }) .Select(g => new { RecetteId = g.Key, Portions = g.Count() })
.ToList(); .ToList();
// 3) Filtrer sur ids valides (évite FK/erreurs)
var validIds = _context.Recettes var validIds = _context.Recettes
.AsNoTracking() .AsNoTracking()
.Select(r => r.Id) // string .Select(r => r.Id)
.ToHashSet(StringComparer.Ordinal); .ToHashSet(StringComparer.Ordinal);
var validGroups = groups.Where(g => validIds.Contains(g.RecetteId)).ToList(); var validGroups = groups.Where(g => validIds.Contains(g.RecetteId)).ToList();
@@ -926,7 +925,6 @@ public class HelloFreshController : AppController
if (validGroups.Count == 0) if (validGroups.Count == 0)
return Ok(new { message = "nothing_valid", archivedPortions = 0, archivedRecipes = 0, skippedIds }); return Ok(new { message = "nothing_valid", archivedPortions = 0, archivedRecipes = 0, skippedIds });
// 4) Date Paris (DateOnly)
DateOnly todayParis; DateOnly todayParis;
try try
{ {
@@ -937,7 +935,6 @@ public class HelloFreshController : AppController
using var tx = _context.Database.BeginTransaction(); using var tx = _context.Database.BeginTransaction();
// 5) Préparer les insertions (1 ligne par portion)
var toInsert = new List<HistoriqueRecette>(capacity: validGroups.Sum(x => x.Portions)); var toInsert = new List<HistoriqueRecette>(capacity: validGroups.Sum(x => x.Portions));
foreach (var g in validGroups) foreach (var g in validGroups)
{ {
@@ -945,14 +942,13 @@ public class HelloFreshController : AppController
{ {
toInsert.Add(new HistoriqueRecette toInsert.Add(new HistoriqueRecette
{ {
RecetteId = g.RecetteId, // string RecetteId = g.RecetteId,
UserId = userId.Value, UserId = userId.Value,
DateHistorique = todayParis DateHistorique = todayParis
}); });
} }
} }
// 6) Insert + Delete (seulement les rows correspondants aux ids valides)
if (toInsert.Count > 0) if (toInsert.Count > 0)
_context.HistoriqueRecettes.AddRange(toInsert); _context.HistoriqueRecettes.AddRange(toInsert);
@@ -962,11 +958,9 @@ public class HelloFreshController : AppController
_context.SavingRecettes.RemoveRange(rowsToDelete); _context.SavingRecettes.RemoveRange(rowsToDelete);
var archivedPortions = _context.SaveChanges(); // compte total opérations (insert + delete) _context.SaveChanges();
tx.Commit(); 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 archivedRecipes = validGroups.Count;
var archivedPieces = validGroups.Sum(g => g.Portions); var archivedPieces = validGroups.Sum(g => g.Portions);
@@ -980,7 +974,6 @@ public class HelloFreshController : AppController
} }
catch (DbUpdateException dbex) catch (DbUpdateException dbex)
{ {
// remonte lessentiel pour debug front/logs
return StatusCode(500, new return StatusCode(500, new
{ {
message = "archive_failed", message = "archive_failed",
@@ -1001,17 +994,15 @@ public class HelloFreshController : AppController
// POST /HelloFresh/HasHistory // POST /HelloFresh/HasHistory
[HttpPost("HasHistory")] [HttpPost("HasHistory")]
[IgnoreAntiforgeryToken] // garde si tu postes depuis du JS sans token [IgnoreAntiforgeryToken]
public async Task<IActionResult> HasHistory([FromBody] string recipeId) public async Task<IActionResult> HasHistory([FromBody] string recipeId)
{ {
// même logique que le reste de ton contrôleur : user via Session
var sessionUserId = HttpContext.Session.GetInt32("UserId"); var sessionUserId = HttpContext.Session.GetInt32("UserId");
if (sessionUserId == null) return Unauthorized("Utilisateur non connecté."); if (sessionUserId == null) return Unauthorized("Utilisateur non connecté.");
if (string.IsNullOrWhiteSpace(recipeId)) if (string.IsNullOrWhiteSpace(recipeId))
return Ok(new { exists = false }); return Ok(new { exists = false });
// ⚠️ compare sur la bonne colonne (RecetteId) et type string
bool exists = await _context.HistoriqueRecettes bool exists = await _context.HistoriqueRecettes
.AsNoTracking() .AsNoTracking()
.AnyAsync(h => h.RecetteId == recipeId && h.UserId == sessionUserId.Value); .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(); var list = tags.OrderBy(s => s, StringComparer.Create(new System.Globalization.CultureInfo("fr-FR"), true)).ToList();
return Ok(list); return Ok(list);
} }
// API: recettes choisies distinctes par nom, avec PDF // API: recettes choisies distinctes par nom, avec PDF
[HttpGet("GetOwnedForReader")] [HttpGet("GetOwnedForReader")]
public IActionResult GetOwnedForReader() public IActionResult GetOwnedForReader()
@@ -1071,7 +1063,6 @@ public class HelloFreshController : AppController
var userId = HttpContext.Session.GetInt32("UserId"); var userId = HttpContext.Session.GetInt32("UserId");
if (userId == null) return Unauthorized("Utilisateur non connecté."); if (userId == null) return Unauthorized("Utilisateur non connecté.");
// 1) portions par recette de l'utilisateur
var owned = _context.SavingRecettes var owned = _context.SavingRecettes
.Where(s => s.UserId == userId) .Where(s => s.UserId == userId)
.GroupBy(s => s.IdSavingRecette) .GroupBy(s => s.IdSavingRecette)
@@ -1080,7 +1071,6 @@ public class HelloFreshController : AppController
if (owned.Count == 0) return Ok(Array.Empty<object>()); if (owned.Count == 0) return Ok(Array.Empty<object>());
// 2) join recettes (on ramène le PDF)
var joined = owned var joined = owned
.Join(_context.Recettes, .Join(_context.Recettes,
g => g.RecetteId, g => g.RecetteId,
@@ -1096,7 +1086,6 @@ public class HelloFreshController : AppController
}) })
.ToList(); .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(); string Key(string s) => (s ?? "").Trim().ToLowerInvariant();
var distinct = joined var distinct = joined
.GroupBy(x => Key(x.name)) .GroupBy(x => Key(x.name))

View File

View File

View File

@@ -82,7 +82,7 @@ body {
} }
.cui-cards { .cui-cards {
padding: 10px; padding: 5px;
overflow: auto; overflow: auto;
height: 100%; height: 100%;
} }
@@ -185,19 +185,16 @@ body {
} }
.cui-view { .cui-view {
position: relative; height: 100%;
flex: 1; overflow: auto;
min-height: 0; -webkit-overflow-scrolling: touch;
background: #0a0d13;
} }
.cui-iframe { .cui-iframe, .viewer-iframe {
position: absolute;
inset: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
border: 0; border: 0;
display: none; display: block;
} }
.cui-placeholder { .cui-placeholder {
@@ -295,6 +292,8 @@ body {
/* quand la sidebar est réduite, même anneau interne */ /* quand la sidebar est réduite, même anneau interne */
.cui-root.collapsed .cui-card.active { .cui-root.collapsed .cui-card.active {
box-shadow: 0 0 0 2px var(--accent) inset; 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 */ /* Titre de la barre du haut : ellipsis propre si le nom est long */

View File

@@ -33,6 +33,15 @@
const normalize = (s) => (s ?? "").toString().trim().toLowerCase(); const normalize = (s) => (s ?? "").toString().trim().toLowerCase();
const proxify = (url) => url ? `/HelloFresh/ProxyPdf?url=${encodeURIComponent(url)}` : ""; 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) { function buildPdfSrc(rawUrl) {
if (!rawUrl) return ""; if (!rawUrl) return "";

View File

@@ -5,7 +5,6 @@ let currentPage = 1;
let lastPageGlobal = 1; let lastPageGlobal = 1;
const countPerPage = 12; const countPerPage = 12;
let currentSearchTerm = ''; let currentSearchTerm = '';
const MAX_PORTIONS = 14;
let CURRENT_CLIENT_LIST = null; // liste filtrée paginée côté client let CURRENT_CLIENT_LIST = null; // liste filtrée paginée côté client
let ALL_RECIPES_CACHE = null; // toutes les recettes (toutes pages) let ALL_RECIPES_CACHE = null; // toutes les recettes (toutes pages)
@@ -19,10 +18,19 @@ const labelMaps = { ingredients: {}, tags: {} };
// idRecette -> portions (0..3) // idRecette -> portions (0..3)
const ownedMap = new Map(); 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 * 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 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 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 !== '•'); 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); 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; const selectedNow = current > 0;
if (!selectedNow) await saveRecipe(recipe.id); else await clearRecipe(recipe.id); if (!selectedNow) await saveRecipe(recipe.id); else await clearRecipe(recipe.id);
applySelectionUIById(recipe.id); applySelectionUIById(recipe.id);
@@ -273,8 +286,15 @@ function buildRecipeCardOwned(recipe) {
// plus/moins existants // plus/moins existants
card.querySelector('.btn-minus')?.addEventListener('click', (e) => { e.stopPropagation(); removeRecette(recipe.id); updateOwnedCountUI(); if (getTotalPortionsFromMap() === 0) closeOwnedIfOpen(); }); 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) // 🟢 Ctrl+clic = ouvrir le PDF (avec fallback GetRecipesDetails)
card.addEventListener('click', async (e) => { card.addEventListener('click', async (e) => {
if (!e.ctrlKey) return; 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 getTotalPortionsFromMap() { let total = 0; ownedMap.forEach(v => { total += (Number(v) || 0); }); return total; }
function updateOwnedCountUI() { function updateOwnedCountUI() {
const el = document.getElementById('ownedCount'); if (!el) return; const el = document.getElementById('ownedCount'); if (!el) return;
const total = getTotalPortionsFromMap(); el.textContent = `(${total}/${MAX_PORTIONS})`; const total = getTotalPortionsFromMap();
el.style.color = (total >= MAX_PORTIONS) ? '#c62828' : ''; 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) * Ingredients meta attach (for page)
***********************/ ***********************/