diff --git a/Web/Areas/Admin/Controllers/PageController.cs b/Web/Areas/Admin/Controllers/PageController.cs index beb456a..a58932f 100644 --- a/Web/Areas/Admin/Controllers/PageController.cs +++ b/Web/Areas/Admin/Controllers/PageController.cs @@ -1,10 +1,13 @@ -using Microsoft.AspNetCore.Authorization; +using Data; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Model; using Services.Interaces; using Services.SlugServices; +using Web.ViewModel.CmsVM; using Web.ViewModel.PageVM; +using Microsoft.EntityFrameworkCore; namespace Web.Areas.Admin.Controllers { @@ -15,12 +18,14 @@ namespace Web.Areas.Admin.Controllers private readonly IPageRepository _pageRepository; private readonly IBannerRepository _bannerRepository; private readonly IFooterRepository _footerRepository; + private readonly SurveyContext _context; - public PageController(IPageRepository pageRepository,IBannerRepository bannerRepository, IFooterRepository footerRepository) + public PageController(IPageRepository pageRepository,IBannerRepository bannerRepository, IFooterRepository footerRepository, SurveyContext context) { _pageRepository = pageRepository; _bannerRepository = bannerRepository; _footerRepository = footerRepository; + _context = context; } public IActionResult Index() { @@ -237,5 +242,573 @@ namespace Web.Areas.Admin.Controllers } + public async Task CmsDashboard() + { + var viewModel = new CmsDashboardViewModel + { + Pages = await _context.Pages + .Include(p => p.banner) + .Include(p => p.footer) + .OrderByDescending(p => p.Id) + .ToListAsync(), + + Banners = await _context.Banners + .OrderByDescending(b => b.Id) + .ToListAsync(), + + Footers = await _context.Footers + .Include(f => f.FooterSocialMedias) + .ThenInclude(fsm => fsm.SocialMedia) + .OrderByDescending(f => f.Id) + .ToListAsync(), + + SocialMedias = await _context.SocialMedia + .OrderByDescending(s => s.Id) + .ToListAsync(), + + Addresses = await _context.Addresss + .OrderByDescending(a => a.Id) + .ToListAsync(), + + // Dropdown options for Page create/edit + BannerOptions = await _context.Banners + .Select(b => new BannerSelectItem { Id = b.Id, Title = b.Title ?? "" }) + .ToListAsync(), + + FooterOptions = await _context.Footers + .Select(f => new FooterSelectItem { Id = f.Id, Name = f.Name ?? "" }) + .ToListAsync(), + + SocialMediaOptions = await _context.SocialMedia + .Select(s => new SocialMediaSelectItem { Id = s.Id, Name = s.Name ?? "", Url = s.Url ?? "" }) + .ToListAsync() + }; + + return View(viewModel); + } + + + // ═══════════════════════════════════════════════════════════ + // PAGE CRUD (AJAX) + // ═══════════════════════════════════════════════════════════ + + [HttpGet] + public async Task GetPage(int id) + { + var page = await _context.Pages + .Include(p => p.banner) + .Include(p => p.footer) + .FirstOrDefaultAsync(p => p.Id == id); + + if (page == null) return Json(new { success = false, message = "Page not found." }); + + return Json(new + { + success = true, + data = new + { + page.Id, + page.Title, + page.Slug, + page.Content, + page.BannerId, + page.FooterId, + BannerTitle = page.banner?.Title ?? "", + FooterName = page.footer?.Name ?? "" + } + }); + } + + [HttpPost] + public async Task CreatePageAjax(string title, string slug, string content, int bannerId, int footerId) + { + try + { + if (string.IsNullOrWhiteSpace(title)) + return Json(new { success = false, message = "Title is required." }); + + var page = new Page + { + Title = title.Trim(), + Slug = string.IsNullOrWhiteSpace(slug) ? title.Trim().ToLower().Replace(" ", "-") : slug.Trim(), + Content = content?.Trim(), + BannerId = bannerId, + FooterId = footerId + }; + + _context.Pages.Add(page); + await _context.SaveChangesAsync(); + return Json(new { success = true, message = "Page created successfully." }); + } + catch (Exception ex) + { + return Json(new { success = false, message = "Error: " + ex.Message }); + } + } + + [HttpPost] + public async Task UpdatePageAjax(int id, string title, string slug, string content, int bannerId, int footerId) + { + try + { + var page = await _context.Pages.FindAsync(id); + if (page == null) return Json(new { success = false, message = "Page not found." }); + + page.Title = title?.Trim(); + page.Slug = string.IsNullOrWhiteSpace(slug) ? title?.Trim().ToLower().Replace(" ", "-") : slug.Trim(); + page.Content = content?.Trim(); + page.BannerId = bannerId; + page.FooterId = footerId; + + await _context.SaveChangesAsync(); + return Json(new { success = true, message = "Page updated successfully." }); + } + catch (Exception ex) + { + return Json(new { success = false, message = "Error: " + ex.Message }); + } + } + + [HttpPost] + public async Task DeletePageAjax(int id) + { + try + { + var page = await _context.Pages.FindAsync(id); + if (page == null) return Json(new { success = false, message = "Page not found." }); + + _context.Pages.Remove(page); + await _context.SaveChangesAsync(); + return Json(new { success = true, message = "Page deleted." }); + } + catch (Exception ex) + { + return Json(new { success = false, message = "Error: " + ex.Message }); + } + } + + + // ═══════════════════════════════════════════════════════════ + // BANNER CRUD (AJAX) + // ═══════════════════════════════════════════════════════════ + + [HttpGet] + public async Task GetBanner(int id) + { + var b = await _context.Banners.FindAsync(id); + if (b == null) return Json(new { success = false, message = "Banner not found." }); + + return Json(new { success = true, data = new { b.Id, b.Title, b.Description, b.Content, b.LinkUrl, b.ImageUrl } }); + } + + [HttpPost] + public async Task CreateBannerAjax(string title, string description, string content, string linkUrl, string imageUrl) + { + try + { + if (string.IsNullOrWhiteSpace(title)) + return Json(new { success = false, message = "Title is required." }); + + var banner = new Banner + { + Title = title.Trim(), + Description = description?.Trim(), + Content = content?.Trim(), + LinkUrl = linkUrl?.Trim(), + ImageUrl = imageUrl?.Trim() + }; + + _context.Banners.Add(banner); + await _context.SaveChangesAsync(); + return Json(new { success = true, message = "Banner created successfully." }); + } + catch (Exception ex) + { + return Json(new { success = false, message = "Error: " + ex.Message }); + } + } + + [HttpPost] + public async Task UpdateBannerAjax(int id, string title, string description, string content, string linkUrl, string imageUrl) + { + try + { + var b = await _context.Banners.FindAsync(id); + if (b == null) return Json(new { success = false, message = "Banner not found." }); + + b.Title = title?.Trim(); + b.Description = description?.Trim(); + b.Content = content?.Trim(); + b.LinkUrl = linkUrl?.Trim(); + b.ImageUrl = imageUrl?.Trim(); + + await _context.SaveChangesAsync(); + return Json(new { success = true, message = "Banner updated successfully." }); + } + catch (Exception ex) + { + return Json(new { success = false, message = "Error: " + ex.Message }); + } + } + + [HttpPost] + public async Task DeleteBannerAjax(int id) + { + try + { + // Check if any page references this banner + var inUse = await _context.Pages.AnyAsync(p => p.BannerId == id); + if (inUse) return Json(new { success = false, message = "Cannot delete — this banner is used by one or more pages." }); + + var b = await _context.Banners.FindAsync(id); + if (b == null) return Json(new { success = false, message = "Banner not found." }); + + _context.Banners.Remove(b); + await _context.SaveChangesAsync(); + return Json(new { success = true, message = "Banner deleted." }); + } + catch (Exception ex) + { + return Json(new { success = false, message = "Error: " + ex.Message }); + } + } + + + // ═══════════════════════════════════════════════════════════ + // FOOTER CRUD (AJAX) + // ═══════════════════════════════════════════════════════════ + + [HttpGet] + public async Task GetFooter(int id) + { + var f = await _context.Footers + .Include(x => x.FooterSocialMedias) + .ThenInclude(fsm => fsm.SocialMedia) + .FirstOrDefaultAsync(x => x.Id == id); + + if (f == null) return Json(new { success = false, message = "Footer not found." }); + + return Json(new + { + success = true, + data = new + { + f.Id, + f.Title, + f.Name, + f.Owner, + f.Content, + f.CreatedBy, + f.UpdatedBy, + f.ImageUlr, + f.Sitecopyright, + LastUpdated = f.LastUpdated.ToString("yyyy-MM-ddTHH:mm"), + SocialMediaIds = f.FooterSocialMedias?.Select(fsm => fsm.SocialId).ToList() ?? new List() + } + }); + } + + [HttpPost] + public async Task CreateFooterAjax( + string title, string name, string owner, string content, + string createdBy, string updatedBy, string imageUrl, + string sitecopyright, int[] socialMediaIds) + { + try + { + if (string.IsNullOrWhiteSpace(name)) + return Json(new { success = false, message = "Name is required." }); + + var footer = new Footer + { + Title = title?.Trim(), + Name = name.Trim(), + Owner = owner?.Trim(), + Content = content?.Trim(), + CreatedBy = createdBy?.Trim(), + UpdatedBy = updatedBy?.Trim(), + ImageUlr = imageUrl?.Trim(), + Sitecopyright = sitecopyright?.Trim(), + LastUpdated = DateTime.UtcNow + }; + + _context.Footers.Add(footer); + await _context.SaveChangesAsync(); + + // Add social media links + if (socialMediaIds != null && socialMediaIds.Length > 0) + { + foreach (var smId in socialMediaIds) + { + _context.FooterSocialMedias.Add(new FooterSocialMedia + { + FooterId = footer.Id, + SocialId = smId + }); + } + await _context.SaveChangesAsync(); + } + + return Json(new { success = true, message = "Footer created successfully." }); + } + catch (Exception ex) + { + return Json(new { success = false, message = "Error: " + ex.Message }); + } + } + + [HttpPost] + public async Task UpdateFooterAjax( + int id, string title, string name, string owner, string content, + string createdBy, string updatedBy, string imageUrl, + string sitecopyright, int[] socialMediaIds) + { + try + { + var f = await _context.Footers + .Include(x => x.FooterSocialMedias) + .FirstOrDefaultAsync(x => x.Id == id); + + if (f == null) return Json(new { success = false, message = "Footer not found." }); + + f.Title = title?.Trim(); + f.Name = name?.Trim(); + f.Owner = owner?.Trim(); + f.Content = content?.Trim(); + f.CreatedBy = createdBy?.Trim(); + f.UpdatedBy = updatedBy?.Trim(); + f.ImageUlr = imageUrl?.Trim(); + f.Sitecopyright = sitecopyright?.Trim(); + f.LastUpdated = DateTime.UtcNow; + + // Update social media links — remove old, add new + var existing = f.FooterSocialMedias?.ToList() ?? new List(); + _context.FooterSocialMedias.RemoveRange(existing); + + if (socialMediaIds != null && socialMediaIds.Length > 0) + { + foreach (var smId in socialMediaIds) + { + _context.FooterSocialMedias.Add(new FooterSocialMedia + { + FooterId = f.Id, + SocialId = smId + }); + } + } + + await _context.SaveChangesAsync(); + return Json(new { success = true, message = "Footer updated successfully." }); + } + catch (Exception ex) + { + return Json(new { success = false, message = "Error: " + ex.Message }); + } + } + + [HttpPost] + public async Task DeleteFooterAjax(int id) + { + try + { + var inUse = await _context.Pages.AnyAsync(p => p.FooterId == id); + if (inUse) return Json(new { success = false, message = "Cannot delete — this footer is used by one or more pages." }); + + var f = await _context.Footers + .Include(x => x.FooterSocialMedias) + .FirstOrDefaultAsync(x => x.Id == id); + + if (f == null) return Json(new { success = false, message = "Footer not found." }); + + // Remove social media links first + if (f.FooterSocialMedias != null && f.FooterSocialMedias.Any()) + _context.FooterSocialMedias.RemoveRange(f.FooterSocialMedias); + + _context.Footers.Remove(f); + await _context.SaveChangesAsync(); + return Json(new { success = true, message = "Footer deleted." }); + } + catch (Exception ex) + { + return Json(new { success = false, message = "Error: " + ex.Message }); + } + } + + + // ═══════════════════════════════════════════════════════════ + // SOCIAL MEDIA CRUD (AJAX) + // ═══════════════════════════════════════════════════════════ + + [HttpGet] + public async Task GetSocialMedia(int id) + { + var s = await _context.SocialMedia.FindAsync(id); + if (s == null) return Json(new { success = false, message = "Social media not found." }); + + return Json(new { success = true, data = new { s.Id, s.Name, s.Url } }); + } + + [HttpPost] + public async Task CreateSocialMediaAjax(string name, string url) + { + try + { + if (string.IsNullOrWhiteSpace(name)) + return Json(new { success = false, message = "Name is required." }); + + var sm = new SocialMedia + { + Name = name.Trim(), + Url = url?.Trim() + }; + + _context.SocialMedia.Add(sm); + await _context.SaveChangesAsync(); + return Json(new { success = true, message = "Social media link created." }); + } + catch (Exception ex) + { + return Json(new { success = false, message = "Error: " + ex.Message }); + } + } + + [HttpPost] + public async Task UpdateSocialMediaAjax(int id, string name, string url) + { + try + { + var s = await _context.SocialMedia.FindAsync(id); + if (s == null) return Json(new { success = false, message = "Social media not found." }); + + s.Name = name?.Trim(); + s.Url = url?.Trim(); + + await _context.SaveChangesAsync(); + return Json(new { success = true, message = "Social media updated." }); + } + catch (Exception ex) + { + return Json(new { success = false, message = "Error: " + ex.Message }); + } + } + + [HttpPost] + public async Task DeleteSocialMediaAjax(int id) + { + try + { + // Check if linked to any footer + var inUse = await _context.FooterSocialMedias.AnyAsync(fsm => fsm.SocialId == id); + if (inUse) return Json(new { success = false, message = "Cannot delete — this social media is linked to one or more footers. Remove the link first." }); + + var s = await _context.SocialMedia.FindAsync(id); + if (s == null) return Json(new { success = false, message = "Social media not found." }); + + _context.SocialMedia.Remove(s); + await _context.SaveChangesAsync(); + return Json(new { success = true, message = "Social media deleted." }); + } + catch (Exception ex) + { + return Json(new { success = false, message = "Error: " + ex.Message }); + } + } + + + // ═══════════════════════════════════════════════════════════ + // ADDRESS CRUD (AJAX) + // ═══════════════════════════════════════════════════════════ + + [HttpGet] + public async Task GetAddress(int id) + { + var a = await _context.Addresss.FindAsync(id); + if (a == null) return Json(new { success = false, message = "Address not found." }); + + return Json(new + { + success = true, + data = new { a.Id, a.Street, a.City, a.State, a.PostalCode, a.Country, a.CVR, a.Email, a.Mobile } + }); + } + + [HttpPost] + public async Task CreateAddressAjax( + string street, string city, string state, string postalCode, + string country, string cvr, string email, string mobile) + { + try + { + if (string.IsNullOrWhiteSpace(street)) + return Json(new { success = false, message = "Street is required." }); + + var address = new Address + { + Street = street.Trim(), + City = city?.Trim(), + State = state?.Trim(), + PostalCode = postalCode?.Trim(), + Country = country?.Trim(), + CVR = cvr?.Trim(), + Email = email?.Trim(), + Mobile = mobile?.Trim() + }; + + _context.Addresss.Add(address); + await _context.SaveChangesAsync(); + return Json(new { success = true, message = "Address created successfully." }); + } + catch (Exception ex) + { + return Json(new { success = false, message = "Error: " + ex.Message }); + } + } + + [HttpPost] + public async Task UpdateAddressAjax( + int id, string street, string city, string state, string postalCode, + string country, string cvr, string email, string mobile) + { + try + { + var a = await _context.Addresss.FindAsync(id); + if (a == null) return Json(new { success = false, message = "Address not found." }); + + a.Street = street?.Trim(); + a.City = city?.Trim(); + a.State = state?.Trim(); + a.PostalCode = postalCode?.Trim(); + a.Country = country?.Trim(); + a.CVR = cvr?.Trim(); + a.Email = email?.Trim(); + a.Mobile = mobile?.Trim(); + + await _context.SaveChangesAsync(); + return Json(new { success = true, message = "Address updated successfully." }); + } + catch (Exception ex) + { + return Json(new { success = false, message = "Error: " + ex.Message }); + } + } + + [HttpPost] + public async Task DeleteAddressAjax(int id) + { + try + { + var a = await _context.Addresss.FindAsync(id); + if (a == null) return Json(new { success = false, message = "Address not found." }); + + _context.Addresss.Remove(a); + await _context.SaveChangesAsync(); + return Json(new { success = true, message = "Address deleted." }); + } + catch (Exception ex) + { + return Json(new { success = false, message = "Error: " + ex.Message }); + } + } + } } diff --git a/Web/Areas/Admin/Views/Page/CmsDashboard.cshtml b/Web/Areas/Admin/Views/Page/CmsDashboard.cshtml new file mode 100644 index 0000000..51d9797 --- /dev/null +++ b/Web/Areas/Admin/Views/Page/CmsDashboard.cshtml @@ -0,0 +1,604 @@ +@model Web.ViewModel.CmsVM.CmsDashboardViewModel +@{ + ViewData["Title"] = "CMS Dashboard"; +} + + + +
+ +
+ + +
+
+
+

CMS Dashboard

+
+
+ + +
+
Overview @Model.TotalItems
+
Pages @Model.PageCount
+
Banners @Model.BannerCount
+
Footers @Model.FooterCount
+
Social Media @Model.SocialMediaCount
+
Address @Model.AddressCount
+
+ + +
+
+
@Model.PageCount
Pages
+
@Model.BannerCount
Banners
+
@Model.FooterCount
Footers
+
@Model.SocialMediaCount
Social Links
+
@Model.AddressCount
Addresses
+
+
+ +
+

Pages

@Model.PageCount items
+
+ @foreach (var p in Model.Pages.Take(4)) + {
@p.Title@(p.banner?.Title ?? "No banner")
} + @if (!Model.Pages.Any()) {
No pages created yet
} +
+
Manage Pages
+
+ +
+

Banners

@Model.BannerCount items
+
+ @foreach (var b in Model.Banners.Take(4)) + {
@b.Title@(b.ImageUrl?.Length > 30 ? b.ImageUrl.Substring(0,30)+"..." : b.ImageUrl)
} + @if (!Model.Banners.Any()) {
No banners created yet
} +
+
Manage Banners
+
+ +
+

Footers

@Model.FooterCount items
+
+ @foreach (var f in Model.Footers.Take(4)) + {
@f.Name@(f.FooterSocialMedias?.Count ?? 0) social links
} + @if (!Model.Footers.Any()) {
No footers created yet
} +
+
Manage Footers
+
+ +
+

Social Media

@Model.SocialMediaCount links
+
+ @foreach (var s in Model.SocialMedias.Take(4)) + {
@s.Name@(s.Url?.Length > 30 ? s.Url.Substring(0,30)+"..." : s.Url)
} + @if (!Model.SocialMedias.Any()) {
No social links yet
} +
+
Manage Social Media
+
+ +
+

Addresses

@Model.AddressCount items
+
+ @foreach (var a in Model.Addresses.Take(4)) + {
@a.Street, @a.City@a.Country
} + @if (!Model.Addresses.Any()) {
No addresses yet
} +
+
Manage Addresses
+
+
+
+ + +
+
+
+
+

Pages

Manage CMS pages with banners and footers

+
+
+
+ + + + @foreach (var p in Model.Pages) + {} + @if (!Model.Pages.Any()) {} + +
TitleSlugBannerFooterActions
@p.Title@p.Slug@(p.banner?.Title ?? "—")@(p.footer?.Name ?? "—")
No pages yet. Create your first page.
+
+
+
+ + +
+
+
+
+

Banners

Hero banners and promotional images

+
+
+
+ + + + @foreach (var b in Model.Banners) + {} + @if (!Model.Banners.Any()) {} + +
TitleDescriptionLink URLImage URLActions
@b.Title@(b.Description?.Length > 50 ? b.Description.Substring(0,50)+"..." : b.Description)@(b.LinkUrl?.Length > 35 ? b.LinkUrl.Substring(0,35)+"..." : b.LinkUrl)@(b.ImageUrl?.Length > 35 ? b.ImageUrl.Substring(0,35)+"..." : b.ImageUrl)
No banners yet.
+
+
+
+ + +
+
+
+
+

Footers

Footer content with social media links

+
+
+
+ + + + @foreach (var f in Model.Footers) + {} + @if (!Model.Footers.Any()) {} + +
NameOwnerCopyrightSocial LinksActions
@f.Name@f.Owner@f.Sitecopyright@if(f.FooterSocialMedias != null){foreach(var fsm in f.FooterSocialMedias){@fsm.SocialMedia?.Name}}
No footers yet.
+
+
+
+ + +
+
+
+
+

Social Media

Social media links for footer integration

+
+
+
+ + + + @foreach (var s in Model.SocialMedias) + {} + @if (!Model.SocialMedias.Any()) {} + +
NameURLActions
@s.Name@s.Url
No social links yet.
+
+
+
+ + +
+
+
+
+

Addresses

Organization contact information

+
+
+
+ + + + @foreach (var a in Model.Addresses) + {} + @if (!Model.Addresses.Any()) {} + +
StreetCityCountryEmailMobileCVRActions
@a.Street@a.City@a.Country@a.Email@a.Mobile@a.CVR
No addresses yet.
+
+
+
+ +
+ + + + + + +@section Scripts { + + +} \ No newline at end of file diff --git a/Web/Areas/Admin/Views/Shared/_AdminLayout.cshtml b/Web/Areas/Admin/Views/Shared/_AdminLayout.cshtml index fdc782f..28ecb94 100644 --- a/Web/Areas/Admin/Views/Shared/_AdminLayout.cshtml +++ b/Web/Areas/Admin/Views/Shared/_AdminLayout.cshtml @@ -151,19 +151,15 @@ NVKN diff --git a/Web/ViewModel/CmsVM/CmsDashboardViewModel.cs b/Web/ViewModel/CmsVM/CmsDashboardViewModel.cs new file mode 100644 index 0000000..a7a0f0c --- /dev/null +++ b/Web/ViewModel/CmsVM/CmsDashboardViewModel.cs @@ -0,0 +1,72 @@ + +using Model; +using System.Collections.Generic; + +namespace Web.ViewModel.CmsVM +{ + /// + /// Main ViewModel for the unified CMS Dashboard. + /// Holds all entity lists for tabs + counts for overview. + /// + public class CmsDashboardViewModel + { + // ── Entity Lists (for tab content) ── + public List Pages { get; set; } = new(); + public List Banners { get; set; } = new(); + public List