Files
administration/Controllers/HelloFresh/HelloFreshController.cs
2025-09-11 00:02:38 +02:00

1099 lines
41 KiB
C#

// Controllers/HelloFreshController.cs
using Microsoft.AspNetCore.Mvc;
using administration.Models;
using administration.Models.HelloFresh;
using Microsoft.AspNetCore.Authorization;
using System.Text.RegularExpressions;
using System.Text.Json;
using administration.Models.HelloFresh.DTO;
using administration.Models.DTO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using static Azure.Core.HttpHeader;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
namespace administration.Controllers;
[Authorize]
[Route("HelloFresh")]
public class HelloFreshController : AppController
{
private readonly HelloFreshContext _context;
private readonly LayoutDataContext _layout;
private readonly IHttpClientFactory _httpFactory;
[ActivatorUtilitiesConstructor]
public HelloFreshController(HelloFreshContext context, LayoutDataContext layout, IHttpClientFactory httpClientFactory)
{
_context = context;
_layout = layout;
_httpFactory = httpClientFactory;
}
[HttpGet("")]
public IActionResult Index() => View();
[HttpGet("ingredients")]
public IActionResult Ingredients() => View();
[HttpGet("historique")]
public IActionResult Historique() => View(); // Views/HelloFresh/History.cshtml
[HttpGet("cuisine")]
public IActionResult Cuisine() => View(); // Views/HelloFresh/History.cshtml
#region Recettes
[HttpGet("GetRecipesByPage")]
public IActionResult GetRecipesByPage([FromQuery] int page = 1, [FromQuery] int count = 12, [FromQuery] string? search = null)
{
try
{
if (page < 1 || count < 1)
return BadRequest("Les paramètres 'page' et 'count' doivent être supérieurs à 0.");
var q = _context.Recettes.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.Trim().ToLowerInvariant();
q = q.Where(r => r.Name.ToLower().Contains(s));
}
var totalItems = q.Count();
var lastPage = Math.Max(1, (int)Math.Ceiling((double)totalItems / count));
if (page > lastPage && totalItems > 0) page = lastPage;
var recettes = q.OrderByDescending(r => r.Id)
.Skip((page - 1) * count)
.Take(count)
.ToList();
return Ok(new
{
recipes = recettes,
currentPage = page,
lastPage,
totalItems,
pageSize = count
});
}
catch (Exception ex)
{
return StatusCode(500, new { message = "Erreur serveur", error = ex.Message });
}
}
// ❌ Supprime la limite de 3 portions
[HttpPost("SaveRecipe")]
public IActionResult SaveRecipe([FromBody] string idSavingRecette)
{
try
{
var userId = HttpContext.Session.GetInt32("UserId");
if (userId == null) return Unauthorized("Utilisateur non connecté.");
if (string.IsNullOrWhiteSpace(idSavingRecette)) return BadRequest("Id invalide.");
var current = _context.SavingRecettes.Count(s => s.UserId == userId && s.IdSavingRecette == idSavingRecette);
_context.SavingRecettes.Add(new SavingRecette { IdSavingRecette = idSavingRecette, UserId = userId.Value });
_context.SaveChanges();
return Ok(new { message = "added", qty = current + 1 });
}
catch (Exception ex)
{
return StatusCode(500, new { message = "Erreur serveur", error = ex.Message });
}
}
[HttpGet("GetRecipesOwned")]
public IActionResult GetRecipesOwned([FromQuery] bool isUserImportant = true)
{
try
{
var sessionUserId = HttpContext.Session.GetInt32("UserId");
if (sessionUserId == null) return Unauthorized("Utilisateur non connecté.");
static string Key(string? s) => (s ?? "").Trim().ToLowerInvariant();
if (isUserImportant)
{
var rows = (
from s in _context.SavingRecettes.AsNoTracking()
where s.UserId == sessionUserId
join r in _context.Recettes.AsNoTracking() on s.IdSavingRecette equals r.Id
select new
{
r.Id,
r.Name,
r.Image,
r.TempsDePreparation,
r.Tags,
r.Pdf,
Portion = 1
}
).ToList();
var result = rows
.GroupBy(x => Key(x.Name))
.Select(g =>
{
var withPdf = g.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.Pdf));
var pick = withPdf ?? g.OrderByDescending(x => x.Id).First();
return new
{
id = pick.Id,
name = pick.Name,
image = pick.Image,
tempsDePreparation = pick.TempsDePreparation,
portions = g.Sum(x => x.Portion),
tags = pick.Tags,
pdf = pick.Pdf
};
})
.OrderBy(x => x.name)
.ToList();
return Ok(result);
}
else
{
var rows = (
from s in _context.SavingRecettes.AsNoTracking()
join r in _context.Recettes.AsNoTracking() on s.IdSavingRecette equals r.Id
join u in _layout.Users.AsNoTracking() on s.UserId equals u.Id
select new
{
r.Id,
r.Name,
r.Image,
r.TempsDePreparation,
r.Tags,
r.Pdf,
User = u.Username,
Portion = 1
}
).ToList();
var result = rows
.GroupBy(x => Key(x.Name))
.Select(g =>
{
var withPdf = g.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.Pdf));
var pick = withPdf ?? g.OrderByDescending(x => x.Id).First();
return new
{
id = pick.Id,
name = pick.Name,
image = pick.Image,
tempsDePreparation = pick.TempsDePreparation,
portions = g.Sum(x => x.Portion),
tags = pick.Tags,
pdf = pick.Pdf,
users = g.Select(x => x.User).Distinct().ToList()
};
})
.OrderBy(x => x.name)
.ToList();
return Ok(result);
}
}
catch (Exception ex)
{
return StatusCode(500, new { message = "Erreur serveur", error = ex.Message });
}
}
[HttpPost("DeleteRecipesOwned")]
public IActionResult DeleteRecipesOwned([FromBody] string idSavingRecette)
{
try
{
var userId = HttpContext.Session.GetInt32("UserId");
if (userId == null) return Unauthorized("Utilisateur non connecté.");
var row = _context.SavingRecettes
.Where(s => s.UserId == userId && s.IdSavingRecette == idSavingRecette)
.OrderByDescending(s => s.Id)
.FirstOrDefault();
if (row == null) return Ok(new { message = "empty", qty = 0 });
_context.SavingRecettes.Remove(row);
_context.SaveChanges();
var remaining = _context.SavingRecettes.Count(s => s.UserId == userId && s.IdSavingRecette == idSavingRecette);
return Ok(new { message = "removed", qty = remaining });
}
catch (Exception ex)
{
return StatusCode(500, new { message = "Erreur serveur", error = ex.Message });
}
}
[HttpPost("ClearRecipeOwned")]
public IActionResult ClearRecipeOwned([FromBody] string idSavingRecette)
{
try
{
var userId = HttpContext.Session.GetInt32("UserId");
if (userId == null) return Unauthorized("Utilisateur non connecté.");
if (string.IsNullOrWhiteSpace(idSavingRecette)) return BadRequest("Id invalide.");
var rows = _context.SavingRecettes.Where(s => s.UserId == userId && s.IdSavingRecette == idSavingRecette).ToList();
if (rows.Count == 0) return Ok(new { message = "empty", qty = 0 });
_context.SavingRecettes.RemoveRange(rows);
_context.SaveChanges();
return Ok(new { message = "cleared", qty = 0 });
}
catch (Exception ex)
{
return StatusCode(500, new { message = "Erreur serveur", error = ex.Message });
}
}
#endregion
[AllowAnonymous]
[HttpGet("ProxyPdf")]
public async Task<IActionResult> ProxyPdf([FromQuery] string url, [FromServices] IHttpClientFactory httpClientFactory)
{
if (string.IsNullOrWhiteSpace(url)) return BadRequest("URL manquante.");
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return BadRequest("URL invalide.");
var allowedHosts = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"www.hellofresh.fr","hellofresh.fr","img.hellofresh.com","assets.hellofresh.com",
"cdn.hellofresh.com","images.ctfassets.net"
};
if (!allowedHosts.Contains(uri.Host)) return BadRequest($"Domaine non autorisé: {uri.Host}");
try
{
var client = httpClientFactory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.UserAgent.ParseAdd(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36");
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, HttpContext.RequestAborted);
if (!resp.IsSuccessStatusCode)
{
var preview = await resp.Content.ReadAsStringAsync();
if (preview?.Length > 500) preview = preview[..500];
return StatusCode((int)resp.StatusCode,
$"Téléchargement impossible. Code distant: {(int)resp.StatusCode}. Aperçu: {preview}");
}
var mediaType = resp.Content.Headers.ContentType?.MediaType ?? "application/pdf";
var isPdf = mediaType.Contains("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}");
await using var upstream = await resp.Content.ReadAsStreamAsync(HttpContext.RequestAborted);
var ms = new MemoryStream();
await upstream.CopyToAsync(ms, HttpContext.RequestAborted);
ms.Position = 0;
Response.Headers["Cache-Control"] = "public, max-age=3600";
Response.Headers["Content-Disposition"] = "inline; filename=\"recette.pdf\"";
// ✅ Range activé → scroll multi-pages iOS/iframe/pdf.js OK
return new FileStreamResult(ms, "application/pdf") { EnableRangeProcessing = true };
}
catch (TaskCanceledException)
{
return StatusCode(504, "Timeout lors de la récupération du PDF.");
}
catch (Exception ex)
{
return StatusCode(500, $"Erreur ProxyPdf: {ex.Message}");
}
}
// ------------------------
// Ingrédients (agrégation)
// ------------------------
private sealed record QtyParse(double? Value, string Unit);
private static QtyParse ParseQuantity(string? q)
{
if (string.IsNullOrWhiteSpace(q)) return new QtyParse(null, "");
var cleaned = q.Trim().Replace('\u00A0', ' ').Replace(',', '.');
var m = Regex.Match(cleaned, @"^\s*([0-9]+(?:\.[0-9]+)?)(?:\s+([A-Za-zéèêàùûïîç%()\/\.-]+))?\s*$");
if (!m.Success) return new QtyParse(null, "");
var val = double.TryParse(m.Groups[1].Value, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var v)
? v : (double?)null;
var unit = m.Groups[2].Success ? m.Groups[2].Value.Trim().ToLowerInvariant() : "";
if (Regex.IsMatch(unit, @"pi[eè]ce", RegexOptions.IgnoreCase)) unit = "pièce(s)";
return new QtyParse(val, unit);
}
private static List<IngredientItemDto> AggregateIngredients(IEnumerable<IngredientItemDto> items, HashSet<string> owned)
{
var map = new Dictionary<string, (double? sum, string unit, IngredientItemDto item)>(StringComparer.Ordinal);
foreach (var it in items)
{
var name = (it.Name ?? "").Trim();
if (string.IsNullOrEmpty(name)) continue;
var kName = name.ToLowerInvariant();
var (val, unit) = ParseQuantity(it.Quantity);
var unitKey = (unit ?? "").ToLowerInvariant();
if (val is null)
{
var k = $"{kName}||{it.Quantity}";
if (!map.ContainsKey(k))
map[k] = (null, unitKey, new IngredientItemDto { Name = name, Quantity = it.Quantity, Image = it.Image, Owned = owned.Contains(kName) });
continue;
}
var key = $"{kName}##{unitKey}";
if (!map.TryGetValue(key, out var acc))
{
map[key] = (val, unitKey, new IngredientItemDto { Name = name, Quantity = it.Quantity, Image = it.Image, Owned = owned.Contains(kName) });
}
else
{
var newSum = (acc.sum ?? 0) + val.Value;
var keep = acc.item;
if (string.IsNullOrWhiteSpace(keep.Image) && !string.IsNullOrWhiteSpace(it.Image))
keep.Image = it.Image;
map[key] = (newSum, unitKey, keep);
}
}
var outList = new List<IngredientItemDto>();
foreach (var kv in map)
{
var (sum, unit, item) = kv.Value;
if (sum is not null)
{
var qty = ((sum % 1.0) == 0.0)
? sum.Value.ToString("0", System.Globalization.CultureInfo.InvariantCulture)
: sum.Value.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture);
item.Quantity = string.IsNullOrWhiteSpace(unit) ? qty : $"{qty} {unit}";
}
outList.Add(item);
}
return outList;
}
[HttpGet("AggregatedIngredients")]
public IActionResult AggregatedIngredients()
{
var userId = HttpContext.Session.GetInt32("UserId");
if (userId == null) return Unauthorized("Utilisateur non connecté.");
var ownedNames = _context.Ingredients
.Select(i => i.NameOwnedIngredients)
.ToList()
.Select(s => (s ?? "").Trim().ToLowerInvariant())
.ToHashSet();
var perRecipe = _context.SavingRecettes
.GroupBy(s => s.IdSavingRecette)
.Select(g => new { RecetteId = g.Key, Portions = g.Count() })
.ToList();
if (perRecipe.Count == 0) return Ok(new List<IngredientItemDto>());
var recettes = _context.Recettes
.Where(r => perRecipe.Select(x => x.RecetteId).Contains(r.Id))
.Select(r => new { r.Id, r.Ingredients })
.ToList();
// ✅ 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 (!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(basePortions.ToString(), out var arr) || arr.ValueKind != JsonValueKind.Array)
continue;
foreach (var el in arr.EnumerateArray())
{
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(),
Quantity = qty,
Image = img,
Owned = ownedNames.Contains(name.Trim().ToLowerInvariant())
});
}
}
catch { /* ignore */ }
}
var aggregated = AggregateIngredients(rawItems, ownedNames);
var sorted = aggregated
.OrderBy(a => a.Owned ? 1 : 0)
.ThenBy(a => a.Name, StringComparer.Create(new System.Globalization.CultureInfo("fr-FR"), ignoreCase: true))
.ToList();
return Ok(sorted);
}
// ------------- IA SmartSearch : passe par /api/ai/select -------------
public sealed class AiRequestDto { public string Query { get; set; } = ""; }
public sealed class SmartSearchResponse
{
public List<IngredientItemDto> Items { get; set; } = new();
public SmartSearchDebug? Debug { get; set; }
}
public sealed class SmartSearchDebug
{
public object? Sent { get; set; }
public string? Llm { get; set; }
}
[HttpPost("SmartSearch")]
public async Task<IActionResult> SmartSearch(
[FromBody] AiRequestDto body,
[FromServices] IHttpClientFactory httpClientFactory,
[FromQuery] bool debug = false)
{
var userId = HttpContext.Session.GetInt32("UserId");
if (userId == null) return Unauthorized("Utilisateur non connecté.");
var ownedNames = _context.Ingredients.Select(i => i.NameOwnedIngredients)
.ToList()
.Select(s => (s ?? "").Trim().ToLowerInvariant())
.ToHashSet();
var perRecipe = _context.SavingRecettes
.Where(s => s.UserId == userId)
.GroupBy(s => s.IdSavingRecette)
.Select(g => new { RecetteId = g.Key, Portions = g.Count() })
.ToList();
if (perRecipe.Count == 0)
return Ok(new { items = Array.Empty<IngredientItemDto>(), debug = (object?)null });
var recettes = _context.Recettes
.Where(r => perRecipe.Select(x => x.RecetteId).Contains(r.Id))
.Select(r => new { r.Id, r.Ingredients })
.ToList();
var portionsById = perRecipe.ToDictionary(x => x.RecetteId, x => Math.Max(1, x.Portions));
var rawItems = new List<IngredientItemDto>();
foreach (var r in recettes)
{
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(basePortions.ToString(), out var arr) || arr.ValueKind != JsonValueKind.Array)
continue;
foreach (var el in arr.EnumerateArray())
{
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(),
Quantity = qty,
Image = img,
Owned = ownedNames.Contains(name.Trim().ToLowerInvariant())
});
}
}
catch { /* ignore per-recette */ }
}
var aggregated = AggregateIngredients(rawItems, ownedNames);
var allowedNames = aggregated.Select(a => a.Name).Distinct().ToList();
var client = httpClientFactory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(60);
var baseUrl = $"{Request.Scheme}://{Request.Host}";
var reqBody = new
{
query = body?.Query ?? "",
allowed = allowedNames,
debug = debug
};
var req = new HttpRequestMessage(HttpMethod.Post, $"{baseUrl}/api/ai/select");
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
req.Content = new StringContent(JsonSerializer.Serialize(reqBody), Encoding.UTF8, "application/json");
try
{
using var resp = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, HttpContext?.RequestAborted ?? default);
var raw = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode)
{
var nq = (body?.Query ?? "").Trim().ToLowerInvariant();
var fallback = string.IsNullOrEmpty(nq)
? aggregated
: aggregated.Where(a => (a.Name ?? "").ToLowerInvariant().Contains(nq)).ToList();
return Ok(new
{
items = fallback,
debug = debug ? new { selectorError = new { status = (int)resp.StatusCode, raw, url = $"{baseUrl}/api/ai/select" } } : null
});
}
var parsed = JsonDocument.Parse(raw).RootElement;
var names = parsed.TryGetProperty("names", out var nEl) && nEl.ValueKind == JsonValueKind.Array
? nEl.EnumerateArray().Select(x => x.GetString() ?? "").Where(s => !string.IsNullOrWhiteSpace(s)).ToList()
: new List<string>();
var order = names.Select((n, i) => new { n = n.Trim().ToLowerInvariant(), i })
.ToDictionary(x => x.n, x => x.i);
var selected = aggregated
.Where(a => order.ContainsKey((a.Name ?? "").Trim().ToLowerInvariant()))
.OrderBy(a => order[(a.Name ?? "").Trim().ToLowerInvariant()])
.ToList();
if (selected.Count == 0)
{
var nq = (body?.Query ?? "").Trim().ToLowerInvariant();
selected = string.IsNullOrEmpty(nq)
? aggregated
: aggregated.Where(a => (a.Name ?? "").ToLowerInvariant().Contains(nq)).ToList();
}
return Ok(new
{
items = selected,
debug = debug ? JsonSerializer.Deserialize<object>(raw) : null
});
}
catch (TaskCanceledException)
{
return StatusCode(504, new { error = "Timeout IA selector" });
}
catch (Exception ex)
{
return StatusCode(500, new { error = ex.Message });
}
}
[HttpGet("GetRecipesDetails")]
public IActionResult GetRecipesDetails([FromQuery] string ids)
{
if (string.IsNullOrWhiteSpace(ids)) return BadRequest("ids manquant");
var idList = ids.Split(',').Select(s => s.Trim()).Where(s => s.Length > 0).ToList();
var recipes = _context.Recettes
.Where(r => idList.Contains(r.Id))
.Select(r => new
{
id = r.Id,
name = r.Name,
image = r.Image,
difficulte = r.Difficulte,
ingredientsJson = r.Ingredients,
pdf = r.Pdf
})
.ToList();
return Ok(recipes);
}
[HttpPost("ToggleOwnedIngredient")]
public IActionResult ToggleOwnedIngredient([FromBody] string ingredientName)
{
try
{
var existing = _context.Ingredients
.FirstOrDefault(i => i.NameOwnedIngredients == ingredientName);
if (existing != null)
{
_context.Ingredients.Remove(existing);
_context.SaveChanges();
return Ok(new { status = "removed" });
}
else
{
_context.Ingredients.Add(new Ingredient { NameOwnedIngredients = ingredientName });
_context.SaveChanges();
return Ok(new { status = "added" });
}
}
catch (Exception ex)
{
return StatusCode(500, new { message = "Erreur serveur", error = ex.Message });
}
}
[HttpGet("GetOwnedIngredients")]
public IActionResult GetOwnedIngredients()
{
try
{
var names = _context.Ingredients
.Select(i => i.NameOwnedIngredients)
.Distinct()
.ToList();
return Ok(names);
}
catch (Exception ex)
{
return StatusCode(500, new { message = "Erreur serveur", error = ex.Message });
}
}
[HttpGet("GetAllRecipes")]
public IActionResult GetAllRecipes()
{
var counts = _context.SavingRecettes
.AsNoTracking()
.GroupBy(s => new { s.IdSavingRecette, s.UserId })
.Select(g => new
{
RecetteId = g.Key.IdSavingRecette,
UserId = g.Key.UserId,
Portions = g.Count()
})
.ToList();
var recettes = _context.Recettes
.AsNoTracking()
.Select(r => new
{
r.Id,
r.Name,
r.Image,
r.TempsDePreparation,
r.Tags,
IngredientsToHad = r.IngredientsToHad
})
.ToList();
var neededUserIds = counts.Select(x => x.UserId).Distinct().ToList();
var users = _layout.Users
.AsNoTracking()
.Where(u => neededUserIds.Contains(u.Id))
.Select(u => new { u.Id, u.Username })
.ToList();
var baseRows =
from c in counts
join r in recettes on c.RecetteId equals r.Id
join u in users on c.UserId equals u.Id into gj
from u in gj.DefaultIfEmpty()
select new
{
RecetteId = r.Id,
r.Name,
r.Image,
r.TempsDePreparation,
c.Portions,
r.Tags,
r.IngredientsToHad,
User = u?.Username
};
var result = baseRows
.GroupBy(x => x.RecetteId)
.Select(g => new
{
id = g.Key,
name = g.First().Name,
image = g.First().Image,
tempsDePreparation = g.First().TempsDePreparation,
portions = g.Sum(x => x.Portions),
tags = g.First().Tags,
Users = g.Where(x => x.User != null)
.Select(x => x.User)
.Distinct()
.ToList(),
ingredientsToHad = g.First().IngredientsToHad
})
.ToList();
return Ok(result);
}
[HttpGet("/HelloFresh/GetAllRecipesWithUsers")]
public IActionResult GetAllRecipesWithUsers()
{
var data = _context.SavingRecettes
.GroupBy(s => new { s.IdSavingRecette, s.UserId })
.Select(g => new
{
RecetteId = g.Key.IdSavingRecette,
UserId = g.Key.UserId,
Portions = g.Count()
})
.Join(_context.Recettes,
g => g.RecetteId, r => r.Id,
(g, r) => new
{
id = r.Id,
name = r.Name,
image = r.Image,
tempsDePreparation = r.TempsDePreparation,
tags = r.Tags,
userId = g.UserId,
portions = g.Portions
})
.ToList();
return Ok(data);
}
[HttpGet("GetHistory")]
public IActionResult GetHistory([FromQuery] bool isUserImportant = true, [FromQuery] string? search = null)
{
try
{
var sessionUserId = HttpContext.Session.GetInt32("UserId");
if (isUserImportant && sessionUserId == null)
return Unauthorized("Utilisateur non connecté.");
var histQ = _context.HistoriqueRecettes.AsNoTracking();
if (isUserImportant) histQ = histQ.Where(h => h.UserId == sessionUserId);
var hist = histQ
.Select(h => new { h.DateHistorique, h.RecetteId, h.UserId })
.ToList();
string K(string? s) => (s ?? "").Trim().ToLowerInvariant();
var recDict = _context.Recettes.AsNoTracking()
.Select(r => new { r.Id, r.Name, r.Image, r.TempsDePreparation })
.ToList()
.GroupBy(r => K(r.Id))
.ToDictionary(g => g.Key, g => g.First());
var joined = hist.Select(h =>
{
recDict.TryGetValue(K(h.RecetteId), out var r);
return new
{
Date = h.DateHistorique,
RecetteId = h.RecetteId,
Name = r != null ? r.Name : null,
Image = r != null ? r.Image : null,
TempsDePreparation = r != null ? r.TempsDePreparation : (int?)null,
UserId = h.UserId
};
}).ToList();
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.Trim().ToLowerInvariant();
joined = joined.Where(x => (x.Name ?? "").ToLowerInvariant().Contains(s)).ToList();
}
var userNames = new Dictionary<int, string>();
if (!isUserImportant)
{
var ids = joined.Select(x => x.UserId).Distinct().ToList();
userNames = _layout.Users.AsNoTracking()
.Where(u => ids.Contains(u.Id))
.Select(u => new { u.Id, u.Username })
.ToList()
.GroupBy(x => x.Id)
.ToDictionary(g => g.Key, g => g.First().Username ?? $"User#{g.Key}");
}
var rows = joined
.GroupBy(x => new { x.Date, x.RecetteId })
.Select(g => new
{
g.Key.Date,
id = g.Key.RecetteId,
name = g.Select(x => x.Name).FirstOrDefault(n => !string.IsNullOrWhiteSpace(n)) ?? $"Recette {g.Key.RecetteId}",
image = g.Select(x => x.Image).FirstOrDefault(i => !string.IsNullOrWhiteSpace(i)),
tempsDePreparation = g.Select(x => x.TempsDePreparation).FirstOrDefault() ?? 0,
portions = g.Count(),
Users = !isUserImportant
? g.Select(x => userNames.TryGetValue(x.UserId, out var u) ? u : $"User#{x.UserId}")
.Distinct()
.ToList()
: new List<string>()
})
.ToList();
var culture = new System.Globalization.CultureInfo("fr-FR");
var result = rows
.GroupBy(x => x.Date)
.OrderByDescending(g => g.Key)
.Select(g => new
{
date = g.Key.ToString("dd MMMM yyyy", culture),
rawDate = g.Key,
items = g.OrderBy(x => x.name, StringComparer.Create(culture, true)).ToList()
})
.ToList();
return Ok(result);
}
catch (Exception ex)
{
return StatusCode(500, new { message = "history_failed", error = ex.Message });
}
}
[HttpPost("ArchiveAll")]
public IActionResult ArchiveAll()
{
var userId = HttpContext.Session.GetInt32("UserId");
if (userId == null) return Unauthorized("Utilisateur non connecté.");
try
{
var selections = _context.SavingRecettes
.Where(s => s.UserId == userId)
.AsNoTracking()
.ToList();
if (selections.Count == 0)
return Ok(new { message = "empty", archivedPortions = 0, archivedRecipes = 0, skippedIds = Array.Empty<string>() });
var groups = selections
.GroupBy(s => s.IdSavingRecette)
.Select(g => new { RecetteId = g.Key, Portions = g.Count() })
.ToList();
var validIds = _context.Recettes
.AsNoTracking()
.Select(r => r.Id)
.ToHashSet(StringComparer.Ordinal);
var validGroups = groups.Where(g => validIds.Contains(g.RecetteId)).ToList();
var skippedIds = groups.Where(g => !validIds.Contains(g.RecetteId)).Select(g => g.RecetteId).Distinct().ToList();
if (validGroups.Count == 0)
return Ok(new { message = "nothing_valid", archivedPortions = 0, archivedRecipes = 0, skippedIds });
DateOnly todayParis;
try
{
var tz = TimeZoneInfo.FindSystemTimeZoneById("Europe/Paris");
todayParis = DateOnly.FromDateTime(TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz));
}
catch { todayParis = DateOnly.FromDateTime(DateTime.Now); }
using var tx = _context.Database.BeginTransaction();
var toInsert = new List<HistoriqueRecette>(capacity: validGroups.Sum(x => x.Portions));
foreach (var g in validGroups)
{
for (int i = 0; i < g.Portions; i++)
{
toInsert.Add(new HistoriqueRecette
{
RecetteId = g.RecetteId,
UserId = userId.Value,
DateHistorique = todayParis
});
}
}
if (toInsert.Count > 0)
_context.HistoriqueRecettes.AddRange(toInsert);
var rowsToDelete = _context.SavingRecettes
.Where(s => s.UserId == userId && validIds.Contains(s.IdSavingRecette))
.ToList();
_context.SavingRecettes.RemoveRange(rowsToDelete);
_context.SaveChanges();
tx.Commit();
var archivedRecipes = validGroups.Count;
var archivedPieces = validGroups.Sum(g => g.Portions);
return Ok(new
{
message = "archived_all",
archivedPortions = archivedPieces,
archivedRecipes,
skippedIds
});
}
catch (DbUpdateException dbex)
{
return StatusCode(500, new
{
message = "archive_failed",
reason = "db_update_exception",
error = dbex.GetBaseException().Message
});
}
catch (Exception ex)
{
return StatusCode(500, new
{
message = "archive_failed",
reason = "exception",
error = ex.Message
});
}
}
// POST /HelloFresh/HasHistory
[HttpPost("HasHistory")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> HasHistory([FromBody] string recipeId)
{
var sessionUserId = HttpContext.Session.GetInt32("UserId");
if (sessionUserId == null) return Unauthorized("Utilisateur non connecté.");
if (string.IsNullOrWhiteSpace(recipeId))
return Ok(new { exists = false });
bool exists = await _context.HistoriqueRecettes
.AsNoTracking()
.AnyAsync(h => h.RecetteId == recipeId && h.UserId == sessionUserId.Value);
return Ok(new { exists });
}
[HttpGet("GetAllIngredients")]
public IActionResult GetAllIngredients()
{
var names = new HashSet<string>(StringComparer.Create(new System.Globalization.CultureInfo("fr-FR"), true));
foreach (var json in _context.Recettes.AsNoTracking().Select(r => r.Ingredients ?? "{}"))
{
try
{
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.ValueKind != JsonValueKind.Object) continue;
foreach (var prop in doc.RootElement.EnumerateObject())
{
if (prop.Value.ValueKind != JsonValueKind.Array) continue;
foreach (var el in prop.Value.EnumerateArray())
{
var n = el.TryGetProperty("Name", out var nEl) ? (nEl.GetString() ?? "").Trim() : "";
if (!string.IsNullOrWhiteSpace(n)) names.Add(n);
}
}
}
catch { /* ignore */ }
}
var list = names.OrderBy(s => s, StringComparer.Create(new System.Globalization.CultureInfo("fr-FR"), true)).ToList();
return Ok(list);
}
[HttpGet("GetAllTags")]
public IActionResult GetAllTags()
{
var tags = new HashSet<string>(StringComparer.Create(new System.Globalization.CultureInfo("fr-FR"), true));
foreach (var row in _context.Recettes.AsNoTracking().Select(r => r.Tags))
{
if (string.IsNullOrWhiteSpace(row)) continue;
foreach (var t in Regex.Split(row, @"[,•]"))
{
var v = (t ?? "").Trim();
if (string.IsNullOrEmpty(v) || v == "•") continue;
tags.Add(v);
}
}
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()
{
var userId = HttpContext.Session.GetInt32("UserId");
if (userId == null) return Unauthorized("Utilisateur non connecté.");
var owned = _context.SavingRecettes
.Where(s => s.UserId == userId)
.GroupBy(s => s.IdSavingRecette)
.Select(g => new { RecetteId = g.Key, Portions = g.Count() })
.ToList();
if (owned.Count == 0) return Ok(Array.Empty<object>());
var joined = owned
.Join(_context.Recettes,
g => g.RecetteId,
r => r.Id,
(g, r) => new
{
id = r.Id,
name = r.Name,
image = r.Image,
pdf = r.Pdf,
portions = g.Portions,
prep = r.TempsDePreparation
})
.ToList();
string Key(string s) => (s ?? "").Trim().ToLowerInvariant();
var distinct = joined
.GroupBy(x => Key(x.name))
.Select(g => g.OrderByDescending(x => x.portions).ThenBy(x => x.name).First())
.OrderBy(x => x.name)
.ToList();
return Ok(distinct);
}
}