Ajout
This commit is contained in:
@@ -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<IngredientItemDto>();
|
||||
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<IngredientItemDto>();
|
||||
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<string>();
|
||||
|
||||
// 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<int, string>();
|
||||
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<string>() });
|
||||
|
||||
// 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<HistoriqueRecette>(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<IActionResult> 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<object>());
|
||||
|
||||
// 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))
|
||||
|
||||
0
Views/Connections/3n2yu2pg.uts~
Normal file
0
Views/Connections/3n2yu2pg.uts~
Normal file
0
Views/Connections/psr2sgj0.jxy~
Normal file
0
Views/Connections/psr2sgj0.jxy~
Normal file
@@ -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 */
|
||||
|
||||
@@ -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 "";
|
||||
|
||||
@@ -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;
|
||||
@@ -744,11 +764,27 @@ 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 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)
|
||||
***********************/
|
||||
|
||||
Reference in New Issue
Block a user