From 6bba003cb6a0045892703ee21bf96edaa40ceb4f Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Wed, 25 Jun 2025 21:27:59 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20Ajuste=20para=20grava=C3=A7=C3=A3o=20de?= =?UTF-8?q?=20IDs=20de=20links?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/BCards.Web/Controllers/AdminController.cs | 272 +++-- .../ViewModels/ManagePageViewModel.cs | 158 ++- src/BCards.Web/Views/Admin/ManagePage.cshtml | 931 +++++++++++------- 3 files changed, 904 insertions(+), 457 deletions(-) diff --git a/src/BCards.Web/Controllers/AdminController.cs b/src/BCards.Web/Controllers/AdminController.cs index 5c82e6c..e64c444 100644 --- a/src/BCards.Web/Controllers/AdminController.cs +++ b/src/BCards.Web/Controllers/AdminController.cs @@ -138,6 +138,29 @@ public class AdminController : Controller _logger.LogInformation($"ManagePage POST: IsNewPage={model.IsNewPage}, DisplayName={model.DisplayName}, Category={model.Category}, Links={model.Links?.Count ?? 0}"); + // Filtrar links vazios ANTES da validação + if (model.Links != null) + { + model.Links = model.Links.Where(l => !l.IsEmpty).ToList(); + + // Reordenar os links restantes + for (int i = 0; i < model.Links.Count; i++) + { + model.Links[i].Order = i; + } + } + + foreach (var link in model.Links) + { + if (string.IsNullOrEmpty(link.Id)) + { + link.Id = Guid.NewGuid().ToString(); + } + } + + // Limpar ModelState de campos que foram removidos + RemoveEmptyLinkValidations(); + if (!ModelState.IsValid) { _logger.LogWarning("ModelState is invalid:"); @@ -145,7 +168,7 @@ public class AdminController : Controller { _logger.LogWarning($"Key: {error.Key}, Errors: {string.Join(", ", error.Value.Errors.Select(e => e.ErrorMessage))}"); } - + // Repopulate dropdowns model.AvailableCategories = await _categoryService.GetAllCategoriesAsync(); model.AvailableThemes = await _themeService.GetAvailableThemesAsync(); @@ -169,25 +192,15 @@ public class AdminController : Controller return View(model); } - // Check if user can create the requested number of links - var activeLinksCount = model.Links?.Count ?? 0; - if (activeLinksCount > model.MaxLinksAllowed) - { - ModelState.AddModelError("", $"Você excedeu o limite de {model.MaxLinksAllowed} links do seu plano atual."); - model.AvailableCategories = await _categoryService.GetAllCategoriesAsync(); - model.AvailableThemes = await _themeService.GetAvailableThemesAsync(); - return View(model); - } - try { // Create new page var userPage = await MapToUserPage(model, user.Id); _logger.LogInformation($"Mapped to UserPage: {userPage.DisplayName}, Category: {userPage.Category}, Slug: {userPage.Slug}"); - + await _userPageService.CreatePageAsync(userPage); _logger.LogInformation("Page created successfully!"); - + TempData["Success"] = "Página criada com sucesso!"; } catch (Exception ex) @@ -214,6 +227,27 @@ public class AdminController : Controller return RedirectToAction("Dashboard"); } + private void RemoveEmptyLinkValidations() + { + var keysToRemove = ModelState.Keys + .Where(key => key.StartsWith("Links[") && ( + key.Contains(".Title") || + key.Contains(".Url") || + key.Contains(".Description") || + key.Contains(".ProductTitle") || + key.Contains(".ProductDescription") || + key.Contains(".ProductPrice") || + key.Contains(".ProductImage") + )) + .ToList(); + + foreach (var key in keysToRemove) + { + ModelState.Remove(key); + } + } + + [HttpPost] [Route("CreatePage")] public async Task CreatePage(CreatePageViewModel model) @@ -475,6 +509,39 @@ public class AdminController : Controller private ManagePageViewModel MapToManageViewModel(UserPage page, List categories, List themes, PlanType userPlanType) { + // Separar links de redes sociais dos links normais + var socialMediaLinks = page.Links?.Where(l => + l.Icon.Contains("whatsapp") || + l.Icon.Contains("facebook") || + l.Icon.Contains("twitter") || + l.Icon.Contains("instagram") + ).ToList() ?? new List(); + + var regularLinks = page.Links?.Where(l => + !l.Icon.Contains("whatsapp") && + !l.Icon.Contains("facebook") && + !l.Icon.Contains("twitter") && + !l.Icon.Contains("instagram") + ).ToList() ?? new List(); + + // Extrair URLs das redes sociais + var whatsappLink = socialMediaLinks.FirstOrDefault(l => l.Icon.Contains("whatsapp")); + var facebookLink = socialMediaLinks.FirstOrDefault(l => l.Icon.Contains("facebook")); + var twitterLink = socialMediaLinks.FirstOrDefault(l => l.Icon.Contains("twitter")); + var instagramLink = socialMediaLinks.FirstOrDefault(l => l.Icon.Contains("instagram")); + + // Extrair número do WhatsApp da URL + string whatsappNumber = ""; + if (whatsappLink != null && whatsappLink.Url.Contains("wa.me/")) + { + var numberPart = whatsappLink.Url.Split("wa.me/").LastOrDefault(); + if (!string.IsNullOrEmpty(numberPart)) + { + // Formatar número para exibição (adicionar espaços, parênteses, etc.) + whatsappNumber = FormatWhatsAppNumber(numberPart); + } + } + return new ManagePageViewModel { Id = page.Id, @@ -485,9 +552,17 @@ public class AdminController : Controller Bio = page.Bio, Slug = page.Slug, SelectedTheme = page.Theme?.Name ?? "minimalist", - Links = page.Links?.Select((l, index) => new ManageLinkViewModel + + // Redes sociais + WhatsAppNumber = whatsappNumber, + FacebookUrl = facebookLink?.Url ?? "", + TwitterUrl = twitterLink?.Url ?? "", + InstagramUrl = instagramLink?.Url ?? "", + + // Links regulares + Links = regularLinks.Select((l, index) => new ManageLinkViewModel { - Id = $"link_{index}", + Id = page.Id + "_" + index, // ID único para identificação Title = l.Title, Url = l.Url, Description = l.Description, @@ -500,7 +575,8 @@ public class AdminController : Controller ProductPrice = l.ProductPrice, ProductDescription = l.ProductDescription, ProductDataCachedAt = l.ProductDataCachedAt - }).ToList() ?? new List(), + }).ToList(), + AvailableCategories = categories, AvailableThemes = themes.Where(t => !t.IsPremium || userPlanType.AllowsCustomThemes()).ToList(), MaxLinksAllowed = userPlanType.GetMaxLinksPerPage() @@ -510,7 +586,7 @@ public class AdminController : Controller private async Task MapToUserPage(ManagePageViewModel model, string userId) { var theme = await _themeService.GetThemeByNameAsync(model.SelectedTheme) ?? _themeService.GetDefaultTheme(); - + var userPage = new UserPage { UserId = userId, @@ -524,44 +600,47 @@ public class AdminController : Controller Links = new List() }; - // Add regular links + // Add regular links (apenas links não vazios) if (model.Links?.Any() == true) { - userPage.Links.AddRange(model.Links.Where(l => !string.IsNullOrEmpty(l.Title) && !string.IsNullOrEmpty(l.Url)) - .Select((l, index) => new LinkItem - { - Title = l.Title, - Url = l.Url.ToLower(), - Description = l.Description, - Icon = l.Icon, - IsActive = l.IsActive, - Order = index, - Type = l.Type, - ProductTitle = l.ProductTitle, - ProductImage = l.ProductImage, - ProductPrice = l.ProductPrice, - ProductDescription = l.ProductDescription, - ProductDataCachedAt = l.ProductDataCachedAt - })); + var validLinks = model.Links.Where(l => !l.IsEmpty).ToList(); + + userPage.Links.AddRange(validLinks.Select((l, index) => new LinkItem + { + Title = l.Title, + Url = l.Url, + Description = l.Description ?? string.Empty, + Icon = l.Icon ?? string.Empty, + IsActive = l.IsActive, + Order = index, + Type = l.Type, + ProductTitle = l.ProductTitle ?? string.Empty, + ProductImage = l.ProductImage ?? string.Empty, + ProductPrice = l.ProductPrice ?? string.Empty, + ProductDescription = l.ProductDescription ?? string.Empty, + ProductDataCachedAt = l.ProductDataCachedAt + })); } - // Add social media links + // Add social media links apenas se preenchidos var socialLinks = new List(); var currentOrder = userPage.Links.Count; - if (!string.IsNullOrEmpty(model.WhatsAppNumber)) + if (!string.IsNullOrWhiteSpace(model.WhatsAppNumber)) { + var cleanNumber = model.WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", ""); socialLinks.Add(new LinkItem { Title = "WhatsApp", - Url = $"https://wa.me/{model.WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "")}", + Url = $"https://wa.me/{cleanNumber}", Icon = "fab fa-whatsapp", IsActive = true, - Order = currentOrder++ + Order = currentOrder++, + Type = LinkType.Normal }); } - if (!string.IsNullOrEmpty(model.FacebookUrl)) + if (!string.IsNullOrWhiteSpace(model.FacebookUrl)) { socialLinks.Add(new LinkItem { @@ -569,11 +648,12 @@ public class AdminController : Controller Url = model.FacebookUrl, Icon = "fab fa-facebook", IsActive = true, - Order = currentOrder++ + Order = currentOrder++, + Type = LinkType.Normal }); } - if (!string.IsNullOrEmpty(model.TwitterUrl)) + if (!string.IsNullOrWhiteSpace(model.TwitterUrl)) { socialLinks.Add(new LinkItem { @@ -581,11 +661,12 @@ public class AdminController : Controller Url = model.TwitterUrl, Icon = "fab fa-x-twitter", IsActive = true, - Order = currentOrder++ + Order = currentOrder++, + Type = LinkType.Normal }); } - if (!string.IsNullOrEmpty(model.InstagramUrl)) + if (!string.IsNullOrWhiteSpace(model.InstagramUrl)) { socialLinks.Add(new LinkItem { @@ -593,7 +674,8 @@ public class AdminController : Controller Url = model.InstagramUrl, Icon = "fab fa-instagram", IsActive = true, - Order = currentOrder++ + Order = currentOrder++, + Type = LinkType.Normal }); } @@ -610,47 +692,52 @@ public class AdminController : Controller page.Slug = model.Slug; page.UpdatedAt = DateTime.UtcNow; - // Update links - page.Links = new List(); - - // Add regular links + // Limpar todos os links existentes + page.Links.Clear(); + + // Adicionar apenas links válidos (não vazios) if (model.Links?.Any() == true) { - page.Links.AddRange(model.Links.Where(l => !string.IsNullOrEmpty(l.Title) && !string.IsNullOrEmpty(l.Url)) - .Select((l, index) => new LinkItem - { - Title = l.Title, - Url = l.Url, - Description = l.Description, - Icon = l.Icon, - IsActive = l.IsActive, - Order = index, - Type = l.Type, - ProductTitle = l.ProductTitle, - ProductImage = l.ProductImage, - ProductPrice = l.ProductPrice, - ProductDescription = l.ProductDescription, - ProductDataCachedAt = l.ProductDataCachedAt - })); + var validLinks = model.Links.Where(l => !l.IsEmpty).ToList(); + + page.Links.AddRange(validLinks.Select((l, index) => new LinkItem + { + Title = l.Title, + Url = l.Url, + Description = l.Description ?? string.Empty, + Icon = l.Icon ?? string.Empty, + IsActive = l.IsActive, + Order = index, + Type = l.Type, + ProductTitle = l.ProductTitle ?? string.Empty, + ProductImage = l.ProductImage ?? string.Empty, + ProductPrice = l.ProductPrice ?? string.Empty, + ProductDescription = l.ProductDescription ?? string.Empty, + ProductDataCachedAt = l.ProductDataCachedAt, + CreatedAt = DateTime.UtcNow // Para novos links ou manter o existente se for uma atualização + })); } - // Add social media links (same logic as create) + // Adicionar links de redes sociais apenas se preenchidos var socialLinks = new List(); var currentOrder = page.Links.Count; - if (!string.IsNullOrEmpty(model.WhatsAppNumber)) + if (!string.IsNullOrWhiteSpace(model.WhatsAppNumber)) { + var cleanNumber = model.WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", ""); socialLinks.Add(new LinkItem { Title = "WhatsApp", - Url = $"https://wa.me/{model.WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "")}", + Url = $"https://wa.me/{cleanNumber}", Icon = "fab fa-whatsapp", IsActive = true, - Order = currentOrder++ + Order = currentOrder++, + Type = LinkType.Normal, + CreatedAt = DateTime.UtcNow }); } - if (!string.IsNullOrEmpty(model.FacebookUrl)) + if (!string.IsNullOrWhiteSpace(model.FacebookUrl)) { socialLinks.Add(new LinkItem { @@ -658,11 +745,13 @@ public class AdminController : Controller Url = model.FacebookUrl, Icon = "fab fa-facebook", IsActive = true, - Order = currentOrder++ + Order = currentOrder++, + Type = LinkType.Normal, + CreatedAt = DateTime.UtcNow }); } - if (!string.IsNullOrEmpty(model.TwitterUrl)) + if (!string.IsNullOrWhiteSpace(model.TwitterUrl)) { socialLinks.Add(new LinkItem { @@ -670,11 +759,13 @@ public class AdminController : Controller Url = model.TwitterUrl, Icon = "fab fa-x-twitter", IsActive = true, - Order = currentOrder++ + Order = currentOrder++, + Type = LinkType.Normal, + CreatedAt = DateTime.UtcNow }); } - if (!string.IsNullOrEmpty(model.InstagramUrl)) + if (!string.IsNullOrWhiteSpace(model.InstagramUrl)) { socialLinks.Add(new LinkItem { @@ -682,10 +773,41 @@ public class AdminController : Controller Url = model.InstagramUrl, Icon = "fab fa-instagram", IsActive = true, - Order = currentOrder++ + Order = currentOrder++, + Type = LinkType.Normal, + CreatedAt = DateTime.UtcNow }); } page.Links.AddRange(socialLinks); } -} \ No newline at end of file + + // Método helper para formatar número do WhatsApp + private string FormatWhatsAppNumber(string cleanNumber) + { + // Remove caracteres não numéricos + cleanNumber = new string(cleanNumber.Where(char.IsDigit).ToArray()); + + if (cleanNumber.Length >= 10) + { + // Formato brasileiro: +55 11 99999-9999 + if (cleanNumber.StartsWith("55") && cleanNumber.Length >= 12) + { + var countryCode = cleanNumber.Substring(0, 2); + var areaCode = cleanNumber.Substring(2, 2); + var firstPart = cleanNumber.Substring(4, cleanNumber.Length - 8); + var lastPart = cleanNumber.Substring(cleanNumber.Length - 4); + + return $"+{countryCode} {areaCode} {firstPart}-{lastPart}"; + } + else + { + // Formato genérico + return "+" + cleanNumber; + } + } + + return cleanNumber; + } +} + diff --git a/src/BCards.Web/ViewModels/ManagePageViewModel.cs b/src/BCards.Web/ViewModels/ManagePageViewModel.cs index f3e460c..c59479f 100644 --- a/src/BCards.Web/ViewModels/ManagePageViewModel.cs +++ b/src/BCards.Web/ViewModels/ManagePageViewModel.cs @@ -3,7 +3,7 @@ using BCards.Web.Models; namespace BCards.Web.ViewModels; -public class ManagePageViewModel +public class ManagePageViewModel : IValidatableObject { public string Id { get; set; } = string.Empty; public bool IsNewPage { get; set; } = true; @@ -26,61 +26,165 @@ public class ManagePageViewModel [Required(ErrorMessage = "Tema é obrigatório")] public string SelectedTheme { get; set; } = "minimalist"; + // Redes sociais sem [Required] - serão validadas no método Validate public string WhatsAppNumber { get; set; } = string.Empty; - public string FacebookUrl { get; set; } = string.Empty; - public string TwitterUrl { get; set; } = string.Empty; - public string InstagramUrl { get; set; } = string.Empty; public List Links { get; set; } = new(); - + // Data for dropdowns and selections public List AvailableCategories { get; set; } = new(); public List AvailableThemes { get; set; } = new(); - + // Plan limitations public int MaxLinksAllowed { get; set; } = 3; public bool CanUseTheme(string themeName) => AvailableThemes.Any(t => t.Name.ToLower() == themeName.ToLower()); + + public IEnumerable Validate(ValidationContext validationContext) + { + var results = new List(); + + // Validar URLs de redes sociais apenas se preenchidas + if (!string.IsNullOrWhiteSpace(FacebookUrl) && !Uri.TryCreate(FacebookUrl, UriKind.Absolute, out _)) + { + results.Add(new ValidationResult("URL do Facebook inválida", new[] { nameof(FacebookUrl) })); + } + + if (!string.IsNullOrWhiteSpace(TwitterUrl) && !Uri.TryCreate(TwitterUrl, UriKind.Absolute, out _)) + { + results.Add(new ValidationResult("URL do Twitter inválida", new[] { nameof(TwitterUrl) })); + } + + if (!string.IsNullOrWhiteSpace(InstagramUrl) && !Uri.TryCreate(InstagramUrl, UriKind.Absolute, out _)) + { + results.Add(new ValidationResult("URL do Instagram inválida", new[] { nameof(InstagramUrl) })); + } + + // Validar número do WhatsApp apenas se preenchido + if (!string.IsNullOrWhiteSpace(WhatsAppNumber)) + { + var cleanNumber = WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", ""); + if (cleanNumber.Length < 10 || !cleanNumber.All(char.IsDigit)) + { + results.Add(new ValidationResult("Número do WhatsApp inválido", new[] { nameof(WhatsAppNumber) })); + } + } + + // Validar links ativos + var activeLinks = Links?.Where(l => !l.IsEmpty).ToList() ?? new List(); + if (activeLinks.Count > MaxLinksAllowed) + { + results.Add(new ValidationResult($"Você pode ter no máximo {MaxLinksAllowed} links ativos", new[] { nameof(Links) })); + } + + return results; + } } - -public class ManageLinkViewModel +public class ManageLinkViewModel : IValidatableObject { - public string Id { get; set; } = "new"; - - [Required(ErrorMessage = "Título é obrigatório")] - [StringLength(50, ErrorMessage = "Título deve ter no máximo 50 caracteres")] + public string Id { get; set; } = Guid.NewGuid().ToString(); public string Title { get; set; } = string.Empty; - - [Required(ErrorMessage = "URL é obrigatória")] - [Url(ErrorMessage = "URL inválida")] public string Url { get; set; } = string.Empty; - - [StringLength(100, ErrorMessage = "Descrição deve ter no máximo 100 caracteres")] public string Description { get; set; } = string.Empty; - public string Icon { get; set; } = string.Empty; public int Order { get; set; } = 0; public bool IsActive { get; set; } = true; // Campos para Links de Produto public LinkType Type { get; set; } = LinkType.Normal; - - [StringLength(100, ErrorMessage = "Título do produto deve ter no máximo 100 caracteres")] public string ProductTitle { get; set; } = string.Empty; - public string ProductImage { get; set; } = string.Empty; - - [StringLength(50, ErrorMessage = "Preço deve ter no máximo 50 caracteres")] public string ProductPrice { get; set; } = string.Empty; - - [StringLength(200, ErrorMessage = "Descrição do produto deve ter no máximo 200 caracteres")] public string ProductDescription { get; set; } = string.Empty; - public DateTime? ProductDataCachedAt { get; set; } -} + // Validação customizada + public IEnumerable Validate(ValidationContext validationContext) + { + var results = new List(); + + // Se tem título OU URL, então ambos são obrigatórios + bool hasTitle = !string.IsNullOrWhiteSpace(Title); + bool hasUrl = !string.IsNullOrWhiteSpace(Url); + + if (hasTitle || hasUrl) + { + if (!hasTitle) + { + results.Add(new ValidationResult( + "Título é obrigatório quando URL é preenchida", + new[] { nameof(Title) })); + } + + if (!hasUrl) + { + results.Add(new ValidationResult( + "URL é obrigatória quando título é preenchido", + new[] { nameof(Url) })); + } + else if (!Uri.TryCreate(Url, UriKind.Absolute, out _)) + { + results.Add(new ValidationResult( + "URL inválida", + new[] { nameof(Url) })); + } + + // Validações específicas para links de produto + if (Type == LinkType.Product && hasTitle && hasUrl) + { + if (string.IsNullOrWhiteSpace(ProductTitle)) + { + results.Add(new ValidationResult( + "Título do produto é obrigatório para links de produto", + new[] { nameof(ProductTitle) })); + } + } + + // Validações de tamanho apenas se os campos estão preenchidos + if (hasTitle && Title.Length > 50) + { + results.Add(new ValidationResult( + "Título deve ter no máximo 50 caracteres", + new[] { nameof(Title) })); + } + + if (!string.IsNullOrWhiteSpace(Description) && Description.Length > 100) + { + results.Add(new ValidationResult( + "Descrição deve ter no máximo 100 caracteres", + new[] { nameof(Description) })); + } + + if (!string.IsNullOrWhiteSpace(ProductTitle) && ProductTitle.Length > 100) + { + results.Add(new ValidationResult( + "Título do produto deve ter no máximo 100 caracteres", + new[] { nameof(ProductTitle) })); + } + + if (!string.IsNullOrWhiteSpace(ProductPrice) && ProductPrice.Length > 50) + { + results.Add(new ValidationResult( + "Preço deve ter no máximo 50 caracteres", + new[] { nameof(ProductPrice) })); + } + + if (!string.IsNullOrWhiteSpace(ProductDescription) && ProductDescription.Length > 200) + { + results.Add(new ValidationResult( + "Descrição do produto deve ter no máximo 200 caracteres", + new[] { nameof(ProductDescription) })); + } + } + + return results; + } + + // Propriedade helper para verificar se o link está vazio + public bool IsEmpty => string.IsNullOrWhiteSpace(Title) && string.IsNullOrWhiteSpace(Url); +} public class DashboardViewModel { public User CurrentUser { get; set; } = new(); diff --git a/src/BCards.Web/Views/Admin/ManagePage.cshtml b/src/BCards.Web/Views/Admin/ManagePage.cshtml index f2f1140..9b0fa9a 100644 --- a/src/BCards.Web/Views/Admin/ManagePage.cshtml +++ b/src/BCards.Web/Views/Admin/ManagePage.cshtml @@ -618,403 +618,624 @@ @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} } \ No newline at end of file