using System.Text.Json; #if POSTGRES using Events.EF.Data.Postgres; #else using Events.EF.Data.MSSQL; #endif using Events.EF.Models; using Events.MVC.Models; using Events.MVC.Models.Registrations; using Events.MVC.Util.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Sieve.Models; using Sieve.Services; namespace Events.MVC.Controllers; public class RegistrationsController : Controller { private readonly EventsContext ctx; private readonly ISieveProcessor sieveProcessor; private readonly PagingSettings pagingSettings; public RegistrationsController(EventsContext ctx, ISieveProcessor sieveProcessor, IOptions pagingSettings) { this.ctx = ctx; this.sieveProcessor = sieveProcessor; this.pagingSettings = pagingSettings.Value; } public async Task Index(int? eventId, SieveModel sieveModel) { var events = await GetEventOptionsAsync(eventId); if (events.Count == 0) { TempData[Constants.TempDataKeys.ToastVariant] = Constants.ToastVariants.Error; TempData[Constants.TempDataKeys.ToastTitle] = Constants.ToastTitles.Error; TempData[Constants.TempDataKeys.ToastMessage] = Constants.Messages.EventsRequiredForRegistrations; return RedirectToAction("Index", "Events"); } if (!await ctx.Sports.AsNoTracking().AnyAsync()) { TempData[Constants.TempDataKeys.ToastVariant] = Constants.ToastVariants.Error; TempData[Constants.TempDataKeys.ToastTitle] = Constants.ToastTitles.Error; TempData[Constants.TempDataKeys.ToastMessage] = Constants.Messages.SportsRequiredForRegistrations; return RedirectToAction("Index", "Sports"); } if (!await ctx.People.AsNoTracking().AnyAsync()) { TempData[Constants.TempDataKeys.ToastVariant] = Constants.ToastVariants.Error; TempData[Constants.TempDataKeys.ToastTitle] = Constants.ToastTitles.Error; TempData[Constants.TempDataKeys.ToastMessage] = Constants.Messages.PeopleRequiredForRegistrations; return RedirectToAction("Index", "People"); } var selectedEventId = eventId ?? int.Parse(events[0].Value); MarkSelectedEvent(events, selectedEventId); var viewModel = await BuildPageViewModelAsync(selectedEventId, sieveModel, events); if (Request.Headers.ContainsKey(Constants.HtmxHeaders.Request)) { return PartialView("_RegistrationsPanel", viewModel); } return View(viewModel); } [HttpGet] public async Task Row(int id, int eventId) { var registration = await ctx.Registrations .Where(r => r.Id == id && r.EventId == eventId) .Select(r => new RegistrationViewModel { Id = r.Id, EventId = r.EventId, PersonId = r.PersonId, SportId = r.SportId, PersonName = r.Person.FirstName + " " + r.Person.LastName, PersonTranscription = r.Person.FirstNameTranscription + " " + r.Person.LastNameTranscription, CountryCode = r.Person.CountryCode, CountryName = r.Person.CountryCodeNavigation.Name, SportName = r.Sport.Name, RegisteredAt = r.RegisteredAt }) .FirstOrDefaultAsync(); if (registration is null) { return NotFound(); } return PartialView("_RegistrationRow", registration); } [HttpGet] public async Task EditRow(int id, int eventId) { var registration = await ctx.Registrations .Where(r => r.Id == id && r.EventId == eventId) .Select(r => new RegistrationViewModel { Id = r.Id, EventId = r.EventId, PersonId = r.PersonId, SportId = r.SportId, RegisteredAt = r.RegisteredAt }) .FirstOrDefaultAsync(); if (registration is null) { return NotFound(); } await PopulateRegistrationOptionsAsync(registration); return PartialView("_RegistrationEditRow", registration); } [HttpGet] public async Task PersonSuggestions(string? personLookup, string? countryFilter) { if (string.IsNullOrWhiteSpace(personLookup)) { return PartialView("_PersonSuggestions", Array.Empty()); } var searchTerm = personLookup.Trim().ToLowerInvariant(); var query = ctx.People .Where(p => p.FirstNameTranscription.ToLower().Contains(searchTerm) || p.LastNameTranscription.ToLower().Contains(searchTerm) || (p.FirstNameTranscription + " " + p.LastNameTranscription).ToLower().Contains(searchTerm)) .AsQueryable(); if (!string.IsNullOrWhiteSpace(countryFilter)) { query = query.Where(p => p.CountryCode == countryFilter); } var suggestions = await query .OrderBy(p => p.LastName) .ThenBy(p => p.FirstName) .Take(10) .Select(p => new SelectListItem { Value = p.Id.ToString(), Text = p.FirstName + " " + p.LastName + "|" + p.FirstNameTranscription + " " + p.LastNameTranscription }) .ToListAsync(); return PartialView("_PersonSuggestions", suggestions); } [HttpPost] [ValidateAntiForgeryToken] public async Task Create(RegistrationViewModel model, SieveModel sieveModel) { if (!await CanCreateRegistrationsAsync()) { Response.StatusCode = StatusCodes.Status409Conflict; return Content(Constants.Messages.RegistrationDependenciesRequired); } if (!ModelState.IsValid) { await PopulateRegistrationOptionsAsync(model); Response.Headers[Constants.HtmxHeaders.Retarget] = "#create-registration-form"; Response.Headers[Constants.HtmxHeaders.Reswap] = Constants.HtmxSwap.OuterHtml; return PartialView("_CreateRegistrationForm", model); } var registration = new Registration { EventId = model.EventId, PersonId = model.PersonId, SportId = model.SportId }; ctx.Registrations.Add(registration); await ctx.SaveChangesAsync(); Response.Headers[Constants.HtmxHeaders.Trigger] = JsonSerializer.Serialize(new Dictionary { [Constants.HtmxEvents.RegistrationCreated] = true, [Constants.HtmxEvents.ShowToast] = new { variant = Constants.ToastVariants.Success, title = Constants.ToastTitles.Success, message = "Registration was added successfully." } }); var viewModel = await BuildPageViewModelAsync(model.EventId, sieveModel); return PartialView("_RegistrationsPanel", viewModel); } [HttpPost] [ValidateAntiForgeryToken] public async Task Edit(int id, RegistrationViewModel model) { if (id != model.Id) { return BadRequest(); } if (!ModelState.IsValid) { await PopulateRegistrationOptionsAsync(model); return PartialView("_RegistrationEditRow", model); } var registration = await ctx.Registrations.FirstOrDefaultAsync(r => r.Id == id && r.EventId == model.EventId); if (registration is null) { return NotFound(); } registration.PersonId = model.PersonId; registration.SportId = model.SportId; await ctx.SaveChangesAsync(); var rowModel = await ctx.Registrations .Where(r => r.Id == id) .Select(r => new RegistrationViewModel { Id = r.Id, EventId = r.EventId, PersonId = r.PersonId, SportId = r.SportId, PersonName = r.Person.FirstName + " " + r.Person.LastName, PersonTranscription = r.Person.FirstNameTranscription + " " + r.Person.LastNameTranscription, CountryCode = r.Person.CountryCode, CountryName = r.Person.CountryCodeNavigation.Name, SportName = r.Sport.Name, RegisteredAt = r.RegisteredAt }) .FirstAsync(); return PartialView("_RegistrationRow", rowModel); } [HttpPost] [ValidateAntiForgeryToken] public async Task Delete(int id, int eventId, SieveModel sieveModel) { var registration = await ctx.Registrations.FirstOrDefaultAsync(r => r.Id == id && r.EventId == eventId); if (registration is null) { return NotFound(); } ctx.Registrations.Remove(registration); await ctx.SaveChangesAsync(); Response.Headers[Constants.HtmxHeaders.Trigger] = JsonSerializer.Serialize(new Dictionary { [Constants.HtmxEvents.ShowToast] = new { variant = Constants.ToastVariants.Success, title = Constants.ToastTitles.Success, message = "Registration was deleted successfully." } }); var viewModel = await BuildPageViewModelAsync(eventId, sieveModel); return PartialView("_RegistrationsPanel", viewModel); } private async Task BuildPageViewModelAsync(int selectedEventId, SieveModel sieveModel, List? eventOptions = null) { sieveModel.SetDefaultPagingAndSorting(pagingSettings.PageSize, "RegisteredAt"); var normalizedFilters = sieveModel.Filters?.Trim() ?? string.Empty; var nameFilter = SieveModelExtensions.ExtractFilterValue(normalizedFilters, "PersonTranscription"); var countryFilter = SieveModelExtensions.ExtractFilterValue(normalizedFilters, "CountryCode", "=="); var baseQuery = ctx.Registrations .Where(r => r.EventId == selectedEventId) .Select(r => new RegistrationViewModel { Id = r.Id, EventId = r.EventId, PersonId = r.PersonId, SportId = r.SportId, PersonName = r.Person.FirstName + " " + r.Person.LastName, PersonTranscription = r.Person.FirstNameTranscription + " " + r.Person.LastNameTranscription, CountryCode = r.Person.CountryCode, CountryName = r.Person.CountryCodeNavigation.Name, SportName = r.Sport.Name, RegisteredAt = r.RegisteredAt }); var totalCount = await baseQuery.CountAsync(); var filteredCount = string.IsNullOrWhiteSpace(normalizedFilters) ? totalCount : await sieveProcessor .Apply(sieveModel, baseQuery, applyFiltering: true, applySorting: false, applyPagination: false) .CountAsync(); var pagingInfo = new PagingInfo { FilteredItemsCount = filteredCount, TotalItemsCount = totalCount, ItemsPerPage = sieveModel.PageSize!.Value, CurrentPage = sieveModel.Page!.Value, Sorts = sieveModel.Sorts ?? "RegisteredAt", Filters = normalizedFilters, NameFilter = nameFilter }; if (pagingInfo.CurrentPage > pagingInfo.TotalPages) { pagingInfo.CurrentPage = pagingInfo.TotalPages; sieveModel.Page = pagingInfo.CurrentPage; } var registrationsData = await sieveProcessor .Apply(sieveModel, baseQuery) .ToListAsync(); var registrations = new PagedList(registrationsData, pagingInfo); eventOptions ??= await GetEventOptionsAsync(selectedEventId); MarkSelectedEvent(eventOptions, selectedEventId); var selectedEventName = eventOptions.FirstOrDefault(e => e.Selected)?.Text ?? string.Empty; var canCreate = await CanCreateRegistrationsAsync(); var countryOptions = await GetCountryOptionsAsync(countryFilter); var createModel = new RegistrationViewModel { EventId = selectedEventId }; await PopulateRegistrationOptionsAsync(createModel); return new RegistrationsPageViewModel { SelectedEventId = selectedEventId, SelectedEventName = selectedEventName, EventOptions = eventOptions, CountryOptions = countryOptions, CountryFilter = countryFilter, Registrations = registrations, CreateModel = createModel, CanCreate = canCreate, CreateDisabledMessage = canCreate ? null : Constants.Messages.RegistrationDependenciesRequired }; } private async Task PopulateRegistrationOptionsAsync(RegistrationViewModel model) { if (model.PersonId > 0) { model.PersonLookup = await ctx.People .Where(p => p.Id == model.PersonId) .Select(p => p.FirstName + " " + p.LastName + " (" + p.FirstNameTranscription + " " + p.LastNameTranscription + ")") .FirstOrDefaultAsync() ?? string.Empty; } else { model.PersonLookup = string.Empty; } model.SportOptions = await ctx.Sports .OrderBy(s => s.Name) .Select(s => new SelectListItem { Value = s.Id.ToString(), Text = s.Name, Selected = s.Id == model.SportId }) .ToListAsync(); } private async Task> GetEventOptionsAsync(int? selectedEventId) { var events = await ctx.Events .AsNoTracking() .OrderBy(e => e.EventDate) .ThenBy(e => e.Name) .ToListAsync(); return events .Select(e => new SelectListItem { Value = e.Id.ToString(), Text = $"{e.Name} ({e.EventDate:dd.MM.yyyy.})", Selected = e.Id == selectedEventId }) .ToList(); } private async Task CanCreateRegistrationsAsync() { return await ctx.Events.AsNoTracking().AnyAsync() && await ctx.People.AsNoTracking().AnyAsync() && await ctx.Sports.AsNoTracking().AnyAsync(); } private async Task> GetCountryOptionsAsync(string? selectedCountryCode) { return await ctx.Countries .OrderBy(c => c.Name) .Select(c => new SelectListItem { Value = c.Code, Text = c.Name, Selected = c.Code == selectedCountryCode }) .ToListAsync(); } private static void MarkSelectedEvent(IEnumerable events, int selectedEventId) { foreach (var eventOption in events) { eventOption.Selected = string.Equals(eventOption.Value, selectedEventId.ToString(), StringComparison.Ordinal); } } }