using KernelRecordsMVC.Application.ViewModels; using KernelRecordsMVC.Domain.Enums; using KernelRecordsMVC.Domain.Models; using KernelRecordsMVC.Infrastructure.Data; using KernelRecordsMVC.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace KernelRecordsMVC.Web.Controllers; public class AdminController : Controller { private readonly KernelRecordsContext _context; public AdminController(KernelRecordsContext context) { _context = context; } // ========================================================= // ADMIN DASHBOARD // ========================================================= [HttpGet] public IActionResult Index() { if (!IsAdmin()) return Forbid(); ViewBag.ProductCount = _context.Products.Count(); ViewBag.ReleaseCount = _context.Releases.Count(); ViewBag.OutOfStockCount = _context.Products.Count(x => x.Stock <= 0); ViewBag.LowStockCount = _context.Products.Count(x => x.Stock > 0 && x.Stock <= 5); return View(); } // ========================================================= // PRODUCTS // ========================================================= [HttpGet] public IActionResult Products() { if (!IsProductManager()) return Forbid(); var products = _context.Products .Include(p => p.Release) .OrderBy(p => p.Release.Title) .ThenBy(p => p.Format) .ToList(); return View(products); } // ========================================================= // CREATE PRODUCT - GET // ========================================================= [HttpGet] public IActionResult CreateProduct() { if (!IsProductManager()) return Forbid(); LoadReleases(); return View( new CreateProductViewModel()); } // ========================================================= // CREATE PRODUCT - POST // ========================================================= [HttpPost] [ValidateAntiForgeryToken] public IActionResult CreateProduct( CreateProductViewModel model) { if (!IsProductManager()) return Forbid(); if (!ModelState.IsValid) { LoadReleases(); return View(model); } var release = _context.Releases .FirstOrDefault(x => x.ReleaseId == model.ReleaseId); if (release == null) { ModelState.AddModelError( nameof(model.ReleaseId), "The selected release does not exist."); LoadReleases(); return View(model); } var alreadyExists = _context.Products.Any(p => p.ReleaseId == model.ReleaseId && p.Format == model.Format); if (alreadyExists) { ModelState.AddModelError( nameof(model.Format), "This release already has a product in this format."); LoadReleases(); return View(model); } using var transaction = _context.Database.BeginTransaction(); try { var product = new Product { ProductId = GetNextProductId(), ReleaseId = model.ReleaseId, Format = model.Format, Price = model.Price, ProductDescription = model.ProductDescription, Stock = model.Stock }; _context.Products.Add(product); _context.SaveChanges(); CreateModification( ModificationType.CREATE, product.ProductId); transaction.Commit(); TempData["Success"] = $"{release.Title} ({product.Format}) was created successfully."; return RedirectToAction( nameof(Products)); } catch { transaction.Rollback(); throw; } } // ========================================================= // EDIT PRODUCT - GET // ========================================================= [HttpGet] public IActionResult EditProduct(long id) { if (!IsProductManager()) return Forbid(); var product = _context.Products .Include(p => p.Release) .FirstOrDefault(p => p.ProductId == id); if (product == null) return NotFound(); var model = new EditProductViewModel { ProductId = product.ProductId, ReleaseId = product.ReleaseId, ReleaseTitle = product.Release.Title, Format = product.Format, Price = product.Price, ProductDescription = product.ProductDescription, Stock = product.Stock, ModificationType = ModificationType.UPDATE }; return View(model); } // ========================================================= // EDIT PRODUCT - POST // ========================================================= [HttpPost] [ValidateAntiForgeryToken] public IActionResult EditProduct( EditProductViewModel model) { if (!IsProductManager()) return Forbid(); var product = _context.Products .Include(p => p.Release) .FirstOrDefault(p => p.ProductId == model.ProductId); if (product == null) return NotFound(); // These values come from the database. model.ReleaseId = product.ReleaseId; model.ReleaseTitle = product.Release.Title; model.Format = product.Format; // Display-only fields should not block // validation of the submitted form. ModelState.Remove( nameof(model.ReleaseId)); ModelState.Remove( nameof(model.ReleaseTitle)); ModelState.Remove( nameof(model.Format)); // ===================================================== // DISCOUNT VALIDATION // ===================================================== if (model.ModificationType == ModificationType.DISCOUNT) { if (!model.Discount.HasValue) { ModelState.AddModelError( nameof(model.Discount), "Please enter a discount percentage."); } else if ( model.Discount.Value <= 0 || model.Discount.Value >= 100) { ModelState.AddModelError( nameof(model.Discount), "Discount must be greater than 0 and less than 100."); } } if (!ModelState.IsValid) return View(model); using var transaction = _context.Database.BeginTransaction(); try { // ================================================= // APPLY DISCOUNT // ================================================= if (model.ModificationType == ModificationType.DISCOUNT) { var discountPercentage = model.Discount!.Value; var oldPrice = product.Price; var discountAmount = oldPrice * (discountPercentage / 100m); var newPrice = oldPrice - discountAmount; newPrice = Math.Round( newPrice, 2, MidpointRounding.AwayFromZero); // Product.Price becomes the // actual current sale price. product.Price = newPrice; _context.Products.Update( product); _context.SaveChanges(); CreateModification( ModificationType.DISCOUNT, product.ProductId, discountPercentage); transaction.Commit(); TempData["Success"] = $"{discountPercentage:0.##}% discount applied. " + $"Price changed from {oldPrice:C} to {newPrice:C}."; return RedirectToAction( nameof(Products)); } // ================================================= // NORMAL UPDATE // ================================================= product.Price = model.Price; product.ProductDescription = model.ProductDescription; product.Stock = model.Stock; _context.Products.Update( product); _context.SaveChanges(); CreateModification( ModificationType.UPDATE, product.ProductId); transaction.Commit(); TempData["Success"] = $"{product.Release.Title} was updated successfully."; return RedirectToAction( nameof(Products)); } catch { transaction.Rollback(); throw; } } // ========================================================= // CREATE RELEASE - GET // ========================================================= [HttpGet] public IActionResult CreateRelease() { if (!IsAdmin()) return Forbid(); LoadArtists(); var model = new CreateReleaseViewModel { ReleaseDate = DateTime.Today, ReleaseType = "ALBUM", // Start an album with one empty track. Tracks = new List { new CreateTrackViewModel() } }; return View(model); } // ========================================================= // CREATE RELEASE - POST // ========================================================= [HttpPost] [ValidateAntiForgeryToken] public IActionResult CreateRelease( CreateReleaseViewModel model) { if (!IsAdmin()) return Forbid(); // ===================================================== // RELEASE TYPE VALIDATION // ===================================================== if (model.ReleaseType != "ALBUM" && model.ReleaseType != "SINGLE") { ModelState.AddModelError( nameof(model.ReleaseType), "Please select Album or Single."); } // ===================================================== // MAIN ARTIST VALIDATION // ===================================================== var mainArtistExists = _context.Artists.Any(a => a.ArtistId == model.MainArtistId); if (!mainArtistExists) { ModelState.AddModelError( nameof(model.MainArtistId), "Please select a valid main artist."); } // ===================================================== // FEATURED ARTISTS // ===================================================== model.FeaturedArtistIds ??= new List(); model.FeaturedArtistIds = model.FeaturedArtistIds .Distinct() .ToList(); // Do not allow the main artist // to also be a featured artist. model.FeaturedArtistIds.Remove( model.MainArtistId); // Validate that featured artists exist. if (model.FeaturedArtistIds.Count > 0) { var validFeaturedArtistCount = _context.Artists.Count(a => model.FeaturedArtistIds .Contains(a.ArtistId)); if (validFeaturedArtistCount != model.FeaturedArtistIds.Count) { ModelState.AddModelError( nameof(model.FeaturedArtistIds), "One or more featured artists are invalid."); } } // ===================================================== // SINGLE VALIDATION // ===================================================== if (model.ReleaseType == "SINGLE") { if (string.IsNullOrWhiteSpace( model.SingleDuration)) { ModelState.AddModelError( nameof(model.SingleDuration), "Duration is required for a single."); } } // ===================================================== // ALBUM VALIDATION // ===================================================== if (model.ReleaseType == "ALBUM") { model.Tracks ??= new List(); // Remove completely empty rows. model.Tracks = model.Tracks .Where(t => !string.IsNullOrWhiteSpace( t.SongName) || !string.IsNullOrWhiteSpace( t.SongDuration)) .ToList(); if (model.Tracks.Count == 0) { ModelState.AddModelError( nameof(model.Tracks), "An album must contain at least one track."); } for (var i = 0; i < model.Tracks.Count; i++) { var track = model.Tracks[i]; if (string.IsNullOrWhiteSpace( track.SongName)) { ModelState.AddModelError( $"Tracks[{i}].SongName", "Track name is required."); } if (string.IsNullOrWhiteSpace( track.SongDuration)) { ModelState.AddModelError( $"Tracks[{i}].SongDuration", "Track duration is required."); } track.ArtistIds ??= new List(); track.ArtistIds = track.ArtistIds .Distinct() .ToList(); // Validate selected track artists. if (track.ArtistIds.Count > 0) { var validTrackArtistCount = _context.Artists.Count(a => track.ArtistIds.Contains( a.ArtistId)); if (validTrackArtistCount != track.ArtistIds.Count) { ModelState.AddModelError( $"Tracks[{i}].ArtistIds", "One or more selected track artists are invalid."); } } } } if (!ModelState.IsValid) { LoadArtists(); return View(model); } using var transaction = _context.Database.BeginTransaction(); try { // ================================================= // CREATE RELEASE // ================================================= var release = new Release { ReleaseId = GetNextReleaseId(), Title = model.Title.Trim(), RecordLabel = string.IsNullOrWhiteSpace( model.RecordLabel) ? null : model.RecordLabel.Trim(), Genre = model.Genre.Trim(), ReleaseDate = model.ReleaseDate, CoverPhoto = model.CoverPhoto.Trim() }; _context.Releases.Add( release); _context.SaveChanges(); // ================================================= // MAIN RELEASE ARTIST // ================================================= var mainReleaseArtist = new ReleaseArtist { ReleaseId = release.ReleaseId, ArtistId = model.MainArtistId, ReleaseOrdinal = 1, Type = ArtistReleaseType.MAIN }; _context.ReleaseArtists.Add( mainReleaseArtist); // ================================================= // FEATURED RELEASE ARTISTS // ================================================= long releaseOrdinal = 2; foreach (var artistId in model.FeaturedArtistIds) { var featuredArtist = new ReleaseArtist { ReleaseId = release.ReleaseId, ArtistId = artistId, ReleaseOrdinal = releaseOrdinal++, Type = ArtistReleaseType.FEATURE }; _context.ReleaseArtists.Add( featuredArtist); } _context.SaveChanges(); // ================================================= // ALBUM // ================================================= if (model.ReleaseType == "ALBUM") { var album = new Album { ReleaseId = release.ReleaseId }; _context.Albums.Add( album); _context.SaveChanges(); // ============================================= // TRACKS // ============================================= foreach (var trackModel in model.Tracks) { var song = new Song { SongId = GetNextSongId(), SongName = trackModel .SongName .Trim(), SongDuration = trackModel .SongDuration .Trim() }; _context.Songs.Add( song); _context.SaveChanges(); // ========================================= // LINK SONG TO ALBUM // ========================================= var albumSong = new AlbumSong { AlbumId = release.ReleaseId, SongId = song.SongId }; _context.AlbumSongs.Add( albumSong); // ========================================= // SONG ARTISTS // ========================================= var trackArtistIds = trackModel.ArtistIds .Distinct() .ToList(); // If no track artists were selected, // automatically use the release's // main artist. if (trackArtistIds.Count == 0) { trackArtistIds.Add( model.MainArtistId); } long songOrdinal = 1; foreach (var artistId in trackArtistIds) { var songArtist = new SongArtist { SongId = song.SongId, ArtistId = artistId, SongOrdinal = songOrdinal++ }; _context.SongArtists.Add( songArtist); } _context.SaveChanges(); } } // ================================================= // SINGLE // ================================================= else { var single = new SingleRelease { ReleaseId = release.ReleaseId, Duration = model.SingleDuration! .Trim() }; _context.SingleReleases.Add( single); _context.SaveChanges(); } transaction.Commit(); TempData["Success"] = $"{release.Title} was created successfully."; return RedirectToAction( nameof(Index)); } catch { transaction.Rollback(); throw; } } // ========================================================= // AUTHORIZATION HELPERS // ========================================================= private bool IsAdmin() { return HttpContext.Session .GetString("Role") == "Admin"; } private bool IsProductManager() { var role = HttpContext.Session .GetString("Role"); var adminType = HttpContext.Session .GetString("AdminType"); return role == "Admin" && ( adminType == "PRODUCT_MANAGER" || adminType == "SUPER_ADMIN" ); } // ========================================================= // CURRENT ADMIN ID // ========================================================= private long? GetCurrentUserId() { var userId = HttpContext.Session .GetInt32("UserId"); if (!userId.HasValue) return null; return userId.Value; } // ========================================================= // LOAD RELEASES // ========================================================= private void LoadReleases() { ViewBag.Releases = _context.Releases .OrderBy(x => x.Title) .ToList(); } // ========================================================= // LOAD ARTISTS // ========================================================= private void LoadArtists() { ViewBag.Artists = _context.Artists .OrderBy(a => a.ArtistName) .ToList(); } // ========================================================= // NEXT PRODUCT ID // ========================================================= private long GetNextProductId() { var maxId = _context.Products .Select(x => (long?)x.ProductId) .Max(); return (maxId ?? 0) + 1; } // ========================================================= // NEXT RELEASE ID // ========================================================= private long GetNextReleaseId() { var maxId = _context.Releases .Select(x => (long?)x.ReleaseId) .Max(); return (maxId ?? 0) + 1; } // ========================================================= // NEXT SONG ID // ========================================================= private long GetNextSongId() { var maxId = _context.Songs .Select(x => (long?)x.SongId) .Max(); return (maxId ?? 0) + 1; } // ========================================================= // NEXT MODIFICATION ID // ========================================================= private long GetNextModificationId() { var maxId = _context.Modifications .Select(x => (long?)x.ModificationId) .Max(); return (maxId ?? 0) + 1; } // ========================================================= // CREATE MODIFICATION RECORD // ========================================================= private void CreateModification( ModificationType type, long productId, decimal? discount = null) { var adminId = GetCurrentUserId(); if (adminId == null) { throw new InvalidOperationException( "The current admin could not be identified."); } var modification = new Modification { ModificationId = GetNextModificationId(), AdminId = adminId.Value, DateModified = DateTime.Today, TypeOfModification = type, Discount = discount }; _context.Modifications.Add( modification); _context.SaveChanges(); var modificationProduct = new ModificationProduct { ModificationId = modification.ModificationId, ProductId = productId }; _context.ModificationProducts.Add( modificationProduct); _context.SaveChanges(); } // ========================================================= // ARTISTS // ========================================================= [HttpGet] public IActionResult Artists() { if (!IsAdmin()) return Forbid(); var artists = _context.Artists .OrderBy(a => a.ArtistName) .ToList(); return View(artists); } // ========================================================= // CREATE ARTIST - GET // ========================================================= [HttpGet] public IActionResult CreateArtist() { if (!IsAdmin()) return Forbid(); return View(new CreateArtistViewModel()); } // ========================================================= // CREATE ARTIST - POST // ========================================================= [HttpPost] [ValidateAntiForgeryToken] public IActionResult CreateArtist(CreateArtistViewModel model) { if (!IsAdmin()) return Forbid(); if (!ModelState.IsValid) return View(model); var duplicateExists = _context.Artists.Any(a => a.ArtistName.ToLower() == model.ArtistName.Trim().ToLower()); if (duplicateExists) { ModelState.AddModelError( nameof(model.ArtistName), "An artist with this name already exists."); return View(model); } var artist = new Artist { ArtistId = GetNextArtistId(), ArtistName = model.ArtistName.Trim(), ArtistDescription = string.IsNullOrWhiteSpace(model.ArtistDescription) ? null : model.ArtistDescription.Trim(), ArtistPhoto = string.IsNullOrWhiteSpace(model.ArtistPhoto) ? null : model.ArtistPhoto.Trim() }; _context.Artists.Add(artist); _context.SaveChanges(); TempData["Success"] = $"{artist.ArtistName} was created successfully."; return RedirectToAction(nameof(Artists)); } // ========================================================= // EDIT ARTIST - GET // ========================================================= [HttpGet] public IActionResult EditArtist(long id) { if (!IsAdmin()) return Forbid(); var artist = _context.Artists .FirstOrDefault(a => a.ArtistId == id); if (artist == null) return NotFound(); var model = new EditArtistViewModel { ArtistId = artist.ArtistId, ArtistName = artist.ArtistName, ArtistDescription = artist.ArtistDescription, ArtistPhoto = artist.ArtistPhoto }; return View(model); } // ========================================================= // EDIT ARTIST - POST // ========================================================= [HttpPost] [ValidateAntiForgeryToken] public IActionResult EditArtist(EditArtistViewModel model) { if (!IsAdmin()) return Forbid(); var artist = _context.Artists .FirstOrDefault(a => a.ArtistId == model.ArtistId); if (artist == null) return NotFound(); if (!ModelState.IsValid) return View(model); var duplicateExists = _context.Artists.Any(a => a.ArtistId != model.ArtistId && a.ArtistName.ToLower() == model.ArtistName.Trim().ToLower()); if (duplicateExists) { ModelState.AddModelError( nameof(model.ArtistName), "Another artist already uses this name."); return View(model); } artist.ArtistName = model.ArtistName.Trim(); artist.ArtistDescription = string.IsNullOrWhiteSpace(model.ArtistDescription) ? null : model.ArtistDescription.Trim(); artist.ArtistPhoto = string.IsNullOrWhiteSpace(model.ArtistPhoto) ? null : model.ArtistPhoto.Trim(); _context.Artists.Update(artist); _context.SaveChanges(); TempData["Success"] = $"{artist.ArtistName} was updated successfully."; return RedirectToAction(nameof(Artists)); } private long GetNextArtistId() { var maxId = _context.Artists .Select(x => (long?)x.ArtistId) .Max(); return (maxId ?? 0) + 1; } // ========================================================= // RELEASES // ========================================================= [HttpGet] public IActionResult Releases() { if (!IsAdmin()) return Forbid(); var releases = _context.Releases .Include(r => r.ReleaseArtists) .ThenInclude(ra => ra.Artist) .OrderBy(r => r.Title) .ToList(); return View(releases); } // ========================================================= // EDIT RELEASE - GET // ========================================================= [HttpGet] public IActionResult EditRelease(long id) { if (!IsAdmin()) return Forbid(); var release = _context.Releases .Include(r => r.ReleaseArtists) .ThenInclude(ra => ra.Artist) .FirstOrDefault(r => r.ReleaseId == id); if (release == null) return NotFound(); var mainArtist = release.ReleaseArtists .FirstOrDefault(ra => ra.Type == ArtistReleaseType.MAIN); var featuredArtists = release.ReleaseArtists .Where(ra => ra.Type == ArtistReleaseType.FEATURE) .OrderBy(ra => ra.ReleaseOrdinal) .Select(ra => ra.ArtistId) .ToList(); var model = new EditReleaseViewModel { ReleaseId = release.ReleaseId, Title = release.Title, RecordLabel = release.RecordLabel, Genre = release.Genre, ReleaseDate = release.ReleaseDate, CoverPhoto = release.CoverPhoto, MainArtistId = mainArtist?.ArtistId ?? 0, FeaturedArtistIds = featuredArtists }; LoadArtists(); return View(model); } // ========================================================= // EDIT RELEASE - POST // ========================================================= [HttpPost] [ValidateAntiForgeryToken] public IActionResult EditRelease( EditReleaseViewModel model) { if (!IsAdmin()) return Forbid(); var release = _context.Releases .Include(r => r.ReleaseArtists) .FirstOrDefault(r => r.ReleaseId == model.ReleaseId); if (release == null) return NotFound(); // ========================================== // ARTIST VALIDATION // ========================================== var mainArtistExists = _context.Artists.Any(a => a.ArtistId == model.MainArtistId); if (!mainArtistExists) { ModelState.AddModelError( nameof(model.MainArtistId), "Please select a valid main artist."); } model.FeaturedArtistIds ??= new List(); model.FeaturedArtistIds = model.FeaturedArtistIds .Distinct() .Where(id => id != model.MainArtistId) .ToList(); if (model.FeaturedArtistIds.Count > 0) { var validFeaturedCount = _context.Artists.Count(a => model.FeaturedArtistIds .Contains(a.ArtistId)); if (validFeaturedCount != model.FeaturedArtistIds.Count) { ModelState.AddModelError( nameof(model.FeaturedArtistIds), "One or more featured artists are invalid."); } } if (!ModelState.IsValid) { LoadArtists(); return View(model); } using var transaction = _context.Database.BeginTransaction(); try { // ========================================== // UPDATE RELEASE INFORMATION // ========================================== release.Title = model.Title.Trim(); release.RecordLabel = string.IsNullOrWhiteSpace( model.RecordLabel) ? null : model.RecordLabel.Trim(); release.Genre = model.Genre.Trim(); release.ReleaseDate = model.ReleaseDate; release.CoverPhoto = model.CoverPhoto.Trim(); _context.Releases.Update( release); // ========================================== // REMOVE OLD RELEASE ARTIST LINKS // ========================================== var existingArtistLinks = release.ReleaseArtists.ToList(); _context.ReleaseArtists.RemoveRange( existingArtistLinks); _context.SaveChanges(); // ========================================== // ADD MAIN ARTIST // ========================================== _context.ReleaseArtists.Add( new ReleaseArtist { ReleaseId = release.ReleaseId, ArtistId = model.MainArtistId, ReleaseOrdinal = 1, Type = ArtistReleaseType.MAIN }); // ========================================== // ADD FEATURED ARTISTS // ========================================== long ordinal = 2; foreach (var artistId in model.FeaturedArtistIds) { _context.ReleaseArtists.Add( new ReleaseArtist { ReleaseId = release.ReleaseId, ArtistId = artistId, ReleaseOrdinal = ordinal++, Type = ArtistReleaseType.FEATURE }); } _context.SaveChanges(); transaction.Commit(); TempData["Success"] = $"{release.Title} was updated successfully."; return RedirectToAction( nameof(Releases)); } catch { transaction.Rollback(); throw; } } }