Events-MVC (example with htmx)
This commit is contained in:
347
Events-MVC/Events.MVC/Controllers/CountriesController.cs
Normal file
347
Events-MVC/Events.MVC/Controllers/CountriesController.cs
Normal file
@@ -0,0 +1,347 @@
|
||||
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.Countries;
|
||||
using Events.MVC.Util.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Sieve.Models;
|
||||
using Sieve.Services;
|
||||
|
||||
namespace Events.MVC.Controllers;
|
||||
|
||||
public class CountriesController : Controller
|
||||
{
|
||||
private readonly EventsContext ctx;
|
||||
private readonly ISieveProcessor sieveProcessor;
|
||||
private readonly PagingSettings pagingSettings;
|
||||
|
||||
public CountriesController(EventsContext ctx, ISieveProcessor sieveProcessor, IOptions<PagingSettings> pagingSettings)
|
||||
{
|
||||
this.ctx = ctx;
|
||||
this.sieveProcessor = sieveProcessor;
|
||||
this.pagingSettings = pagingSettings.Value;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Index(SieveModel sieveModel)
|
||||
{
|
||||
var viewModel = await BuildCountriesListAsync(sieveModel);
|
||||
if (Request.Headers.ContainsKey(Constants.HtmxHeaders.Request))
|
||||
{
|
||||
return PartialView("_CountriesList", viewModel);
|
||||
}
|
||||
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Row(string id)
|
||||
{
|
||||
var country = await ctx.Countries
|
||||
.AsNoTracking()
|
||||
.Select(c => new CountryViewModel
|
||||
{
|
||||
Code = c.Code,
|
||||
Alpha3 = c.Alpha3,
|
||||
Name = c.Name
|
||||
})
|
||||
.FirstOrDefaultAsync(c => c.Code == id);
|
||||
|
||||
if (country is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return PartialView("_CountryRow", country);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> EditRow(string id)
|
||||
{
|
||||
var country = await ctx.Countries
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(c => c.Code == id);
|
||||
|
||||
if (country is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return PartialView("_CountryEditRow", MapCountryToViewModel(country, includeTranslations: true));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Create(CountryViewModel model, SieveModel sieveModel)
|
||||
{
|
||||
NormalizeTranslations(model);
|
||||
ValidateTranslations(model);
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
Response.Headers[Constants.HtmxHeaders.Retarget] = "#create-country-form";
|
||||
Response.Headers[Constants.HtmxHeaders.Reswap] = Constants.HtmxSwap.OuterHtml;
|
||||
return PartialView("_CreateCountryForm", model);
|
||||
}
|
||||
|
||||
var country = new Country
|
||||
{
|
||||
Code = model.Code.Trim().ToUpperInvariant(),
|
||||
Alpha3 = model.Alpha3.Trim().ToUpperInvariant(),
|
||||
Name = model.Name.Trim(),
|
||||
Translations = SerializeTranslations(model.Translations)
|
||||
};
|
||||
|
||||
ctx.Countries.Add(country);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
Response.Headers[Constants.HtmxHeaders.Trigger] = JsonSerializer.Serialize(new Dictionary<string, object?>
|
||||
{
|
||||
[Constants.HtmxEvents.CountryCreated] = true,
|
||||
[Constants.HtmxEvents.ShowToast] = new
|
||||
{
|
||||
variant = Constants.ToastVariants.Success,
|
||||
title = Constants.ToastTitles.Success,
|
||||
message = $"Country '{country.Name}' was added successfully."
|
||||
}
|
||||
});
|
||||
|
||||
var viewModel = await BuildCountriesListAsync(sieveModel);
|
||||
return PartialView("_CountriesList", viewModel);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Edit(string id, CountryViewModel model)
|
||||
{
|
||||
if (!string.Equals(id, model.Code, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
NormalizeTranslations(model);
|
||||
ValidateTranslations(model);
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return PartialView("_CountryEditRow", model);
|
||||
}
|
||||
|
||||
var country = await ctx.Countries.FirstOrDefaultAsync(c => c.Code == id);
|
||||
if (country is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
country.Alpha3 = model.Alpha3.Trim().ToUpperInvariant();
|
||||
country.Name = model.Name.Trim();
|
||||
country.Translations = SerializeTranslations(model.Translations);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
return PartialView("_CountryRow", new CountryViewModel
|
||||
{
|
||||
Code = country.Code,
|
||||
Alpha3 = country.Alpha3,
|
||||
Name = country.Name
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Delete(string id, SieveModel sieveModel)
|
||||
{
|
||||
var country = await ctx.Countries
|
||||
.Include(c => c.People)
|
||||
.FirstOrDefaultAsync(c => c.Code == id);
|
||||
|
||||
if (country is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (country.People.Count > 0)
|
||||
{
|
||||
Response.StatusCode = StatusCodes.Status409Conflict;
|
||||
return Content("The country cannot be deleted because related people exist.");
|
||||
}
|
||||
|
||||
ctx.Countries.Remove(country);
|
||||
var deletedName = country.Name;
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
Response.Headers[Constants.HtmxHeaders.Trigger] = JsonSerializer.Serialize(new Dictionary<string, object?>
|
||||
{
|
||||
[Constants.HtmxEvents.ShowToast] = new
|
||||
{
|
||||
variant = Constants.ToastVariants.Success,
|
||||
title = Constants.ToastTitles.Success,
|
||||
message = $"Country '{deletedName}' was deleted successfully."
|
||||
}
|
||||
});
|
||||
|
||||
var viewModel = await BuildCountriesListAsync(sieveModel);
|
||||
return PartialView("_CountriesList", viewModel);
|
||||
}
|
||||
|
||||
private async Task<PagedList<CountryViewModel>> BuildCountriesListAsync(SieveModel sieveModel)
|
||||
{
|
||||
sieveModel.SetDefaultPagingAndSorting(pagingSettings.PageSize, "Name");
|
||||
var normalizedFilters = sieveModel.Filters?.Trim() ?? string.Empty;
|
||||
var nameFilter = SieveModelExtensions.ExtractFilterValue(normalizedFilters, "Name");
|
||||
|
||||
var baseQuery = ctx.Countries
|
||||
.AsNoTracking()
|
||||
.Select(c => new CountryViewModel
|
||||
{
|
||||
Code = c.Code,
|
||||
Alpha3 = c.Alpha3,
|
||||
Name = c.Name
|
||||
});
|
||||
|
||||
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 ?? "Name",
|
||||
Filters = normalizedFilters,
|
||||
NameFilter = nameFilter
|
||||
};
|
||||
|
||||
if (pagingInfo.CurrentPage > pagingInfo.TotalPages)
|
||||
{
|
||||
pagingInfo.CurrentPage = pagingInfo.TotalPages;
|
||||
sieveModel.Page = pagingInfo.CurrentPage;
|
||||
}
|
||||
|
||||
var countries = await sieveProcessor
|
||||
.Apply(sieveModel, baseQuery)
|
||||
.ToListAsync();
|
||||
|
||||
return new PagedList<CountryViewModel>(countries, pagingInfo);
|
||||
}
|
||||
|
||||
private void ValidateTranslations(CountryViewModel model)
|
||||
{
|
||||
var languages = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var hasErrors = false;
|
||||
|
||||
for (var i = 0; i < model.Translations.Count; i++)
|
||||
{
|
||||
var translation = model.Translations[i] ?? new CountryTranslationViewModel();
|
||||
var language = translation.LanguageCode?.Trim() ?? string.Empty;
|
||||
var name = translation.Name?.Trim() ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrEmpty(language) && string.IsNullOrEmpty(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(language))
|
||||
{
|
||||
ModelState.AddModelError($"Translations[{i}].LanguageCode", "Enter a language code.");
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
ModelState.AddModelError($"Translations[{i}].Name", "Enter a translation.");
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(language) && !languages.Add(language))
|
||||
{
|
||||
ModelState.AddModelError($"Translations[{i}].LanguageCode", "The language code has already been entered.");
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasErrors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Check the translations. Every row must have both a language code and a translation, and language codes must be unique.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void NormalizeTranslations(CountryViewModel model)
|
||||
{
|
||||
model.Translations ??= [];
|
||||
|
||||
model.Translations = model.Translations
|
||||
.Where(t => t is not null)
|
||||
.Select(t => new CountryTranslationViewModel
|
||||
{
|
||||
LanguageCode = t!.LanguageCode?.Trim().ToLowerInvariant() ?? string.Empty,
|
||||
Name = t.Name?.Trim() ?? string.Empty
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static CountryViewModel MapCountryToViewModel(Country country, bool includeTranslations)
|
||||
{
|
||||
return new CountryViewModel
|
||||
{
|
||||
Code = country.Code,
|
||||
Alpha3 = country.Alpha3,
|
||||
Name = country.Name,
|
||||
Translations = includeTranslations ? ParseTranslations(country.Translations) : []
|
||||
};
|
||||
}
|
||||
|
||||
private static List<CountryTranslationViewModel> ParseTranslations(string? translationsJson)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(translationsJson))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var translations = JsonSerializer.Deserialize<Dictionary<string, string>>(translationsJson);
|
||||
if (translations is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return translations
|
||||
.OrderBy(t => t.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(t => new CountryTranslationViewModel
|
||||
{
|
||||
LanguageCode = t.Key,
|
||||
Name = t.Value
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static string? SerializeTranslations(IEnumerable<CountryTranslationViewModel> translations)
|
||||
{
|
||||
var dictionary = translations
|
||||
.Where(t => t is not null && !string.IsNullOrWhiteSpace(t.LanguageCode) && !string.IsNullOrWhiteSpace(t.Name))
|
||||
.ToDictionary(t => t.LanguageCode.Trim().ToLowerInvariant(), t => t.Name.Trim(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return dictionary.Count == 0 ? null : JsonSerializer.Serialize(dictionary);
|
||||
}
|
||||
}
|
||||
241
Events-MVC/Events.MVC/Controllers/EventsController.cs
Normal file
241
Events-MVC/Events.MVC/Controllers/EventsController.cs
Normal file
@@ -0,0 +1,241 @@
|
||||
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.Events;
|
||||
using Events.MVC.Util.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Sieve.Models;
|
||||
using Sieve.Services;
|
||||
|
||||
namespace Events.MVC.Controllers;
|
||||
|
||||
public class EventsController : Controller
|
||||
{
|
||||
private readonly EventsContext ctx;
|
||||
private readonly ISieveProcessor sieveProcessor;
|
||||
private readonly PagingSettings pagingSettings;
|
||||
|
||||
public EventsController(EventsContext ctx, ISieveProcessor sieveProcessor, IOptions<PagingSettings> pagingSettings)
|
||||
{
|
||||
this.ctx = ctx;
|
||||
this.sieveProcessor = sieveProcessor;
|
||||
this.pagingSettings = pagingSettings.Value;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Index(SieveModel sieveModel)
|
||||
{
|
||||
var viewModel = await BuildEventsListAsync(sieveModel);
|
||||
if (Request.Headers.ContainsKey(Constants.HtmxHeaders.Request))
|
||||
{
|
||||
return PartialView("_EventsList", viewModel);
|
||||
}
|
||||
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Row(int id)
|
||||
{
|
||||
var eventModel = await ctx.Events
|
||||
.AsNoTracking()
|
||||
.Select(e => new EventViewModel
|
||||
{
|
||||
Id = e.Id,
|
||||
Name = e.Name,
|
||||
EventDate = e.EventDate,
|
||||
ParticipantsCount = e.Registrations.Count
|
||||
})
|
||||
.FirstOrDefaultAsync(e => e.Id == id);
|
||||
|
||||
if (eventModel is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return PartialView("_EventRow", eventModel);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> EditRow(int id)
|
||||
{
|
||||
var eventModel = await ctx.Events
|
||||
.AsNoTracking()
|
||||
.Select(e => new EventViewModel
|
||||
{
|
||||
Id = e.Id,
|
||||
Name = e.Name,
|
||||
EventDate = e.EventDate
|
||||
})
|
||||
.FirstOrDefaultAsync(e => e.Id == id);
|
||||
|
||||
if (eventModel is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return PartialView("_EventEditRow", eventModel);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Create(EventViewModel model, SieveModel sieveModel)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
Response.Headers[Constants.HtmxHeaders.Retarget] = "#create-event-form";
|
||||
Response.Headers[Constants.HtmxHeaders.Reswap] = Constants.HtmxSwap.OuterHtml;
|
||||
return PartialView("_CreateEventForm", model);
|
||||
}
|
||||
|
||||
var eventEntity = new Event
|
||||
{
|
||||
Name = model.Name,
|
||||
EventDate = model.EventDate
|
||||
};
|
||||
ctx.Events.Add(eventEntity);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
Response.Headers[Constants.HtmxHeaders.Trigger] = JsonSerializer.Serialize(new Dictionary<string, object?>
|
||||
{
|
||||
[Constants.HtmxEvents.EventCreated] = true,
|
||||
[Constants.HtmxEvents.ShowToast] = new
|
||||
{
|
||||
variant = Constants.ToastVariants.Success,
|
||||
title = Constants.ToastTitles.Success,
|
||||
message = $"Event '{model.Name}' was added successfully."
|
||||
}
|
||||
});
|
||||
|
||||
var viewModel = await BuildEventsListAsync(sieveModel);
|
||||
return PartialView("_EventsList", viewModel);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Edit(int id, EventViewModel model)
|
||||
{
|
||||
if (id != model.Id)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return PartialView("_EventEditRow", model);
|
||||
}
|
||||
|
||||
var existingEvent = await ctx.Events.FirstOrDefaultAsync(e => e.Id == id);
|
||||
if (existingEvent is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
existingEvent.Name = model.Name;
|
||||
existingEvent.EventDate = model.EventDate;
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
return PartialView("_EventRow", new EventViewModel
|
||||
{
|
||||
Id = existingEvent.Id,
|
||||
Name = existingEvent.Name,
|
||||
EventDate = existingEvent.EventDate,
|
||||
ParticipantsCount = await ctx.Registrations.CountAsync(r => r.EventId == existingEvent.Id)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Delete(int id, SieveModel sieveModel)
|
||||
{
|
||||
var eventEntity = await ctx.Events
|
||||
.Include(e => e.Registrations)
|
||||
.FirstOrDefaultAsync(e => e.Id == id);
|
||||
|
||||
if (eventEntity is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (eventEntity.Registrations.Count > 0)
|
||||
{
|
||||
Response.StatusCode = StatusCodes.Status409Conflict;
|
||||
return Content("The event cannot be deleted because registrations exist.");
|
||||
}
|
||||
|
||||
ctx.Events.Remove(eventEntity);
|
||||
var deletedName = eventEntity.Name;
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
Response.Headers[Constants.HtmxHeaders.Trigger] = JsonSerializer.Serialize(new Dictionary<string, object?>
|
||||
{
|
||||
[Constants.HtmxEvents.ShowToast] = new
|
||||
{
|
||||
variant = Constants.ToastVariants.Success,
|
||||
title = Constants.ToastTitles.Success,
|
||||
message = $"Event '{deletedName}' was deleted successfully."
|
||||
}
|
||||
});
|
||||
|
||||
var viewModel = await BuildEventsListAsync(sieveModel);
|
||||
return PartialView("_EventsList", viewModel);
|
||||
}
|
||||
|
||||
private async Task<PagedList<EventViewModel>> BuildEventsListAsync(SieveModel sieveModel)
|
||||
{
|
||||
sieveModel.SetDefaultPagingAndSorting(pagingSettings.PageSize, "EventDate");
|
||||
var normalizedFilters = sieveModel.Filters?.Trim() ?? string.Empty;
|
||||
var nameFilter = SieveModelExtensions.ExtractFilterValue(normalizedFilters, "Name");
|
||||
|
||||
var baseQuery = ctx.Events
|
||||
.AsNoTracking()
|
||||
.Select(e => new EventViewModel
|
||||
{
|
||||
Id = e.Id,
|
||||
Name = e.Name,
|
||||
EventDate = e.EventDate,
|
||||
ParticipantsCount = e.Registrations.Count
|
||||
});
|
||||
|
||||
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 ?? "EventDate",
|
||||
Filters = normalizedFilters,
|
||||
NameFilter = nameFilter
|
||||
};
|
||||
|
||||
if (pagingInfo.CurrentPage > pagingInfo.TotalPages)
|
||||
{
|
||||
pagingInfo.CurrentPage = pagingInfo.TotalPages;
|
||||
sieveModel.Page = pagingInfo.CurrentPage;
|
||||
}
|
||||
|
||||
var events = await sieveProcessor
|
||||
.Apply(sieveModel, baseQuery)
|
||||
.ToListAsync();
|
||||
|
||||
return new PagedList<EventViewModel>(events, pagingInfo);
|
||||
}
|
||||
}
|
||||
21
Events-MVC/Events.MVC/Controllers/HomeController.cs
Normal file
21
Events-MVC/Events.MVC/Controllers/HomeController.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Diagnostics;
|
||||
using Events.MVC.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Events.MVC.Controllers;
|
||||
|
||||
public class HomeController : Controller
|
||||
{
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
public IActionResult Error()
|
||||
{
|
||||
return View(new ErrorViewModel
|
||||
{
|
||||
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier
|
||||
});
|
||||
}
|
||||
}
|
||||
337
Events-MVC/Events.MVC/Controllers/PeopleController.cs
Normal file
337
Events-MVC/Events.MVC/Controllers/PeopleController.cs
Normal file
@@ -0,0 +1,337 @@
|
||||
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.People;
|
||||
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 PeopleController : Controller
|
||||
{
|
||||
private readonly EventsContext ctx;
|
||||
private readonly ISieveProcessor sieveProcessor;
|
||||
private readonly PagingSettings pagingSettings;
|
||||
|
||||
public PeopleController(EventsContext ctx, ISieveProcessor sieveProcessor, IOptions<PagingSettings> pagingSettings)
|
||||
{
|
||||
this.ctx = ctx;
|
||||
this.sieveProcessor = sieveProcessor;
|
||||
this.pagingSettings = pagingSettings.Value;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Index(SieveModel sieveModel)
|
||||
{
|
||||
if (!await ctx.Countries.AsNoTracking().AnyAsync())
|
||||
{
|
||||
TempData[Constants.TempDataKeys.ToastVariant] = Constants.ToastVariants.Error;
|
||||
TempData[Constants.TempDataKeys.ToastTitle] = Constants.ToastTitles.Error;
|
||||
TempData[Constants.TempDataKeys.ToastMessage] = Constants.Messages.CountriesRequiredForPeople;
|
||||
return RedirectToAction("Index", "Countries");
|
||||
}
|
||||
|
||||
var viewModel = await BuildPeopleListAsync(sieveModel);
|
||||
if (Request.Headers.ContainsKey(Constants.HtmxHeaders.Request))
|
||||
{
|
||||
return PartialView("_PeopleList", viewModel);
|
||||
}
|
||||
|
||||
ViewData[Constants.ViewDataKeys.CreatePersonModel] = new PersonViewModel
|
||||
{
|
||||
CountryOptions = await GetCountryOptionsAsync()
|
||||
};
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Row(int id)
|
||||
{
|
||||
var person = await ctx.People
|
||||
.AsNoTracking()
|
||||
.Select(p => new PersonViewModel
|
||||
{
|
||||
Id = p.Id,
|
||||
FirstName = p.FirstName,
|
||||
LastName = p.LastName,
|
||||
FirstNameTranscription = p.FirstNameTranscription,
|
||||
LastNameTranscription = p.LastNameTranscription,
|
||||
FullName = p.FirstName + " " + p.LastName,
|
||||
FullNameTranscription = p.FirstNameTranscription + " " + p.LastNameTranscription,
|
||||
BirthDate = p.BirthDate,
|
||||
CountryName = p.CountryCodeNavigation.Name,
|
||||
RegistrationsCount = p.Registrations.Count
|
||||
})
|
||||
.FirstOrDefaultAsync(p => p.Id == id);
|
||||
|
||||
if (person is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return PartialView("_PersonRow", person);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> EditRow(int id)
|
||||
{
|
||||
var person = await ctx.People
|
||||
.AsNoTracking()
|
||||
.Select(p => new PersonViewModel
|
||||
{
|
||||
Id = p.Id,
|
||||
FirstName = p.FirstName,
|
||||
LastName = p.LastName,
|
||||
FirstNameTranscription = p.FirstNameTranscription,
|
||||
LastNameTranscription = p.LastNameTranscription,
|
||||
AddressLine = p.AddressLine,
|
||||
PostalCode = p.PostalCode,
|
||||
City = p.City,
|
||||
AddressCountry = p.AddressCountry,
|
||||
Email = p.Email,
|
||||
ContactPhone = p.ContactPhone,
|
||||
BirthDate = p.BirthDate,
|
||||
DocumentNumber = p.DocumentNumber,
|
||||
CountryCode = p.CountryCode
|
||||
})
|
||||
.FirstOrDefaultAsync(p => p.Id == id);
|
||||
|
||||
if (person is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
person.CountryOptions = await GetCountryOptionsAsync(person.CountryCode);
|
||||
return PartialView("_PersonEditRow", person);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Create(PersonViewModel model, SieveModel sieveModel)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
model.CountryOptions = await GetCountryOptionsAsync(model.CountryCode);
|
||||
Response.Headers[Constants.HtmxHeaders.Retarget] = "#create-person-form";
|
||||
Response.Headers[Constants.HtmxHeaders.Reswap] = Constants.HtmxSwap.OuterHtml;
|
||||
return PartialView("_CreatePersonForm", model);
|
||||
}
|
||||
|
||||
var person = new Person
|
||||
{
|
||||
FirstName = model.FirstName.TrimToNull(),
|
||||
LastName = model.LastName.TrimToNull(),
|
||||
FirstNameTranscription = model.FirstNameTranscription.Trim(),
|
||||
LastNameTranscription = model.LastNameTranscription.Trim(),
|
||||
AddressLine = model.AddressLine.TrimToNull(),
|
||||
PostalCode = model.PostalCode.TrimToNull(),
|
||||
City = model.City.TrimToNull(),
|
||||
AddressCountry = model.AddressCountry.TrimToNull(),
|
||||
Email = model.Email.TrimToNull(),
|
||||
ContactPhone = model.ContactPhone.TrimToNull(),
|
||||
BirthDate = model.BirthDate!.Value,
|
||||
DocumentNumber = model.DocumentNumber.Trim(),
|
||||
CountryCode = model.CountryCode.Trim().ToUpperInvariant()
|
||||
};
|
||||
|
||||
ctx.People.Add(person);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
Response.Headers[Constants.HtmxHeaders.Trigger] = JsonSerializer.Serialize(new Dictionary<string, object?>
|
||||
{
|
||||
[Constants.HtmxEvents.PersonCreated] = true,
|
||||
[Constants.HtmxEvents.ShowToast] = new
|
||||
{
|
||||
variant = Constants.ToastVariants.Success,
|
||||
title = Constants.ToastTitles.Success,
|
||||
message = $"Person '{person.FirstName} {person.LastName}' was added successfully."
|
||||
}
|
||||
});
|
||||
|
||||
var viewModel = await BuildPeopleListAsync(sieveModel);
|
||||
return PartialView("_PeopleList", viewModel);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Edit(int id, PersonViewModel model)
|
||||
{
|
||||
if (id != model.Id)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
model.CountryOptions = await GetCountryOptionsAsync(model.CountryCode);
|
||||
return PartialView("_PersonEditRow", model);
|
||||
}
|
||||
|
||||
var person = await ctx.People.FirstOrDefaultAsync(p => p.Id == id);
|
||||
if (person is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
person.FirstName = model.FirstName.TrimToNull();
|
||||
person.LastName = model.LastName.TrimToNull();
|
||||
person.FirstNameTranscription = model.FirstNameTranscription.Trim();
|
||||
person.LastNameTranscription = model.LastNameTranscription.Trim();
|
||||
person.AddressLine = model.AddressLine.TrimToNull();
|
||||
person.PostalCode = model.PostalCode.TrimToNull();
|
||||
person.City = model.City.TrimToNull();
|
||||
person.AddressCountry = model.AddressCountry.TrimToNull();
|
||||
person.Email = model.Email.TrimToNull();
|
||||
person.ContactPhone = model.ContactPhone.TrimToNull();
|
||||
person.BirthDate = model.BirthDate!.Value;
|
||||
person.DocumentNumber = model.DocumentNumber.Trim();
|
||||
person.CountryCode = model.CountryCode.Trim().ToUpperInvariant();
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
var rowModel = await ctx.People
|
||||
.AsNoTracking()
|
||||
.Where(p => p.Id == id)
|
||||
.Select(p => new PersonViewModel
|
||||
{
|
||||
Id = p.Id,
|
||||
FirstName = p.FirstName,
|
||||
LastName = p.LastName,
|
||||
FirstNameTranscription = p.FirstNameTranscription,
|
||||
LastNameTranscription = p.LastNameTranscription,
|
||||
FullName = p.FirstName + " " + p.LastName,
|
||||
FullNameTranscription = p.FirstNameTranscription + " " + p.LastNameTranscription,
|
||||
BirthDate = p.BirthDate,
|
||||
CountryName = p.CountryCodeNavigation.Name,
|
||||
RegistrationsCount = p.Registrations.Count
|
||||
})
|
||||
.FirstAsync();
|
||||
|
||||
return PartialView("_PersonRow", rowModel);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Delete(int id, SieveModel sieveModel)
|
||||
{
|
||||
var person = await ctx.People
|
||||
.Include(p => p.Registrations)
|
||||
.FirstOrDefaultAsync(p => p.Id == id);
|
||||
|
||||
if (person is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (person.Registrations.Count > 0)
|
||||
{
|
||||
Response.StatusCode = StatusCodes.Status409Conflict;
|
||||
return Content("The person cannot be deleted because registrations exist.");
|
||||
}
|
||||
|
||||
ctx.People.Remove(person);
|
||||
var deletedName = $"{person.FirstName} {person.LastName}";
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
Response.Headers[Constants.HtmxHeaders.Trigger] = JsonSerializer.Serialize(new Dictionary<string, object?>
|
||||
{
|
||||
[Constants.HtmxEvents.ShowToast] = new
|
||||
{
|
||||
variant = Constants.ToastVariants.Success,
|
||||
title = Constants.ToastTitles.Success,
|
||||
message = $"Person '{deletedName}' was deleted successfully."
|
||||
}
|
||||
});
|
||||
|
||||
var viewModel = await BuildPeopleListAsync(sieveModel);
|
||||
return PartialView("_PeopleList", viewModel);
|
||||
}
|
||||
|
||||
private async Task<PeoplePageViewModel> BuildPeopleListAsync(SieveModel sieveModel)
|
||||
{
|
||||
sieveModel.SetDefaultPagingAndSorting(pagingSettings.PageSize, "LastNameTranscription");
|
||||
var normalizedFilters = sieveModel.Filters?.Trim() ?? string.Empty;
|
||||
var nameFilter = SieveModelExtensions.ExtractFilterValue(normalizedFilters, "FullNameTranscription");
|
||||
var countryFilter = SieveModelExtensions.ExtractFilterValue(normalizedFilters, "CountryCode", "==");
|
||||
|
||||
var baseQuery = ctx.People
|
||||
.AsNoTracking()
|
||||
.Select(p => new PersonViewModel
|
||||
{
|
||||
Id = p.Id,
|
||||
FirstName = p.FirstName,
|
||||
LastName = p.LastName,
|
||||
FirstNameTranscription = p.FirstNameTranscription,
|
||||
LastNameTranscription = p.LastNameTranscription,
|
||||
FullName = p.FirstName + " " + p.LastName,
|
||||
FullNameTranscription = p.FirstNameTranscription + " " + p.LastNameTranscription,
|
||||
CountryCode = p.CountryCode,
|
||||
BirthDate = p.BirthDate,
|
||||
CountryName = p.CountryCodeNavigation.Name,
|
||||
RegistrationsCount = p.Registrations.Count
|
||||
});
|
||||
|
||||
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 ?? "LastNameTranscription",
|
||||
Filters = normalizedFilters,
|
||||
NameFilter = nameFilter
|
||||
};
|
||||
|
||||
if (pagingInfo.CurrentPage > pagingInfo.TotalPages)
|
||||
{
|
||||
pagingInfo.CurrentPage = pagingInfo.TotalPages;
|
||||
sieveModel.Page = pagingInfo.CurrentPage;
|
||||
}
|
||||
|
||||
var people = await sieveProcessor
|
||||
.Apply(sieveModel, baseQuery)
|
||||
.ToListAsync();
|
||||
|
||||
return new PeoplePageViewModel
|
||||
{
|
||||
People = new PagedList<PersonViewModel>(people, pagingInfo),
|
||||
CountryOptions = await GetCountryOptionsAsync(countryFilter),
|
||||
CountryFilter = countryFilter
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<List<SelectListItem>> GetCountryOptionsAsync(string? selectedCode = null)
|
||||
{
|
||||
return await ctx.Countries
|
||||
.AsNoTracking()
|
||||
.OrderBy(c => c.Name)
|
||||
.Select(c => new SelectListItem
|
||||
{
|
||||
Value = c.Code,
|
||||
Text = c.Name,
|
||||
Selected = c.Code == selectedCode
|
||||
})
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
}
|
||||
430
Events-MVC/Events.MVC/Controllers/RegistrationsController.cs
Normal file
430
Events-MVC/Events.MVC/Controllers/RegistrationsController.cs
Normal file
@@ -0,0 +1,430 @@
|
||||
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> pagingSettings)
|
||||
{
|
||||
this.ctx = ctx;
|
||||
this.sieveProcessor = sieveProcessor;
|
||||
this.pagingSettings = pagingSettings.Value;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> 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<IActionResult> Row(int id, int eventId)
|
||||
{
|
||||
var registration = await ctx.Registrations
|
||||
.AsNoTracking()
|
||||
.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<IActionResult> EditRow(int id, int eventId)
|
||||
{
|
||||
var registration = await ctx.Registrations
|
||||
.AsNoTracking()
|
||||
.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<IActionResult> PersonSuggestions(string? personLookup, string? countryFilter)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(personLookup))
|
||||
{
|
||||
return PartialView("_PersonSuggestions", Array.Empty<SelectListItem>());
|
||||
}
|
||||
|
||||
var searchTerm = personLookup.Trim().ToLowerInvariant();
|
||||
var query = ctx.People
|
||||
.AsNoTracking()
|
||||
.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<IActionResult> 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<string, object?>
|
||||
{
|
||||
[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<IActionResult> 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
|
||||
.AsNoTracking()
|
||||
.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<IActionResult> 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<string, object?>
|
||||
{
|
||||
[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<RegistrationsPageViewModel> BuildPageViewModelAsync(int selectedEventId, SieveModel sieveModel, List<SelectListItem>? 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
|
||||
.AsNoTracking()
|
||||
.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<RegistrationViewModel>(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
|
||||
.AsNoTracking()
|
||||
.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
|
||||
.AsNoTracking()
|
||||
.OrderBy(s => s.Name)
|
||||
.Select(s => new SelectListItem
|
||||
{
|
||||
Value = s.Id.ToString(),
|
||||
Text = s.Name,
|
||||
Selected = s.Id == model.SportId
|
||||
})
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private async Task<List<SelectListItem>> 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<bool> CanCreateRegistrationsAsync()
|
||||
{
|
||||
return await ctx.Events.AsNoTracking().AnyAsync()
|
||||
&& await ctx.People.AsNoTracking().AnyAsync()
|
||||
&& await ctx.Sports.AsNoTracking().AnyAsync();
|
||||
}
|
||||
|
||||
private async Task<List<SelectListItem>> GetCountryOptionsAsync(string? selectedCountryCode)
|
||||
{
|
||||
return await ctx.Countries
|
||||
.AsNoTracking()
|
||||
.OrderBy(c => c.Name)
|
||||
.Select(c => new SelectListItem
|
||||
{
|
||||
Value = c.Code,
|
||||
Text = c.Name,
|
||||
Selected = c.Code == selectedCountryCode
|
||||
})
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private static void MarkSelectedEvent(IEnumerable<SelectListItem> events, int selectedEventId)
|
||||
{
|
||||
foreach (var eventOption in events)
|
||||
{
|
||||
eventOption.Selected = string.Equals(eventOption.Value, selectedEventId.ToString(), StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
228
Events-MVC/Events.MVC/Controllers/SportsController.cs
Normal file
228
Events-MVC/Events.MVC/Controllers/SportsController.cs
Normal file
@@ -0,0 +1,228 @@
|
||||
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.Sports;
|
||||
using Events.MVC.Util.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Sieve.Models;
|
||||
using Sieve.Services;
|
||||
|
||||
namespace Events.MVC.Controllers;
|
||||
|
||||
public class SportsController : Controller
|
||||
{
|
||||
private readonly EventsContext ctx;
|
||||
private readonly ISieveProcessor sieveProcessor;
|
||||
private readonly PagingSettings pagingSettings;
|
||||
|
||||
public SportsController(EventsContext ctx, ISieveProcessor sieveProcessor, IOptions<PagingSettings> pagingSettings)
|
||||
{
|
||||
this.ctx = ctx;
|
||||
this.sieveProcessor = sieveProcessor;
|
||||
this.pagingSettings = pagingSettings.Value;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Index(SieveModel sieveModel)
|
||||
{
|
||||
var viewModel = await BuildSportsListAsync(sieveModel);
|
||||
if (Request.Headers.ContainsKey(Constants.HtmxHeaders.Request))
|
||||
{
|
||||
return PartialView("_SportsList", viewModel);
|
||||
}
|
||||
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Row(int id)
|
||||
{
|
||||
var sport = await ctx.Sports
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(s => s.Id == id);
|
||||
|
||||
if (sport is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return PartialView("_SportRow", new SportViewModel
|
||||
{
|
||||
Id = sport.Id,
|
||||
Name = sport.Name
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> EditRow(int id)
|
||||
{
|
||||
var sport = await ctx.Sports
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(s => s.Id == id);
|
||||
|
||||
if (sport is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return PartialView("_SportEditRow", new SportViewModel
|
||||
{
|
||||
Id = sport.Id,
|
||||
Name = sport.Name
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Create(SportViewModel model, SieveModel sieveModel)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
Response.Headers[Constants.HtmxHeaders.Retarget] = "#create-sport-form";
|
||||
Response.Headers[Constants.HtmxHeaders.Reswap] = Constants.HtmxSwap.OuterHtml;
|
||||
return PartialView("_CreateSportForm", model);
|
||||
}
|
||||
|
||||
var sport = new Sport
|
||||
{
|
||||
Name = model.Name
|
||||
};
|
||||
ctx.Sports.Add(sport);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
Response.Headers[Constants.HtmxHeaders.Trigger] = JsonSerializer.Serialize(new Dictionary<string, object?>
|
||||
{
|
||||
[Constants.HtmxEvents.SportCreated] = true,
|
||||
[Constants.HtmxEvents.ShowToast] = new
|
||||
{
|
||||
variant = Constants.ToastVariants.Success,
|
||||
title = Constants.ToastTitles.Success,
|
||||
message = $"Sport '{model.Name}' was added successfully."
|
||||
}
|
||||
});
|
||||
var viewModel = await BuildSportsListAsync(sieveModel);
|
||||
return PartialView("_SportsList", viewModel);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Edit(int id, SportViewModel model)
|
||||
{
|
||||
if (id != model.Id)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return PartialView("_SportEditRow", model);
|
||||
}
|
||||
|
||||
var existingSport = await ctx.Sports.FirstOrDefaultAsync(s => s.Id == id);
|
||||
if (existingSport is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
existingSport.Name = model.Name;
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
return PartialView("_SportRow", new SportViewModel
|
||||
{
|
||||
Id = existingSport.Id,
|
||||
Name = existingSport.Name
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Delete(int id, SieveModel sieveModel)
|
||||
{
|
||||
var sport = await ctx.Sports
|
||||
.Include(s => s.Registrations)
|
||||
.FirstOrDefaultAsync(s => s.Id == id);
|
||||
|
||||
if (sport is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (sport.Registrations.Count > 0)
|
||||
{
|
||||
Response.StatusCode = StatusCodes.Status409Conflict;
|
||||
return Content("The sport cannot be deleted because registrations exist.");
|
||||
}
|
||||
|
||||
ctx.Sports.Remove(sport);
|
||||
var deletedName = sport.Name;
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
Response.Headers[Constants.HtmxHeaders.Trigger] = JsonSerializer.Serialize(new Dictionary<string, object?>
|
||||
{
|
||||
[Constants.HtmxEvents.ShowToast] = new
|
||||
{
|
||||
variant = Constants.ToastVariants.Success,
|
||||
title = Constants.ToastTitles.Success,
|
||||
message = $"Sport '{deletedName}' was deleted successfully."
|
||||
}
|
||||
});
|
||||
var viewModel = await BuildSportsListAsync(sieveModel);
|
||||
return PartialView("_SportsList", viewModel);
|
||||
}
|
||||
|
||||
private async Task<PagedList<SportViewModel>> BuildSportsListAsync(SieveModel sieveModel)
|
||||
{
|
||||
sieveModel.SetDefaultPagingAndSorting(pagingSettings.PageSize, "Name");
|
||||
var normalizedFilters = sieveModel.Filters?.Trim() ?? string.Empty;
|
||||
var nameFilter = SieveModelExtensions.ExtractFilterValue(normalizedFilters, "Name");
|
||||
|
||||
var baseQuery = ctx.Sports
|
||||
.AsNoTracking()
|
||||
.Select(s => new SportViewModel
|
||||
{
|
||||
Id = s.Id,
|
||||
Name = s.Name
|
||||
});
|
||||
|
||||
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 ?? "Name",
|
||||
Filters = normalizedFilters,
|
||||
NameFilter = nameFilter
|
||||
};
|
||||
|
||||
if (pagingInfo.CurrentPage > pagingInfo.TotalPages)
|
||||
{
|
||||
pagingInfo.CurrentPage = pagingInfo.TotalPages;
|
||||
sieveModel.Page = pagingInfo.CurrentPage;
|
||||
}
|
||||
|
||||
var sports = await sieveProcessor
|
||||
.Apply(sieveModel, baseQuery)
|
||||
.ToListAsync();
|
||||
|
||||
return new PagedList<SportViewModel>(sports, pagingInfo);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user