Correction PDF
This commit is contained in:
13
.config/dotnet-tools.json
Normal file
13
.config/dotnet-tools.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"dotnet-ef": {
|
||||||
|
"version": "8.0.20",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-ef"
|
||||||
|
],
|
||||||
|
"rollForward": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -104,5 +104,15 @@ namespace administration.Controllers
|
|||||||
|
|
||||||
return Content("✅ Mot de passe mis à jour avec succès");
|
return Content("✅ Mot de passe mis à jour avec succès");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("/tools/hash")]
|
||||||
|
[Authorize] // et/ou protège par un flag d'env
|
||||||
|
public IActionResult HashTool(string username, string password, [FromServices] IPasswordHasher<User> hasher)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) return BadRequest("username/password requis");
|
||||||
|
var hash = hasher.HashPassword(new User { Username = username }, password);
|
||||||
|
return Content(hash);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,14 +50,20 @@ public class HelloFreshController : AppController
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (page < 1 || count < 1)
|
if (page < 1 || count < 1) return BadRequest("Les paramètres 'page' et 'count' doivent être supérieurs à 0.");
|
||||||
return BadRequest("Les paramètres 'page' et 'count' doivent être supérieurs à 0.");
|
|
||||||
|
// ✅ Base query : uniquement les recettes avec un PDF non vide
|
||||||
|
var q = _context.Recettes
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(r => r.Pdf != null && r.Pdf != "") // éviter IsNullOrWhiteSpace pour une traduction SQL sûre
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
var q = _context.Recettes.AsQueryable();
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
if (!string.IsNullOrWhiteSpace(search))
|
||||||
{
|
{
|
||||||
var s = search.Trim().ToLowerInvariant();
|
var s = search.Trim();
|
||||||
q = q.Where(r => r.Name.ToLower().Contains(s));
|
q = q.Where(r => EF.Functions.Like(r.Name, $"%{s}%"));
|
||||||
|
// Optionnel : chercher aussi dans les tags
|
||||||
|
// q = q.Where(r => EF.Functions.Like(r.Name, $"%{s}%") || (r.Tags != null && EF.Functions.Like(r.Tags, $"%{s}%")));
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalItems = q.Count();
|
var totalItems = q.Count();
|
||||||
@@ -84,6 +90,8 @@ public class HelloFreshController : AppController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ❌ Supprime la limite de 3 portions
|
// ❌ Supprime la limite de 3 portions
|
||||||
[HttpPost("SaveRecipe")]
|
[HttpPost("SaveRecipe")]
|
||||||
public IActionResult SaveRecipe([FromBody] string idSavingRecette)
|
public IActionResult SaveRecipe([FromBody] string idSavingRecette)
|
||||||
@@ -393,12 +401,16 @@ 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é.");
|
||||||
|
|
||||||
|
var scopeIdsOwned = PantryScopeUserIds();
|
||||||
var ownedNames = _context.Ingredients
|
var ownedNames = _context.Ingredients
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(i => scopeIdsOwned.Contains(i.UserId)) // 👈 union Mae+Byakuya
|
||||||
.Select(i => i.NameOwnedIngredients)
|
.Select(i => i.NameOwnedIngredients)
|
||||||
.ToList()
|
.ToList()
|
||||||
.Select(s => (s ?? "").Trim().ToLowerInvariant())
|
.Select(s => (s ?? "").Trim().ToLowerInvariant())
|
||||||
.ToHashSet();
|
.ToHashSet();
|
||||||
|
|
||||||
|
|
||||||
var perRecipe = _context.SavingRecettes
|
var perRecipe = _context.SavingRecettes
|
||||||
.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() })
|
||||||
@@ -494,11 +506,16 @@ 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é.");
|
||||||
|
|
||||||
var ownedNames = _context.Ingredients.Select(i => i.NameOwnedIngredients)
|
var scopeIdsOwned = PantryScopeUserIds();
|
||||||
|
var ownedNames = _context.Ingredients
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(i => scopeIdsOwned.Contains(i.UserId)) // 👈 union Mae+Byakuya
|
||||||
|
.Select(i => i.NameOwnedIngredients)
|
||||||
.ToList()
|
.ToList()
|
||||||
.Select(s => (s ?? "").Trim().ToLowerInvariant())
|
.Select(s => (s ?? "").Trim().ToLowerInvariant())
|
||||||
.ToHashSet();
|
.ToHashSet();
|
||||||
|
|
||||||
|
|
||||||
var perRecipe = _context.SavingRecettes
|
var perRecipe = _context.SavingRecettes
|
||||||
.Where(s => s.UserId == userId)
|
.Where(s => s.UserId == userId)
|
||||||
.GroupBy(s => s.IdSavingRecette)
|
.GroupBy(s => s.IdSavingRecette)
|
||||||
@@ -640,7 +657,7 @@ public class HelloFreshController : AppController
|
|||||||
var idList = ids.Split(',').Select(s => s.Trim()).Where(s => s.Length > 0).ToList();
|
var idList = ids.Split(',').Select(s => s.Trim()).Where(s => s.Length > 0).ToList();
|
||||||
|
|
||||||
var recipes = _context.Recettes
|
var recipes = _context.Recettes
|
||||||
.Where(r => idList.Contains(r.Id))
|
.Where(r => idList.Contains(r.Id) && !string.IsNullOrEmpty(r.Pdf)) // 👈 filtre: PDF non vide
|
||||||
.Select(r => new
|
.Select(r => new
|
||||||
{
|
{
|
||||||
id = r.Id,
|
id = r.Id,
|
||||||
@@ -654,14 +671,19 @@ public class HelloFreshController : AppController
|
|||||||
|
|
||||||
return Ok(recipes);
|
return Ok(recipes);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("ToggleOwnedIngredient")]
|
[HttpPost("ToggleOwnedIngredient")]
|
||||||
public IActionResult ToggleOwnedIngredient([FromBody] string ingredientName)
|
public IActionResult ToggleOwnedIngredient([FromBody] string ingredientName)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var userId = HttpContext.Session.GetInt32("UserId");
|
||||||
|
if (userId == null) return Unauthorized("Utilisateur non connecté.");
|
||||||
|
|
||||||
|
var name = (ingredientName ?? "").Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(name)) return BadRequest("Nom d'ingrédient invalide.");
|
||||||
|
|
||||||
var existing = _context.Ingredients
|
var existing = _context.Ingredients
|
||||||
.FirstOrDefault(i => i.NameOwnedIngredients == ingredientName);
|
.FirstOrDefault(i => i.UserId == userId.Value && i.NameOwnedIngredients == name);
|
||||||
|
|
||||||
if (existing != null)
|
if (existing != null)
|
||||||
{
|
{
|
||||||
@@ -671,7 +693,11 @@ public class HelloFreshController : AppController
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_context.Ingredients.Add(new Ingredient { NameOwnedIngredients = ingredientName });
|
_context.Ingredients.Add(new Ingredient
|
||||||
|
{
|
||||||
|
UserId = userId.Value, // 👈 lie l’ingrédient au user courant
|
||||||
|
NameOwnedIngredients = name
|
||||||
|
});
|
||||||
_context.SaveChanges();
|
_context.SaveChanges();
|
||||||
return Ok(new { status = "added" });
|
return Ok(new { status = "added" });
|
||||||
}
|
}
|
||||||
@@ -682,12 +708,18 @@ public class HelloFreshController : AppController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpGet("GetOwnedIngredients")]
|
[HttpGet("GetOwnedIngredients")]
|
||||||
public IActionResult GetOwnedIngredients()
|
public IActionResult GetOwnedIngredients()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var scopeIds = PantryScopeUserIds();
|
||||||
|
if (scopeIds.Count == 0) return Unauthorized("Utilisateur non connecté.");
|
||||||
|
|
||||||
var names = _context.Ingredients
|
var names = _context.Ingredients
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(i => scopeIds.Contains(i.UserId)) // 👈 union Mae+Byakuya, sinon user seul
|
||||||
.Select(i => i.NameOwnedIngredients)
|
.Select(i => i.NameOwnedIngredients)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -700,6 +732,7 @@ public class HelloFreshController : AppController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpGet("GetAllRecipes")]
|
[HttpGet("GetAllRecipes")]
|
||||||
public IActionResult GetAllRecipes()
|
public IActionResult GetAllRecipes()
|
||||||
{
|
{
|
||||||
@@ -714,8 +747,10 @@ public class HelloFreshController : AppController
|
|||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
// 👇 Ne garder que les recettes avec un PDF non vide
|
||||||
var recettes = _context.Recettes
|
var recettes = _context.Recettes
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
|
.Where(r => r.Pdf != null && r.Pdf != "") // équiv. à !string.IsNullOrEmpty(r.Pdf)
|
||||||
.Select(r => new
|
.Select(r => new
|
||||||
{
|
{
|
||||||
r.Id,
|
r.Id,
|
||||||
@@ -723,7 +758,8 @@ public class HelloFreshController : AppController
|
|||||||
r.Image,
|
r.Image,
|
||||||
r.TempsDePreparation,
|
r.TempsDePreparation,
|
||||||
r.Tags,
|
r.Tags,
|
||||||
IngredientsToHad = r.IngredientsToHad
|
IngredientsToHad = r.IngredientsToHad,
|
||||||
|
r.Pdf // utilisé pour le filtre (pas renvoyé ensuite)
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
@@ -773,6 +809,7 @@ public class HelloFreshController : AppController
|
|||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpGet("/HelloFresh/GetAllRecipesWithUsers")]
|
[HttpGet("/HelloFresh/GetAllRecipesWithUsers")]
|
||||||
public IActionResult GetAllRecipesWithUsers()
|
public IActionResult GetAllRecipesWithUsers()
|
||||||
{
|
{
|
||||||
@@ -1095,4 +1132,100 @@ public class HelloFreshController : AppController
|
|||||||
|
|
||||||
return Ok(distinct);
|
return Ok(distinct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET /HelloFresh/GetRecipesOwnedByUsers?names=Mae,Byakuya
|
||||||
|
[HttpGet("GetRecipesOwnedByUsers")]
|
||||||
|
public IActionResult GetRecipesOwnedByUsers([FromQuery] string names)
|
||||||
|
{
|
||||||
|
var raw = (names ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Select(s => s.Trim())
|
||||||
|
.Where(s => s.Length > 0)
|
||||||
|
.ToList();
|
||||||
|
if (raw.Count == 0) return Ok(new List<object>());
|
||||||
|
|
||||||
|
// usernames -> ids
|
||||||
|
var users = _layout.Users.AsNoTracking()
|
||||||
|
.Where(u => raw.Contains(u.Username))
|
||||||
|
.Select(u => new { u.Id, u.Username })
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (users.Count == 0) return Ok(new List<object>());
|
||||||
|
|
||||||
|
var userIds = users.Select(u => u.Id).ToList();
|
||||||
|
var idToName = users.ToDictionary(u => u.Id, u => u.Username);
|
||||||
|
|
||||||
|
// portions par (Recette, User)
|
||||||
|
var counts = _context.SavingRecettes.AsNoTracking()
|
||||||
|
.Where(s => userIds.Contains(s.UserId))
|
||||||
|
.GroupBy(s => new { s.IdSavingRecette, s.UserId })
|
||||||
|
.Select(g => new { RecetteId = g.Key.IdSavingRecette, UserId = g.Key.UserId, Portions = g.Count() })
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (counts.Count == 0) return Ok(new List<object>());
|
||||||
|
|
||||||
|
var recetteIds = counts.Select(c => c.RecetteId).Distinct().ToList();
|
||||||
|
var recs = _context.Recettes.AsNoTracking()
|
||||||
|
.Where(r => recetteIds.Contains(r.Id))
|
||||||
|
.Select(r => new
|
||||||
|
{
|
||||||
|
r.Id,
|
||||||
|
r.Name,
|
||||||
|
r.Image,
|
||||||
|
r.TempsDePreparation,
|
||||||
|
r.Tags,
|
||||||
|
r.IngredientsToHad
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
var recDict = recs.ToDictionary(r => r.Id, r => r, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
// Agrégation par recette limitée aux users demandés
|
||||||
|
var result = counts
|
||||||
|
.GroupBy(c => c.RecetteId)
|
||||||
|
.Select(g =>
|
||||||
|
{
|
||||||
|
recDict.TryGetValue(g.Key, out var r);
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
id = g.Key,
|
||||||
|
name = r?.Name ?? g.Key,
|
||||||
|
image = r?.Image,
|
||||||
|
tempsDePreparation = r?.TempsDePreparation ?? 0,
|
||||||
|
portions = g.Sum(x => x.Portions),
|
||||||
|
tags = r?.Tags,
|
||||||
|
users = g.Select(x => idToName.TryGetValue(x.UserId, out var un) ? un : null)
|
||||||
|
.Where(un => !string.IsNullOrWhiteSpace(un))
|
||||||
|
.Distinct()
|
||||||
|
.ToList(),
|
||||||
|
ingredientsToHad = r?.IngredientsToHad
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.OrderBy(x => x.name, StringComparer.Create(new System.Globalization.CultureInfo("fr-FR"), true))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Scope "pantry" (ingrédients possédés) ---
|
||||||
|
// Mae & Byakuya partagent leur garde-manger ; les autres sont en solo.
|
||||||
|
private List<int> PantryScopeUserIds()
|
||||||
|
{
|
||||||
|
var sessionUserId = HttpContext.Session.GetInt32("UserId");
|
||||||
|
if (sessionUserId == null) return new List<int>();
|
||||||
|
|
||||||
|
var me = _layout.Users.AsNoTracking().FirstOrDefault(u => u.Id == sessionUserId.Value);
|
||||||
|
if (me == null) return new List<int> { sessionUserId.Value };
|
||||||
|
|
||||||
|
// Liste blanche des prénoms qui partagent entre eux
|
||||||
|
var core = new[] { "Mae", "Byakuya" };
|
||||||
|
var isCore = core.Any(n => string.Equals(n, me.Username, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (!isCore) return new List<int> { me.Id };
|
||||||
|
|
||||||
|
// Récupère les IDs de Mae & Byakuya
|
||||||
|
return _layout.Users.AsNoTracking()
|
||||||
|
.Where(u => core.Contains(u.Username))
|
||||||
|
.Select(u => u.Id)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,6 @@ public partial class FinancesContext : DbContext
|
|||||||
|
|
||||||
public virtual DbSet<User> Users { get; set; }
|
public virtual DbSet<User> Users { get; set; }
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
|
||||||
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
|
|
||||||
=> optionsBuilder.UseSqlServer("Server=217.154.116.43;Database=Finances;User Id=sa;Password=Mf33ksTRLrPKSqQ4cTXitgiSN6BPBt89;TrustServerCertificate=True;");
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
modelBuilder.Entity<AdditionalSource>(entity =>
|
modelBuilder.Entity<AdditionalSource>(entity =>
|
||||||
|
|||||||
@@ -23,22 +23,13 @@ public partial class HelloFreshContext : DbContext
|
|||||||
|
|
||||||
public virtual DbSet<SavingRecette> SavingRecettes { get; set; }
|
public virtual DbSet<SavingRecette> SavingRecettes { get; set; }
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
|
||||||
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
|
|
||||||
=> optionsBuilder.UseSqlServer("Server=217.154.116.43;Database=HelloFresh;User Id=sa;Password=Mf33ksTRLrPKSqQ4cTXitgiSN6BPBt89;TrustServerCertificate=True;");
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
modelBuilder.Entity<HistoriqueRecette>(entity =>
|
modelBuilder.Entity<HistoriqueRecette>(entity =>
|
||||||
{
|
{
|
||||||
entity.Property(e => e.Id).HasColumnName("id");
|
entity.Property(e => e.Id).HasColumnName("id");
|
||||||
// Map DateOnly -> date SQL
|
entity.Property(e => e.DateHistorique).HasColumnName("dateHistorique");
|
||||||
entity.Property(e => e.DateHistorique)
|
|
||||||
.HasConversion(
|
|
||||||
v => v.ToDateTime(TimeOnly.MinValue), // vers SQL
|
|
||||||
v => DateOnly.FromDateTime(v) // depuis SQL
|
|
||||||
)
|
|
||||||
.HasColumnType("date");
|
|
||||||
entity.Property(e => e.RecetteId).HasColumnName("recetteId");
|
entity.Property(e => e.RecetteId).HasColumnName("recetteId");
|
||||||
entity.Property(e => e.UserId).HasColumnName("userId");
|
entity.Property(e => e.UserId).HasColumnName("userId");
|
||||||
});
|
});
|
||||||
@@ -47,6 +38,7 @@ public partial class HelloFreshContext : DbContext
|
|||||||
{
|
{
|
||||||
entity.Property(e => e.Id).HasColumnName("id");
|
entity.Property(e => e.Id).HasColumnName("id");
|
||||||
entity.Property(e => e.NameOwnedIngredients).HasColumnName("nameOwnedIngredients");
|
entity.Property(e => e.NameOwnedIngredients).HasColumnName("nameOwnedIngredients");
|
||||||
|
entity.Property(e => e.UserId).HasColumnName("userId");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<Recette>(entity =>
|
modelBuilder.Entity<Recette>(entity =>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ public partial class HistoriqueRecette
|
|||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
public string RecetteId { get; set; } = ""; // <-- string pour matcher Recette.Id
|
public string RecetteId { get; set; } = null!;
|
||||||
|
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -8,4 +8,6 @@ public partial class Ingredient
|
|||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
public string NameOwnedIngredients { get; set; } = null!;
|
public string NameOwnedIngredients { get; set; } = null!;
|
||||||
|
|
||||||
|
public int UserId { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ public partial class LayoutDataContext : DbContext
|
|||||||
|
|
||||||
public virtual DbSet<User> Users { get; set; }
|
public virtual DbSet<User> Users { get; set; }
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
|
||||||
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
|
|
||||||
=> optionsBuilder.UseSqlServer("Server=217.154.116.43,1433;Database=LayoutData;User Id=sa;Password=Mf33ksTRLrPKSqQ4cTXitgiSN6BPBt89;TrustServerCertificate=True;");
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
|||||||
35
Program.cs
35
Program.cs
@@ -26,14 +26,31 @@ namespace administration
|
|||||||
if (string.IsNullOrEmpty(dbConnection))
|
if (string.IsNullOrEmpty(dbConnection))
|
||||||
throw new Exception("❌ ADMIN_DB_CONNECTION est introuvable.");
|
throw new Exception("❌ ADMIN_DB_CONNECTION est introuvable.");
|
||||||
|
|
||||||
builder.Services.AddDbContext<FinancesContext>(options =>
|
bool inContainer = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true";
|
||||||
options.UseSqlServer(dbConnection, sqlOptions => sqlOptions.EnableRetryOnFailure()));
|
string hostOverride = builder.Configuration["DB_HOST"] ?? Environment.GetEnvironmentVariable("DB_HOST");
|
||||||
|
|
||||||
builder.Services.AddDbContext<HelloFreshContext>(options =>
|
string Fix(string? cs, string? dbNameFallback)
|
||||||
options.UseSqlServer(builder.Configuration.GetConnectionString("HelloFreshConnection")));
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(cs) && !string.IsNullOrWhiteSpace(dbNameFallback))
|
||||||
|
{
|
||||||
|
var def = builder.Configuration.GetConnectionString("DefaultConnection") ?? "";
|
||||||
|
if (!string.IsNullOrWhiteSpace(def))
|
||||||
|
cs = System.Text.RegularExpressions.Regex.Replace(def, @"Database=([^;]+)", $"Database={dbNameFallback}");
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(cs) && !inContainer && !string.IsNullOrWhiteSpace(hostOverride))
|
||||||
|
{
|
||||||
|
cs = cs.Replace("Server=sqlserver,1433", $"Server={hostOverride},1433", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
return cs ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
builder.Services.AddDbContext<LayoutDataContext>(options =>
|
var csLayout = Fix(builder.Configuration.GetConnectionString("DefaultConnection"), "LayoutData");
|
||||||
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
|
var csHello = Fix(builder.Configuration.GetConnectionString("HelloFresh"), "HelloFresh");
|
||||||
|
var csFinance = Fix(builder.Configuration.GetConnectionString("Finances"), "Finances");
|
||||||
|
|
||||||
|
builder.Services.AddDbContext<LayoutDataContext>(o => o.UseSqlServer(csLayout));
|
||||||
|
builder.Services.AddDbContext<HelloFreshContext>(o => o.UseSqlServer(csHello));
|
||||||
|
builder.Services.AddDbContext<FinancesContext>(o => o.UseSqlServer(csFinance));
|
||||||
|
|
||||||
AppSettings.Initialize(builder.Configuration);
|
AppSettings.Initialize(builder.Configuration);
|
||||||
builder.Services.AddSingleton<OllamaService>();
|
builder.Services.AddSingleton<OllamaService>();
|
||||||
@@ -118,6 +135,12 @@ var ollamaBuilder = builder.Services.AddHttpClient("ollama", (sp, client) =>
|
|||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
// ==============================================
|
// ==============================================
|
||||||
// 6️⃣ Pipeline
|
// 6️⃣ Pipeline
|
||||||
|
app.MapGet("/_dbping", async (HelloFreshContext hf, LayoutDataContext ld) =>
|
||||||
|
{
|
||||||
|
try { await hf.Database.ExecuteSqlRawAsync("SELECT 1"); } catch (Exception ex) { return Results.Problem($"HelloFresh: {ex.GetBaseException().Message}"); }
|
||||||
|
try { await ld.Database.ExecuteSqlRawAsync("SELECT 1"); } catch (Exception ex) { return Results.Problem($"LayoutData: {ex.GetBaseException().Message}"); }
|
||||||
|
return Results.Ok("OK");
|
||||||
|
});
|
||||||
|
|
||||||
if (!app.Environment.IsDevelopment())
|
if (!app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -39,4 +39,15 @@
|
|||||||
<ul id="neededIngredients" class="hf-grid"></ul>
|
<ul id="neededIngredients" class="hf-grid"></ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@{
|
||||||
|
var currentUser = Context.Session.GetString("UserName") ?? "";
|
||||||
|
bool isCore = currentUser.Equals("Mae", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| currentUser.Equals("Byakuya", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
<script>
|
||||||
|
window.CURRENT_USER = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(currentUser));
|
||||||
|
window.IS_CORE_USER = @Html.Raw(isCore.ToString().ToLower());
|
||||||
|
// 👉 liste blanche pour la vue "partagée"
|
||||||
|
window.CORE_USERS = ["Mae","Byakuya"];
|
||||||
|
</script>
|
||||||
<script src="~/js/HelloFresh/ingredients.js" asp-append-version="true"></script>
|
<script src="~/js/HelloFresh/ingredients.js" asp-append-version="true"></script>
|
||||||
|
|||||||
@@ -28,15 +28,20 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AngleSharp" Version="1.3.0" />
|
<PackageReference Include="AngleSharp" Version="1.3.0" />
|
||||||
<PackageReference Include="DotNetEnv" Version="3.1.1" />
|
<PackageReference Include="DotNetEnv" Version="3.1.1" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.20" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.20">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.7" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.20" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.20">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.SqlServer.Server" Version="1.0.0" />
|
<PackageReference Include="Microsoft.SqlServer.Server" Version="1.0.0" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.*" />
|
||||||
<PackageReference Include="System.Data.SqlClient" Version="4.9.0" />
|
<PackageReference Include="System.Data.SqlClient" Version="4.9.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
{
|
{
|
||||||
"Ollama": {
|
|
||||||
"Url": "https://ollama.byakurepo.online",
|
|
||||||
"Model": "qwen2.5:1.5b-instruct" // plus rapide que 3b
|
|
||||||
},
|
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Administration.Controllers.SearchController": "Debug",
|
"Microsoft.AspNetCore": "Warning"
|
||||||
"administration.Controllers.HelloFreshController": "Debug"
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "Server=217.154.116.43,1433;Database=LayoutData;User Id=admin_app;Password=A!pP_2025#V9yTb3Q;Encrypt=True;TrustServerCertificate=True;MultipleActiveResultSets=True;",
|
||||||
|
"HelloFresh": "Server=217.154.116.43,1433;Database=HelloFresh;User Id=admin_app;Password=A!pP_2025#V9yTb3Q;Encrypt=True;TrustServerCertificate=True;MultipleActiveResultSets=True;",
|
||||||
|
"Finances": "Server=217.154.116.43,1433;Database=Finances;User Id=admin_app;Password=A!pP_2025#V9yTb3Q;Encrypt=True;TrustServerCertificate=True;MultipleActiveResultSets=True;"
|
||||||
|
},
|
||||||
|
|
||||||
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,6 @@
|
|||||||
"Model": "qwen2.5:3b-instruct"
|
"Model": "qwen2.5:3b-instruct"
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"HelloFreshConnection": "Server=217.154.116.43;Database=HelloFresh;User Id=sa;Password=Wi0a6kxXjAhtSswP22FYZW9UBxRrgght8MMncp8F;TrustServerCertificate=True;Encrypt=False"
|
"HelloFreshConnection": "Server=217.154.116.43;Database=HelloFresh;User Id=admin_app;Password=A!pP_2025#V9yTb3Q;TrustServerCertificate=True;Encrypt=False"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,13 +33,6 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,19 @@ const splitTags = (str) => (str || '').split(/[,•]/).map(t => t.trim()).filter
|
|||||||
const esc = (s) => { const d = document.createElement('div'); d.textContent = (s ?? ''); return d.innerHTML; };
|
const esc = (s) => { const d = document.createElement('div'); d.textContent = (s ?? ''); return d.innerHTML; };
|
||||||
function capFirst(s) { if (s == null) return ''; s = s.toString().trim(); if (!s) return ''; const f = s[0].toLocaleUpperCase('fr-FR'); return f + s.slice(1); }
|
function capFirst(s) { if (s == null) return ''; s = s.toString().trim(); if (!s) return ''; const f = s[0].toLocaleUpperCase('fr-FR'); return f + s.slice(1); }
|
||||||
|
|
||||||
|
const DEBOUNCE_MS = 250;
|
||||||
|
function debounce(fn, wait = DEBOUNCE_MS) {
|
||||||
|
let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); };
|
||||||
|
}
|
||||||
|
function onlySearchActive() {
|
||||||
|
return normalize(currentSearchTerm) !== '' &&
|
||||||
|
appliedFilters.ingredients.length === 0 &&
|
||||||
|
appliedFilters.tags.length === 0 &&
|
||||||
|
appliedFilters.timeMin == null &&
|
||||||
|
appliedFilters.timeMax == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/***********************
|
/***********************
|
||||||
* COLORS for tags (persistées localStorage)
|
* COLORS for tags (persistées localStorage)
|
||||||
***********************/
|
***********************/
|
||||||
@@ -326,28 +339,44 @@ function buildRecipeCardOwned(recipe) {
|
|||||||
async function loadRecipes(page = 1) {
|
async function loadRecipes(page = 1) {
|
||||||
try {
|
try {
|
||||||
const url = new URL('/HelloFresh/GetRecipesByPage', window.location.origin);
|
const url = new URL('/HelloFresh/GetRecipesByPage', window.location.origin);
|
||||||
url.searchParams.set('page', page); url.searchParams.set('count', countPerPage);
|
url.searchParams.set('page', page);
|
||||||
const res = await fetch(url.toString()); if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
url.searchParams.set('count', countPerPage);
|
||||||
|
|
||||||
|
// 👉 On laisse le serveur faire la recherche quand il n'y a QUE la recherche active
|
||||||
|
if (onlySearchActive()) {
|
||||||
|
url.searchParams.set('search', currentSearchTerm || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url.toString());
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
const { recipes, currentPage: serverPage, lastPage } = data;
|
const { recipes, currentPage: serverPage, lastPage } = data;
|
||||||
currentPage = serverPage; lastPageGlobal = lastPage;
|
currentPage = serverPage; lastPageGlobal = lastPage;
|
||||||
|
|
||||||
const grid = document.getElementById('recipeGrid'); grid.innerHTML = '';
|
const grid = document.getElementById('recipeGrid'); grid.innerHTML = '';
|
||||||
if (!recipes || recipes.length === 0) {
|
if (!recipes || recipes.length === 0) {
|
||||||
grid.innerHTML = '<p class="placeholder">Aucune recette disponible pour le moment.</p>';
|
grid.innerHTML = '<p class="placeholder">Aucune recette disponible pour le moment.</p>';
|
||||||
renderPagination(currentPage, lastPageGlobal); return;
|
renderPagination(currentPage, lastPageGlobal);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
recipes.forEach(r => grid.appendChild(buildRecipeCardList(r)));
|
recipes.forEach(r => grid.appendChild(buildRecipeCardList(r)));
|
||||||
if (window.tippy) {
|
if (window.tippy) {
|
||||||
tippy('[data-tippy-content]', { placement: 'top', arrow: true, delay: [100, 0], theme: 'light-border', maxWidth: 250, allowHTML: true });
|
tippy('[data-tippy-content]', { placement: 'top', arrow: true, delay: [100, 0], theme: 'light-border', maxWidth: 250, allowHTML: true });
|
||||||
}
|
}
|
||||||
await hydrateIngredientsForPage(recipes);
|
await hydrateIngredientsForPage(recipes);
|
||||||
refreshSelectedBorders();
|
refreshSelectedBorders();
|
||||||
if ((currentSearchTerm ?? '').trim() !== '') applySearchFilter();
|
|
||||||
|
// ⚠️ si tu avais une ancienne fonction applySearchFilter(), on ne l'appelle plus ici.
|
||||||
|
// if ((currentSearchTerm ?? '').trim() !== '' && typeof applySearchFilter === 'function') applySearchFilter();
|
||||||
|
|
||||||
refreshDoneFlagsOnVisibleCards();
|
refreshDoneFlagsOnVisibleCards();
|
||||||
renderPagination(currentPage, lastPageGlobal);
|
renderPagination(currentPage, lastPageGlobal);
|
||||||
} catch (e) { console.error('loadRecipes error', e); }
|
} catch (e) { console.error('loadRecipes error', e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function loadRecipesOwned({ onEmpty = 'placeholder' } = {}) {
|
async function loadRecipesOwned({ onEmpty = 'placeholder' } = {}) {
|
||||||
const gridOwned = document.getElementById('recipeGridOwned'); if (!gridOwned) return false;
|
const gridOwned = document.getElementById('recipeGridOwned'); if (!gridOwned) return false;
|
||||||
const wasOpen = document.getElementById('recipeOwnedWrap')?.classList.contains('open');
|
const wasOpen = document.getElementById('recipeOwnedWrap')?.classList.contains('open');
|
||||||
@@ -450,19 +479,40 @@ function anyFilterActive() {
|
|||||||
appliedFilters.timeMin != null ||
|
appliedFilters.timeMin != null ||
|
||||||
appliedFilters.timeMax != null;
|
appliedFilters.timeMax != null;
|
||||||
}
|
}
|
||||||
async function fetchAllRecipes() {
|
|
||||||
if (ALL_RECIPES_CACHE) return ALL_RECIPES_CACHE;
|
async function fetchAllRecipes(opts = {}) {
|
||||||
|
const search = (opts.search || '').trim();
|
||||||
|
if (!search && ALL_RECIPES_CACHE) return ALL_RECIPES_CACHE;
|
||||||
|
|
||||||
let page = 1, last = 1, all = [];
|
let page = 1, last = 1, all = [];
|
||||||
do {
|
do {
|
||||||
const url = new URL('/HelloFresh/GetRecipesByPage', window.location.origin);
|
const url = new URL('/HelloFresh/GetRecipesByPage', window.location.origin);
|
||||||
url.searchParams.set('page', page); url.searchParams.set('count', countPerPage);
|
url.searchParams.set('page', page);
|
||||||
const res = await fetch(url.toString()); if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
url.searchParams.set('count', countPerPage);
|
||||||
|
if (search) url.searchParams.set('search', search);
|
||||||
|
|
||||||
|
const res = await fetch(url.toString());
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
all.push(...(data.recipes || []));
|
all.push(...(data.recipes || []));
|
||||||
last = data.lastPage || 1; page++;
|
last = data.lastPage || 1;
|
||||||
|
page++;
|
||||||
} while (page <= last);
|
} while (page <= last);
|
||||||
ALL_RECIPES_CACHE = all; return all;
|
|
||||||
|
if (!search) ALL_RECIPES_CACHE = all;
|
||||||
|
return all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchDetailsBatched(ids, batchSize = 150) {
|
||||||
|
const out = [];
|
||||||
|
for (let i = 0; i < ids.length; i += batchSize) {
|
||||||
|
const part = ids.slice(i, i + batchSize);
|
||||||
|
const res = await fetch(`/HelloFresh/GetRecipesDetails?ids=${encodeURIComponent(part.join(','))}`);
|
||||||
|
if (res.ok) out.push(...(await res.json()));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
function recipeMatchesFilters(cardMeta) {
|
function recipeMatchesFilters(cardMeta) {
|
||||||
const term = normalize(currentSearchTerm);
|
const term = normalize(currentSearchTerm);
|
||||||
if (term && !cardMeta.name.includes(term)) return false;
|
if (term && !cardMeta.name.includes(term)) return false;
|
||||||
@@ -487,14 +537,16 @@ function extractIngredientNames(ingredientsJson) {
|
|||||||
} catch { }
|
} catch { }
|
||||||
return [...out];
|
return [...out];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildMetaFor(recipes) {
|
async function buildMetaFor(recipes) {
|
||||||
const ids = recipes.map(r => r.id).join(',');
|
const ids = recipes.map(r => r.id);
|
||||||
const metaById = {};
|
const metaById = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/HelloFresh/GetRecipesDetails?ids=${encodeURIComponent(ids)}`);
|
const details = await fetchDetailsBatched(ids, 150); // ← chunks pour éviter les URLs géantes
|
||||||
const details = await res.json(); // [{id, ingredientsJson}]
|
|
||||||
details.forEach(d => { metaById[d.id] = { ing: extractIngredientNames(d.ingredientsJson).map(normalize) }; });
|
details.forEach(d => { metaById[d.id] = { ing: extractIngredientNames(d.ingredientsJson).map(normalize) }; });
|
||||||
} catch (e) { console.warn('details fail', e); }
|
} catch (e) { console.warn('details fail', e); }
|
||||||
|
|
||||||
recipes.forEach(r => {
|
recipes.forEach(r => {
|
||||||
const tagsVal = (r.tags ?? r.Tags ?? '').trim();
|
const tagsVal = (r.tags ?? r.Tags ?? '').trim();
|
||||||
const tags = splitTags(tagsVal).map(normalize);
|
const tags = splitTags(tagsVal).map(normalize);
|
||||||
@@ -502,24 +554,45 @@ async function buildMetaFor(recipes) {
|
|||||||
const name = normalize(r.name || '');
|
const name = normalize(r.name || '');
|
||||||
metaById[r.id] = Object.assign({ tags, time, name, ing: [] }, metaById[r.id] || {});
|
metaById[r.id] = Object.assign({ tags, time, name, ing: [] }, metaById[r.id] || {});
|
||||||
});
|
});
|
||||||
|
|
||||||
return metaById;
|
return metaById;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyAllFilters() {
|
async function applyAllFilters() {
|
||||||
if (!anyFilterActive()) {
|
const hasSearch = normalize(currentSearchTerm) !== '';
|
||||||
|
const hasOtherFilters = appliedFilters.ingredients.length || appliedFilters.tags.length ||
|
||||||
|
appliedFilters.timeMin != null || appliedFilters.timeMax != null;
|
||||||
|
|
||||||
|
// A) Rien d'actif → liste classique
|
||||||
|
if (!hasSearch && !hasOtherFilters) {
|
||||||
CURRENT_CLIENT_LIST = null;
|
CURRENT_CLIENT_LIST = null;
|
||||||
await loadRecipes(1);
|
await loadRecipes(1);
|
||||||
updateActiveFilterBadge(); // peut masquer le badge si rien d’actif
|
updateActiveFilterBadge();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const all = await fetchAllRecipes();
|
|
||||||
|
// B) Uniquement la recherche → serveur (rapide, paginé)
|
||||||
|
if (hasSearch && !hasOtherFilters) {
|
||||||
|
CURRENT_CLIENT_LIST = null; // on revient au flux serveur paginé
|
||||||
|
currentPage = 1;
|
||||||
|
await loadRecipes(1); // loadRecipes enverra ?search=...
|
||||||
|
updateActiveFilterBadge();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// C) Recherche + (tags/ingrédients/temps) OU filtres sans recherche
|
||||||
|
// → on réduit d'abord côté serveur avec ?search=..., puis filtre côté client
|
||||||
|
const all = await fetchAllRecipes({ search: hasSearch ? currentSearchTerm : '' });
|
||||||
const meta = await buildMetaFor(all);
|
const meta = await buildMetaFor(all);
|
||||||
const filtered = all.filter(r => recipeMatchesFilters(meta[r.id] || {}));
|
const filtered = all.filter(r => recipeMatchesFilters(meta[r.id] || {}));
|
||||||
|
|
||||||
CURRENT_CLIENT_LIST = filtered;
|
CURRENT_CLIENT_LIST = filtered;
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
lastPageGlobal = Math.max(1, Math.ceil(filtered.length / countPerPage));
|
lastPageGlobal = Math.max(1, Math.ceil(filtered.length / countPerPage));
|
||||||
await renderClientPage();
|
await renderClientPage();
|
||||||
updateActiveFilterBadge(); // badge = basé sur appliedFilters UNIQUEMENT
|
updateActiveFilterBadge();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderClientPage() {
|
async function renderClientPage() {
|
||||||
const list = CURRENT_CLIENT_LIST || [];
|
const list = CURRENT_CLIENT_LIST || [];
|
||||||
const start = (currentPage - 1) * countPerPage;
|
const start = (currentPage - 1) * countPerPage;
|
||||||
@@ -809,9 +882,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
// Live search
|
// Live search
|
||||||
const searchInput = document.querySelector('#divSearch input');
|
const searchInput = document.querySelector('#divSearch input');
|
||||||
if (searchInput) {
|
if (searchInput) {
|
||||||
searchInput.addEventListener('input', async (e) => {
|
const debounced = debounce(async () => { await applyAllFilters(); }, 250);
|
||||||
|
searchInput.addEventListener('input', (e) => {
|
||||||
currentSearchTerm = e.target.value;
|
currentSearchTerm = e.target.value;
|
||||||
await applyAllFilters(); // search est immédiat
|
debounced();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,18 @@ const els = { list: null, search: null };
|
|||||||
// --- Exclusions par recette ---
|
// --- Exclusions par recette ---
|
||||||
const RECIPE_ING_CACHE = new Map(); // id -> { '1': [noms], '2': [...], ... }
|
const RECIPE_ING_CACHE = new Map(); // id -> { '1': [noms], '2': [...], ... }
|
||||||
const EXCLUDED_BY_RECIPE = new Map(); // id -> Set(noms normalisés)
|
const EXCLUDED_BY_RECIPE = new Map(); // id -> Set(noms normalisés)
|
||||||
|
|
||||||
|
const GET_ALL_RECIPES_URL = "/HelloFresh/GetAllRecipes";
|
||||||
|
const GET_MY_OWNED_URL = "/HelloFresh/GetRecipesOwned?isUserImportant=true";
|
||||||
|
|
||||||
|
const CORE_USERS = Array.isArray(window.CORE_USERS) ? window.CORE_USERS : ["Mae", "Byakuya"];
|
||||||
|
|
||||||
// État “Ingrédients à avoir”
|
// État “Ingrédients à avoir”
|
||||||
let LAST_ING_TO_HAD = null;
|
let LAST_ING_TO_HAD = null;
|
||||||
let LAST_PORTIONS = 1;
|
let LAST_PORTIONS = 1;
|
||||||
let lastNeededSignature = "";
|
let lastNeededSignature = "";
|
||||||
let prevNotShippedCount = -1;
|
let prevNotShippedCount = -1;
|
||||||
|
const IS_CORE_USER = (window.IS_CORE_USER === true || window.IS_CORE_USER === 'true');
|
||||||
|
|
||||||
const ACCENT_RX = /[\u0300-\u036f]/g;
|
const ACCENT_RX = /[\u0300-\u036f]/g;
|
||||||
const normalizeName = (s) => (s || "").trim().toLowerCase();
|
const normalizeName = (s) => (s || "").trim().toLowerCase();
|
||||||
@@ -295,21 +302,40 @@ function reapplyCurrentSearch() {
|
|||||||
async function loadAndRenderIngredients() {
|
async function loadAndRenderIngredients() {
|
||||||
if (!els.list) return;
|
if (!els.list) return;
|
||||||
els.list.innerHTML = "Chargement…";
|
els.list.innerHTML = "Chargement…";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/HelloFresh/AggregatedIngredients");
|
if (IS_CORE_USER) {
|
||||||
if (!res.ok) { const txt = await res.text().catch(() => ""); console.error("[AggregatedIngredients] HTTP", res.status, txt); els.list.innerHTML = `Erreur chargement ingrédients (${res.status})`; return; }
|
// 🔒 Vue partagée: ingrédients = uniquement Mae/Byakuya
|
||||||
const data = await res.json();
|
const url = `/HelloFresh/GetRecipesOwnedByUsers?names=${encodeURIComponent(CORE_USERS.join(","))}`;
|
||||||
ALL_AGG_INGS = Array.isArray(data) ? data : [];
|
const res = await fetch(url, { credentials: "same-origin" });
|
||||||
OWNED_SET = new Set(ALL_AGG_INGS.filter(x => x?.Name && x.Owned === true).map(x => x.Name.trim().toLowerCase()));
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
const rows = await res.json();
|
||||||
|
window.allRecipes = Array.isArray(rows) ? rows : [];
|
||||||
|
|
||||||
|
const items = await buildAggregatedIngredientsFromRows(window.allRecipes);
|
||||||
|
ALL_AGG_INGS = items;
|
||||||
|
|
||||||
|
const list = sortIngredientsForDisplay(applyExclusionsTo(items));
|
||||||
|
els.list.innerHTML = "";
|
||||||
|
renderItems(list.length ? list : []);
|
||||||
|
if (!list.length) els.list.innerHTML = "<em>Aucun ingrédient</em>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utilisateur "tiers" : ingrédients = SES recettes (déjà en place)
|
||||||
|
const mineAgg = await buildMyAggregatedIngredients();
|
||||||
|
ALL_AGG_INGS = mineAgg;
|
||||||
|
const list = sortIngredientsForDisplay(applyExclusionsTo(mineAgg));
|
||||||
els.list.innerHTML = "";
|
els.list.innerHTML = "";
|
||||||
const initialMerged = mergeDuplicates([...ALL_AGG_INGS]);
|
renderItems(list.length ? list : []);
|
||||||
const initial = sortIngredientsForDisplay(applyExclusionsTo(initialMerged));
|
if (!list.length) els.list.innerHTML = "<em>Aucun ingrédient</em>";
|
||||||
els.list.innerHTML = "";
|
} catch (e) {
|
||||||
renderItems(initial.length ? initial : []);
|
console.error("[ingredients] loadAndRenderIngredients exception", e);
|
||||||
if (!initial.length) els.list.innerHTML = "<em>Aucun ingrédient</em>";
|
els.list.innerHTML = "Erreur chargement ingrédients";
|
||||||
} catch (e) { console.error("[AggregatedIngredients] exception", e); els.list.innerHTML = "Erreur chargement ingrédients"; }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ========== Not shipped (DOM) + BDD ==========
|
// ========== Not shipped (DOM) + BDD ==========
|
||||||
function getNotShippedIngredientsFromDOM(root = document) {
|
function getNotShippedIngredientsFromDOM(root = document) {
|
||||||
const nodes = Array.from(root.querySelectorAll(".ingredient-item-not-shipped"));
|
const nodes = Array.from(root.querySelectorAll(".ingredient-item-not-shipped"));
|
||||||
@@ -536,13 +562,15 @@ function buildRecipeCardList(recipe) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const usersVal = recipe.Users ?? recipe.users ?? recipe.User ?? recipe.user ?? [];
|
if (IS_CORE_USER) {
|
||||||
const usersArr = Array.isArray(usersVal) ? usersVal.filter(Boolean) : String(usersVal || "").split(/[,•]/).map(s => s.trim()).filter(Boolean);
|
const usersVal = recipe.Users ?? recipe.users ?? recipe.User ?? recipe.user ?? [];
|
||||||
if (usersArr.length) {
|
const usersArr = Array.isArray(usersVal) ? usersVal.filter(Boolean) : String(usersVal || "").split(/[,•]/).map(s => s.trim()).filter(Boolean);
|
||||||
const img = card.querySelector(".image-container");
|
if (usersArr.length) {
|
||||||
const wrap = document.createElement("div"); wrap.className = "label-container";
|
const img = card.querySelector(".image-container");
|
||||||
usersArr.forEach(u => { const span = document.createElement("span"); span.className = "label-badge user-badge"; span.textContent = u; wrap.appendChild(span); });
|
const wrap = document.createElement("div"); wrap.className = "label-container";
|
||||||
img?.appendChild(wrap);
|
usersArr.forEach(u => { const span = document.createElement("span"); span.className = "label-badge user-badge"; span.textContent = u; wrap.appendChild(span); });
|
||||||
|
img?.appendChild(wrap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mémorise la dernière recette pour un fallback, via détection auto
|
// Mémorise la dernière recette pour un fallback, via détection auto
|
||||||
@@ -558,42 +586,227 @@ function buildRecipeCardList(recipe) {
|
|||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GET_ALL_RECIPES_URL = "/HelloFresh/GetAllRecipes";
|
|
||||||
async function loadAllRecipesHorizontal() {
|
async function loadAllRecipesHorizontal() {
|
||||||
const strip = document.getElementById("recipeStrip");
|
const strip = document.getElementById("recipeStrip");
|
||||||
if (!strip) return;
|
if (!strip) return;
|
||||||
|
|
||||||
try {
|
// === Vue "partagée" (Mae/Byakuya) : ne montrer QUE leurs recettes ===
|
||||||
const r = await fetch("/HelloFresh/GetRecipesOwned?isUserImportant=false", { credentials: "same-origin" });
|
if (IS_CORE_USER) {
|
||||||
if (r.ok) { const data = await r.json(); ownedMap.clear(); (data || []).forEach(x => ownedMap.set(String(x.id), clampQty(x.portions || 0))); }
|
try {
|
||||||
} catch { }
|
const url = `/HelloFresh/GetRecipesOwnedByUsers?names=${encodeURIComponent(CORE_USERS.join(","))}`;
|
||||||
|
const res = await fetch(url, { credentials: "same-origin" });
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
const onlyCore = await res.json();
|
||||||
|
window.allRecipes = Array.isArray(onlyCore) ? onlyCore : [];
|
||||||
|
|
||||||
|
strip.innerHTML = "";
|
||||||
|
if (!window.allRecipes.length) {
|
||||||
|
strip.innerHTML = '<p class="placeholder">Aucune recette (Mae/Byakuya).</p>';
|
||||||
|
updateNeededList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.allRecipes.sort((a, b) => String(a.name ?? "").localeCompare(String(b.name ?? ""), "fr", { sensitivity: "base" }));
|
||||||
|
window.allRecipes.forEach(r => strip.appendChild(buildRecipeCardList(r)));
|
||||||
|
updateNeededList("afterCoreRecipes");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("loadAllRecipesHorizontal (core) error", e);
|
||||||
|
strip.innerHTML = '<p class="placeholder">Erreur de chargement.</p>';
|
||||||
|
updateNeededList();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Utilisateur "tiers" : uniquement SES recettes ===
|
||||||
try {
|
try {
|
||||||
const res = await fetch(GET_ALL_RECIPES_URL, { credentials: "same-origin" });
|
const res = await fetch(GET_MY_OWNED_URL, { credentials: "same-origin" });
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
const all = await res.json();
|
const mine = await res.json();
|
||||||
window.allRecipes = all;
|
window.allRecipes = Array.isArray(mine) ? mine : [];
|
||||||
|
|
||||||
strip.innerHTML = "";
|
strip.innerHTML = "";
|
||||||
if (!Array.isArray(all) || all.length === 0) { strip.innerHTML = '<p class="placeholder">Aucune recette disponible.</p>'; updateNeededList(); return; }
|
if (!window.allRecipes.length) {
|
||||||
|
strip.innerHTML = '<p class="placeholder">Vous n’avez pas encore de recettes sélectionnées.</p>';
|
||||||
all.sort((a, b) => {
|
updateNeededList();
|
||||||
const ua = String(a.userId ?? ""), ub = String(b.userId ?? "");
|
return;
|
||||||
if (ua !== ub) return ua.localeCompare(ub);
|
}
|
||||||
return String(a.name ?? "").localeCompare(String(b.name ?? ""), "fr", { sensitivity: "base" });
|
|
||||||
});
|
|
||||||
|
|
||||||
all.forEach(r => strip.appendChild(buildRecipeCardList(r)));
|
|
||||||
updateNeededList("afterGetAllRecipes");
|
|
||||||
console.debug("[needed] recipes loaded:", Array.isArray(window.allRecipes) ? window.allRecipes.length : 0);
|
|
||||||
|
|
||||||
|
window.allRecipes.sort((a, b) => String(a.name ?? "").localeCompare(String(b.name ?? ""), "fr", { sensitivity: "base" }));
|
||||||
|
window.allRecipes.forEach(r => strip.appendChild(buildRecipeCardList(r)));
|
||||||
|
updateNeededList("afterGetMyRecipes");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("loadAllRecipesHorizontal error", e);
|
console.error("loadAllRecipesHorizontal (mine) error", e);
|
||||||
strip.innerHTML = '<p class="placeholder">Erreur de chargement.</p>';
|
strip.innerHTML = '<p class="placeholder">Erreur de chargement.</p>';
|
||||||
updateNeededList();
|
updateNeededList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshOwnedSet() {
|
||||||
|
try {
|
||||||
|
const r = await fetch("/HelloFresh/GetOwnedIngredients");
|
||||||
|
if (r.ok) {
|
||||||
|
const names = await r.json();
|
||||||
|
OWNED_SET = new Set((names || []).map(normalizeName));
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildAggregatedIngredientsFromRows(rows) {
|
||||||
|
const ids = (rows || []).map(r => r.id);
|
||||||
|
const details = await fetchDetailsBatched(ids);
|
||||||
|
const byId = new Map(details.map(d => [String(d.id), d]));
|
||||||
|
await refreshOwnedSet();
|
||||||
|
|
||||||
|
const rawItems = [];
|
||||||
|
for (const r of (rows || [])) {
|
||||||
|
const det = byId.get(String(r.id));
|
||||||
|
if (!det || !det.ingredientsJson) continue;
|
||||||
|
|
||||||
|
let portions = Number(r.portions || 1);
|
||||||
|
if (!Number.isFinite(portions) || portions < 1) portions = 1;
|
||||||
|
|
||||||
|
const base = Math.min(4, Math.max(1, portions));
|
||||||
|
const scale = (portions <= 4) ? 1 : (portions / 4);
|
||||||
|
|
||||||
|
let obj; try { obj = JSON.parse(det.ingredientsJson); } catch { obj = null; }
|
||||||
|
const arr = (obj && Array.isArray(obj[String(base)])) ? obj[String(base)] : [];
|
||||||
|
for (const el of arr) {
|
||||||
|
const it = normalizeItem(el);
|
||||||
|
if (!it.Name) continue;
|
||||||
|
|
||||||
|
const { value, unit } = parseQuantity(it.Quantity);
|
||||||
|
if (!Number.isNaN(value) && scale !== 1) {
|
||||||
|
const scaled = value * scale;
|
||||||
|
it.Quantity = unit ? `${fmtNumber(scaled)} ${unit}` : fmtNumber(scaled);
|
||||||
|
}
|
||||||
|
it.Owned = isOwned(it.Name);
|
||||||
|
rawItems.push(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return aggregateRawItems(rawItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function fmtNumber(n) {
|
||||||
|
const i = Math.round(n);
|
||||||
|
return (Math.abs(n - i) < 1e-9) ? String(i) : String(Number(n.toFixed(2))).replace(/\.?0+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function aggregateRawItems(rawItems) {
|
||||||
|
// Somme numérique par (name, unit) + “meilleure” quantité textuelle sinon
|
||||||
|
const numMap = new Map(); // key: name##unit -> {sum, unit, item}
|
||||||
|
const textMap = new Map(); // key: name -> {set:Set(qty), item}
|
||||||
|
|
||||||
|
for (const raw of rawItems) {
|
||||||
|
const it = normalizeItem(raw);
|
||||||
|
if (!it.Name) continue;
|
||||||
|
const keyName = normalizeName(it.Name);
|
||||||
|
const q = it.Quantity || "";
|
||||||
|
const { value, unit } = parseQuantity(q);
|
||||||
|
|
||||||
|
if (!Number.isNaN(value)) {
|
||||||
|
const k = `${keyName}##${unit || ""}`;
|
||||||
|
const prev = numMap.get(k);
|
||||||
|
if (!prev) {
|
||||||
|
numMap.set(k, { sum: value, unit: unit || "", item: { ...it } });
|
||||||
|
} else {
|
||||||
|
prev.sum += value;
|
||||||
|
// garder une image si absente
|
||||||
|
if (!prev.item.Image && it.Image) prev.item.Image = it.Image;
|
||||||
|
prev.item.Owned = prev.item.Owned || it.Owned;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const p = textMap.get(keyName);
|
||||||
|
if (!p) {
|
||||||
|
const s = new Set(); if (q) s.add(q);
|
||||||
|
textMap.set(keyName, { set: s, item: { ...it } });
|
||||||
|
} else {
|
||||||
|
if (q) p.set.add(q);
|
||||||
|
if (!p.item.Image && it.Image) p.item.Image = it.Image;
|
||||||
|
p.item.Owned = p.item.Owned || it.Owned;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = [];
|
||||||
|
|
||||||
|
// numérique → format “val unit”
|
||||||
|
for (const { sum, unit, item } of numMap.values()) {
|
||||||
|
const qty = unit ? `${fmtNumber(sum)} ${unit}` : fmtNumber(sum);
|
||||||
|
out.push({ ...item, Quantity: qty });
|
||||||
|
}
|
||||||
|
|
||||||
|
// textuel → pickBestQuantity
|
||||||
|
for (const { set, item } of textMap.values()) {
|
||||||
|
const qty = pickBestQuantity(Array.from(set));
|
||||||
|
out.push({ ...item, Quantity: qty });
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDetailsBatched(ids, batchSize = 120) {
|
||||||
|
const all = [];
|
||||||
|
for (let i = 0; i < ids.length; i += batchSize) {
|
||||||
|
const part = ids.slice(i, i + batchSize);
|
||||||
|
const res = await fetch(`/HelloFresh/GetRecipesDetails?ids=${encodeURIComponent(part.join(','))}`);
|
||||||
|
if (res.ok) all.push(...await res.json());
|
||||||
|
}
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildMyAggregatedIngredients() {
|
||||||
|
// 1) mes recettes + portions
|
||||||
|
const r = await fetch(GET_MY_OWNED_URL, { credentials: "same-origin" });
|
||||||
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||||
|
const mine = await r.json(); // [{id, portions, ...}]
|
||||||
|
|
||||||
|
if (!Array.isArray(mine) || mine.length === 0) return [];
|
||||||
|
|
||||||
|
// 2) détails (ingrédients par 1..4)
|
||||||
|
const ids = mine.map(x => x.id);
|
||||||
|
const details = await fetchDetailsBatched(ids);
|
||||||
|
const byId = new Map(details.map(d => [String(d.id), d]));
|
||||||
|
|
||||||
|
// 3) Owned flags (facultatif, pour tri visuel)
|
||||||
|
await refreshOwnedSet();
|
||||||
|
|
||||||
|
// 4) construire la liste brute + mise à l’échelle des quantités (>4 portions)
|
||||||
|
const rawItems = [];
|
||||||
|
for (const rcp of mine) {
|
||||||
|
const id = String(rcp.id);
|
||||||
|
const det = byId.get(id);
|
||||||
|
if (!det || !det.ingredientsJson) continue;
|
||||||
|
|
||||||
|
let portions = Number(rcp.portions || 1);
|
||||||
|
if (!Number.isFinite(portions) || portions < 1) portions = 1;
|
||||||
|
|
||||||
|
const base = Math.min(4, Math.max(1, portions));
|
||||||
|
const scale = (portions <= 4) ? 1 : (portions / 4);
|
||||||
|
|
||||||
|
let obj;
|
||||||
|
try { obj = JSON.parse(det.ingredientsJson); } catch { obj = null; }
|
||||||
|
const arr = (obj && Array.isArray(obj[String(base)])) ? obj[String(base)] : [];
|
||||||
|
for (const el of arr) {
|
||||||
|
const it = normalizeItem(el);
|
||||||
|
if (!it.Name) continue;
|
||||||
|
|
||||||
|
// scale quantité si numérique
|
||||||
|
const { value, unit } = parseQuantity(it.Quantity);
|
||||||
|
if (!Number.isNaN(value) && scale !== 1) {
|
||||||
|
const scaled = value * scale;
|
||||||
|
it.Quantity = unit ? `${fmtNumber(scaled)} ${unit}` : fmtNumber(scaled);
|
||||||
|
}
|
||||||
|
|
||||||
|
it.Owned = isOwned(it.Name);
|
||||||
|
rawItems.push(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return aggregateRawItems(rawItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ======================
|
// ======================
|
||||||
// Init
|
// Init
|
||||||
// ======================
|
// ======================
|
||||||
|
|||||||
@@ -55,6 +55,22 @@
|
|||||||
|
|
||||||
})(jQuery); // End of use strict
|
})(jQuery); // End of use strict
|
||||||
|
|
||||||
|
async function openRecipePdfById(id, initialUrl) {
|
||||||
|
let pdfUrl = initialUrl || '';
|
||||||
|
if (!pdfUrl) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/HelloFresh/GetRecipesDetails?ids=${encodeURIComponent(id)}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const arr = await res.json();
|
||||||
|
pdfUrl = (arr?.[0]?.pdf || arr?.[0]?.Pdf || '');
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
if (!pdfUrl) { alert("Aucun PDF pour cette recette."); return; }
|
||||||
|
const proxied = `/HelloFresh/ProxyPdf?url=${encodeURIComponent(pdfUrl)}`;
|
||||||
|
showPdfModal(proxied);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 💶 Formate un nombre en chaîne de caractères au format français avec division par 1000.
|
* 💶 Formate un nombre en chaîne de caractères au format français avec division par 1000.
|
||||||
* Exemples :
|
* Exemples :
|
||||||
|
|||||||
Reference in New Issue
Block a user