diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/MVC-SimpleCRUD-Layered.Application.csproj b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/MVC-SimpleCRUD-Layered.Application.csproj new file mode 100644 index 0000000..0139252 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/MVC-SimpleCRUD-Layered.Application.csproj @@ -0,0 +1,19 @@ + + + + net10.0 + enable + enable + MVC_SimpleCRUD_Layered.Application + + + + + + + + + + + + diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/Models/PagedList.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/Models/PagedList.cs new file mode 100644 index 0000000..a4e02a3 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/Models/PagedList.cs @@ -0,0 +1,3 @@ +namespace MVC_SimpleCRUD_Layered.Application.Models; + +public record PagedList(List Data, PagingInfo PagingInfo); diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/Models/PagingInfo.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/Models/PagingInfo.cs new file mode 100644 index 0000000..d061ed3 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/Models/PagingInfo.cs @@ -0,0 +1,37 @@ +namespace MVC_SimpleCRUD_Layered.Application.Models; + +public class PagingInfo +{ + public int TotalItemsCount { get; set; } + + public int FilteredItemsCount { get; set; } + + public int ItemsPerPage { get; set; } + + public int CurrentPage { get; set; } + + public string? Sorts { get; set; } + + public string? SearchText { get; set; } + + public int TotalPages => Math.Max(1, (int)Math.Ceiling((decimal)FilteredItemsCount / ItemsPerPage)); + + public bool IsFiltered => !string.IsNullOrWhiteSpace(SearchText); + + public string ToggleSort(string propertyName) + { + return string.Equals(Sorts, propertyName, StringComparison.OrdinalIgnoreCase) + ? $"-{propertyName}" + : propertyName; + } + + public bool IsSortedBy(string propertyName) + { + return string.Equals(Sorts?.TrimStart('-'), propertyName, StringComparison.OrdinalIgnoreCase); + } + + public bool IsDescending() + { + return Sorts?.StartsWith("-", StringComparison.Ordinal) == true; + } +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/Models/PagingSettings.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/Models/PagingSettings.cs new file mode 100644 index 0000000..89c58db --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/Models/PagingSettings.cs @@ -0,0 +1,10 @@ +namespace MVC_SimpleCRUD_Layered.Application.Models; + +public class PagingSettings +{ + public const string SectionName = "Paging"; + + public int PageSize { get; set; } = 20; + + public int PageOffset { get; set; } = 5; +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/CountryOption.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/CountryOption.cs new file mode 100644 index 0000000..0c270e2 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/CountryOption.cs @@ -0,0 +1,3 @@ +namespace MVC_SimpleCRUD_Layered.Application.People; + +public record CountryOption(string Code, string Name); diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/DeletePersonResult.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/DeletePersonResult.cs new file mode 100644 index 0000000..34bc1c0 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/DeletePersonResult.cs @@ -0,0 +1,19 @@ +namespace MVC_SimpleCRUD_Layered.Application.People; + +public record DeletePersonResult(bool Found, bool Success, string? PersonName, string? ErrorMessage) +{ + public static DeletePersonResult NotFound() + { + return new DeletePersonResult(false, false, null, null); + } + + public static DeletePersonResult Deleted(string personName) + { + return new DeletePersonResult(true, true, personName, null); + } + + public static DeletePersonResult Failed(string personName, string errorMessage) + { + return new DeletePersonResult(true, false, personName, errorMessage); + } +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/IPeopleService.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/IPeopleService.cs new file mode 100644 index 0000000..8e63d1f --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/IPeopleService.cs @@ -0,0 +1,20 @@ +using MVC_SimpleCRUD_Layered.Application.Models; + +namespace MVC_SimpleCRUD_Layered.Application.People; + +public interface IPeopleService +{ + Task> GetAllForSimpleListAsync(); + + Task> GetPagedListAsync(PeopleIndexRequest request); + + Task GetFormForEditAsync(int id, PeopleIndexRequest returnRequest); + + Task> GetCountryOptionsAsync(); + + Task CreateAsync(PersonForm form); + + Task UpdateAsync(int id, PersonForm form); + + Task DeleteAsync(int id); +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/PeopleIndexRequest.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/PeopleIndexRequest.cs new file mode 100644 index 0000000..500a6c0 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/PeopleIndexRequest.cs @@ -0,0 +1,12 @@ +namespace MVC_SimpleCRUD_Layered.Application.People; + +public class PeopleIndexRequest +{ + public int Page { get; set; } = 1; + + public int PageSize { get; set; } + + public string? Sorts { get; set; } + + public string? SearchText { get; set; } +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/PeopleService.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/PeopleService.cs new file mode 100644 index 0000000..48ce0cf --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/PeopleService.cs @@ -0,0 +1,221 @@ +#if POSTGRES +using MVC_SimpleCRUD_Layered.Data.Data.Postgres; +#else +using MVC_SimpleCRUD_Layered.Data.Data.MSSQL; +#endif +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using MVC_SimpleCRUD_Layered.Application.Models; +using MVC_SimpleCRUD_Layered.Application.Util.Extensions; +using MVC_SimpleCRUD_Layered.Data.Models; +using Sieve.Models; +using Sieve.Services; + +namespace MVC_SimpleCRUD_Layered.Application.People; + +public class PeopleService : IPeopleService +{ + private readonly EventsContext ctx; + private readonly ISieveProcessor sieveProcessor; + private readonly PagingSettings pagingSettings; + + public PeopleService(EventsContext ctx, IOptionsSnapshot pagingSettings, ISieveProcessor sieveProcessor) + { + this.ctx = ctx; + this.sieveProcessor = sieveProcessor; + this.pagingSettings = pagingSettings.Value; + } + + public async Task> GetAllForSimpleListAsync() + { + return await ProjectPeople + .OrderBy(p => p.LastNameTranscription) + .ToListAsync(); + } + + public async Task> GetPagedListAsync(PeopleIndexRequest request) + { + var page = request.Page < 1 ? 1 : request.Page; + var pageSize = request.PageSize <= 0 ? pagingSettings.PageSize : request.PageSize; + var sorts = request.Sorts.NullIfWhiteSpace() ?? nameof(PersonInfo.LastNameTranscription); + + var sieveModel = new SieveModel + { + Page = page, + PageSize = pageSize, + Sorts = sorts + }; + + var totalCount = await ctx.People.CountAsync(); + var query = ProjectPeople; + + int filteredCount; + var searchText = request.SearchText.NullIfWhiteSpace(); + if (searchText is not null) + { + sieveModel.Filters = $"({nameof(PersonInfo.FirstNameTranscription)}|{nameof(PersonInfo.LastNameTranscription)}|{nameof(PersonInfo.CountryName)})@=*{searchText}"; + filteredCount = await sieveProcessor + .Apply(sieveModel, query, applyPagination: false, applySorting: false) + .CountAsync(); + } + else + { + filteredCount = totalCount; + } + + var pagingInfo = new PagingInfo + { + FilteredItemsCount = filteredCount, + TotalItemsCount = totalCount, + ItemsPerPage = pageSize, + CurrentPage = page, + Sorts = sorts, + SearchText = searchText + }; + + if (pagingInfo.CurrentPage > pagingInfo.TotalPages) + { + pagingInfo.CurrentPage = pagingInfo.TotalPages; + sieveModel.Page = pagingInfo.CurrentPage; + } + + var people = filteredCount > 0 + ? await sieveProcessor.Apply(sieveModel, query).ToListAsync() + : []; + + return new PagedList(people, pagingInfo); + } + + public async Task GetFormForEditAsync(int id, PeopleIndexRequest returnRequest) + { + var person = await ctx.People.AsNoTracking().FirstOrDefaultAsync(p => p.Id == id); + return person is null ? null : CreateForm(person, returnRequest); + } + + public async Task> GetCountryOptionsAsync() + { + return await ctx.Countries + .AsNoTracking() + .OrderBy(d => d.Name) + .Select(d => new CountryOption(d.Code, d.Name)) + .ToListAsync(); + } + + public async Task CreateAsync(PersonForm form) + { + var person = new Person(); + CopyToPerson(form, person); + ctx.People.Add(person); + + try + { + await ctx.SaveChangesAsync(); + return SavePersonResult.Ok(GetDisplayName(person)); + } + catch (DbUpdateException exc) + { + return SavePersonResult.Failed(GetErrorMessage(exc)); + } + } + + public async Task UpdateAsync(int id, PersonForm form) + { + var person = await ctx.People.FindAsync(id); + if (person is null) + { + return SavePersonResult.Failed($"Person with id {id} was not found."); + } + + CopyToPerson(form, person); + + try + { + await ctx.SaveChangesAsync(); + return SavePersonResult.Ok(GetDisplayName(person)); + } + catch (DbUpdateException exc) + { + return SavePersonResult.Failed(GetErrorMessage(exc)); + } + } + + public async Task DeleteAsync(int id) + { + var person = await ctx.People.FindAsync(id); + if (person is null) + { + return DeletePersonResult.NotFound(); + } + + var personName = GetDisplayName(person); + ctx.People.Remove(person); + + try + { + await ctx.SaveChangesAsync(); + return DeletePersonResult.Deleted(personName); + } + catch (DbUpdateException exc) + { + return DeletePersonResult.Failed(personName, GetErrorMessage(exc)); + } + } + + private IQueryable ProjectPeople => ctx.People + .Select(p => new PersonInfo + { + Id = p.Id, + FirstName = p.FirstName, + LastName = p.LastName, + FirstNameTranscription = p.FirstNameTranscription, + LastNameTranscription = p.LastNameTranscription, + BirthDate = p.BirthDate, + CountryName = p.CountryCodeNavigation.Name, + }); + + private static PersonForm CreateForm(Person person, PeopleIndexRequest returnRequest) + { + return new PersonForm + { + Id = person.Id, + FirstName = person.FirstName, + LastName = person.LastName, + FirstNameTranscription = person.FirstNameTranscription, + LastNameTranscription = person.LastNameTranscription, + AddressLine = person.AddressLine, + PostalCode = person.PostalCode, + City = person.City, + AddressCountry = person.AddressCountry, + Email = person.Email, + ContactPhone = person.ContactPhone, + BirthDate = person.BirthDate, + DocumentNumber = person.DocumentNumber, + CountryCode = person.CountryCode, + Page = returnRequest.Page, + PageSize = returnRequest.PageSize, + Sorts = returnRequest.Sorts, + SearchText = returnRequest.SearchText + }; + } + + private static void CopyToPerson(PersonForm form, Person person) + { + person.FirstName = form.FirstName.NullIfWhiteSpace(); + person.LastName = form.LastName.NullIfWhiteSpace(); + person.FirstNameTranscription = form.FirstNameTranscription; + person.LastNameTranscription = form.LastNameTranscription; + person.AddressLine = form.AddressLine.NullIfWhiteSpace(); + person.PostalCode = form.PostalCode.NullIfWhiteSpace(); + person.City = form.City.NullIfWhiteSpace(); + person.AddressCountry = form.AddressCountry.NullIfWhiteSpace(); + person.Email = form.Email.NullIfWhiteSpace(); + person.ContactPhone = form.ContactPhone.NullIfWhiteSpace(); + person.BirthDate = form.BirthDate!.Value; + person.DocumentNumber = form.DocumentNumber; + person.CountryCode = form.CountryCode; + } + + private static string GetDisplayName(Person person) => $"{person.FirstNameTranscription} {person.LastNameTranscription}"; + + private static string GetErrorMessage(DbUpdateException exc) => exc.InnerException?.Message ?? exc.Message; +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/PersonForm.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/PersonForm.cs new file mode 100644 index 0000000..0171cec --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/PersonForm.cs @@ -0,0 +1,75 @@ +using System.ComponentModel.DataAnnotations; +namespace MVC_SimpleCRUD_Layered.Application.People; + +public class PersonForm : IValidatableObject +{ + public int? Id { get; set; } + + [StringLength(100)] + [Display(Name = "First name (native)")] + public string? FirstName { get; set; } + + [StringLength(100)] + [Display(Name = "Last name (native)")] + public string? LastName { get; set; } + + [Required, StringLength(100)] + [Display(Name = "First name (English)")] + public string FirstNameTranscription { get; set; } = string.Empty; + + [Required, StringLength(100)] + [Display(Name = "Last name (English)")] + public string LastNameTranscription { get; set; } = string.Empty; + + [StringLength(200)] + [Display(Name = "Address")] + public string? AddressLine { get; set; } + + [StringLength(20)] + [Display(Name = "Postal Code")] + public string? PostalCode { get; set; } + + [StringLength(100)] + public string? City { get; set; } + + [StringLength(100)] + [Display(Name = "Address Country")] + public string? AddressCountry { get; set; } + + [StringLength(255), EmailAddress] + public string? Email { get; set; } + + [StringLength(50), Phone] + [Display(Name = "Contact Phone")] + public string? ContactPhone { get; set; } + + [Required] + [Display(Name = "Birth Date")] + public DateOnly? BirthDate { get; set; } + + [Required, StringLength(50)] + [Display(Name = "Document Number")] + public string DocumentNumber { get; set; } = string.Empty; + + [Required, StringLength(3)] + [Display(Name = "Country")] + public string CountryCode { get; set; } = string.Empty; + + public int Page { get; set; } = 1; + + public int PageSize { get; set; } + + public string? Sorts { get; set; } + + public string? SearchText { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (string.IsNullOrWhiteSpace(Email) && string.IsNullOrWhiteSpace(ContactPhone)) + { + yield return new ValidationResult( + "Email or contact phone is required.", + new[] { nameof(Email), nameof(ContactPhone) }); + } + } +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/PersonInfo.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/PersonInfo.cs new file mode 100644 index 0000000..ed233be --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/PersonInfo.cs @@ -0,0 +1,30 @@ +using Sieve.Attributes; + +namespace MVC_SimpleCRUD_Layered.Application.People; + +public class PersonInfo +{ + [Sieve(CanFilter = true, CanSort = true)] + public int Id { get; set; } + + [Sieve(CanFilter = true, CanSort = true)] + public string? FirstName { get; set; } + + [Sieve(CanFilter = true, CanSort = true)] + public string? LastName { get; set; } + + public string OriginalName => ((FirstName ?? string.Empty) + " " + (LastName ?? string.Empty)).Trim(); + + [Sieve(CanFilter = true, CanSort = true)] + public required string FirstNameTranscription { get; set; } + + [Sieve(CanFilter = true, CanSort = true)] + public required string LastNameTranscription { get; set; } + + + [Sieve(CanFilter = true, CanSort = true)] + public DateOnly BirthDate { get; set; } + + [Sieve(CanFilter = true, CanSort = true)] + public required string CountryName { get; set; } +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/SavePersonResult.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/SavePersonResult.cs new file mode 100644 index 0000000..0ecad90 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/People/SavePersonResult.cs @@ -0,0 +1,14 @@ +namespace MVC_SimpleCRUD_Layered.Application.People; + +public record SavePersonResult(bool Success, string? PersonName, string? ErrorMessage) +{ + public static SavePersonResult Ok(string personName) + { + return new SavePersonResult(true, personName, null); + } + + public static SavePersonResult Failed(string errorMessage) + { + return new SavePersonResult(false, null, errorMessage); + } +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/Util/Extensions/StringExtensions.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/Util/Extensions/StringExtensions.cs new file mode 100644 index 0000000..77b910f --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Application/Util/Extensions/StringExtensions.cs @@ -0,0 +1,9 @@ +namespace MVC_SimpleCRUD_Layered.Application.Util.Extensions; + +public static class StringExtensions +{ + public static string? NullIfWhiteSpace(this string? value) + { + return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + } +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Data/MSSQL/EventsContext.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Data/MSSQL/EventsContext.cs new file mode 100644 index 0000000..671f02a --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Data/MSSQL/EventsContext.cs @@ -0,0 +1,143 @@ +// This file has been auto generated by EF Core Power Tools. +#nullable enable +using System; +using System.Collections.Generic; +using MVC_SimpleCRUD_Layered.Data.Models; +using Microsoft.EntityFrameworkCore; + +namespace MVC_SimpleCRUD_Layered.Data.Data.MSSQL; + +public partial class EventsContext : DbContext +{ + public EventsContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet Countries { get; set; } + + public virtual DbSet Events { get; set; } + + public virtual DbSet People { get; set; } + + public virtual DbSet Registrations { get; set; } + + public virtual DbSet Sports { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Code); + + entity.ToTable("Country"); + + entity.HasIndex(e => e.Name, "UQ_Country_Name").IsUnique(); + + entity.Property(e => e.Code) + .HasMaxLength(3) + .IsUnicode(false); + entity.Property(e => e.Alpha3) + .HasMaxLength(3) + .IsUnicode(false) + .IsFixedLength(); + entity.Property(e => e.Name) + .HasMaxLength(100) + .IsUnicode(false); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("Event"); + + entity.Property(e => e.Name) + .HasMaxLength(150) + .IsUnicode(false); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("Person"); + + entity.HasIndex(e => new { e.DocumentNumber, e.CountryCode }, "UQ_Person_DocumentNumber_CountryCode").IsUnique(); + + entity.Property(e => e.AddressCountry) + .HasMaxLength(100) + .IsUnicode(false); + entity.Property(e => e.AddressLine) + .HasMaxLength(200) + .IsUnicode(false); + entity.Property(e => e.City) + .HasMaxLength(100) + .IsUnicode(false); + entity.Property(e => e.ContactPhone) + .HasMaxLength(50) + .IsUnicode(false); + entity.Property(e => e.CountryCode) + .HasMaxLength(3) + .IsUnicode(false); + entity.Property(e => e.DocumentNumber) + .HasMaxLength(50) + .IsUnicode(false); + entity.Property(e => e.Email) + .HasMaxLength(255) + .IsUnicode(false); + entity.Property(e => e.FirstName) + .HasMaxLength(100) + .IsUnicode(false); + entity.Property(e => e.FirstNameTranscription) + .HasMaxLength(100) + .IsUnicode(false); + entity.Property(e => e.LastName) + .HasMaxLength(100) + .IsUnicode(false); + entity.Property(e => e.LastNameTranscription) + .HasMaxLength(100) + .IsUnicode(false); + entity.Property(e => e.PostalCode) + .HasMaxLength(20) + .IsUnicode(false); + + entity.HasOne(d => d.CountryCodeNavigation).WithMany(p => p.People) + .HasForeignKey(d => d.CountryCode) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK_Person_Country"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("Registration"); + + entity.HasIndex(e => new { e.PersonId, e.SportId, e.EventId }, "UQ_Registration_PersonId_SportId_EventId").IsUnique(); + + entity.Property(e => e.RegisteredAt).HasDefaultValueSql("(sysutcdatetime())", "DF_Registration_RegisteredAt"); + + entity.HasOne(d => d.Event).WithMany(p => p.Registrations) + .HasForeignKey(d => d.EventId) + .HasConstraintName("FK_Registration_Event"); + + entity.HasOne(d => d.Person).WithMany(p => p.Registrations) + .HasForeignKey(d => d.PersonId) + .HasConstraintName("FK_Registration_Person"); + + entity.HasOne(d => d.Sport).WithMany(p => p.Registrations) + .HasForeignKey(d => d.SportId) + .HasConstraintName("FK_Registration_Sport"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("Sport"); + + entity.HasIndex(e => e.Name, "UQ_Sport_Name").IsUnique(); + + entity.Property(e => e.Name) + .HasMaxLength(100) + .IsUnicode(false); + }); + + OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Data/Postgres/EventsContext.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Data/Postgres/EventsContext.cs new file mode 100644 index 0000000..a637609 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Data/Postgres/EventsContext.cs @@ -0,0 +1,165 @@ +// This file has been auto generated by EF Core Power Tools. +#nullable enable +using System; +using System.Collections.Generic; +using MVC_SimpleCRUD_Layered.Data.Models; +using Microsoft.EntityFrameworkCore; + +namespace MVC_SimpleCRUD_Layered.Data.Data.Postgres; + +public partial class EventsContext : DbContext +{ + public EventsContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet Countries { get; set; } + + public virtual DbSet Events { get; set; } + + public virtual DbSet People { get; set; } + + public virtual DbSet Registrations { get; set; } + + public virtual DbSet Sports { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Code).HasName("country_pkey"); + + entity.ToTable("country"); + + entity.HasIndex(e => e.Name, "country_name_key").IsUnique(); + + entity.Property(e => e.Code) + .HasMaxLength(3) + .HasColumnName("code"); + entity.Property(e => e.Alpha3) + .HasMaxLength(3) + .IsFixedLength() + .HasColumnName("alpha3"); + entity.Property(e => e.Name) + .HasMaxLength(100) + .HasColumnName("name"); + entity.Property(e => e.Translations) + .HasColumnType("jsonb") + .HasColumnName("translations"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("event_pkey"); + + entity.ToTable("event"); + + entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.EventDate).HasColumnName("event_date"); + entity.Property(e => e.Name) + .HasMaxLength(150) + .HasColumnName("name"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("person_pkey"); + + entity.ToTable("person"); + + entity.HasIndex(e => new { e.DocumentNumber, e.CountryCode }, "person_document_number_country_code_key").IsUnique(); + + entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.AddressCountry) + .HasMaxLength(100) + .HasColumnName("address_country"); + entity.Property(e => e.AddressLine) + .HasMaxLength(200) + .HasColumnName("address_line"); + entity.Property(e => e.BirthDate).HasColumnName("birth_date"); + entity.Property(e => e.City) + .HasMaxLength(100) + .HasColumnName("city"); + entity.Property(e => e.ContactPhone) + .HasMaxLength(50) + .HasColumnName("contact_phone"); + entity.Property(e => e.CountryCode) + .HasMaxLength(3) + .HasColumnName("country_code"); + entity.Property(e => e.DocumentNumber) + .HasMaxLength(50) + .HasColumnName("document_number"); + entity.Property(e => e.Email) + .HasMaxLength(255) + .HasColumnName("email"); + entity.Property(e => e.FirstName) + .HasMaxLength(100) + .HasColumnName("first_name"); + entity.Property(e => e.FirstNameTranscription) + .HasMaxLength(100) + .HasColumnName("first_name_transcription"); + entity.Property(e => e.LastName) + .HasMaxLength(100) + .HasColumnName("last_name"); + entity.Property(e => e.LastNameTranscription) + .HasMaxLength(100) + .HasColumnName("last_name_transcription"); + entity.Property(e => e.PostalCode) + .HasMaxLength(20) + .HasColumnName("postal_code"); + + entity.HasOne(d => d.CountryCodeNavigation).WithMany(p => p.People) + .HasForeignKey(d => d.CountryCode) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("person_country_code_fkey"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("registration_pkey"); + + entity.ToTable("registration"); + + entity.HasIndex(e => new { e.PersonId, e.SportId, e.EventId }, "registration_person_id_sport_id_event_id_key").IsUnique(); + + entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.EventId).HasColumnName("event_id"); + entity.Property(e => e.PersonId).HasColumnName("person_id"); + entity.Property(e => e.RegisteredAt) + .HasDefaultValueSql("CURRENT_TIMESTAMP") + .HasColumnName("registered_at"); + entity.Property(e => e.SportId).HasColumnName("sport_id"); + + entity.HasOne(d => d.Event).WithMany(p => p.Registrations) + .HasForeignKey(d => d.EventId) + .HasConstraintName("registration_event_id_fkey"); + + entity.HasOne(d => d.Person).WithMany(p => p.Registrations) + .HasForeignKey(d => d.PersonId) + .HasConstraintName("registration_person_id_fkey"); + + entity.HasOne(d => d.Sport).WithMany(p => p.Registrations) + .HasForeignKey(d => d.SportId) + .HasConstraintName("registration_sport_id_fkey"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("sport_pkey"); + + entity.ToTable("sport"); + + entity.HasIndex(e => e.Name, "sport_name_key").IsUnique(); + + entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.Name) + .HasMaxLength(100) + .HasColumnName("name"); + }); + + OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); +} \ No newline at end of file diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/MVC-SimpleCRUD-Layered.Data.csproj b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/MVC-SimpleCRUD-Layered.Data.csproj new file mode 100644 index 0000000..d578085 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/MVC-SimpleCRUD-Layered.Data.csproj @@ -0,0 +1,15 @@ + + + + net10.0 + enable + enable + MVC_SimpleCRUD_Layered.Data + + + + + + + + diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Models/Country.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Models/Country.cs new file mode 100644 index 0000000..caafc02 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Models/Country.cs @@ -0,0 +1,19 @@ +// This file has been auto generated by EF Core Power Tools. +#nullable enable +using System; +using System.Collections.Generic; + +namespace MVC_SimpleCRUD_Layered.Data.Models; + +public partial class Country +{ + public string Code { get; set; } = null!; + + public string Alpha3 { get; set; } = null!; + + public string Name { get; set; } = null!; + + public string? Translations { get; set; } + + public virtual ICollection People { get; set; } = new List(); +} \ No newline at end of file diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Models/Event.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Models/Event.cs new file mode 100644 index 0000000..ee558af --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Models/Event.cs @@ -0,0 +1,17 @@ +// This file has been auto generated by EF Core Power Tools. +#nullable enable +using System; +using System.Collections.Generic; + +namespace MVC_SimpleCRUD_Layered.Data.Models; + +public partial class Event +{ + public int Id { get; set; } + + public string Name { get; set; } = null!; + + public DateOnly EventDate { get; set; } + + public virtual ICollection Registrations { get; set; } = new List(); +} \ No newline at end of file diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Models/Person.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Models/Person.cs new file mode 100644 index 0000000..d1bd8ed --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Models/Person.cs @@ -0,0 +1,41 @@ +// This file has been auto generated by EF Core Power Tools. +#nullable enable +using System; +using System.Collections.Generic; + +namespace MVC_SimpleCRUD_Layered.Data.Models; + +public partial class Person +{ + public int Id { get; set; } + + public string? FirstName { get; set; } + + public string? LastName { get; set; } + + public string FirstNameTranscription { get; set; } = null!; + + public string LastNameTranscription { get; set; } = null!; + + public string? AddressLine { get; set; } + + public string? PostalCode { get; set; } + + public string? City { get; set; } + + public string? AddressCountry { get; set; } + + public string? Email { get; set; } + + public string? ContactPhone { get; set; } + + public DateOnly BirthDate { get; set; } + + public string DocumentNumber { get; set; } = null!; + + public string CountryCode { get; set; } = null!; + + public virtual Country CountryCodeNavigation { get; set; } = null!; + + public virtual ICollection Registrations { get; set; } = new List(); +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Models/Registration.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Models/Registration.cs new file mode 100644 index 0000000..3be35ae --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Models/Registration.cs @@ -0,0 +1,25 @@ +// This file has been auto generated by EF Core Power Tools. +#nullable enable +using System; +using System.Collections.Generic; + +namespace MVC_SimpleCRUD_Layered.Data.Models; + +public partial class Registration +{ + public int Id { get; set; } + + public int PersonId { get; set; } + + public int SportId { get; set; } + + public int EventId { get; set; } + + public DateTime RegisteredAt { get; set; } + + public virtual Event Event { get; set; } = null!; + + public virtual Person Person { get; set; } = null!; + + public virtual Sport Sport { get; set; } = null!; +} \ No newline at end of file diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Models/Sport.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Models/Sport.cs new file mode 100644 index 0000000..fc22388 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/Models/Sport.cs @@ -0,0 +1,15 @@ +// This file has been auto generated by EF Core Power Tools. +#nullable enable +using System; +using System.Collections.Generic; + +namespace MVC_SimpleCRUD_Layered.Data.Models; + +public partial class Sport +{ + public int Id { get; set; } + + public string Name { get; set; } = null!; + + public virtual ICollection Registrations { get; set; } = new List(); +} \ No newline at end of file diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/efpt.mssql.config.json b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/efpt.mssql.config.json new file mode 100644 index 0000000..0284cfe --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/efpt.mssql.config.json @@ -0,0 +1,70 @@ +{ + "CodeGenerationMode": 6, + "ContextClassName": "EventsContext", + "ContextNamespace": null, + "FilterSchemas": false, + "IncludeConnectionString": false, + "IrregularWords": null, + "MinimumProductVersion": "2.6.1465", + "ModelNamespace": null, + "OutputContextPath": "Data\/MSSQL", + "OutputPath": "Models", + "PluralRules": null, + "PreserveCasingWithRegex": true, + "ProjectRootNamespace": "MVC_SimpleCRUD_Layered.Data", + "Schemas": null, + "SelectedHandlebarsLanguage": 2, + "SelectedToBeGenerated": 0, + "SingularRules": null, + "T4TemplatePath": null, + "Tables": [ + { + "Name": "[dbo].[Country]", + "ObjectType": 0 + }, + { + "Name": "[dbo].[Event]", + "ObjectType": 0 + }, + { + "Name": "[dbo].[Person]", + "ObjectType": 0 + }, + { + "Name": "[dbo].[Registration]", + "ObjectType": 0 + }, + { + "Name": "[dbo].[Sport]", + "ObjectType": 0 + } + ], + "UiHint": null, + "UncountableWords": null, + "UseAsyncStoredProcedureCalls": true, + "UseBoolPropertiesWithoutDefaultSql": false, + "UseDatabaseNames": false, + "UseDatabaseNamesForRoutines": true, + "UseDateOnlyTimeOnly": true, + "UseDbContextSplitting": false, + "UseDecimalDataAnnotationForSprocResult": true, + "UseFluentApiOnly": true, + "UseHandleBars": false, + "UseHierarchyId": false, + "UseInflector": true, + "UseInternalAccessModifiersForSprocsAndFunctions": false, + "UseLegacyPluralizer": false, + "UseManyToManyEntity": false, + "UseNoDefaultConstructor": true, + "UseNoNavigations": false, + "UseNoObjectFilter": false, + "UseNodaTime": false, + "UseNullableReferences": true, + "UsePrefixNavigationNaming": false, + "UseSchemaFolders": false, + "UseSchemaNamespaces": false, + "UseSpatial": false, + "UseT4": false, + "UseT4Split": false, + "UseTypedTvpParameters": true +} \ No newline at end of file diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/efpt.postgres.config.json b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/efpt.postgres.config.json new file mode 100644 index 0000000..baa165c --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Data/efpt.postgres.config.json @@ -0,0 +1,70 @@ +{ + "CodeGenerationMode": 6, + "ContextClassName": "EventsContext", + "ContextNamespace": null, + "FilterSchemas": false, + "IncludeConnectionString": false, + "IrregularWords": null, + "MinimumProductVersion": "2.6.1465", + "ModelNamespace": null, + "OutputContextPath": "Data\/Postgres", + "OutputPath": "Models", + "PluralRules": null, + "PreserveCasingWithRegex": true, + "ProjectRootNamespace": "MVC_SimpleCRUD_Layered.Data", + "Schemas": null, + "SelectedHandlebarsLanguage": 2, + "SelectedToBeGenerated": 0, + "SingularRules": null, + "T4TemplatePath": null, + "Tables": [ + { + "Name": "public.country", + "ObjectType": 0 + }, + { + "Name": "public.event", + "ObjectType": 0 + }, + { + "Name": "public.person", + "ObjectType": 0 + }, + { + "Name": "public.registration", + "ObjectType": 0 + }, + { + "Name": "public.sport", + "ObjectType": 0 + } + ], + "UiHint": null, + "UncountableWords": null, + "UseAsyncStoredProcedureCalls": true, + "UseBoolPropertiesWithoutDefaultSql": false, + "UseDatabaseNames": false, + "UseDatabaseNamesForRoutines": true, + "UseDateOnlyTimeOnly": true, + "UseDbContextSplitting": false, + "UseDecimalDataAnnotationForSprocResult": true, + "UseFluentApiOnly": true, + "UseHandleBars": false, + "UseHierarchyId": false, + "UseInflector": true, + "UseInternalAccessModifiersForSprocsAndFunctions": false, + "UseLegacyPluralizer": false, + "UseManyToManyEntity": false, + "UseNoDefaultConstructor": true, + "UseNoNavigations": false, + "UseNoObjectFilter": false, + "UseNodaTime": false, + "UseNullableReferences": true, + "UsePrefixNavigationNaming": false, + "UseSchemaFolders": false, + "UseSchemaNamespaces": false, + "UseSpatial": false, + "UseT4": false, + "UseT4Split": false, + "UseTypedTvpParameters": true +} \ No newline at end of file diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Constants.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Constants.cs new file mode 100644 index 0000000..af902e0 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Constants.cs @@ -0,0 +1,33 @@ +namespace MVC_SimpleCRUD_Layered.Web; + +public static class Constants +{ + public static class TempDataKeys + { + public const string ToastMessage = "ToastMessage"; + public const string ToastVariant = "ToastVariant"; + public const string ToastTitle = "ToastTitle"; + } + + public static class ToastVariants + { + public const string Success = "success"; + public const string Danger = "danger"; + } + + public static class ToastTitles + { + public const string Notification = "Notification"; + public const string Error = "Error"; + } + + public static class ViewDataKeys + { + public const string PagingInfo = "PagingInfo"; + } + + public static class ValidationMessages + { + public const string EmailOrContactPhoneRequired = "Email or contact phone is required."; + } +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Controllers/HomeController.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Controllers/HomeController.cs new file mode 100644 index 0000000..1abd7fc --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Controllers/HomeController.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; + +namespace MVC_SimpleCRUD_Layered.Web.Controllers; + +public class HomeController : Controller +{ + public IActionResult Index() + { + return View(); + } +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Controllers/PeopleController.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Controllers/PeopleController.cs new file mode 100644 index 0000000..118730f --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Controllers/PeopleController.cs @@ -0,0 +1,168 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using MVC_SimpleCRUD_Layered.Application.Models; +using MVC_SimpleCRUD_Layered.Application.People; + +namespace MVC_SimpleCRUD_Layered.Web.Controllers; + +public class PeopleController : Controller +{ + private readonly IPeopleService peopleService; + + public PeopleController(IPeopleService peopleService) + { + this.peopleService = peopleService; + } + + public async Task Index(int page = 1, int pageSize = 0, string? sorts = null, string? searchText = null) + { + PagedList model = await peopleService.GetPagedListAsync(new PeopleIndexRequest + { + Page = page, + PageSize = pageSize, + Sorts = sorts, + SearchText = searchText + }); + + return View(model); + } + + [HttpGet] + public async Task Create(int page = 1, int pageSize = 0, string? sorts = null, string? searchText = null) + { + var model = new PersonForm + { + Page = page, + PageSize = pageSize, + Sorts = sorts, + SearchText = searchText + }; + + await PrepareDropDownLists(model.CountryCode); + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(PersonForm model) + { + if (!ModelState.IsValid) + { + await PrepareDropDownLists(model.CountryCode); + return View(model); + } + + var result = await peopleService.CreateAsync(model); + if (result.Success) + { + ShowToast($"Person {result.PersonName} added."); + return RedirectToIndex(model); + } + + ModelState.AddModelError(string.Empty, result.ErrorMessage ?? "Person could not be saved."); + await PrepareDropDownLists(model.CountryCode); + return View(model); + } + + [HttpGet] + public async Task Edit(int id, int page = 1, int pageSize = 0, string? sorts = null, string? searchText = null) + { + var model = await peopleService.GetFormForEditAsync(id, new PeopleIndexRequest + { + Page = page, + PageSize = pageSize, + Sorts = sorts, + SearchText = searchText + }); + + if (model is null) + { + ShowToast($"Person with id {id} was not found.", Constants.ToastVariants.Danger, Constants.ToastTitles.Error); + return RedirectToAction(nameof(Index), new { page, pageSize, sorts, searchText }); + } + + await PrepareDropDownLists(model.CountryCode); + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(int id, PersonForm model) + { + if (model.Id != id) + { + return BadRequest(); + } + + if (!ModelState.IsValid) + { + await PrepareDropDownLists(model.CountryCode); + return View(model); + } + + var result = await peopleService.UpdateAsync(id, model); + if (result.Success) + { + ShowToast($"Person {result.PersonName} updated."); + return RedirectToIndex(model); + } + + if (string.Equals(result.ErrorMessage, $"Person with id {id} was not found.", StringComparison.Ordinal)) + { + ShowToast(result.ErrorMessage!, Constants.ToastVariants.Danger, Constants.ToastTitles.Error); + return RedirectToIndex(model); + } + + ModelState.AddModelError(string.Empty, result.ErrorMessage ?? "Person could not be saved."); + await PrepareDropDownLists(model.CountryCode); + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Delete(int id, int page = 1, int pageSize = 0, string? sorts = null, string? searchText = null) + { + var result = await peopleService.DeleteAsync(id); + + if (!result.Found) + { + ShowToast($"Person with id {id} was not found.", Constants.ToastVariants.Danger, Constants.ToastTitles.Error); + return RedirectToAction(nameof(Index), new { page, pageSize, sorts, searchText }); + } + + if (result.Success) + { + ShowToast($"Person {result.PersonName} deleted."); + } + else + { + ShowToast($"Person {result.PersonName} could not be deleted. {result.ErrorMessage}", Constants.ToastVariants.Danger, Constants.ToastTitles.Error); + } + + return RedirectToAction(nameof(Index), new { page, pageSize, sorts, searchText }); + } + + private async Task PrepareDropDownLists(string? selectedCountryCode = null) + { + var countries = await peopleService.GetCountryOptionsAsync(); + ViewBag.Countries = new SelectList(countries, nameof(CountryOption.Code), nameof(CountryOption.Name), selectedCountryCode); + } + + private RedirectToActionResult RedirectToIndex(PersonForm model) + { + return RedirectToAction(nameof(Index), new + { + page = model.Page, + pageSize = model.PageSize, + sorts = model.Sorts, + searchText = model.SearchText + }); + } + + private void ShowToast(string message, string variant = Constants.ToastVariants.Success, string title = Constants.ToastTitles.Notification) + { + TempData[Constants.TempDataKeys.ToastMessage] = message; + TempData[Constants.TempDataKeys.ToastVariant] = variant; + TempData[Constants.TempDataKeys.ToastTitle] = title; + } +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Controllers/PeopleSimpleController.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Controllers/PeopleSimpleController.cs new file mode 100644 index 0000000..f2286a8 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Controllers/PeopleSimpleController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc; +using MVC_SimpleCRUD_Layered.Application.People; + +namespace MVC_SimpleCRUD_Layered.Web.Controllers; + +public class PeopleSimpleController : Controller +{ + private readonly IPeopleService peopleService; + + public PeopleSimpleController(IPeopleService peopleService) + { + this.peopleService = peopleService; + } + + public async Task Index() + { + var people = await peopleService.GetAllForSimpleListAsync(); + return View(people); + } +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/MVC-SimpleCRUD-Layered.Web.csproj b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/MVC-SimpleCRUD-Layered.Web.csproj new file mode 100644 index 0000000..b508e54 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/MVC-SimpleCRUD-Layered.Web.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + MVC_SimpleCRUD_Layered.Web + + + + PI + + + + $(DefineConstants);MSSQL + + + + + + + + + + + + + + + diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Program.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Program.cs new file mode 100644 index 0000000..e56a7f9 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Program.cs @@ -0,0 +1,62 @@ +#if POSTGRES +using MVC_SimpleCRUD_Layered.Data.Data.Postgres; +#else +using MVC_SimpleCRUD_Layered.Data.Data.MSSQL; +#endif +using Microsoft.EntityFrameworkCore; +using MVC_SimpleCRUD_Layered.Application.Models; +using MVC_SimpleCRUD_Layered.Application.People; +using MVC_SimpleCRUD_Layered.Web; +using NLog; +using NLog.Web; +using Sieve.Services; + +var logger = NLog.LogManager.Setup().GetCurrentClassLogger(); +logger.Debug("init main"); +try +{ + var builder = WebApplication.CreateBuilder(args); + builder.Host.UseNLog(new NLogAspNetCoreOptions() { RemoveLoggerFactoryFilter = false }); + + #region Configure services + builder.Services.AddControllersWithViews(); + builder.Services.Configure(builder.Configuration.GetSection(PagingSettings.SectionName)); +#if POSTGRES + builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("EventsPostgres"))); +#else + builder.Services.AddDbContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString("EventsMssql"))); +#endif + builder.Services.AddScoped(); + builder.Services.AddScoped(); + #endregion + + var app = builder.Build(); + + #region configure middleware pipeline + //middleware order https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/#middleware-order + + if (app.Environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseStaticFiles(); + + app.UseRouting(); + + app.MapDefaultControllerRoute(); + + #endregion + app.Run(); +} +catch (Exception exception) +{ + // NLog: catch setup errors + logger.Error(exception, "Stopped program because of exception"); + throw; +} +finally +{ + // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) + NLog.LogManager.Shutdown(); +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Properties/launchSettings.json b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Properties/launchSettings.json new file mode 100644 index 0000000..00ffcb5 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "MVC-SimpleCRUD-Layered.Web": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7220", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Util/TagHelpers/PagerTagHelper.cs b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Util/TagHelpers/PagerTagHelper.cs new file mode 100644 index 0000000..c4e0da9 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Util/TagHelpers/PagerTagHelper.cs @@ -0,0 +1,128 @@ + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.Options; +using MVC_SimpleCRUD_Layered.Application.Models; + +namespace MVC_SimpleCRUD_Layered.Web.Util.TagHelpers; + +[HtmlTargetElement("pager", Attributes = "page-info,page-action")] +public class PagerTagHelper : TagHelper +{ + private readonly IUrlHelperFactory urlHelperFactory; + private readonly PagingSettings pagingSettings; + + public PagerTagHelper(IUrlHelperFactory urlHelperFactory, IOptions pagingSettings) + { + this.urlHelperFactory = urlHelperFactory; + this.pagingSettings = pagingSettings.Value; + } + + [ViewContext] + [HtmlAttributeNotBound] + public ViewContext ViewContext { get; set; } = null!; + + public PagingInfo PageInfo { get; set; } = new(); + + public string PageAction { get; set; } = string.Empty; + + public string PageTitle { get; set; } = "Enter page number"; + + [HtmlAttributeName(DictionaryAttributePrefix = "page-route-")] + public Dictionary PageRouteValues { get; set; } = []; + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + output.TagName = "nav"; + output.Attributes.SetAttribute("aria-label", "Pager"); + + var paginationList = new TagBuilder("ul"); + paginationList.AddCssClass("pagination"); + paginationList.AddCssClass("mb-0"); + + var firstPageInRange = Math.Max(1, PageInfo.CurrentPage - pagingSettings.PageOffset); + var lastPageInRange = Math.Min(PageInfo.TotalPages, PageInfo.CurrentPage + pagingSettings.PageOffset); + + if (firstPageInRange > 1) + { + paginationList.InnerHtml.AppendHtml(BuildListItemForPage(1, "1..")); + } + + for (var page = firstPageInRange; page <= lastPageInRange; page++) + { + paginationList.InnerHtml.AppendHtml( + page == PageInfo.CurrentPage + ? BuildListItemForCurrentPage(page) + : BuildListItemForPage(page)); + } + + if (lastPageInRange < PageInfo.TotalPages) + { + paginationList.InnerHtml.AppendHtml(BuildListItemForPage(PageInfo.TotalPages, $"..{PageInfo.TotalPages}")); + } + + output.Content.AppendHtml(paginationList); + } + + private TagBuilder BuildListItemForPage(int page) + { + return BuildListItemForPage(page, page.ToString()); + } + + private TagBuilder BuildListItemForPage(int page, string text) + { + var urlHelper = urlHelperFactory.GetUrlHelper(ViewContext); + var url = urlHelper.Action(PageAction, BuildRouteValues(page)) ?? string.Empty; + + var anchor = new TagBuilder("a"); + anchor.InnerHtml.Append(text); + anchor.Attributes["href"] = url; + anchor.AddCssClass("page-link"); + + var listItem = new TagBuilder("li"); + listItem.AddCssClass("page-item"); + listItem.InnerHtml.AppendHtml(anchor); + return listItem; + } + + private TagBuilder BuildListItemForCurrentPage(int page) + { + var urlHelper = urlHelperFactory.GetUrlHelper(ViewContext); + var urlTemplate = urlHelper.Action(PageAction, BuildRouteValues("__page__")) ?? string.Empty; + + var input = new TagBuilder("input"); + input.Attributes["type"] = "text"; + input.Attributes["value"] = page.ToString(); + input.Attributes["data-current"] = page.ToString(); + input.Attributes["data-min"] = "1"; + input.Attributes["data-max"] = PageInfo.TotalPages.ToString(); + input.Attributes["data-url-template"] = urlTemplate; + input.Attributes["title"] = PageTitle; + input.AddCssClass("page-link"); + input.AddCssClass("pagebox"); + + var listItem = new TagBuilder("li"); + listItem.AddCssClass("page-item"); + listItem.AddCssClass("active"); + listItem.InnerHtml.AppendHtml(input); + + return listItem; + } + + private RouteValueDictionary BuildRouteValues(object pageValue) + { + var routeValues = new RouteValueDictionary(PageRouteValues.ToDictionary(kvp => kvp.Key, kvp => (object?)kvp.Value)); + routeValues["page"] = pageValue; + routeValues["pageSize"] = PageInfo.ItemsPerPage; + routeValues["sorts"] = PageInfo.Sorts; + if (!string.IsNullOrWhiteSpace(PageInfo.SearchText)) + { + routeValues["searchText"] = PageInfo.SearchText; + } + + return routeValues; + } +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/Home/Index.cshtml b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/Home/Index.cshtml new file mode 100644 index 0000000..5abaa28 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/Home/Index.cshtml @@ -0,0 +1,10 @@ +@{ + ViewData["Title"] = "Home"; +} + +
+
+

Events

+

This sample demonstrates how to create a simple CRUD ASP.NET Core MVC application using data from Person table

+
+
diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/People/Create.cshtml b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/People/Create.cshtml new file mode 100644 index 0000000..1ee9fe6 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/People/Create.cshtml @@ -0,0 +1,15 @@ +@model PersonForm +@{ + ViewData["Title"] = "Add person"; + ViewData["FormAction"] = "Create"; +} + +
+

Add person

+
+ + + +@section Scripts { + +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/People/Edit.cshtml b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/People/Edit.cshtml new file mode 100644 index 0000000..b46e4f9 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/People/Edit.cshtml @@ -0,0 +1,15 @@ +@model PersonForm +@{ + ViewData["Title"] = "Edit person"; + ViewData["FormAction"] = "Edit"; +} + +
+

Edit person

+
+ + + +@section Scripts { + +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/People/Index.cshtml b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/People/Index.cshtml new file mode 100644 index 0000000..724a278 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/People/Index.cshtml @@ -0,0 +1,137 @@ +@using Microsoft.AspNetCore.Mvc.ViewFeatures +@model PagedList +@{ + ViewData["Title"] = "People"; +} +

People list

+ +@{ + (string PropertyName, string Title, bool Sortable)[] columns = + [ + (nameof(PersonInfo.FirstNameTranscription), "First Name", true), + (nameof(PersonInfo.LastNameTranscription), "Last Name", true), + (nameof(PersonInfo.OriginalName), "Original Name", false), + (nameof(PersonInfo.BirthDate), "Birth Date", true), + (nameof(PersonInfo.CountryName), "Country Name", true) + ]; + + var personRowViewData = new ViewDataDictionary(ViewData) + { + { Constants.ViewDataKeys.PagingInfo, Model.PagingInfo } + }; +} + + +
+
+
+ + + + +
+

People list

+ @(Model.PagingInfo.IsFiltered ? $"{Model.PagingInfo.FilteredItemsCount} / {Model.PagingInfo.TotalItemsCount}" : Model.PagingInfo.TotalItemsCount.ToString()) + + Add + +
+ +
+ + + + @if (Model.PagingInfo.IsFiltered) + { + + Clear + + } +
+
+ +
+ + + + @foreach (var column in columns) + { + + } + + + + + @if (Model.Data.Count == 0) + { + + + + } + else + { + @foreach (var person in Model.Data) + { + + } + } + +
+ @if (column.Sortable) + { + + @column.Title@(Model.PagingInfo.IsSortedBy(column.PropertyName) ? (Model.PagingInfo.IsDescending() ? " ?" : " ?") : "") + + } + else + { + @column.Title + } +
No data to display.
+
+ +
+
+ Page @Model.PagingInfo.CurrentPage of @Model.PagingInfo.TotalPages +
+ + + + +
+
+ + +
+
+
diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/People/_PersonForm.cshtml b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/People/_PersonForm.cshtml new file mode 100644 index 0000000..5539f8b --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/People/_PersonForm.cshtml @@ -0,0 +1,100 @@ +@using Microsoft.AspNetCore.Mvc.Rendering +@model PersonForm + +@{ + var formAction = ViewData["FormAction"]?.ToString() ?? "Create"; + var countries = ViewBag.Countries as SelectList ?? new SelectList(Array.Empty()); +} + +
+
+
+ + + + + + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+ +
diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/People/_PersonRow.cshtml b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/People/_PersonRow.cshtml new file mode 100644 index 0000000..160189f --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/People/_PersonRow.cshtml @@ -0,0 +1,42 @@ +@model PersonInfo +@{ + var pagingInfo = ViewData[Constants.ViewDataKeys.PagingInfo] as PagingInfo ?? new PagingInfo(); +} + + + @Model.FirstNameTranscription + @Model.LastNameTranscription + @Model.OriginalName + @Model.BirthDate.ToString("dd.MM.yyyy.") + @Model.CountryName + +
+ + Edit + + +
+ + + + + +
+
+ + diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/PeopleSimple/Index.cshtml b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/PeopleSimple/Index.cshtml new file mode 100644 index 0000000..fc91340 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/PeopleSimple/Index.cshtml @@ -0,0 +1,43 @@ +@model IEnumerable +@{ + ViewData["Title"] = "People"; +} +

People list

+ + + + + + + + + + + + + @foreach (var person in Model) + { + + + + + + + + } + +
First nameLast nameOriginal nameBirth dateCountry
@person.FirstNameTranscription@person.LastNameTranscription@person.OriginalName@person.BirthDate.ToString("yyy-MM-dd")@person.CountryName
+ +@section styles{ + +} + +@section scripts{ + + + +} \ No newline at end of file diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/Shared/_Layout.cshtml b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000..5c6a9a8 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/Shared/_Layout.cshtml @@ -0,0 +1,69 @@ +@using System.Text.Json + + + + + + @ViewData["Title"] - Events + + + @RenderSection("Styles", required: false) + + +
+ +
+ +
+ @RenderBody() +
+ +
+
+
+ Notification + +
+
+
+
+ + + + + + + @RenderSection("Scripts", required: false) + + diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/Shared/_ValidationScriptsPartial.cshtml b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/Shared/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000..d72a309 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/Shared/_ValidationScriptsPartial.cshtml @@ -0,0 +1,2 @@ + + diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/_ViewImports.cshtml b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/_ViewImports.cshtml new file mode 100644 index 0000000..c3808d1 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/_ViewImports.cshtml @@ -0,0 +1,5 @@ +@using MVC_SimpleCRUD_Layered.Application.Models +@using MVC_SimpleCRUD_Layered.Application.People +@using MVC_SimpleCRUD_Layered.Web +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, MVC-SimpleCRUD-Layered.Web diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/_ViewStart.cshtml b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/_ViewStart.cshtml new file mode 100644 index 0000000..820a2f6 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/appsettings.Development.json b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/appsettings.json b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/appsettings.json new file mode 100644 index 0000000..2168225 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/appsettings.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Paging": { + "PageSize": 10, + "PageOffset": 5 + }, + "ConnectionStrings": { + "EventsMssql": "Data Source=.,3030;Initial Catalog=Events;User Id=************;Password=**********;TrustServerCertificate=True", + "EventsPostgres": "Host=localhost;Port=5432;Database=events;Username=***********;Password=**************;Persist Security Info=True" + } +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/libman.json b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/libman.json new file mode 100644 index 0000000..b5acc37 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/libman.json @@ -0,0 +1,32 @@ +{ + "version": "1.0", + "defaultProvider": "cdnjs", + "libraries": [ + { + "library": "bootstrap@5.3.8", + "destination": "wwwroot/lib/bootstrap/", + "files": [ + "css/bootstrap.min.css", + "js/bootstrap.bundle.min.js" + ] + }, + { + "library": "jquery@3.7.1", + "destination": "wwwroot/lib/jquery/" + }, + { + "library": "jquery-validate@1.21.0", + "destination": "wwwroot/lib/jquery-validate/", + "files": [ + "jquery.validate.min.js" + ] + }, + { + "library": "jquery-validation-unobtrusive@4.0.0", + "destination": "wwwroot/lib/jquery-validation-unobtrusive/", + "files": [ + "jquery.validate.unobtrusive.min.js" + ] + } + ] +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/wwwroot/css/site.css b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/wwwroot/css/site.css new file mode 100644 index 0000000..ccce651 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/wwwroot/css/site.css @@ -0,0 +1,16 @@ +main { + padding-left: 50px; + padding-right: 50px; +} + +.validation-summary-errors { font-weight: bold; color:#a94442;} +.validation-summary-valid { display: none;} +.input-validation-error{ + border-color:red; +} +.pagebox { + width: 45px; +} +.pagebox::selection{ + background-color:grey; +} \ No newline at end of file diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/wwwroot/js/pager.js b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/wwwroot/js/pager.js new file mode 100644 index 0000000..bc89b89 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/wwwroot/js/pager.js @@ -0,0 +1,48 @@ +(function () { + function validRange(value, min, max) { + if (!/^\d+$/.test(value)) { + return false; + } + + var page = parseInt(value, 10); + return page >= min && page <= max; + } + + function goToPage(input) { + var value = input.value.trim(); + var min = parseInt(input.dataset.min, 10); + var max = parseInt(input.dataset.max, 10); + + if (!validRange(value, min, max)) { + input.value = input.dataset.current || ""; + return; + } + + var url = (input.dataset.urlTemplate || "").replace("__page__", value); + if (!url) { + return; + } + + window.location.href = url; + } + + document.addEventListener("focusin", function (event) { + if (event.target.matches(".pagebox")) { + event.target.select(); + } + }); + + document.addEventListener("keydown", function (event) { + if (!event.target.matches(".pagebox")) { + return; + } + + if (event.key === "Enter") { + event.preventDefault(); + goToPage(event.target); + } + else if (event.key === "Escape") { + event.target.value = event.target.dataset.current || ""; + } + }); +})(); diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/wwwroot/js/site.js b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/wwwroot/js/site.js new file mode 100644 index 0000000..43b0cc6 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.Web/wwwroot/js/site.js @@ -0,0 +1,30 @@ +$(function () { + $(document).on('click', '.delete', function (event) { + if (!confirm("Delete entry?")) { + event.preventDefault(); + } + }); +}); + +function showAppToast(options) { + var toastOptions = options || {}; + var variant = toastOptions.variant || "success"; + var title = toastOptions.title || "Notification"; + var message = toastOptions.message || ""; + var toast = document.getElementById("app-toast"); + var header = document.getElementById("app-toast-header"); + var titleElement = document.getElementById("app-toast-title"); + var body = document.getElementById("app-toast-body"); + + if (!toast || !header || !titleElement || !body) { + return; + } + + toast.className = "toast border-0 shadow-sm"; + header.className = "toast-header text-bg-" + variant; + titleElement.textContent = title; + body.textContent = message; + + var bootstrapToast = new bootstrap.Toast(toast); + bootstrapToast.show(); +} diff --git a/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.slnx b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.slnx new file mode 100644 index 0000000..7ac6564 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/MVC-SimpleCRUD-Layered.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/MVC-SimpleCRUD-Layered/README.md b/MVC-SimpleCRUD-Layered/README.md new file mode 100644 index 0000000..dc88e67 --- /dev/null +++ b/MVC-SimpleCRUD-Layered/README.md @@ -0,0 +1,15 @@ +# MVC-SimpleCRUD-Layered + +This solution keeps the behavior of `MVC-SimpleCRUD`, but separates the code into a simple layered architecture: + +- `MVC-SimpleCRUD-Layered.Web` contains controllers, Razor views, tag helpers, static files, and UI concerns. +- `MVC-SimpleCRUD-Layered.Application` contains application models and the `IPeopleService` / `PeopleService` use-case logic. +- `MVC-SimpleCRUD-Layered.Data` contains EF Core generated models and `EventsContext` implementations. + +Request flow: + +```text +Browser -> PeopleController -> IPeopleService -> EventsContext -> Database +``` + +The main teaching contrast is that controllers no longer use `EventsContext` directly. They receive HTTP input, handle MVC concerns, and delegate the person CRUD workflow to the application layer.