From 81a38b4fad3037a9797a48b6e1c83b30003513bf Mon Sep 17 00:00:00 2001 From: jzhartman Date: Sat, 13 Jun 2026 23:11:49 -0400 Subject: [PATCH 1/2] Added project from personal repo to forked repo for review --- .../ApplicationTests/AddContactTests.cs | 73 +++++++ .../DeleteContactHandlerTests.cs | 73 +++++++ .../PhoneBook.Tests/PhoneBook.Tests.csproj | 29 +++ .../AddCategory/AddCategoryHandler.cs | 33 +++ .../AddCategory/AddCategoryRequest.cs | 3 + .../Categories/DTOs/CategoryResponse.cs | 3 + .../DeleteCategoryByIdHandler.cs | 41 ++++ .../GetAllCategoriesHandler.cs | 46 +++++ .../GetCategoryById/GetCategoryByIdHandler.cs | 33 +++ .../UpdateCategoryNameHandler.cs | 32 +++ .../UpdateCategoryNameRequest.cs | 3 + .../Contacts/AddContact/AddContactHandler.cs | 37 ++++ .../Contacts/AddContact/AddContactRequest.cs | 10 + .../Contacts/DTOs/ContactResponse.cs | 11 + .../Contacts/DTOs/ContactResponseHelper.cs | 26 +++ .../DeleteContact/DeleteContactHandler.cs | 39 ++++ .../EditContact/EditContactHandler.cs | 38 ++++ .../GetAllContacts/GetAllContactsHandler.cs | 29 +++ .../GetContactById/GetContactByIdHandler.cs | 37 ++++ .../GetAllContactsByCategoryIdHandler.cs | 30 +++ .../SaveChanges/SaveChangesHandler.cs | 28 +++ .../SetCategoryIdForContactsToDefault.cs | 37 ++++ .../Application/DependencyInjection.cs | 42 ++++ .../Application/Email/EmailRequest.cs | 3 + .../Application/Email/SendEmailHandler.cs | 31 +++ .../Interfaces/ICategoryRepository.cs | 14 ++ .../Interfaces/IContactRepository.cs | 16 ++ .../Application/Interfaces/IEmailService.cs | 8 + .../ConsoleUI/DependencyInjection.cs | 44 ++++ .../ConsoleUI/Enums/EditContactExitCode.cs | 8 + .../ConsoleUI/Enums/MainMenuOptions.cs | 9 + .../ConsoleUI/Enums/ManageSubMenuOptions.cs | 11 + .../PhoneBook/ConsoleUI/Input/UserInput.cs | 142 +++++++++++++ .../ConsoleUI/Models/EditContactViewModel.cs | 29 +++ .../ConsoleUI/Models/FullContactViewModel.cs | 26 +++ .../PhoneBook/ConsoleUI/Output/Messages.cs | 73 +++++++ .../PhoneBook/ConsoleUI/Program.cs | 42 ++++ .../Services/Categories/AddCategoryService.cs | 70 +++++++ .../Categories/CategorySelectionService.cs | 52 +++++ .../Categories/DeleteCategoryService.cs | 91 +++++++++ .../Categories/EditCategoryService.cs | 67 ++++++ .../Categories/ManageCategoriesMenuService.cs | 76 +++++++ .../Services/Contacts/AddContactService.cs | 99 +++++++++ .../Contacts/ContactSelectionService.cs | 77 +++++++ .../Services/Contacts/DeleteContactService.cs | 78 +++++++ .../Services/Contacts/EditContactService.cs | 188 +++++++++++++++++ .../Contacts/GenerateFullContactService.cs | 48 +++++ .../Services/Contacts/ViewContactService.cs | 153 ++++++++++++++ .../Services/Email/SendEmailService.cs | 72 +++++++ .../ConsoleUI/Services/MainMenuService.cs | 64 ++++++ .../ConsoleUI/Views/CategorySelectionView.cs | 20 ++ .../ConsoleUI/Views/ContactDetailsView.cs | 29 +++ .../ConsoleUI/Views/ContactSelectionView.cs | 24 +++ .../ConsoleUI/Views/EditContactView.cs | 36 ++++ .../PhoneBook/ConsoleUI/Views/MainMenuView.cs | 26 +++ .../Views/ManageCategoriesMenuView.cs | 26 +++ .../PhoneBook/Domain/Entities/Contact.cs | 14 ++ .../Domain/Entities/ContactCategory.cs | 9 + .../PhoneBook/Domain/Validation/Error.cs | 6 + .../Errors/CategoryRepositoryErrors.cs | 21 ++ .../Errors/ContactRepositoryErrors.cs | 19 ++ .../Domain/Validation/Errors/EmailErrors.cs | 8 + .../Domain/Validation/Errors/Errors.cs | 9 + .../PhoneBook/Domain/Validation/Result.cs | 31 +++ .../ContactCategoryConfiguration.cs | 39 ++++ .../Configurations/ContactConfiguration.cs | 105 ++++++++++ .../Database/DatabaseInitializer.cs | 31 +++ .../Database/IDatabaseInitializer.cs | 6 + .../Infrastructure/DependencyInjection.cs | 32 +++ .../Infrastructure/Email/EmailService.cs | 41 ++++ .../Infrastructure/Email/SmtpSettings.cs | 12 ++ .../Infrastructure/PhoneBookDbContext.cs | 20 ++ .../Repositories/CategoryRepository.cs | 126 ++++++++++++ .../Repositories/ContactRepository.cs | 173 ++++++++++++++++ .../20260606022815_InitialCreate.Designer.cs | 174 ++++++++++++++++ .../20260606022815_InitialCreate.cs | 92 +++++++++ ...20260613032625_UpdatedSeedData.Designer.cs | 192 ++++++++++++++++++ .../20260613032625_UpdatedSeedData.cs | 39 ++++ .../PhoneBookDbContextModelSnapshot.cs | 189 +++++++++++++++++ .../PhoneBook/PhoneBook.csproj | 33 +++ .../PhoneBook/Properties/launchSettings.json | 8 + .../PhoneBook/appsettings.json | 15 ++ phonebook.jzhartman/PhoneBookApp.slnx | 4 + phonebook.jzhartman/README.md | 1 + 84 files changed, 3934 insertions(+) create mode 100644 phonebook.jzhartman/PhoneBook.Tests/ApplicationTests/AddContactTests.cs create mode 100644 phonebook.jzhartman/PhoneBook.Tests/ApplicationTests/DeleteContactHandlerTests.cs create mode 100644 phonebook.jzhartman/PhoneBook.Tests/PhoneBook.Tests.csproj create mode 100644 phonebook.jzhartman/PhoneBook/Application/Categories/AddCategory/AddCategoryHandler.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Categories/AddCategory/AddCategoryRequest.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Categories/DTOs/CategoryResponse.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Categories/DeleteCategory/DeleteCategoryByIdHandler.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Categories/GetAllCategories/GetAllCategoriesHandler.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Categories/GetCategoryById/GetCategoryByIdHandler.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Categories/UpdateCategory/UpdateCategoryNameHandler.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Categories/UpdateCategory/UpdateCategoryNameRequest.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Contacts/AddContact/AddContactHandler.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Contacts/AddContact/AddContactRequest.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Contacts/DTOs/ContactResponse.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Contacts/DTOs/ContactResponseHelper.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Contacts/DeleteContact/DeleteContactHandler.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Contacts/EditContact/EditContactHandler.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Contacts/GetAllContacts/GetAllContactsHandler.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Contacts/GetContactById/GetContactByIdHandler.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Contacts/GetContactsByCategoryId/GetAllContactsByCategoryIdHandler.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Contacts/SaveChanges/SaveChangesHandler.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Contacts/SetCategoryIdToDefault/SetCategoryIdForContactsToDefault.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/DependencyInjection.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Email/EmailRequest.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Email/SendEmailHandler.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Interfaces/ICategoryRepository.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Interfaces/IContactRepository.cs create mode 100644 phonebook.jzhartman/PhoneBook/Application/Interfaces/IEmailService.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/DependencyInjection.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Enums/EditContactExitCode.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Enums/MainMenuOptions.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Enums/ManageSubMenuOptions.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Input/UserInput.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Models/EditContactViewModel.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Models/FullContactViewModel.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Output/Messages.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Program.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/AddCategoryService.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/CategorySelectionService.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/DeleteCategoryService.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/EditCategoryService.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/ManageCategoriesMenuService.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/AddContactService.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/ContactSelectionService.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/DeleteContactService.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/EditContactService.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/GenerateFullContactService.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/ViewContactService.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Email/SendEmailService.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Services/MainMenuService.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Views/CategorySelectionView.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Views/ContactDetailsView.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Views/ContactSelectionView.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Views/EditContactView.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Views/MainMenuView.cs create mode 100644 phonebook.jzhartman/PhoneBook/ConsoleUI/Views/ManageCategoriesMenuView.cs create mode 100644 phonebook.jzhartman/PhoneBook/Domain/Entities/Contact.cs create mode 100644 phonebook.jzhartman/PhoneBook/Domain/Entities/ContactCategory.cs create mode 100644 phonebook.jzhartman/PhoneBook/Domain/Validation/Error.cs create mode 100644 phonebook.jzhartman/PhoneBook/Domain/Validation/Errors/CategoryRepositoryErrors.cs create mode 100644 phonebook.jzhartman/PhoneBook/Domain/Validation/Errors/ContactRepositoryErrors.cs create mode 100644 phonebook.jzhartman/PhoneBook/Domain/Validation/Errors/EmailErrors.cs create mode 100644 phonebook.jzhartman/PhoneBook/Domain/Validation/Errors/Errors.cs create mode 100644 phonebook.jzhartman/PhoneBook/Domain/Validation/Result.cs create mode 100644 phonebook.jzhartman/PhoneBook/Infrastructure/Configurations/ContactCategoryConfiguration.cs create mode 100644 phonebook.jzhartman/PhoneBook/Infrastructure/Configurations/ContactConfiguration.cs create mode 100644 phonebook.jzhartman/PhoneBook/Infrastructure/Database/DatabaseInitializer.cs create mode 100644 phonebook.jzhartman/PhoneBook/Infrastructure/Database/IDatabaseInitializer.cs create mode 100644 phonebook.jzhartman/PhoneBook/Infrastructure/DependencyInjection.cs create mode 100644 phonebook.jzhartman/PhoneBook/Infrastructure/Email/EmailService.cs create mode 100644 phonebook.jzhartman/PhoneBook/Infrastructure/Email/SmtpSettings.cs create mode 100644 phonebook.jzhartman/PhoneBook/Infrastructure/PhoneBookDbContext.cs create mode 100644 phonebook.jzhartman/PhoneBook/Infrastructure/Repositories/CategoryRepository.cs create mode 100644 phonebook.jzhartman/PhoneBook/Infrastructure/Repositories/ContactRepository.cs create mode 100644 phonebook.jzhartman/PhoneBook/Migrations/20260606022815_InitialCreate.Designer.cs create mode 100644 phonebook.jzhartman/PhoneBook/Migrations/20260606022815_InitialCreate.cs create mode 100644 phonebook.jzhartman/PhoneBook/Migrations/20260613032625_UpdatedSeedData.Designer.cs create mode 100644 phonebook.jzhartman/PhoneBook/Migrations/20260613032625_UpdatedSeedData.cs create mode 100644 phonebook.jzhartman/PhoneBook/Migrations/PhoneBookDbContextModelSnapshot.cs create mode 100644 phonebook.jzhartman/PhoneBook/PhoneBook.csproj create mode 100644 phonebook.jzhartman/PhoneBook/Properties/launchSettings.json create mode 100644 phonebook.jzhartman/PhoneBook/appsettings.json create mode 100644 phonebook.jzhartman/PhoneBookApp.slnx create mode 100644 phonebook.jzhartman/README.md diff --git a/phonebook.jzhartman/PhoneBook.Tests/ApplicationTests/AddContactTests.cs b/phonebook.jzhartman/PhoneBook.Tests/ApplicationTests/AddContactTests.cs new file mode 100644 index 00000000..e3c46ab1 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook.Tests/ApplicationTests/AddContactTests.cs @@ -0,0 +1,73 @@ +using Moq; +using PhoneBook.Application.Contacts.AddContact; +using PhoneBook.Application.Interfaces; +using PhoneBook.Domain.Entities; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.Tests.ApplicationTests; + +public class AddContactTests +{ + [Fact] + public async Task HandleAsync_ShouldReturnFailure_WhenRepositoryReturnsNull() + { + // Arrange + var repoMock = new Mock(); + repoMock + .Setup(r => r.AddAsync(It.IsAny())) + .ReturnsAsync((Result?)null); + + var handler = new AddContactHandler(repoMock.Object); + var request = new AddContactRequest("Billy", "Smith", "bs@mail.com", "555-0147"); + + // Act + var result = await handler.HandleAsync(request); + + // Assert + Assert.True(result.IsFailure); + Assert.Contains(ContactRepositoryErrors.AddResponseNull, result.Errors); + + } + + [Fact] + public async Task HandleAsync_ShouldReturnFailure_WhenRespositoryReturnsFailure() + { + // Arrange + Error[] errors = { new("TestError", "Test error description") }; + + var repoMock = new Mock(); + repoMock + .Setup(r => r.AddAsync(It.IsAny())) + .ReturnsAsync(Result.Failure(errors)); + + var handler = new AddContactHandler(repoMock.Object); + var request = new AddContactRequest("Billy", "Smith", "bs@mail.com", "555-0147"); + + // Act + var result = await handler.HandleAsync(request); + + // Assert + Assert.True(result.IsFailure); + Assert.Contains(errors, result.Errors.ToArray()); + } + + [Fact] + public async Task HandleAsync_ShouldReturnSuccess_WhenRepositoryReturnsSuccess() + { + // Arrange + var repoMock = new Mock(); + repoMock + .Setup(r => r.AddAsync(It.IsAny())) + .ReturnsAsync(Result.Success(new Contact())); + + var handler = new AddContactHandler(repoMock.Object); + var request = new AddContactRequest("Billy", "Smith", "bs@mail.com", "555-0147"); + + // Act + var result = await handler.HandleAsync(request); + + // Assert + Assert.True(result.IsSuccess); + } +} diff --git a/phonebook.jzhartman/PhoneBook.Tests/ApplicationTests/DeleteContactHandlerTests.cs b/phonebook.jzhartman/PhoneBook.Tests/ApplicationTests/DeleteContactHandlerTests.cs new file mode 100644 index 00000000..0391da89 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook.Tests/ApplicationTests/DeleteContactHandlerTests.cs @@ -0,0 +1,73 @@ +using Moq; +using PhoneBook.Application.Contacts.DeleteContact; +using PhoneBook.Application.Contacts.DTOs; +using PhoneBook.Application.Interfaces; +using PhoneBook.Domain.Entities; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.Tests.ApplicationTests; + +public class DeleteContactHandlerTests +{ + [Fact] + public async Task HandleAsync_ShouldReturnFailure_WhenRepositoryReturnsNull() + { + // Arrange + var repoMock = new Mock(); + repoMock + .Setup(r => r.DeleteAsync(It.IsAny())) + .ReturnsAsync((Result?)null); + + var handler = new DeleteContactHandler(repoMock.Object); + var request = new ContactResponse(1, "Billy", "Smith", "555-0147", "bs@mail.com", 1); + + // Act + var result = await handler.HandleAsync(request); + + // Assert + Assert.True(result.IsFailure); + Assert.Contains(ContactRepositoryErrors.DeleteResponseNull, result.Errors); + } + + [Fact] + public async Task HandleAsync_ShouldReturnFailure_WhenRepositoryResturnsFailure() + { + // Arrange + Error[] errors = { new("TestError", "Test error description") }; + + var repoMock = new Mock(); + repoMock + .Setup(r => r.DeleteAsync(It.IsAny())) + .ReturnsAsync((Result.Failure(errors))); + + var handler = new DeleteContactHandler(repoMock.Object); + var request = new ContactResponse(1, "Billy", "Smith", "555-0147", "bs@mail.com", 1); + + // Act + var result = await handler.HandleAsync(request); + + // Assert + Assert.True(result.IsFailure); + Assert.Contains(errors, result.Errors.ToArray()); + } + + [Fact] + public async Task HandleAsync_ShouldReturnSuccessWhenRepositoryReturnsSuccess() + { + // Arrange + var repoMock = new Mock(); + repoMock + .Setup(r => r.DeleteAsync(It.IsAny())) + .ReturnsAsync((Result.Success(new Contact()))); + + var handler = new DeleteContactHandler(repoMock.Object); + var request = new ContactResponse(1, "Billy", "Smith", "555-0147", "bs@mail.com", 1); + + // Act + var result = await handler.HandleAsync(request); + + // Assert + Assert.True(result.IsSuccess); + } +} diff --git a/phonebook.jzhartman/PhoneBook.Tests/PhoneBook.Tests.csproj b/phonebook.jzhartman/PhoneBook.Tests/PhoneBook.Tests.csproj new file mode 100644 index 00000000..4e331432 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook.Tests/PhoneBook.Tests.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + \ No newline at end of file diff --git a/phonebook.jzhartman/PhoneBook/Application/Categories/AddCategory/AddCategoryHandler.cs b/phonebook.jzhartman/PhoneBook/Application/Categories/AddCategory/AddCategoryHandler.cs new file mode 100644 index 00000000..2d8c4b67 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Categories/AddCategory/AddCategoryHandler.cs @@ -0,0 +1,33 @@ +using PhoneBook.Application.Interfaces; +using PhoneBook.Domain.Entities; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.Application.Categories.AddCategory; + +public class AddCategoryHandler +{ + private readonly ICategoryRepository _repo; + + public AddCategoryHandler(ICategoryRepository repo) + { + _repo = repo; + } + + public async Task HandleAsync(AddCategoryRequest category) + { + var result = await _repo.AddAsync( + new ContactCategory + { + Name = category.Name + }); + + if (result is null) + return Result.Failure(CategoryRepositoryErrors.AddResponseNull); + + if (result.IsFailure) + return Result.Failure(result.Errors); + + return Result.Success(); + } +} diff --git a/phonebook.jzhartman/PhoneBook/Application/Categories/AddCategory/AddCategoryRequest.cs b/phonebook.jzhartman/PhoneBook/Application/Categories/AddCategory/AddCategoryRequest.cs new file mode 100644 index 00000000..2cab6f7f --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Categories/AddCategory/AddCategoryRequest.cs @@ -0,0 +1,3 @@ +namespace PhoneBook.Application.Categories.AddCategory; + +public record AddCategoryRequest(string Name); diff --git a/phonebook.jzhartman/PhoneBook/Application/Categories/DTOs/CategoryResponse.cs b/phonebook.jzhartman/PhoneBook/Application/Categories/DTOs/CategoryResponse.cs new file mode 100644 index 00000000..812624c2 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Categories/DTOs/CategoryResponse.cs @@ -0,0 +1,3 @@ +namespace PhoneBook.Application.Categories.DTOs; + +public record CategoryResponse(int Id, string Name); \ No newline at end of file diff --git a/phonebook.jzhartman/PhoneBook/Application/Categories/DeleteCategory/DeleteCategoryByIdHandler.cs b/phonebook.jzhartman/PhoneBook/Application/Categories/DeleteCategory/DeleteCategoryByIdHandler.cs new file mode 100644 index 00000000..ddb41ce1 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Categories/DeleteCategory/DeleteCategoryByIdHandler.cs @@ -0,0 +1,41 @@ +using PhoneBook.Application.Categories.DTOs; +using PhoneBook.Application.Interfaces; +using PhoneBook.Domain.Entities; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.Application.Categories.DeleteCategory; + +internal class DeleteCategoryByIdHandler +{ + private readonly ICategoryRepository _categoryRepo; + private readonly IContactRepository _contactRepo; + + public DeleteCategoryByIdHandler(ICategoryRepository categoryRepo, IContactRepository contactRepo) + { + _categoryRepo = categoryRepo; + _contactRepo = contactRepo; + } + + public async Task HandleAsync(CategoryResponse categoryResponse) + { + if (categoryResponse.Name.ToUpper() == "UNCATEGORIZED") + return Result.Failure(CategoryRepositoryErrors.DeleteDefault); + + var category = new ContactCategory + { + Id = categoryResponse.Id, + Name = categoryResponse.Name + }; + + var deleteCategoryResponse = await _categoryRepo.DeleteAsync(category); + + if (deleteCategoryResponse is null) + return Result.Failure(CategoryRepositoryErrors.DeleteResponseNull); + + if (deleteCategoryResponse.IsFailure) + return Result.Failure(deleteCategoryResponse.Errors); + + return Result.Success(); + } +} diff --git a/phonebook.jzhartman/PhoneBook/Application/Categories/GetAllCategories/GetAllCategoriesHandler.cs b/phonebook.jzhartman/PhoneBook/Application/Categories/GetAllCategories/GetAllCategoriesHandler.cs new file mode 100644 index 00000000..48b0b031 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Categories/GetAllCategories/GetAllCategoriesHandler.cs @@ -0,0 +1,46 @@ +using PhoneBook.Application.Categories.DTOs; +using PhoneBook.Application.Interfaces; +using PhoneBook.Domain.Entities; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.Application.Categories.GetAllCategories; + +internal class GetAllCategoriesHandler +{ + private readonly ICategoryRepository _repo; + + public GetAllCategoriesHandler(ICategoryRepository repo) + { + _repo = repo; + } + + public async Task>> HandleAsync() + { + var result = await _repo.GetAllAsync(); + + if (result is null || result.Value is null || result.Value.Count < 1) + return Result>.Failure(new Error[] { CategoryRepositoryErrors.CategoryNotFound }); + + if (result.IsFailure) + return Result>.Failure(result.Errors); + + return Result>.Success(MapToCategoryResponse(result.Value)); + } + + private List MapToCategoryResponse(List categories) + { + var categoryResponseList = new List(); + + foreach (var category in categories) + { + categoryResponseList.Add(new CategoryResponse + ( + category.Id, + category.Name + )); + } + + return categoryResponseList; + } +} diff --git a/phonebook.jzhartman/PhoneBook/Application/Categories/GetCategoryById/GetCategoryByIdHandler.cs b/phonebook.jzhartman/PhoneBook/Application/Categories/GetCategoryById/GetCategoryByIdHandler.cs new file mode 100644 index 00000000..64890f05 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Categories/GetCategoryById/GetCategoryByIdHandler.cs @@ -0,0 +1,33 @@ +using PhoneBook.Application.Categories.DTOs; +using PhoneBook.Application.Interfaces; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.Application.Categories.GetCategoryById; + +public class GetCategoryByIdHandler +{ + private readonly ICategoryRepository _repo; + + public GetCategoryByIdHandler(ICategoryRepository repo) + { + _repo = repo; + } + + public async Task> HandleAsync(int categoryId) + { + var result = await _repo.GetByIdAsync(categoryId); + + if (result is null || result.Value is null) + return Result.Failure(CategoryRepositoryErrors.CategoryNotFound); + + if (result.IsFailure) + return Result.Failure(result.Errors); + + return Result.Success(new CategoryResponse + ( + result.Value.Id, + result.Value.Name + )); + } +} diff --git a/phonebook.jzhartman/PhoneBook/Application/Categories/UpdateCategory/UpdateCategoryNameHandler.cs b/phonebook.jzhartman/PhoneBook/Application/Categories/UpdateCategory/UpdateCategoryNameHandler.cs new file mode 100644 index 00000000..5811da15 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Categories/UpdateCategory/UpdateCategoryNameHandler.cs @@ -0,0 +1,32 @@ +using PhoneBook.Application.Interfaces; +using PhoneBook.Domain.Entities; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.Application.Categories.UpdateCategory; + +public class UpdateCategoryNameHandler +{ + private readonly ICategoryRepository _repo; + + public UpdateCategoryNameHandler(ICategoryRepository repo) + { + _repo = repo; + } + + public async Task HandleAsync(UpdateCategoryNameRequest category) + { + if (category.OriginalName.ToUpper() == "UNCATEGORIZED") + return Result.Failure(CategoryRepositoryErrors.UpdateDefault); + + var result = await _repo.UpdateAsync(new ContactCategory { Id = category.Id, Name = category.NewName }); + + if (result is null) + return Result.Failure(CategoryRepositoryErrors.UpdateResponseNull); + + if (result.IsFailure) + return Result.Failure(result.Errors); + + return Result.Success(); + } +} diff --git a/phonebook.jzhartman/PhoneBook/Application/Categories/UpdateCategory/UpdateCategoryNameRequest.cs b/phonebook.jzhartman/PhoneBook/Application/Categories/UpdateCategory/UpdateCategoryNameRequest.cs new file mode 100644 index 00000000..c8cc3e59 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Categories/UpdateCategory/UpdateCategoryNameRequest.cs @@ -0,0 +1,3 @@ +namespace PhoneBook.Application.Categories.UpdateCategory; + +public record UpdateCategoryNameRequest(int Id, string OriginalName, string NewName); \ No newline at end of file diff --git a/phonebook.jzhartman/PhoneBook/Application/Contacts/AddContact/AddContactHandler.cs b/phonebook.jzhartman/PhoneBook/Application/Contacts/AddContact/AddContactHandler.cs new file mode 100644 index 00000000..3acf7604 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Contacts/AddContact/AddContactHandler.cs @@ -0,0 +1,37 @@ +using PhoneBook.Application.Interfaces; +using PhoneBook.Domain.Entities; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.Application.Contacts.AddContact; + +public class AddContactHandler +{ + private readonly IContactRepository _repo; + + public AddContactHandler(IContactRepository repo) + { + _repo = repo; + } + + public async Task HandleAsync(AddContactRequest contact) + { + var result = await _repo.AddAsync( + new Contact + { + FirstName = contact.FirstName, + LastName = contact.LastName, + PhoneNumber = contact.PhoneNumber, + Email = contact.Email, + CategoryId = contact.CategoryId + }); + + if (result is null) + return Result.Failure(ContactRepositoryErrors.AddResponseNull); + + if (result.IsFailure) + return Result.Failure(result.Errors); + + return Result.Success(); + } +} diff --git a/phonebook.jzhartman/PhoneBook/Application/Contacts/AddContact/AddContactRequest.cs b/phonebook.jzhartman/PhoneBook/Application/Contacts/AddContact/AddContactRequest.cs new file mode 100644 index 00000000..e9782153 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Contacts/AddContact/AddContactRequest.cs @@ -0,0 +1,10 @@ +namespace PhoneBook.Application.Contacts.AddContact; + +public record AddContactRequest +( + string FirstName, + string LastName, + string PhoneNumber, + string Email, + int CategoryId = 1 +); \ No newline at end of file diff --git a/phonebook.jzhartman/PhoneBook/Application/Contacts/DTOs/ContactResponse.cs b/phonebook.jzhartman/PhoneBook/Application/Contacts/DTOs/ContactResponse.cs new file mode 100644 index 00000000..a7a8de72 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Contacts/DTOs/ContactResponse.cs @@ -0,0 +1,11 @@ +namespace PhoneBook.Application.Contacts.DTOs; + +public record ContactResponse +( + int ContactId, + string FirstName, + string LastName, + string PhoneNumber, + string Email, + int CategoryId +); \ No newline at end of file diff --git a/phonebook.jzhartman/PhoneBook/Application/Contacts/DTOs/ContactResponseHelper.cs b/phonebook.jzhartman/PhoneBook/Application/Contacts/DTOs/ContactResponseHelper.cs new file mode 100644 index 00000000..2f52f3e5 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Contacts/DTOs/ContactResponseHelper.cs @@ -0,0 +1,26 @@ +using PhoneBook.Domain.Entities; + +namespace PhoneBook.Application.Contacts.DTOs; + +public static class ContactResponseHelper +{ + public static List MapToContactResponse(List contacts) + { + var contactResponseList = new List(); + + foreach (var contact in contacts) + { + contactResponseList.Add(new ContactResponse + ( + contact.Id, + contact.FirstName, + contact.LastName, + contact.PhoneNumber, + contact.Email, + contact.CategoryId + )); + } + + return contactResponseList; + } +} diff --git a/phonebook.jzhartman/PhoneBook/Application/Contacts/DeleteContact/DeleteContactHandler.cs b/phonebook.jzhartman/PhoneBook/Application/Contacts/DeleteContact/DeleteContactHandler.cs new file mode 100644 index 00000000..f4e69a70 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Contacts/DeleteContact/DeleteContactHandler.cs @@ -0,0 +1,39 @@ +using PhoneBook.Application.Contacts.DTOs; +using PhoneBook.Application.Interfaces; +using PhoneBook.Domain.Entities; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.Application.Contacts.DeleteContact; + +public class DeleteContactHandler +{ + private readonly IContactRepository _repo; + + public DeleteContactHandler(IContactRepository repo) + { + _repo = repo; + } + + public async Task HandleAsync(ContactResponse contact) + { + var result = await _repo.DeleteAsync( + new Contact + { + Id = contact.ContactId, + FirstName = contact.FirstName, + LastName = contact.LastName, + PhoneNumber = contact.PhoneNumber, + Email = contact.Email, + CategoryId = contact.CategoryId + }); + + if (result is null) + return Result.Failure(ContactRepositoryErrors.DeleteResponseNull); + + if (result.IsFailure) + return Result.Failure(result.Errors); + + return Result.Success(); + } +} diff --git a/phonebook.jzhartman/PhoneBook/Application/Contacts/EditContact/EditContactHandler.cs b/phonebook.jzhartman/PhoneBook/Application/Contacts/EditContact/EditContactHandler.cs new file mode 100644 index 00000000..51f57d6b --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Contacts/EditContact/EditContactHandler.cs @@ -0,0 +1,38 @@ +using PhoneBook.Application.Contacts.DTOs; +using PhoneBook.Application.Interfaces; +using PhoneBook.Domain.Entities; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.Application.Contacts.EditContact; + +public class EditContactHandler +{ + private readonly IContactRepository _repo; + + public EditContactHandler(IContactRepository repo) + { + _repo = repo; + } + + public async Task HandleAsync(ContactResponse contact) + { + var result = await _repo.UpdateAsync(new Contact + { + Id = contact.ContactId, + FirstName = contact.FirstName, + LastName = contact.LastName, + PhoneNumber = contact.PhoneNumber, + Email = contact.Email, + CategoryId = contact.CategoryId + }); + + if (result is null) + return Result.Failure(ContactRepositoryErrors.UpdateResponseNull); + + if (result.IsFailure) + return Result.Failure(result.Errors); + + return Result.Success(); + } +} diff --git a/phonebook.jzhartman/PhoneBook/Application/Contacts/GetAllContacts/GetAllContactsHandler.cs b/phonebook.jzhartman/PhoneBook/Application/Contacts/GetAllContacts/GetAllContactsHandler.cs new file mode 100644 index 00000000..42f62639 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Contacts/GetAllContacts/GetAllContactsHandler.cs @@ -0,0 +1,29 @@ +using PhoneBook.Application.Contacts.DTOs; +using PhoneBook.Application.Interfaces; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.Application.Contacts.GetAllContacts; + +public class GetAllContactsHandler +{ + private readonly IContactRepository _repo; + + public GetAllContactsHandler(IContactRepository repo) + { + _repo = repo; + } + + public async Task>> HandleAsync() + { + var result = await _repo.GetAllAsync(); + + if (result is null || result.Value is null || result.Value.Count < 1) + return Result>.Failure(ContactRepositoryErrors.ContactNotFound); + + if (result.IsFailure) + return Result>.Failure(result.Errors); + + return Result>.Success(ContactResponseHelper.MapToContactResponse(result.Value)); + } +} diff --git a/phonebook.jzhartman/PhoneBook/Application/Contacts/GetContactById/GetContactByIdHandler.cs b/phonebook.jzhartman/PhoneBook/Application/Contacts/GetContactById/GetContactByIdHandler.cs new file mode 100644 index 00000000..51bb1bc5 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Contacts/GetContactById/GetContactByIdHandler.cs @@ -0,0 +1,37 @@ +using PhoneBook.Application.Contacts.DTOs; +using PhoneBook.Application.Interfaces; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.Application.GetById; + +public class GetContactByIdHandler +{ + private readonly IContactRepository _repo; + + public GetContactByIdHandler(IContactRepository repo) + { + _repo = repo; + } + + public async Task> HandleAsync(int contactId) + { + var result = await _repo.GetByIdAsync(contactId); + + if (result is null || result.Value is null) + return Result.Failure(ContactRepositoryErrors.ContactNotFound); + + if (result.IsFailure) + return Result.Failure(result.Errors); + + return Result.Success(new ContactResponse + ( + result.Value.Id, + result.Value.FirstName, + result.Value.LastName, + result.Value.PhoneNumber, + result.Value.Email, + result.Value.CategoryId + )); + } +} diff --git a/phonebook.jzhartman/PhoneBook/Application/Contacts/GetContactsByCategoryId/GetAllContactsByCategoryIdHandler.cs b/phonebook.jzhartman/PhoneBook/Application/Contacts/GetContactsByCategoryId/GetAllContactsByCategoryIdHandler.cs new file mode 100644 index 00000000..e99de1a6 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Contacts/GetContactsByCategoryId/GetAllContactsByCategoryIdHandler.cs @@ -0,0 +1,30 @@ +using PhoneBook.Application.Categories.DTOs; +using PhoneBook.Application.Contacts.DTOs; +using PhoneBook.Application.Interfaces; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.Application.Contacts.GetContactsByCategoryId; + +public class GetAllContactsByCategoryIdHandler +{ + private readonly IContactRepository _repo; + + public GetAllContactsByCategoryIdHandler(IContactRepository repo) + { + _repo = repo; + } + + public async Task>> HandleAsync(CategoryResponse category) + { + var result = await _repo.GetByCategoryIdAsync(category.Id); + + if (result is null || result.Value is null || result.Value.Count < 1) + return Result>.Failure(ContactRepositoryErrors.ContactNotFound); + + if (result.IsFailure) + return Result>.Failure(result.Errors); + + return Result>.Success(ContactResponseHelper.MapToContactResponse(result.Value)); + } +} diff --git a/phonebook.jzhartman/PhoneBook/Application/Contacts/SaveChanges/SaveChangesHandler.cs b/phonebook.jzhartman/PhoneBook/Application/Contacts/SaveChanges/SaveChangesHandler.cs new file mode 100644 index 00000000..faa506b9 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Contacts/SaveChanges/SaveChangesHandler.cs @@ -0,0 +1,28 @@ +using PhoneBook.Application.Interfaces; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.Application.Contacts.SaveChanges; + +internal class SaveChangesHandler +{ + private readonly IContactRepository _repo; + + public SaveChangesHandler(IContactRepository repo) + { + _repo = repo; + } + + public async Task HandleAsync() + { + var result = await _repo.SaveChangesAsync(); + + if (result is null) + return Result.Failure(ContactRepositoryErrors.SaveResponseNull); + + if (result.IsFailure) + return Result.Failure(result.Errors); + + return Result.Success(); + } +} diff --git a/phonebook.jzhartman/PhoneBook/Application/Contacts/SetCategoryIdToDefault/SetCategoryIdForContactsToDefault.cs b/phonebook.jzhartman/PhoneBook/Application/Contacts/SetCategoryIdToDefault/SetCategoryIdForContactsToDefault.cs new file mode 100644 index 00000000..bd62d1c8 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Contacts/SetCategoryIdToDefault/SetCategoryIdForContactsToDefault.cs @@ -0,0 +1,37 @@ +using PhoneBook.Application.Categories.DTOs; +using PhoneBook.Application.Interfaces; +using PhoneBook.Domain.Entities; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.Application.Contacts.SetCategoryIdToDefault; + +internal class SetCategoryIdForContactsToDefaultHandler +{ + private readonly IContactRepository _contactRepo; + + public SetCategoryIdForContactsToDefaultHandler(IContactRepository contactRepo) + { + _contactRepo = contactRepo; + } + + internal async Task HandleAsync(CategoryResponse category) + { + if (category.Name.ToUpper() == "UNCATEGORIZED") + return Result.Failure(CategoryRepositoryErrors.DeleteDefault); + + var result = await _contactRepo.SetCategoryIdForContactsToDefaultByCategoryIdAsync( + new ContactCategory + { + Id = category.Id, + Name = category.Name + }); + + if (result is null) + return Result.Failure(ContactRepositoryErrors.NullResponse); + if (result.IsFailure) + return Result.Failure(result.Errors); + + return Result.Success(); + } +} diff --git a/phonebook.jzhartman/PhoneBook/Application/DependencyInjection.cs b/phonebook.jzhartman/PhoneBook/Application/DependencyInjection.cs new file mode 100644 index 00000000..8f5c0941 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/DependencyInjection.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.DependencyInjection; +using PhoneBook.Application.Categories.AddCategory; +using PhoneBook.Application.Categories.DeleteCategory; +using PhoneBook.Application.Categories.GetAllCategories; +using PhoneBook.Application.Categories.GetCategoryById; +using PhoneBook.Application.Categories.UpdateCategory; +using PhoneBook.Application.Contacts.AddContact; +using PhoneBook.Application.Contacts.DeleteContact; +using PhoneBook.Application.Contacts.EditContact; +using PhoneBook.Application.Contacts.GetAllContacts; +using PhoneBook.Application.Contacts.GetContactsByCategoryId; +using PhoneBook.Application.Contacts.SaveChanges; +using PhoneBook.Application.Contacts.SetCategoryIdToDefault; +using PhoneBook.Application.Email; +using PhoneBook.Application.GetById; + +namespace PhoneBook.Application; + +public static class DependencyInjection +{ + public static IServiceCollection AddApplication(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + + return services; + } +} diff --git a/phonebook.jzhartman/PhoneBook/Application/Email/EmailRequest.cs b/phonebook.jzhartman/PhoneBook/Application/Email/EmailRequest.cs new file mode 100644 index 00000000..d2d99435 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Email/EmailRequest.cs @@ -0,0 +1,3 @@ +namespace PhoneBook.Application.Email; + +public record EmailRequest(string recipient, string subject, string body); diff --git a/phonebook.jzhartman/PhoneBook/Application/Email/SendEmailHandler.cs b/phonebook.jzhartman/PhoneBook/Application/Email/SendEmailHandler.cs new file mode 100644 index 00000000..bb342f9b --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Email/SendEmailHandler.cs @@ -0,0 +1,31 @@ +using PhoneBook.Application.Interfaces; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.Application.Email; + +public class SendEmailHandler +{ + private readonly IEmailService _email; + + public SendEmailHandler(IEmailService email) + { + _email = email; + } + + public async Task HandleAsync(EmailRequest request) + { + var result = await _email.SendEmailAsync( + recipient: request.recipient, + subject: request.subject, + body: request.body); + + if (result is null) + return Result.Failure(EmailErrors.NullResponse); + + if (result.IsFailure) + return Result.Failure(result.Errors); + + return Result.Success(); + } +} diff --git a/phonebook.jzhartman/PhoneBook/Application/Interfaces/ICategoryRepository.cs b/phonebook.jzhartman/PhoneBook/Application/Interfaces/ICategoryRepository.cs new file mode 100644 index 00000000..00d45066 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Interfaces/ICategoryRepository.cs @@ -0,0 +1,14 @@ +using PhoneBook.Domain.Entities; +using PhoneBook.Domain.Validation; + +namespace PhoneBook.Application.Interfaces; + +public interface ICategoryRepository +{ + Task AddAsync(ContactCategory category); + Task DeleteAsync(ContactCategory category); + Task>> GetAllAsync(); + Task> GetByIdAsync(int id); + Task SaveChangesAsync(); + Task UpdateAsync(ContactCategory category); +} \ No newline at end of file diff --git a/phonebook.jzhartman/PhoneBook/Application/Interfaces/IContactRepository.cs b/phonebook.jzhartman/PhoneBook/Application/Interfaces/IContactRepository.cs new file mode 100644 index 00000000..394ea339 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Interfaces/IContactRepository.cs @@ -0,0 +1,16 @@ +using PhoneBook.Domain.Entities; +using PhoneBook.Domain.Validation; + +namespace PhoneBook.Application.Interfaces; + +public interface IContactRepository +{ + Task> GetByIdAsync(int id); + Task>> GetAllAsync(); + Task AddAsync(Contact contact); + Task UpdateAsync(Contact contact); + Task DeleteAsync(Contact contact); + Task SaveChangesAsync(); + Task>> GetByCategoryIdAsync(int id); + Task SetCategoryIdForContactsToDefaultByCategoryIdAsync(ContactCategory category); +} diff --git a/phonebook.jzhartman/PhoneBook/Application/Interfaces/IEmailService.cs b/phonebook.jzhartman/PhoneBook/Application/Interfaces/IEmailService.cs new file mode 100644 index 00000000..3c8e7a40 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Application/Interfaces/IEmailService.cs @@ -0,0 +1,8 @@ +using PhoneBook.Domain.Validation; + +namespace PhoneBook.Application.Interfaces; + +public interface IEmailService +{ + Task SendEmailAsync(string recipient, string subject, string body); +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/DependencyInjection.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/DependencyInjection.cs new file mode 100644 index 00000000..b568ab0a --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/DependencyInjection.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.DependencyInjection; +using PhoneBook.ConsoleUI.Input; +using PhoneBook.ConsoleUI.Output; +using PhoneBook.ConsoleUI.Services; +using PhoneBook.ConsoleUI.Services.Categories; +using PhoneBook.ConsoleUI.Services.Contacts; +using PhoneBook.ConsoleUI.Services.Email; +using PhoneBook.ConsoleUI.Views; + +namespace PhoneBook.ConsoleUI; + +internal static class DependencyInjection +{ + internal static IServiceCollection AddPresentation(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + + return services; + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Enums/EditContactExitCode.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Enums/EditContactExitCode.cs new file mode 100644 index 00000000..aa9fc14e --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Enums/EditContactExitCode.cs @@ -0,0 +1,8 @@ +namespace PhoneBook.ConsoleUI.Enums; + +internal enum EditContactExitCode +{ + None, + Save, + Cancel +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Enums/MainMenuOptions.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Enums/MainMenuOptions.cs new file mode 100644 index 00000000..48a281aa --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Enums/MainMenuOptions.cs @@ -0,0 +1,9 @@ +namespace PhoneBook.ConsoleUI.Enums; + +internal enum MainMenuOptions +{ + AddContact, + ViewContact, + ManageCategories, + Exit +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Enums/ManageSubMenuOptions.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Enums/ManageSubMenuOptions.cs new file mode 100644 index 00000000..5664cb73 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Enums/ManageSubMenuOptions.cs @@ -0,0 +1,11 @@ +namespace PhoneBook.ConsoleUI.Enums; + +internal enum ManageSubMenuOptions +{ + Add, + Delete, + Edit, + Exit, + Email, + Unknown +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Input/UserInput.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Input/UserInput.cs new file mode 100644 index 00000000..d0db6325 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Input/UserInput.cs @@ -0,0 +1,142 @@ +using PhoneBook.ConsoleUI.Models; +using Spectre.Console; + +namespace PhoneBook.ConsoleUI.Input; + +internal class UserInput +{ + internal string GetNameFromUser(string message) + { + var namePrompt = new TextPrompt(message) + .AllowEmpty() + .Validate(input => + { + if (string.IsNullOrWhiteSpace(input)) + return ValidationResult.Error("[red]Required Field:[/] Name cannot be empty."); + + return ValidationResult.Success(); + }); + + return AnsiConsole.Prompt(namePrompt); + } + + internal string GetEmailAddressFromUser() + { + var emailPrompt = new TextPrompt("Enter your [green]EMAIL[/] (format must match: name@example.com):") + .AllowEmpty() + .Validate(input => + { + if (input.Contains("@") && input.IndexOf("@") >= 1 && input.Count('@') == 1 && + input.Contains(".") && input.IndexOf(".") > input.IndexOf("@" + 1) && + input.Length >= 5) + return ValidationResult.Success(); + + if (string.IsNullOrWhiteSpace(input)) + return ValidationResult.Error("[red]Required Field:[/] Email cannot be empty."); + + return ValidationResult.Error("[red]Invalid Entry:[/] Please enter a valid email address with the format [yellow]name@domain.com[/]."); + }); + + return AnsiConsole.Prompt(emailPrompt); + } + + internal string GetPhoneNumberFromUser() + { + var invalidChars = new[] { '-', '(', ')', '+' }; + + var phoneNumberPrompt = new TextPrompt("Enter your [green]PHONE NUMBER[/] (7 to 15 digits, '-', '(', ')', and '+' will be ignored):") + .AllowEmpty() + .Validate(input => + { + var inputMinusCommonSymbols = new string(input.Where(c => !invalidChars.Contains(c)).ToArray()); + + if (inputMinusCommonSymbols.Any(char.IsNumber) == false) + return ValidationResult.Error("[red]Invalid Phone Number[/] Phone number cannot contain letters or symbols."); + + if (string.IsNullOrWhiteSpace(inputMinusCommonSymbols)) + return ValidationResult.Error("[red]Required Field:[/] Phone number cannot be empty."); + + if (inputMinusCommonSymbols.Length < 7 || inputMinusCommonSymbols.Length > 15) + return ValidationResult.Error("[red]Invalid Phone Number[/] Phone number length must be between 7 and 15 numbers long."); + + return ValidationResult.Success(); + }); + + return new string(AnsiConsole.Prompt(phoneNumberPrompt).Where(c => !invalidChars.Contains(c)).ToArray()).Replace(" ", ""); + } + internal string GetEmailSubjectFromUser() + { + var subjectPrompt = new TextPrompt("Enter email [green]SUBJECT[/]:") + .AllowEmpty() + .Validate(input => + { + if (string.IsNullOrWhiteSpace(input)) + return ValidationResult.Error("[red]Required Field:[/] Subject cannot be empty."); + + if (input.Length > 50) + return ValidationResult.Error("[red]Exceeded Max Lenght:[/] Subject must be 50 characters or less."); + + return ValidationResult.Success(); + }); + + return AnsiConsole.Prompt(subjectPrompt); + } + internal string GetEmailBodyFromUser() + { + var bodyPrompt = new TextPrompt("Enter email [green]BODY[/]:") + .AllowEmpty() + .Validate(input => + { + if (string.IsNullOrWhiteSpace(input)) + return ValidationResult.Error("[red]Required Field:[/] Body cannot be empty."); + + return ValidationResult.Success(); + }); + + return AnsiConsole.Prompt(bodyPrompt); + } + + internal bool GetAddConfirmationFromUser(string name, string addType) + { + return AnsiConsole.Confirm($"Confirm adding [green]{name}[/] to the {addType}?"); + } + internal bool GetDeleteConfirmationFromUser(string name, string deleteType) + { + return AnsiConsole.Confirm($"Confirm deleting [green]{name}[/] from the {deleteType}?"); + } + internal bool GetRenameCategoryConfirmationFromUser(string originalName, string newName) + { + return AnsiConsole.Confirm($"Confirm renaming [yellow]{originalName}[/] to [green]{newName}[/]?"); + } + internal bool GetEditContactConfirmationFromUser(FullContactViewModel originalContact, EditContactViewModel newContact) + { + string preamble = $"Confirm the following changes for the contact {originalContact.FirstName} {originalContact.LastName}:"; + string changes = string.Empty; + + if (newContact.ChangedFirstName) changes += $"\t[yellow]{originalContact.FirstName}[/] to [green]{newContact.FirstName}[/]\r\n"; + if (newContact.ChangedLastName) changes += $"\t[yellow]{originalContact.LastName}[/] to [green]{newContact.LastName}[/]\r\n"; + if (newContact.ChangedPhoneNumber) changes += $"\t[yellow]{originalContact.PhoneNumber}[/] to [green]{newContact.PhoneNumber}[/]\r\n"; + if (newContact.ChangedEmail) changes += $"\t[yellow]{originalContact.Email}[/] to [green]{newContact.Email}[/]\r\n"; + if (newContact.ChangedCategory) changes += $"\t[yellow]{originalContact.CategoryName}[/] to [green]{newContact.CategoryName}[/]\r\n"; + + return AnsiConsole.Confirm($"{preamble}\r\n\r\n{changes}\r\nConfirm:"); + } + internal bool GetEmailContentsConfirmationFromUser(FullContactViewModel contact, string subject, string body) + { + string email = "Please confirm the following email:\r\n\r\n"; + email += $"[green]To:[/] [yellow]{contact.Email}[/] ({contact.FirstName} {contact.LastName})\r\n"; + email += $"[green]Subject:[/] {subject}\r\n\r\n"; + email += $"[green]Body:[/]\r\n{body}\r\n\r\n"; + + return AnsiConsole.Confirm($"{email}\r\nConfirm send:"); + } + internal bool GetRetrySendConfirmationFromUser() + { + return AnsiConsole.Confirm($"Retry sending email?"); + } + internal void PressAnyKeyToContinue() + { + Console.Write("Press any key to continue..."); + Console.ReadLine(); + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Models/EditContactViewModel.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Models/EditContactViewModel.cs new file mode 100644 index 00000000..faf067c5 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Models/EditContactViewModel.cs @@ -0,0 +1,29 @@ +namespace PhoneBook.ConsoleUI.Models; + +internal class EditContactViewModel +{ + internal bool ChangedFirstName { get; set; } = false; + internal bool ChangedLastName { get; set; } = false; + internal bool ChangedPhoneNumber { get; set; } = false; + internal bool ChangedEmail { get; set; } = false; + internal bool ChangedCategory { get; set; } = false; + + internal int Id { get; set; } + internal string FirstName { get; set; } + internal string LastName { get; set; } + internal string PhoneNumber { get; set; } + internal string Email { get; set; } + internal int CategoryId { get; set; } + internal string CategoryName { get; set; } + + internal EditContactViewModel(FullContactViewModel originalContact) + { + Id = originalContact.Id; + FirstName = originalContact.FirstName; + LastName = originalContact.LastName; + PhoneNumber = originalContact.PhoneNumber; + Email = originalContact.Email; + CategoryId = originalContact.CategoryId; + CategoryName = originalContact.CategoryName; + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Models/FullContactViewModel.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Models/FullContactViewModel.cs new file mode 100644 index 00000000..dbe2dbec --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Models/FullContactViewModel.cs @@ -0,0 +1,26 @@ +using PhoneBook.Application.Categories.DTOs; +using PhoneBook.Application.Contacts.DTOs; + +namespace PhoneBook.ConsoleUI.Models; + +internal class FullContactViewModel +{ + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string PhoneNumber { get; set; } + public string Email { get; set; } + public int CategoryId { get; set; } + public string CategoryName { get; set; } + + public FullContactViewModel(ContactResponse contact, CategoryResponse category) + { + Id = contact.ContactId; + FirstName = contact.FirstName; + LastName = contact.LastName; + PhoneNumber = contact.PhoneNumber; + Email = contact.Email; + CategoryId = category.Id; + CategoryName = category.Name; + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Output/Messages.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Output/Messages.cs new file mode 100644 index 00000000..8deeced9 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Output/Messages.cs @@ -0,0 +1,73 @@ +using PhoneBook.Domain.Validation; +using Spectre.Console; + +namespace PhoneBook.ConsoleUI.Output; + +internal class Messages +{ + internal void ErrorMessage(IEnumerable errors) + { + foreach (var error in errors) + { + AnsiConsole.MarkupLineInterpolated($"[red]ERROR:[/] {error.Description}"); + } + } + + internal void AddCancelledMessage(string name, string addType) + { + AnsiConsole.MarkupLineInterpolated($"Cancelled adding [green]{name}[/] to {addType}."); + } + internal void AddSucessfulMessage(string name, string addType) + { + AnsiConsole.MarkupLineInterpolated($"Successfully added [green]{name}[/] to {addType}"); + } + internal void DeleteCancelledMessage(string name, string addType) + { + AnsiConsole.MarkupLineInterpolated($"Cancelled deleting [green]{name}[/] from {addType}."); + } + internal void DeleteSucessfulMessage(string name, string addType) + { + AnsiConsole.MarkupLineInterpolated($"Successfully deleted [green]{name}[/] from {addType}"); + } + internal void RenameCategorySuccessfulMessage(string originalName, string newName) + { + AnsiConsole.MarkupLineInterpolated($"Successfully changed [yellow]{originalName}[/] to [green]{newName}[/]"); + } + internal void RenameCategoryCancelledMessage(string originalName, string newName) + { + AnsiConsole.MarkupLineInterpolated($"Cancelled changing [yellow]{originalName}[/] to [green]{newName}[/]"); + } + + internal void EditContactCancelledMessage(string name) + { + AnsiConsole.MarkupLineInterpolated($"Cancelled editing [green]{name}[/]"); + } + internal void EditContactSuccessfulMessage(string name) + { + AnsiConsole.MarkupLineInterpolated($"Successfully updated [green]{name}[/]"); + } + internal void EditContactCancelSaveMessage(string name) + { + AnsiConsole.MarkupLineInterpolated($"Changes to [green]{name}[/] not saved"); + } + + internal void EmailSendSuccessfulMessage() + { + AnsiConsole.MarkupLineInterpolated($"Email successfully sent!"); + } + internal void EmailSendCancelledMessage() + { + AnsiConsole.MarkupLineInterpolated($"Email cancelled!"); + } + + internal void RetryingSendEmailMessage(int retryCount) + { + AnsiConsole.MarkupLineInterpolated($"Attempting to resend email. Total attempts: {retryCount}..."); + } + + internal void SettingDefaultCategory() + { + AnsiConsole.MarkupLineInterpolated($"Category will be set to the default value: [yellow]UNCATEGORIZED[/]."); + + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Program.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Program.cs new file mode 100644 index 00000000..9d978a6c --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Program.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using PhoneBook.Application; +using PhoneBook.ConsoleUI; +using PhoneBook.ConsoleUI.Services; +using PhoneBook.Infrastructure; +using PhoneBook.Infrastructure.Database; + +namespace PhoneBook.Presentation; + +internal class Program +{ + private static async Task Main(string[] args) + { + var host = Host.CreateDefaultBuilder(args) + .ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.AddDebug(); + }) + .ConfigureServices((context, services) => + { + services.AddPresentation(); + services.AddInfrastructure(context); + services.AddApplication(); + }) + .Build(); + + + using var scope = host.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.Migrate(); + + var initializer = scope.ServiceProvider.GetRequiredService(); + await initializer.EnsureUncategorizedCategoryExistsAsync(); + + var mainMenu = host.Services.GetRequiredService(); + await mainMenu.RunAsync(); + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/AddCategoryService.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/AddCategoryService.cs new file mode 100644 index 00000000..72b5b28a --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/AddCategoryService.cs @@ -0,0 +1,70 @@ +using PhoneBook.Application.Categories.AddCategory; +using PhoneBook.Application.Contacts.SaveChanges; +using PhoneBook.ConsoleUI.Input; +using PhoneBook.ConsoleUI.Output; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.ConsoleUI.Services.Categories; + +internal class AddCategoryService +{ + private readonly AddCategoryHandler _addCategoryHandler; + private readonly SaveChangesHandler _saveChangesHandler; + private readonly UserInput _userInput; + private readonly Messages _messages; + + public AddCategoryService(AddCategoryHandler addCategoryHandler, SaveChangesHandler saveChangesHandler, + UserInput userInput, Messages messages) + { + _addCategoryHandler = addCategoryHandler; + _saveChangesHandler = saveChangesHandler; + _userInput = userInput; + _messages = messages; + } + + internal async Task RunAsync() + { + var name = _userInput.GetNameFromUser($"Enter category [green]NAME[/]:"); + + bool confirmAdd = _userInput.GetAddConfirmationFromUser(name, "category list"); + + if (confirmAdd) + { + var addResult = await _addCategoryHandler.HandleAsync(new AddCategoryRequest(name)); + var errors = new List(); + + if (addResult.IsSuccess) + { + var saveResult = await _saveChangesHandler.HandleAsync(); + + if (saveResult is null) + errors.Add(ContactRepositoryErrors.SaveResponseNull); + + else if (saveResult.IsFailure) + errors.AddRange(saveResult.Errors); + + else if (saveResult.IsSuccess) + { + _messages.AddSucessfulMessage(name, "category list"); + _userInput.PressAnyKeyToContinue(); + return; + } + } + + if (addResult.IsFailure) + errors.AddRange(addResult.Errors); + + if (addResult is null) + errors.Add(CategoryRepositoryErrors.AddResponseNull); + + _messages.ErrorMessage(errors); + } + else + { + _messages.AddCancelledMessage(name, "category list"); + } + + _userInput.PressAnyKeyToContinue(); + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/CategorySelectionService.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/CategorySelectionService.cs new file mode 100644 index 00000000..0f706505 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/CategorySelectionService.cs @@ -0,0 +1,52 @@ +using PhoneBook.Application.Categories.DTOs; +using PhoneBook.Application.Categories.GetAllCategories; +using PhoneBook.ConsoleUI.Input; +using PhoneBook.ConsoleUI.Output; +using PhoneBook.ConsoleUI.Views; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.ConsoleUI.Services.Categories; + +internal class CategorySelectionService +{ + private readonly GetAllCategoriesHandler _getAllCategoriesHandler; + private readonly CategorySelectionView _categorySelectionView; + private readonly Messages _messages; + private readonly UserInput _userInput; + + public CategorySelectionService(GetAllCategoriesHandler getAllCategoriesHandler, CategorySelectionView categorySelectionView, + Messages messages, UserInput userInput) + { + _getAllCategoriesHandler = getAllCategoriesHandler; + _categorySelectionView = categorySelectionView; + _messages = messages; + _userInput = userInput; + } + + internal async Task RunAsync(bool allowAll = false, bool allowAdd = false) + { + var result = await _getAllCategoriesHandler.HandleAsync(); + + if (result is null) + { + _messages.ErrorMessage(new[] { CategoryRepositoryErrors.NullResponse }); + _userInput.PressAnyKeyToContinue(); + return null; + } + + if (result.IsFailure || result.Value is null) + { + _messages.ErrorMessage(result.Errors); + _userInput.PressAnyKeyToContinue(); + return null; + } + + if (allowAll) + result.Value.Insert(0, new(-1, "ALL")); + + if (allowAdd) + result.Value.Insert(0, new(-1, "ADD NEW CATEGORY")); + + return _categorySelectionView.Render(result.Value); + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/DeleteCategoryService.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/DeleteCategoryService.cs new file mode 100644 index 00000000..3b759886 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/DeleteCategoryService.cs @@ -0,0 +1,91 @@ +using PhoneBook.Application.Categories.DeleteCategory; +using PhoneBook.Application.Contacts.SaveChanges; +using PhoneBook.Application.Contacts.SetCategoryIdToDefault; +using PhoneBook.ConsoleUI.Input; +using PhoneBook.ConsoleUI.Output; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.ConsoleUI.Services.Categories; + +internal class DeleteCategoryService +{ + private readonly SetCategoryIdForContactsToDefaultHandler _setCategoryIdForContactsToDefaultHandler; + private readonly DeleteCategoryByIdHandler _deleteCategoryByIdHandler; + private readonly CategorySelectionService _categorySelectionService; + private readonly SaveChangesHandler _saveChangesHandler; + private readonly Messages _messages; + private readonly UserInput _userInput; + + public DeleteCategoryService(DeleteCategoryByIdHandler deleteCategoryByIdHandler, CategorySelectionService categorySelectionService, + SetCategoryIdForContactsToDefaultHandler setCategoryIdForContactsToDefaultHandler, + SaveChangesHandler saveChangesHandler, Messages messages, UserInput userInput) + { + _setCategoryIdForContactsToDefaultHandler = setCategoryIdForContactsToDefaultHandler; + _deleteCategoryByIdHandler = deleteCategoryByIdHandler; + _categorySelectionService = categorySelectionService; + _saveChangesHandler = saveChangesHandler; + _messages = messages; + _userInput = userInput; + } + + internal async Task RunAsync() + { + var category = await _categorySelectionService.RunAsync(); + + if (category is null) + { + _messages.ErrorMessage(new[] { CategoryRepositoryErrors.CategoryNotFound }); + _userInput.PressAnyKeyToContinue(); + return; + } + + var confirmDelete = _userInput.GetDeleteConfirmationFromUser(category.Name, "category list"); + + if (confirmDelete) + { + var setToDefaultResponse = await _setCategoryIdForContactsToDefaultHandler.HandleAsync(category); + + var deleteResult = await _deleteCategoryByIdHandler.HandleAsync(category); + + var errors = new List(); + + if (setToDefaultResponse.IsSuccess && deleteResult.IsSuccess) + { + var saveResult = await _saveChangesHandler.HandleAsync(); + + if (saveResult is null) + errors.Add(ContactRepositoryErrors.SaveResponseNull); + + else if (saveResult.IsFailure) + errors.AddRange(saveResult.Errors); + + else if (saveResult.IsSuccess) + { + _messages.DeleteSucessfulMessage(category.Name, "category list"); + _userInput.PressAnyKeyToContinue(); + return; + } + } + + if (setToDefaultResponse.IsFailure) + errors.AddRange(setToDefaultResponse.Errors); + + if (setToDefaultResponse is null) + errors.Add(CategoryRepositoryErrors.NullResponse); + + if (deleteResult.IsFailure) + errors.AddRange(deleteResult.Errors); + + if (deleteResult is null) + errors.Add(CategoryRepositoryErrors.DeleteResponseNull); + + _messages.ErrorMessage(errors); + } + else + { + _messages.DeleteCancelledMessage(category.Name, "category list"); + } + _userInput.PressAnyKeyToContinue(); + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/EditCategoryService.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/EditCategoryService.cs new file mode 100644 index 00000000..4f880b2e --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/EditCategoryService.cs @@ -0,0 +1,67 @@ +using PhoneBook.Application.Categories.UpdateCategory; +using PhoneBook.ConsoleUI.Input; +using PhoneBook.ConsoleUI.Output; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.ConsoleUI.Services.Categories; + +internal class EditCategoryService +{ + private readonly CategorySelectionService _categorySelectionService; + private readonly UpdateCategoryNameHandler _updateCategoryNameHandler; + private readonly UserInput _userInput; + private readonly Messages _messages; + public EditCategoryService(CategorySelectionService categorySelectionService, UpdateCategoryNameHandler updateCategoryNameHandler, + UserInput userInput, Messages messages) + { + _categorySelectionService = categorySelectionService; + _updateCategoryNameHandler = updateCategoryNameHandler; + _userInput = userInput; + _messages = messages; + } + + internal async Task RunAsync() + { + Console.Clear(); + var category = await _categorySelectionService.RunAsync(); + + if (category is null) + { + _messages.ErrorMessage(new[] { CategoryRepositoryErrors.NullResponse }); + _userInput.PressAnyKeyToContinue(); + return; ; + } + + var newName = _userInput.GetNameFromUser($"Enter new name for category [green]{category.Name}[/]:"); + + var confirmRename = _userInput.GetRenameCategoryConfirmationFromUser(category.Name, newName); + + if (confirmRename) + { + var result = await _updateCategoryNameHandler.HandleAsync(new(category.Id, category.Name, newName)); + + var errors = new List(); + + if (result is null) + errors.Add(CategoryRepositoryErrors.NullResponse); + else if (result.IsFailure) + errors.AddRange(result.Errors); + else + _messages.RenameCategorySuccessfulMessage(category.Name, newName); + + if (errors.Count > 0) + { + _messages.ErrorMessage(errors); + _userInput.PressAnyKeyToContinue(); + return; ; + } + } + else + { + _messages.RenameCategoryCancelledMessage(category.Name, newName); + } + + _userInput.PressAnyKeyToContinue(); + } +} \ No newline at end of file diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/ManageCategoriesMenuService.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/ManageCategoriesMenuService.cs new file mode 100644 index 00000000..6e2ba3ec --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Categories/ManageCategoriesMenuService.cs @@ -0,0 +1,76 @@ +using PhoneBook.Application.Categories.GetCategoryById; +using PhoneBook.ConsoleUI.Enums; +using PhoneBook.ConsoleUI.Input; +using PhoneBook.ConsoleUI.Output; +using PhoneBook.ConsoleUI.Services.Categories; +using PhoneBook.ConsoleUI.Views; + +namespace PhoneBook.ConsoleUI.Services; + +internal class ManageCategoriesMenuService +{ + private readonly ManageCategoriesMenuView _manageCategoriesMenuView; + private readonly DeleteCategoryService _deleteCategoryService; + private readonly CategorySelectionService _categorySelectionService; + private readonly AddCategoryService _addCategoryService; + private readonly EditCategoryService _editCategoryService; + private readonly GetCategoryByIdHandler _getCategoryByIdHandler; + private readonly Messages _messages; + private readonly UserInput _userInput; + + public ManageCategoriesMenuService(ManageCategoriesMenuView manageCategoriesMenuView, DeleteCategoryService deleteCategoryService, + CategorySelectionService categorySelectionService, GetCategoryByIdHandler getCategoryByIdHandler, + AddCategoryService addCategoryService, EditCategoryService editCategoryService, + Messages messages, UserInput userInput) + { + _manageCategoriesMenuView = manageCategoriesMenuView; + + _deleteCategoryService = deleteCategoryService; + _categorySelectionService = categorySelectionService; + _getCategoryByIdHandler = getCategoryByIdHandler; + + _addCategoryService = addCategoryService; + _editCategoryService = editCategoryService; + + _messages = messages; + _userInput = userInput; + } + + internal async Task RunAsync() + { + bool returnToMainMenu = false; + var excludedOption = new List { ManageSubMenuOptions.Unknown, ManageSubMenuOptions.Email }; + ManageSubMenuOptions[] menuOptions = Enum.GetValues(typeof(ManageSubMenuOptions)) + .Cast() + .Where(o => !excludedOption.Contains(o)) + .ToArray(); + + Console.Clear(); + + while (returnToMainMenu == false) + { + Console.Clear(); + var selection = _manageCategoriesMenuView.Render(menuOptions); + + switch (selection) + { + case ManageSubMenuOptions.Add: + await _addCategoryService.RunAsync(); + break; + case ManageSubMenuOptions.Delete: + await _deleteCategoryService.RunAsync(); + break; + case ManageSubMenuOptions.Edit: + await _editCategoryService.RunAsync(); + break; + case ManageSubMenuOptions.Exit: + returnToMainMenu = true; + break; + default: + Console.WriteLine("INVALID MENU SELECTION"); + _userInput.PressAnyKeyToContinue(); + break; + } + } + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/AddContactService.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/AddContactService.cs new file mode 100644 index 00000000..25a75c89 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/AddContactService.cs @@ -0,0 +1,99 @@ +using PhoneBook.Application.Contacts.AddContact; +using PhoneBook.Application.Contacts.SaveChanges; +using PhoneBook.ConsoleUI.Input; +using PhoneBook.ConsoleUI.Output; +using PhoneBook.ConsoleUI.Services.Categories; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.ConsoleUI.Services.Contacts; + +internal class AddContactService +{ + private readonly CategorySelectionService _categorySelectionService; + private readonly AddContactHandler _addContactHandler; + private readonly SaveChangesHandler _saveChangesHandler; + private readonly UserInput _userInput; + private readonly Messages _messages; + + public AddContactService(AddContactHandler addContactHandler, SaveChangesHandler saveChangesHandler, + CategorySelectionService categorySelectionService, UserInput userInput, Messages messages) + { + _categorySelectionService = categorySelectionService; + _addContactHandler = addContactHandler; + _saveChangesHandler = saveChangesHandler; + _userInput = userInput; + _messages = messages; + } + + internal async Task RunAsync() + { + var firstName = _userInput.GetNameFromUser($"Enter your [green]FIRST NAME[/]:"); + var lastName = _userInput.GetNameFromUser($"Enter your [green]LAST NAME[/]:"); + var phoneNumber = _userInput.GetPhoneNumberFromUser(); + var email = _userInput.GetEmailAddressFromUser(); + var categoryId = await GetCategoryIdFromUserAsync(); + + bool confirmAdd = _userInput.GetAddConfirmationFromUser($"{firstName} {lastName}", "contact list"); + + if (confirmAdd) + { + var addResult = await _addContactHandler.HandleAsync(new(firstName, lastName, phoneNumber, email, categoryId)); + var errors = new List(); + + if (addResult.IsSuccess) + { + var saveResult = await _saveChangesHandler.HandleAsync(); + + if (saveResult is null) + errors.Add(ContactRepositoryErrors.SaveResponseNull); + + else if (saveResult.IsFailure) + errors.AddRange(saveResult.Errors); + + else if (saveResult.IsSuccess) + { + _messages.AddSucessfulMessage($"{firstName} {lastName}", "contact list"); + _userInput.PressAnyKeyToContinue(); + return; + } + } + + if (addResult.IsFailure) + errors.AddRange(addResult.Errors); + + if (addResult is null) + errors.Add(ContactRepositoryErrors.AddResponseNull); + + _messages.ErrorMessage(errors); + } + else + { + _messages.AddCancelledMessage($"{firstName} {lastName}", "contact list"); + } + _userInput.PressAnyKeyToContinue(); + } + + private async Task GetCategoryIdFromUserAsync() + { + bool categoryIdIsValid = false; + int categoryId = 0; + + while (categoryIdIsValid == false) + { + var category = await _categorySelectionService.RunAsync(); + + if (category is not null) + { + categoryIdIsValid = true; + categoryId = category.Id; + } + else + { + _messages.SettingDefaultCategory(); + } + } + + return categoryId; + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/ContactSelectionService.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/ContactSelectionService.cs new file mode 100644 index 00000000..360b5570 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/ContactSelectionService.cs @@ -0,0 +1,77 @@ +using PhoneBook.Application.Categories.DTOs; +using PhoneBook.Application.Categories.GetCategoryById; +using PhoneBook.Application.Contacts.DTOs; +using PhoneBook.Application.Contacts.GetAllContacts; +using PhoneBook.Application.Contacts.GetContactsByCategoryId; +using PhoneBook.ConsoleUI.Input; +using PhoneBook.ConsoleUI.Models; +using PhoneBook.ConsoleUI.Output; +using PhoneBook.ConsoleUI.Views; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + + +namespace PhoneBook.ConsoleUI.Services.Contacts; + +internal class ContactSelectionService +{ + private readonly GetAllContactsHandler _getAllContactsHandler; + private readonly GetAllContactsByCategoryIdHandler _getAllContactsByCategoryIdHandler; + private readonly GetCategoryByIdHandler _getCategoryByIdHandler; + private readonly ContactSelectionView _contactSelectionView; + private readonly GenerateFullContactService _generateFullContactService; + private readonly Messages _messages; + private readonly UserInput _userInput; + + public ContactSelectionService(GetAllContactsHandler getAllContactsHandler, ContactSelectionView contactSelectionView, + GetAllContactsByCategoryIdHandler getAllContactsByCategoryIdHandler, + GetCategoryByIdHandler getCategoryByIdHandler, GenerateFullContactService generateFullContactService, + Messages messages, UserInput userInput) + { + _getAllContactsHandler = getAllContactsHandler; + _getAllContactsByCategoryIdHandler = getAllContactsByCategoryIdHandler; + _getCategoryByIdHandler = getCategoryByIdHandler; + _contactSelectionView = contactSelectionView; + _generateFullContactService = generateFullContactService; + + _messages = messages; + _userInput = userInput; + } + + public async Task RunAsync(CategoryResponse category) + { + var contactResult = await GetContactListAsync(category); + + if (contactResult is null) + return null; + + var contact = _contactSelectionView.Render(contactResult); + + return await _generateFullContactService.RunAsync(contact); + } + + private async Task?> GetContactListAsync(CategoryResponse category) + { + Result> contactResult; + + if (category.Id == -1 && category.Name.ToUpper() == "ALL") + contactResult = await _getAllContactsHandler.HandleAsync(); + else + contactResult = await _getAllContactsByCategoryIdHandler.HandleAsync(category); + + if (contactResult.IsFailure || contactResult.Value is null) + { + _messages.ErrorMessage(contactResult.Errors); + _userInput.PressAnyKeyToContinue(); + return null; + } + if (contactResult.Value.Count == 0) + { + _messages.ErrorMessage(new Error[] { ContactRepositoryErrors.NoContactsInCategory }); + _userInput.PressAnyKeyToContinue(); + return null; + } + + return contactResult.Value; + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/DeleteContactService.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/DeleteContactService.cs new file mode 100644 index 00000000..c57cdbac --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/DeleteContactService.cs @@ -0,0 +1,78 @@ +using PhoneBook.Application.Contacts.DeleteContact; +using PhoneBook.Application.Contacts.DTOs; +using PhoneBook.Application.Contacts.SaveChanges; +using PhoneBook.ConsoleUI.Input; +using PhoneBook.ConsoleUI.Models; +using PhoneBook.ConsoleUI.Output; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.ConsoleUI.Services; + +internal class DeleteContactService +{ + private readonly DeleteContactHandler _deleteContactHandler; + private readonly SaveChangesHandler _saveChangesHandler; + private readonly Messages _messages; + private readonly UserInput _userInput; + + public DeleteContactService(DeleteContactHandler deleteContactHandler, SaveChangesHandler saveChangesHandler, + Messages messages, UserInput userInput) + { + _deleteContactHandler = deleteContactHandler; + _saveChangesHandler = saveChangesHandler; + _messages = messages; + _userInput = userInput; + } + + internal async Task RunAsync(FullContactViewModel contact) + { + var contactRequest = new ContactResponse(contact.Id, + contact.FirstName, + contact.LastName, + contact.PhoneNumber, + contact.Email, + contact.CategoryId); + + var confirmDelete = _userInput.GetDeleteConfirmationFromUser($"{contact.FirstName} {contact.LastName}", "contact list"); + + if (confirmDelete) + { + var deleteResult = await _deleteContactHandler.HandleAsync(contactRequest); + var errors = new List(); + + if (deleteResult.IsSuccess) + { + var saveResult = await _saveChangesHandler.HandleAsync(); + + if (saveResult is null) + errors.Add(ContactRepositoryErrors.SaveResponseNull); + + else if (saveResult.IsFailure) + errors.AddRange(saveResult.Errors); + + else if (saveResult.IsSuccess) + { + _messages.DeleteSucessfulMessage($"{contact.FirstName} {contact.LastName}", "contact list"); + _userInput.PressAnyKeyToContinue(); + return true; + } + } + + if (deleteResult.IsFailure) + errors.AddRange(deleteResult.Errors); + + if (deleteResult is null) + errors.Add(ContactRepositoryErrors.DeleteResponseNull); + + _messages.ErrorMessage(errors); + } + else + { + _messages.DeleteCancelledMessage($"{contact.FirstName} {contact.LastName}", "contact list"); + } + _userInput.PressAnyKeyToContinue(); + + return false; + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/EditContactService.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/EditContactService.cs new file mode 100644 index 00000000..7ef95dd0 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/EditContactService.cs @@ -0,0 +1,188 @@ +using PhoneBook.Application.Contacts.DTOs; +using PhoneBook.Application.Contacts.EditContact; +using PhoneBook.Application.Contacts.SaveChanges; +using PhoneBook.ConsoleUI.Enums; +using PhoneBook.ConsoleUI.Input; +using PhoneBook.ConsoleUI.Models; +using PhoneBook.ConsoleUI.Output; +using PhoneBook.ConsoleUI.Services.Categories; +using PhoneBook.ConsoleUI.Views; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; +using Spectre.Console; + +namespace PhoneBook.ConsoleUI.Services; + +internal class EditContactService +{ + private readonly EditContactView _editContactView; + private readonly UserInput _userInput; + private readonly Messages _messages; + private readonly EditContactHandler _editContactHandler; + private readonly SaveChangesHandler _saveChangesHandler; + private readonly CategorySelectionService _categorySelectionService; + + public EditContactService(EditContactView editContactView, UserInput userInput, Messages messages, + EditContactHandler editContactHandler, SaveChangesHandler saveChangesHandler, + CategorySelectionService categorySelectionService) + { + _editContactView = editContactView; + _userInput = userInput; + _messages = messages; + _editContactHandler = editContactHandler; + _saveChangesHandler = saveChangesHandler; + _categorySelectionService = categorySelectionService; + } + + public async Task RunAsync(FullContactViewModel originalContact) + { + bool stillEditing = true; + var contact = new EditContactViewModel(originalContact); + + while (stillEditing) + { + Console.Clear(); + AnsiConsole.MarkupLine($"[green]Editing[/] contact entry for {contact.FirstName} {contact.LastName}:"); + AnsiConsole.WriteLine(); + + _editContactView.Render(contact); + AnsiConsole.WriteLine(); + + RenderEditContactKeyOptions(); + + var keyInfo = Console.ReadKey(true); + var exitCode = await ManageKeyPressMenuFromCategoryAsync(keyInfo, originalContact, contact); + + if (exitCode == EditContactExitCode.Save) + { + var confirmEdits = _userInput.GetEditContactConfirmationFromUser(originalContact, contact); + if (confirmEdits) + { + _messages.EditContactSuccessfulMessage($"{contact.FirstName} {contact.LastName}"); + stillEditing = false; + await UpdateContactAsync(contact); + } + else + { + _messages.EditContactCancelSaveMessage($"{originalContact.FirstName} {originalContact.LastName}"); + _userInput.PressAnyKeyToContinue(); + } + } + + if (exitCode == EditContactExitCode.Cancel) + { + stillEditing = false; + _messages.EditContactCancelledMessage($"{originalContact.FirstName} {originalContact.LastName}"); + } + } + _userInput.PressAnyKeyToContinue(); + } + + private void RenderEditContactKeyOptions() + { + var table = new Table() + .RoundedBorder() + .BorderColor(Spectre.Console.Color.Blue) + .Title("Select an field to edit:") + .ShowRowSeparators(); + + table.AddColumn("Key"); + table.AddColumn("Operation"); + + table.AddRow("1", "First Name"); + table.AddRow("2", "Last Name"); + table.AddRow("3", "Phone Number"); + table.AddRow("4", "Email"); + table.AddRow("5", "Category"); + table.AddRow("6", "Save Changes"); + table.AddRow("7", "Cancel"); + + AnsiConsole.Write(table); + } + + private async Task ManageKeyPressMenuFromCategoryAsync(ConsoleKeyInfo keyInfo, + FullContactViewModel originalContact, + EditContactViewModel contact) + { + switch (keyInfo.Key) + { + case ConsoleKey.D1: + ManageUpdateFirstName(originalContact, contact); + break; + case ConsoleKey.D2: + ManageUpdateLastName(originalContact, contact); + break; + case ConsoleKey.D3: + ManageUpdatePhoneNumber(originalContact, contact); + break; + case ConsoleKey.D4: + ManageUpdateEmail(originalContact, contact); + break; + case ConsoleKey.D5: + await ManageUpdateCategoryAsync(originalContact, contact); + break; + case ConsoleKey.D6: + return EditContactExitCode.Save; + case ConsoleKey.D7: + return EditContactExitCode.Cancel; + default: + return EditContactExitCode.None; + } + return EditContactExitCode.None; + } + + private async Task UpdateContactAsync(EditContactViewModel contact) + { + var updateResult = await _editContactHandler.HandleAsync(new ContactResponse(contact.Id, + contact.FirstName, + contact.LastName, + contact.PhoneNumber, + contact.Email, + contact.CategoryId)); + + var errors = new List(); + + if (updateResult.IsSuccess) + return; + + if (updateResult.IsFailure) + errors.AddRange(updateResult.Errors); + + if (updateResult is null) + errors.Add(ContactRepositoryErrors.UpdateResponseNull); + + _messages.ErrorMessage(errors); + _userInput.PressAnyKeyToContinue(); + } + + private async Task ManageUpdateCategoryAsync(FullContactViewModel originalContact, EditContactViewModel contact) + { + var newCategory = await _categorySelectionService.RunAsync(true); + contact.CategoryName = newCategory.Name; + contact.CategoryId = newCategory.Id; + contact.ChangedCategory = (contact.CategoryName == originalContact.CategoryName) ? false : true; + } + + private void ManageUpdateFirstName(FullContactViewModel originalContact, EditContactViewModel contact) + { + contact.FirstName = _userInput.GetNameFromUser("Please enter the new first name:"); + contact.ChangedFirstName = (contact.FirstName == originalContact.FirstName) ? false : true; + } + private void ManageUpdateLastName(FullContactViewModel originalContact, EditContactViewModel contact) + { + contact.LastName = _userInput.GetNameFromUser("Please enter the new last name:"); + contact.ChangedLastName = (contact.LastName == originalContact.LastName) ? false : true; + } + + private void ManageUpdatePhoneNumber(FullContactViewModel originalContact, EditContactViewModel contact) + { + contact.PhoneNumber = _userInput.GetPhoneNumberFromUser(); + contact.ChangedPhoneNumber = (contact.PhoneNumber == originalContact.PhoneNumber) ? false : true; + } + + private void ManageUpdateEmail(FullContactViewModel originalContact, EditContactViewModel contact) + { + contact.Email = _userInput.GetEmailAddressFromUser(); + contact.ChangedEmail = (contact.Email == originalContact.Email) ? false : true; + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/GenerateFullContactService.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/GenerateFullContactService.cs new file mode 100644 index 00000000..4e57184a --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/GenerateFullContactService.cs @@ -0,0 +1,48 @@ +using PhoneBook.Application.Categories.GetCategoryById; +using PhoneBook.Application.Contacts.DTOs; +using PhoneBook.ConsoleUI.Input; +using PhoneBook.ConsoleUI.Models; +using PhoneBook.ConsoleUI.Output; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.ConsoleUI.Services.Contacts; + +internal class GenerateFullContactService +{ + private readonly GetCategoryByIdHandler _getCategoryByIdHandler; + private readonly Messages _messages; + private readonly UserInput _userInput; + + public GenerateFullContactService(GetCategoryByIdHandler getCategoryByIdHandler, Messages messages, UserInput userInput) + { + _getCategoryByIdHandler = getCategoryByIdHandler; + _messages = messages; + _userInput = userInput; + } + + public async Task RunAsync(ContactResponse contact) + { + var categoryResult = await _getCategoryByIdHandler.HandleAsync(contact.CategoryId); + + var errors = new List(); + + if (categoryResult.Value is null || categoryResult is null) + errors.Add(ContactRepositoryErrors.NullResponse); + + else if (categoryResult.IsFailure) + errors.AddRange(categoryResult.Errors); + + else if (categoryResult.Value.Id != contact.CategoryId) + errors.Add(new Error("CategoryIdMismatch", "The category id of the returned data did not match")); + + if (errors.Count > 0) + { + _messages.ErrorMessage(errors); + _userInput.PressAnyKeyToContinue(); + return null; + } + + return new FullContactViewModel(contact, categoryResult.Value); + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/ViewContactService.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/ViewContactService.cs new file mode 100644 index 00000000..076f3667 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Contacts/ViewContactService.cs @@ -0,0 +1,153 @@ +using PhoneBook.Application.GetById; +using PhoneBook.ConsoleUI.Enums; +using PhoneBook.ConsoleUI.Input; +using PhoneBook.ConsoleUI.Models; +using PhoneBook.ConsoleUI.Output; +using PhoneBook.ConsoleUI.Services.Categories; +using PhoneBook.ConsoleUI.Services.Email; +using PhoneBook.ConsoleUI.Views; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; +using Spectre.Console; + +namespace PhoneBook.ConsoleUI.Services.Contacts; + +internal class ViewContactService +{ + private readonly CategorySelectionService _categorySelectionService; + private readonly ContactSelectionService _contactSelectionService; + private readonly DeleteContactService _deleteContactService; + private readonly GenerateFullContactService _generateFullContactService; + private readonly GetContactByIdHandler _getContactByIdHandler; + private readonly SendEmailService _sendEmailService; + private readonly EditContactService _editContactService; + private readonly ContactDetailsView _contactDetailsView; + private readonly Messages _messages; + private readonly UserInput _userInput; + + public ViewContactService(ContactSelectionService contactSelectionService, DeleteContactService deleteContactService, + CategorySelectionService categorySelectionService, GenerateFullContactService generateFullContactService, + GetContactByIdHandler getContactByIdHandler, SendEmailService sendEmailService, + EditContactService editContactService, ContactDetailsView contactDetailsView, + Messages messages, UserInput userInput) + { + _categorySelectionService = categorySelectionService; + _contactSelectionService = contactSelectionService; + _deleteContactService = deleteContactService; + _editContactService = editContactService; + _generateFullContactService = generateFullContactService; + _sendEmailService = sendEmailService; + + _getContactByIdHandler = getContactByIdHandler; + + _contactDetailsView = contactDetailsView; + + _messages = messages; + _userInput = userInput; + } + + internal async Task RunAsync() + { + bool returnToMainMenu = false; + ManageSubMenuOptions[] menuOptions = Enum.GetValues(); + + Console.Clear(); + var category = await _categorySelectionService.RunAsync(true); + + var contact = await _contactSelectionService.RunAsync(category ?? new(-1, "ALL")); + + if (contact is null) + return; + + while (returnToMainMenu == false) + { + Console.Clear(); + + AnsiConsole.WriteLine($"Viewing contact entry for {contact.FirstName} {contact.LastName}:"); + AnsiConsole.WriteLine(); + + _contactDetailsView.Render(contact); + AnsiConsole.WriteLine(); + + RenderContactDetailKeyOptions(); + + var keyInfo = Console.ReadKey(true); + var operation = await ManageKeyPressMenuAsync(keyInfo, contact); + + if (operation == ManageSubMenuOptions.Exit || operation == ManageSubMenuOptions.Delete) + returnToMainMenu = true; + + if (operation == ManageSubMenuOptions.Edit) + { + var updatedContact = await ReloadUpdatedContactAsync(contact.Id); + + if (updatedContact is null) + return; + + contact = updatedContact; + } + } + } + + private async Task ReloadUpdatedContactAsync(int contactId) + { + var updatedContact = await _getContactByIdHandler.HandleAsync(contactId); + + var errors = new List(); + + if (updatedContact is null || updatedContact.Value is null) + errors.Add(ContactRepositoryErrors.NullResponse); + else if (updatedContact.IsFailure) + errors.AddRange(updatedContact.Errors); + + if (errors.Count > 0) + { + _messages.ErrorMessage(errors); + _userInput.PressAnyKeyToContinue(); + return null; + } + + return await _generateFullContactService.RunAsync(updatedContact.Value); + } + + private void RenderContactDetailKeyOptions() + { + var table = new Table() + .RoundedBorder() + .BorderColor(Spectre.Console.Color.Blue) + .ShowRowSeparators(); + + table.AddColumn("Key"); + table.AddColumn("Operation"); + + table.AddRow("1", "Delete Contact"); + table.AddRow("2", "Edit Contact"); + table.AddRow("3", "Send Email"); + table.AddRow("4", "Return to Main Menu"); + + AnsiConsole.Write(table); + } + + private async Task ManageKeyPressMenuAsync(ConsoleKeyInfo keyInfo, FullContactViewModel contact) + { + switch (keyInfo.Key) + { + case ConsoleKey.D1: + var contactDeleted = await _deleteContactService.RunAsync(contact); + return (contactDeleted) ? ManageSubMenuOptions.Delete : ManageSubMenuOptions.Unknown; + case ConsoleKey.D2: + await _editContactService.RunAsync(contact); + return ManageSubMenuOptions.Edit; + case ConsoleKey.D3: + await _sendEmailService.RunAsync(contact); + return ManageSubMenuOptions.Email; + case ConsoleKey.D4: + return ManageSubMenuOptions.Exit; + default: + _messages.ErrorMessage(new[] { Errors.InvalidKeyPress }); + Console.WriteLine("ERROR! Invalid key press"); + _userInput.PressAnyKeyToContinue(); + return ManageSubMenuOptions.Unknown; + } + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Email/SendEmailService.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Email/SendEmailService.cs new file mode 100644 index 00000000..7c832713 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/Email/SendEmailService.cs @@ -0,0 +1,72 @@ +using PhoneBook.Application.Email; +using PhoneBook.ConsoleUI.Input; +using PhoneBook.ConsoleUI.Models; +using PhoneBook.ConsoleUI.Output; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.ConsoleUI.Services.Email; + +internal class SendEmailService +{ + private readonly Messages _messages; + private readonly UserInput _userInput; + private readonly SendEmailHandler _sendEmailHandler; + + public SendEmailService(Messages messages, UserInput userInput, SendEmailHandler sendEmailHandler) + { + _messages = messages; + _userInput = userInput; + _sendEmailHandler = sendEmailHandler; + } + internal async Task RunAsync(FullContactViewModel contact) + { + var subject = _userInput.GetEmailSubjectFromUser(); + var body = _userInput.GetEmailBodyFromUser(); + + bool confirmEmail = _userInput.GetEmailContentsConfirmationFromUser(contact, subject, body); + + if (confirmEmail) + { + bool retrySend = true; + int retryCount = 0; + + while (retrySend) + { + if (retryCount > 0) + _messages.RetryingSendEmailMessage(retryCount); + + var emailResult = await _sendEmailHandler.HandleAsync(new(contact.Email, subject, body)); + var errors = new List(); + + if (emailResult is null) + { + errors.Add(EmailErrors.NullResponse); + } + else if (emailResult.IsFailure) + { + errors.AddRange(emailResult.Errors); + } + else + { + _messages.EmailSendSuccessfulMessage(); + _userInput.PressAnyKeyToContinue(); + retrySend = false; + } + + if (errors.Count > 0) + { + _messages.ErrorMessage(errors); + + retrySend = _userInput.GetRetrySendConfirmationFromUser(); + if (retrySend) retryCount++; + } + } + } + else + { + _messages.EmailSendCancelledMessage(); + _userInput.PressAnyKeyToContinue(); + } + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/MainMenuService.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/MainMenuService.cs new file mode 100644 index 00000000..0af8da4d --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Services/MainMenuService.cs @@ -0,0 +1,64 @@ +using PhoneBook.ConsoleUI.Enums; +using PhoneBook.ConsoleUI.Services.Categories; +using PhoneBook.ConsoleUI.Services.Contacts; +using PhoneBook.ConsoleUI.Views; + +namespace PhoneBook.ConsoleUI.Services; + +internal class MainMenuService +{ + private readonly MainMenuView _mainMenu; + + private readonly AddContactService _addContactService; + private readonly ViewContactService _viewContactService; + private readonly ManageCategoriesMenuService _manageCategoriesMenuService; + + private readonly AddCategoryService _addCategoryService; + + public MainMenuService(MainMenuView mainMenu, + AddContactService addContactService, ViewContactService viewContactService, + ManageCategoriesMenuService manageCategoriesMenuService, AddCategoryService addCategoryService) + { + _mainMenu = mainMenu; + + _addContactService = addContactService; + _viewContactService = viewContactService; + _manageCategoriesMenuService = manageCategoriesMenuService; + + _addCategoryService = addCategoryService; + } + + internal async Task RunAsync() + { + bool exitApp = false; + MainMenuOptions[] menuOptions = Enum.GetValues(); + + while (exitApp == false) + { + Console.Clear(); + var selection = _mainMenu.Render(menuOptions); + + switch (selection) + { + case MainMenuOptions.AddContact: + await _addContactService.RunAsync(); + break; + case MainMenuOptions.ViewContact: + await _viewContactService.RunAsync(); + break; + case MainMenuOptions.ManageCategories: + await _manageCategoriesMenuService.RunAsync(); + break; + case MainMenuOptions.Exit: + Console.WriteLine("Goodbye..."); + Console.ReadLine(); + exitApp = true; + break; + default: + Console.WriteLine("ERROR!!!! Self destructing..."); + Console.ReadLine(); + break; + } + } + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Views/CategorySelectionView.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Views/CategorySelectionView.cs new file mode 100644 index 00000000..39580faf --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Views/CategorySelectionView.cs @@ -0,0 +1,20 @@ +using PhoneBook.Application.Categories.DTOs; +using Spectre.Console; + +namespace PhoneBook.ConsoleUI.Views; + +internal class CategorySelectionView +{ + public CategoryResponse Render(IEnumerable categories) + { + var selection = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Select a category from below: ") + .PageSize(15) + .WrapAround() + .UseConverter(c => c.Name) + .AddChoices(categories)); + + return selection; + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Views/ContactDetailsView.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Views/ContactDetailsView.cs new file mode 100644 index 00000000..11fb5e92 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Views/ContactDetailsView.cs @@ -0,0 +1,29 @@ +using PhoneBook.ConsoleUI.Models; +using Spectre.Console; + +namespace PhoneBook.ConsoleUI.Views; + +internal class ContactDetailsView +{ + public void Render(FullContactViewModel contact) + { + AnsiConsole.MarkupInterpolated($"[bold blue]Contact Entry:[/]"); + + AnsiConsole.WriteLine(); + AnsiConsole.WriteLine(); + + var table = new Table() + .HideHeaders() + .NoBorder(); + + table.AddColumn("Key"); + table.AddColumn("Value"); + + table.AddRow("[blue]Name:[/]", $"{contact.FirstName} {contact.LastName}"); + table.AddRow("[blue]Phone Number:[/]", $"{contact.PhoneNumber}"); + table.AddRow("[blue]Email:[/]", $"{contact.Email}"); + table.AddRow("[blue]Category:[/]", $"{contact.CategoryName}"); + + AnsiConsole.Write(table); + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Views/ContactSelectionView.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Views/ContactSelectionView.cs new file mode 100644 index 00000000..aaf51330 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Views/ContactSelectionView.cs @@ -0,0 +1,24 @@ +using PhoneBook.Application.Contacts.DTOs; +using Spectre.Console; + +namespace PhoneBook.ConsoleUI.Views; + +internal class ContactSelectionView +{ + public ContactResponse Render(IEnumerable contacts) + { + contacts = contacts.OrderBy(c => c.LastName); + + var selection = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Select a contact from below: ") + .PageSize(15) + .WrapAround() + .UseConverter(c => $"{c.LastName}, {c.FirstName}") + .AddChoices(contacts) + .EnableSearch() + .SearchPlaceholderText("Type to search contacts by name")); + + return selection; + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Views/EditContactView.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Views/EditContactView.cs new file mode 100644 index 00000000..7ea7c3d6 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Views/EditContactView.cs @@ -0,0 +1,36 @@ +using PhoneBook.ConsoleUI.Models; +using Spectre.Console; + +namespace PhoneBook.ConsoleUI.Views; + +internal class EditContactView +{ + public void Render(EditContactViewModel contact) + { + var firstNameTextColor = (contact.ChangedFirstName) ? "[green]" : "[white]"; + var lastNameTextColor = (contact.ChangedLastName) ? "[green]" : "[white]"; + var phoneNumberTextColor = (contact.ChangedPhoneNumber) ? "[green]" : "[white]"; + var emailTextColor = (contact.ChangedEmail) ? "[green]" : "[white]"; + var categoryTextColor = (contact.ChangedCategory) ? "[green]" : "[white]"; + + + AnsiConsole.MarkupInterpolated($"[bold blue]Contact Entry:[/]"); + + AnsiConsole.WriteLine(); + AnsiConsole.WriteLine(); + + var table = new Table() + .HideHeaders() + .NoBorder(); + + table.AddColumn("Key"); + table.AddColumn("Value"); + + table.AddRow("[blue]Name:[/]", $"{firstNameTextColor}{contact.FirstName}[/] {lastNameTextColor}{contact.LastName}[/]"); + table.AddRow("[blue]Phone Number:[/]", $"{phoneNumberTextColor}{contact.PhoneNumber}[/]"); + table.AddRow("[blue]Email:[/]", $"{emailTextColor}{contact.Email}[/]"); + table.AddRow("[blue]Category:[/]", $"{categoryTextColor}{contact.CategoryName}[/]"); + + AnsiConsole.Write(table); + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Views/MainMenuView.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Views/MainMenuView.cs new file mode 100644 index 00000000..7569e766 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Views/MainMenuView.cs @@ -0,0 +1,26 @@ +using PhoneBook.ConsoleUI.Enums; +using Spectre.Console; + +namespace PhoneBook.ConsoleUI.Views; + +internal class MainMenuView +{ + internal MainMenuOptions Render(MainMenuOptions[] menuOptions) + { + Console.Clear(); + var selection = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Select a menu option:") + .UseConverter(m => m switch + { + MainMenuOptions.AddContact => "Add Contact", + MainMenuOptions.ViewContact => "View Contact", + MainMenuOptions.ManageCategories => "Manage Categories", + MainMenuOptions.Exit => "Exit Application", + _ => m.ToString() + }) + .AddChoices(menuOptions)); + + return selection; + } +} diff --git a/phonebook.jzhartman/PhoneBook/ConsoleUI/Views/ManageCategoriesMenuView.cs b/phonebook.jzhartman/PhoneBook/ConsoleUI/Views/ManageCategoriesMenuView.cs new file mode 100644 index 00000000..0e5c3854 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/ConsoleUI/Views/ManageCategoriesMenuView.cs @@ -0,0 +1,26 @@ +using PhoneBook.ConsoleUI.Enums; +using Spectre.Console; + +namespace PhoneBook.ConsoleUI.Views; + +internal class ManageCategoriesMenuView +{ + internal ManageSubMenuOptions Render(ManageSubMenuOptions[] menuOptions) + { + Console.Clear(); + var selection = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Managing Categories -- Select a menu option:") + .UseConverter(m => m switch + { + ManageSubMenuOptions.Add => "Add Category", + ManageSubMenuOptions.Delete => "Delete Category", + ManageSubMenuOptions.Edit => "Rename Category", + ManageSubMenuOptions.Exit => "Return to Main Menu", + _ => m.ToString() + }) + .AddChoices(menuOptions)); + + return selection; + } +} \ No newline at end of file diff --git a/phonebook.jzhartman/PhoneBook/Domain/Entities/Contact.cs b/phonebook.jzhartman/PhoneBook/Domain/Entities/Contact.cs new file mode 100644 index 00000000..8fbab8d2 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Domain/Entities/Contact.cs @@ -0,0 +1,14 @@ +namespace PhoneBook.Domain.Entities; + +public class Contact +{ + public int Id { get; set; } + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public string PhoneNumber { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + + public int CategoryId { get; set; } + + public ContactCategory? Category { get; set; } +} diff --git a/phonebook.jzhartman/PhoneBook/Domain/Entities/ContactCategory.cs b/phonebook.jzhartman/PhoneBook/Domain/Entities/ContactCategory.cs new file mode 100644 index 00000000..26d41fb6 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Domain/Entities/ContactCategory.cs @@ -0,0 +1,9 @@ +namespace PhoneBook.Domain.Entities; + +public class ContactCategory +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + + public List Contacts { get; set; } = new(); +} diff --git a/phonebook.jzhartman/PhoneBook/Domain/Validation/Error.cs b/phonebook.jzhartman/PhoneBook/Domain/Validation/Error.cs new file mode 100644 index 00000000..5f4427d7 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Domain/Validation/Error.cs @@ -0,0 +1,6 @@ +namespace PhoneBook.Domain.Validation; + +public sealed record Error(string Code, string Description) +{ + public static readonly Error None = new(string.Empty, string.Empty); +} diff --git a/phonebook.jzhartman/PhoneBook/Domain/Validation/Errors/CategoryRepositoryErrors.cs b/phonebook.jzhartman/PhoneBook/Domain/Validation/Errors/CategoryRepositoryErrors.cs new file mode 100644 index 00000000..4fe706fe --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Domain/Validation/Errors/CategoryRepositoryErrors.cs @@ -0,0 +1,21 @@ +namespace PhoneBook.Domain.Validation.Errors; + +internal class CategoryRepositoryErrors +{ + public static readonly Error None = Error.None; + + public static readonly Error NullResponse = new("NullResponse", "The database response returned null."); + public static readonly Error AddResponseNull = new("AddResponseNull", "Add action returned null value"); + public static readonly Error DeleteResponseNull = new("DeleteResponseNull", "Delete action returned null value"); + public static readonly Error UpdateResponseNull = new("UpdateResponseNull", "Update action returned null value"); + + public static readonly Error DeleteDefault = new("DeleteDefault", "Cannot delete the default category UNCATEGORIZED!"); + public static readonly Error UpdateDefault = new("UpdateDefault", "Cannot rename default category."); + + public static readonly Error CategoryNotFound = new("CategoryNotFound", "The selected category was not found."); + public static readonly Error CategoryExists = new("CategoryExists", "A category with that name already exists."); + public static readonly Error CategoryDoesNotExist = new("CategoryDoesNotExist", "A contact with that name does not exist."); + + public static readonly Error UpdateDataFailed = new("UpdateDataFailed", "Database returned no records were successfully updated"); + +} diff --git a/phonebook.jzhartman/PhoneBook/Domain/Validation/Errors/ContactRepositoryErrors.cs b/phonebook.jzhartman/PhoneBook/Domain/Validation/Errors/ContactRepositoryErrors.cs new file mode 100644 index 00000000..29f1631a --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Domain/Validation/Errors/ContactRepositoryErrors.cs @@ -0,0 +1,19 @@ +namespace PhoneBook.Domain.Validation.Errors; + +public static class ContactRepositoryErrors +{ + public static readonly Error None = Error.None; + + public static readonly Error NullResponse = new("NullResponse", "The database response returned null."); + public static readonly Error AddResponseNull = new("AddResponseNull", "Add action returned null value"); + public static readonly Error DeleteResponseNull = new("DeleteResponseNull", "Delete action returned null value"); + public static readonly Error UpdateResponseNull = new("UpdateResponseNull", "Update action returned null value"); + public static readonly Error SaveResponseNull = new("SaveResponseNull", "Save action returned null value"); + + public static readonly Error ContactNotFound = new("ContactNotFound", "The selected contact was not found."); + public static readonly Error ContactExists = new("ContactExists", "A contact with that information already exists."); + public static readonly Error ContactDoesNotExist = new("ContactDoesNotExist", "A contact with that information does not exist."); + public static readonly Error NoContactsInCategory = new("NoContactsInCategory", "No contacts where found in the selected category"); + + public static readonly Error UpdateDataFailed = new("UpdateDataFailed", "Database returned no records were successfully updated"); +} diff --git a/phonebook.jzhartman/PhoneBook/Domain/Validation/Errors/EmailErrors.cs b/phonebook.jzhartman/PhoneBook/Domain/Validation/Errors/EmailErrors.cs new file mode 100644 index 00000000..dd5c3a73 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Domain/Validation/Errors/EmailErrors.cs @@ -0,0 +1,8 @@ +namespace PhoneBook.Domain.Validation.Errors; + +internal class EmailErrors +{ + public static readonly Error None = Error.None; + + public static readonly Error NullResponse = new("NullResponse", "The email service response returned null."); +} diff --git a/phonebook.jzhartman/PhoneBook/Domain/Validation/Errors/Errors.cs b/phonebook.jzhartman/PhoneBook/Domain/Validation/Errors/Errors.cs new file mode 100644 index 00000000..5a92b36e --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Domain/Validation/Errors/Errors.cs @@ -0,0 +1,9 @@ +namespace PhoneBook.Domain.Validation.Errors; + +public static class Errors +{ + public static readonly Error None = Error.None; + + public static readonly Error GenericNull = new("GenericNull", ""); + public static readonly Error InvalidKeyPress = new("InvalidKeyPress", "The selected key was not a valid option."); +} \ No newline at end of file diff --git a/phonebook.jzhartman/PhoneBook/Domain/Validation/Result.cs b/phonebook.jzhartman/PhoneBook/Domain/Validation/Result.cs new file mode 100644 index 00000000..65b08fdd --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Domain/Validation/Result.cs @@ -0,0 +1,31 @@ +namespace PhoneBook.Domain.Validation; + +public class Result +{ + public bool IsSuccess { get; } + public bool IsFailure => !IsSuccess; + public List Errors { get; } + + protected Result(bool isSuccess, IEnumerable errors) + { + IsSuccess = isSuccess; + Errors = errors.ToList(); + } + + public static Result Success() => new(true, new List()); + public static Result Failure(params Error[] errors) => new(false, errors); + public static Result Failure(IEnumerable errors) => new(false, errors); +} + +public class Result : Result +{ + public T? Value { get; set; } + private Result(bool isSuccess, T? value, IEnumerable errors) : base(isSuccess, errors) + { + Value = value; + } + + public static Result Success(T? value) => new(true, value, new List()); + public static new Result Failure(params Error[] errors) => new(false, default, errors); + public static new Result Failure(IEnumerable errors) => new(false, default, errors); +} \ No newline at end of file diff --git a/phonebook.jzhartman/PhoneBook/Infrastructure/Configurations/ContactCategoryConfiguration.cs b/phonebook.jzhartman/PhoneBook/Infrastructure/Configurations/ContactCategoryConfiguration.cs new file mode 100644 index 00000000..33246cd0 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Infrastructure/Configurations/ContactCategoryConfiguration.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using PhoneBook.Domain.Entities; + +namespace PhoneBook.Infrastructure.Configurations; + +internal class ContactCategoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ContactCategories"); + + builder.HasKey(cat => cat.Id); + + builder.HasData(new List + { + new ContactCategory + { + Id = 1, + Name = "Uncategorized" + }, + new ContactCategory + { + Id = 2, + Name = "Family" + }, + new ContactCategory + { + Id = 3, + Name = "Friends" + }, + new ContactCategory + { + Id = 4, + Name = "Work" + } + }); + } +} diff --git a/phonebook.jzhartman/PhoneBook/Infrastructure/Configurations/ContactConfiguration.cs b/phonebook.jzhartman/PhoneBook/Infrastructure/Configurations/ContactConfiguration.cs new file mode 100644 index 00000000..d1733745 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Infrastructure/Configurations/ContactConfiguration.cs @@ -0,0 +1,105 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using PhoneBook.Domain.Entities; + +namespace PhoneBook.Infrastructure.Configurations; + +internal class ContactConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Contacts"); + + builder.HasKey(c => c.Id); + + builder.HasOne(c => c.Category) + .WithMany(cat => cat.Contacts) + .HasForeignKey(c => c.CategoryId) + .IsRequired(); + + builder.HasData(new List + { + new Contact + { + Id = 1, + FirstName = "Malcolm", + LastName = "Reynolds", + PhoneNumber = "1111111111", + Email = "browncoat@serenity.com", + CategoryId = 1 + }, + new Contact + { + Id = 2, + FirstName = "Inara", + LastName = "Serra", + PhoneNumber = "2222222222", + Email = "companion@serenity.com", + CategoryId = 1 + }, + new Contact + { + Id = 3, + FirstName = "Shepherd", + LastName = "Book", + PhoneNumber = "3333333333", + Email = "book@serenity.com", + CategoryId = 1 + }, + new Contact + { + Id = 4, + FirstName = "Jayne", + LastName = "Cobb", + PhoneNumber = "4444444444", + Email = "vera@serenity.com", + CategoryId = 1 + }, + new Contact + { + Id = 5, + FirstName = "Kaylee", + LastName = "Frye", + PhoneNumber = "5555555555", + Email = "mechanic@serenity.com", + CategoryId = 1 + }, + new Contact + { + Id = 6, + FirstName = "Simon", + LastName = "Tam", + PhoneNumber = "6666666666", + Email = "awesomeDoctor@serenity.com", + CategoryId = 1 + }, + new Contact + { + Id = 7, + FirstName = "River", + LastName = "Tam", + PhoneNumber = "7777777777", + Email = "miranda@serenity.com", + CategoryId = 1 + }, + new Contact + { + Id = 8, + FirstName = "Hoban", + LastName = "Washburne", + PhoneNumber = "8888888", + Email = "leafOnTheWind@serenity.com", + CategoryId = 1 + }, + new Contact + { + Id = 9, + FirstName = "Zoe", + LastName = "Washburne", + PhoneNumber = "99999999", + Email = "browncoats@serenity.com", + CategoryId = 1 + } + }); + } +} diff --git a/phonebook.jzhartman/PhoneBook/Infrastructure/Database/DatabaseInitializer.cs b/phonebook.jzhartman/PhoneBook/Infrastructure/Database/DatabaseInitializer.cs new file mode 100644 index 00000000..05486ec1 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Infrastructure/Database/DatabaseInitializer.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore; +using PhoneBook.Domain.Entities; + +namespace PhoneBook.Infrastructure.Database; + +internal class DatabaseInitializer : IDatabaseInitializer +{ + private readonly PhoneBookDbContext _db; + + public DatabaseInitializer(PhoneBookDbContext db) + { + _db = db; + } + + public async Task EnsureUncategorizedCategoryExistsAsync() + { + var exists = await _db.ContactCategories + .AnyAsync(c => c.Name == "Uncategorized"); + + if (!exists) + { + _db.ContactCategories.Add(new ContactCategory + { + Id = 1, + Name = "Uncategorized" + }); + + await _db.SaveChangesAsync(); + } + } +} diff --git a/phonebook.jzhartman/PhoneBook/Infrastructure/Database/IDatabaseInitializer.cs b/phonebook.jzhartman/PhoneBook/Infrastructure/Database/IDatabaseInitializer.cs new file mode 100644 index 00000000..855a8654 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Infrastructure/Database/IDatabaseInitializer.cs @@ -0,0 +1,6 @@ +namespace PhoneBook.Infrastructure.Database; + +internal interface IDatabaseInitializer +{ + Task EnsureUncategorizedCategoryExistsAsync(); +} \ No newline at end of file diff --git a/phonebook.jzhartman/PhoneBook/Infrastructure/DependencyInjection.cs b/phonebook.jzhartman/PhoneBook/Infrastructure/DependencyInjection.cs new file mode 100644 index 00000000..16bfe2d0 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Infrastructure/DependencyInjection.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using PhoneBook.Application.Interfaces; +using PhoneBook.Infrastructure.Database; +using PhoneBook.Infrastructure.Email; +using PhoneBook.Infrastructure.Repositories; + +namespace PhoneBook.Infrastructure; + +public static class DependencyInjection +{ + public static IServiceCollection AddInfrastructure(this IServiceCollection services, HostBuilderContext context) + { + var config = context.Configuration; + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + var connectionString = context.Configuration.GetConnectionString("PhoneBook"); + + services.AddDbContext(options => + options.UseSqlite(connectionString)); + + services.Configure(config.GetSection("SmtpSettings")); + services.AddTransient(); + + return services; + } +} diff --git a/phonebook.jzhartman/PhoneBook/Infrastructure/Email/EmailService.cs b/phonebook.jzhartman/PhoneBook/Infrastructure/Email/EmailService.cs new file mode 100644 index 00000000..668ba9ee --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Infrastructure/Email/EmailService.cs @@ -0,0 +1,41 @@ +using MailKit.Net.Smtp; +using Microsoft.Extensions.Options; +using MimeKit; +using PhoneBook.Application.Interfaces; +using PhoneBook.Domain.Validation; + +namespace PhoneBook.Infrastructure.Email; + +internal class EmailService : IEmailService +{ + private readonly SmtpSettings _settings; + + public EmailService(IOptions settings) + { + _settings = settings.Value; + } + + public async Task SendEmailAsync(string recipient, string subject, string body) + { + try + { + var message = new MimeMessage(); + message.From.Add(new MailboxAddress(_settings.FromName, _settings.FromEmail)); + message.To.Add(new MailboxAddress(recipient, recipient)); + message.Subject = subject; + message.Body = new TextPart("plain") { Text = body }; + + using var client = new SmtpClient(); + await client.ConnectAsync(_settings.Host, _settings.Port, _settings.UseSsl); + await client.AuthenticateAsync(_settings.Username, _settings.Password); + await client.SendAsync(message); + await client.DisconnectAsync(true); + + return Result.Success(); + } + catch (Exception ex) + { + return Result.Failure(new[] { new Error("EmailError", ex.Message) }); + } + } +} diff --git a/phonebook.jzhartman/PhoneBook/Infrastructure/Email/SmtpSettings.cs b/phonebook.jzhartman/PhoneBook/Infrastructure/Email/SmtpSettings.cs new file mode 100644 index 00000000..dd2c9627 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Infrastructure/Email/SmtpSettings.cs @@ -0,0 +1,12 @@ +namespace PhoneBook.Infrastructure.Email; + +public class SmtpSettings +{ + public string Host { get; set; } = ""; + public int Port { get; set; } + public bool UseSsl { get; set; } + public string Username { get; set; } = ""; + public string Password { get; set; } = ""; + public string FromName { get; set; } = ""; + public string FromEmail { get; set; } = ""; +} \ No newline at end of file diff --git a/phonebook.jzhartman/PhoneBook/Infrastructure/PhoneBookDbContext.cs b/phonebook.jzhartman/PhoneBook/Infrastructure/PhoneBookDbContext.cs new file mode 100644 index 00000000..33ba1283 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Infrastructure/PhoneBookDbContext.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using PhoneBook.Domain.Entities; + +namespace PhoneBook.Infrastructure; + +public class PhoneBookDbContext : DbContext +{ + public DbSet Contacts { get; set; } + public DbSet ContactCategories { get; set; } + + public PhoneBookDbContext(DbContextOptions options) : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfigurationsFromAssembly(typeof(PhoneBookDbContext).Assembly); + } +} diff --git a/phonebook.jzhartman/PhoneBook/Infrastructure/Repositories/CategoryRepository.cs b/phonebook.jzhartman/PhoneBook/Infrastructure/Repositories/CategoryRepository.cs new file mode 100644 index 00000000..77640beb --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Infrastructure/Repositories/CategoryRepository.cs @@ -0,0 +1,126 @@ +using Microsoft.EntityFrameworkCore; +using PhoneBook.Application.Interfaces; +using PhoneBook.Domain.Entities; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.Infrastructure.Repositories; + +public class CategoryRepository : ICategoryRepository +{ + private readonly PhoneBookDbContext _context; + + public CategoryRepository(PhoneBookDbContext context) + { + _context = context; + } + + public async Task>> GetAllAsync() + { + try + { + var categories = await _context.ContactCategories.AsNoTracking().ToListAsync(); + + return Result>.Success(categories); + } + catch (Exception ex) + { + return Result>.Failure(new Error("DatabaseError", ex.Message)); + } + } + public async Task> GetByIdAsync(int id) + { + try + { + var category = await _context.ContactCategories.AsNoTracking().FirstOrDefaultAsync(cat => cat.Id == id); + + return Result.Success(category); + } + catch (Exception ex) + { + return Result.Failure(new Error("DatabaseError", ex.Message)); + } + } + + public async Task AddAsync(ContactCategory category) + { + try + { + if (await CategoryExists(category)) + return Result.Failure(CategoryRepositoryErrors.CategoryExists); + + await _context.ContactCategories.AddAsync(category); + return Result.Success(); + } + catch (Exception ex) + { + return Result.Failure(new Error("DatabaseError", ex.Message)); + } + } + + public async Task DeleteAsync(ContactCategory category) + { + try + { + if (!(await CategoryExists(category))) + return Result.Failure(CategoryRepositoryErrors.CategoryDoesNotExist); + + await _context.ContactCategories + .Where(cat => cat.Id == category.Id) + .ExecuteDeleteAsync(); + return Result.Success(); + } + catch (Exception ex) + { + + return Result.Failure(new Error("DatabaseError", ex.Message)); + } + } + public async Task UpdateAsync(ContactCategory category) + { + try + { + if (await CategoryExistsByNameCaseIndependent(category)) + return Result.Failure(CategoryRepositoryErrors.CategoryExists); + + var response = await _context.ContactCategories + .Where(cat => cat.Id == category.Id) + .ExecuteUpdateAsync(setters => setters + .SetProperty(c => c.Name, category.Name) + ); + + if (response <= 0) + return Result.Failure(CategoryRepositoryErrors.UpdateDataFailed); + + return Result.Success(); + } + catch (Exception ex) + { + return Result.Failure(new Error("DatabaseError", ex.Message)); + } + } + + public async Task SaveChangesAsync() + { + try + { + var response = await _context.SaveChangesAsync(); + + return Result.Success(); + } + catch (Exception ex) + { + return Result.Failure(new Error("DatabaseError", ex.Message)); + } + } + + private async Task CategoryExistsByNameCaseIndependent(ContactCategory category) + { + return await _context.ContactCategories.AnyAsync(cat => cat.Name == category.Name); + } + + private async Task CategoryExists(ContactCategory category) + { + return await _context.ContactCategories.AnyAsync(cat => cat.Name.ToUpper() == category.Name.ToUpper()); + } +} diff --git a/phonebook.jzhartman/PhoneBook/Infrastructure/Repositories/ContactRepository.cs b/phonebook.jzhartman/PhoneBook/Infrastructure/Repositories/ContactRepository.cs new file mode 100644 index 00000000..34e8c619 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Infrastructure/Repositories/ContactRepository.cs @@ -0,0 +1,173 @@ +using Microsoft.EntityFrameworkCore; +using PhoneBook.Application.Interfaces; +using PhoneBook.Domain.Entities; +using PhoneBook.Domain.Validation; +using PhoneBook.Domain.Validation.Errors; + +namespace PhoneBook.Infrastructure.Repositories; + +public class ContactRepository : IContactRepository +{ + private readonly PhoneBookDbContext _context; + + public ContactRepository(PhoneBookDbContext context) + { + _context = context; + } + + public async Task AddAsync(Contact contact) + { + try + { + if (await ContactExists(contact)) + return Result.Failure(ContactRepositoryErrors.ContactExists); + + await _context.Contacts.AddAsync(contact); + + return Result.Success(); + } + catch (Exception ex) + { + return Result.Failure(new Error("DatabaseError", ex.Message)); + } + } + + public async Task DeleteAsync(Contact contact) + { + try + { + if (!(await ContactExists(contact))) + return Result.Failure(ContactRepositoryErrors.ContactDoesNotExist); + + await _context.Contacts + .Where(c => c.Id == contact.Id) + .ExecuteDeleteAsync(); + + return Result.Success(); + } + catch (Exception ex) + { + + return Result.Failure(new Error("DatabaseError", ex.Message)); + } + } + + public async Task>> GetAllAsync() + { + try + { + var contacts = await _context.Contacts + .AsNoTracking() + .ToListAsync(); + + return Result>.Success(contacts); + } + catch (Exception ex) + { + return Result>.Failure(new Error("DatabaseError", ex.Message)); + } + } + + public async Task> GetByIdAsync(int id) + { + try + { + var contact = await _context.Contacts + .AsNoTracking() + .FirstOrDefaultAsync(c => c.Id == id); + + return Result.Success(contact); + } + catch (Exception ex) + { + return Result.Failure(new Error("DatabaseError", ex.Message)); + } + } + + public async Task>> GetByCategoryIdAsync(int id) + { + try + { + var contacts = await _context.Contacts + .AsNoTracking() + .Where(c => c.CategoryId == id) + .ToListAsync(); + + return Result>.Success(contacts); + } + catch (Exception ex) + { + return Result>.Failure(new Error("DatabaseError", ex.Message)); + } + } + + public async Task UpdateAsync(Contact contact) + { + try + { + var response = await _context.Contacts + .Where(c => c.Id == contact.Id) + .ExecuteUpdateAsync(setters => setters + .SetProperty(c => c.FirstName, contact.FirstName) + .SetProperty(c => c.LastName, contact.LastName) + .SetProperty(c => c.PhoneNumber, contact.PhoneNumber) + .SetProperty(c => c.Email, contact.Email) + .SetProperty(c => c.CategoryId, contact.CategoryId) + ); + + if (response <= 0) + return Result.Failure(ContactRepositoryErrors.UpdateDataFailed); + + return Result.Success(); + + } + catch (Exception ex) + { + return Result.Failure(new Error("DatabaseError", ex.Message)); + } + } + public async Task SetCategoryIdForContactsToDefaultByCategoryIdAsync(ContactCategory category) + { + try + { + if (!await _context.ContactCategories.AnyAsync(cat => cat.Name.ToUpper() == category.Name.ToUpper())) + return Result.Failure(CategoryRepositoryErrors.CategoryDoesNotExist); + + if (!await _context.Contacts.AnyAsync(c => c.CategoryId == category.Id)) + return Result.Success(); + + var response = await _context.Contacts + .Where(c => c.CategoryId == category.Id) + .ExecuteUpdateAsync(setters => setters + .SetProperty(c => c.CategoryId, 1)); + + if (response <= 0) + return Result.Failure(ContactRepositoryErrors.UpdateDataFailed); + + return Result.Success(); + } + catch (Exception ex) + { + return Result.Failure(new Error("DatabaseError", ex.Message)); + } + } + public async Task SaveChangesAsync() + { + try + { + var response = await _context.SaveChangesAsync(); + + return Result.Success(); + } + catch (Exception ex) + { + return Result.Failure(new Error("DatabaseError", ex.Message)); + } + } + + private async Task ContactExists(Contact contact) + { + return await _context.Contacts.AnyAsync(c => c.FirstName == contact.FirstName && c.LastName == contact.LastName + && c.PhoneNumber == contact.PhoneNumber && c.Email == contact.Email); + } +} diff --git a/phonebook.jzhartman/PhoneBook/Migrations/20260606022815_InitialCreate.Designer.cs b/phonebook.jzhartman/PhoneBook/Migrations/20260606022815_InitialCreate.Designer.cs new file mode 100644 index 00000000..87cf1f36 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Migrations/20260606022815_InitialCreate.Designer.cs @@ -0,0 +1,174 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PhoneBook.Infrastructure; + +#nullable disable + +namespace PhoneBook.Migrations +{ + [DbContext(typeof(PhoneBookDbContext))] + [Migration("20260606022815_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); + + modelBuilder.Entity("PhoneBook.Domain.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Contacts", (string)null); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + Email = "browncoat@serenity.com", + FirstName = "Malcolm", + LastName = "Reynolds", + PhoneNumber = "1111111111" + }, + new + { + Id = 2, + CategoryId = 1, + Email = "companion@serenity.com", + FirstName = "Inara", + LastName = "Serra", + PhoneNumber = "2222222222" + }, + new + { + Id = 3, + CategoryId = 1, + Email = "book@serenity.com", + FirstName = "Shepherd", + LastName = "Book", + PhoneNumber = "3333333333" + }, + new + { + Id = 4, + CategoryId = 1, + Email = "vera@serenity.com", + FirstName = "Jayne", + LastName = "Cobb", + PhoneNumber = "4444444444" + }, + new + { + Id = 5, + CategoryId = 1, + Email = "mechanic@serenity.com", + FirstName = "Kaylee", + LastName = "Frye", + PhoneNumber = "5555555555" + }, + new + { + Id = 6, + CategoryId = 1, + Email = "awesomeDoctor@serenity.com", + FirstName = "Simon", + LastName = "Tam", + PhoneNumber = "6666666666" + }, + new + { + Id = 7, + CategoryId = 1, + Email = "miranda@serenity.com", + FirstName = "River", + LastName = "Tam", + PhoneNumber = "7777777777" + }); + }); + + modelBuilder.Entity("PhoneBook.Domain.Entities.ContactCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ContactCategories", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "Uncategorized" + }, + new + { + Id = 2, + Name = "Family" + }, + new + { + Id = 3, + Name = "Friends" + }, + new + { + Id = 4, + Name = "Work" + }); + }); + + modelBuilder.Entity("PhoneBook.Domain.Entities.Contact", b => + { + b.HasOne("PhoneBook.Domain.Entities.ContactCategory", "Category") + .WithMany("Contacts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("PhoneBook.Domain.Entities.ContactCategory", b => + { + b.Navigation("Contacts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/phonebook.jzhartman/PhoneBook/Migrations/20260606022815_InitialCreate.cs b/phonebook.jzhartman/PhoneBook/Migrations/20260606022815_InitialCreate.cs new file mode 100644 index 00000000..58085495 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Migrations/20260606022815_InitialCreate.cs @@ -0,0 +1,92 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace PhoneBook.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ContactCategories", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ContactCategories", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Contacts", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + FirstName = table.Column(type: "TEXT", nullable: false), + LastName = table.Column(type: "TEXT", nullable: false), + PhoneNumber = table.Column(type: "TEXT", nullable: false), + Email = table.Column(type: "TEXT", nullable: false), + CategoryId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Contacts", x => x.Id); + table.ForeignKey( + name: "FK_Contacts_ContactCategories_CategoryId", + column: x => x.CategoryId, + principalTable: "ContactCategories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "ContactCategories", + columns: new[] { "Id", "Name" }, + values: new object[,] + { + { 1, "Uncategorized" }, + { 2, "Family" }, + { 3, "Friends" }, + { 4, "Work" } + }); + + migrationBuilder.InsertData( + table: "Contacts", + columns: new[] { "Id", "CategoryId", "Email", "FirstName", "LastName", "PhoneNumber" }, + values: new object[,] + { + { 1, 1, "browncoat@serenity.com", "Malcolm", "Reynolds", "1111111111" }, + { 2, 1, "companion@serenity.com", "Inara", "Serra", "2222222222" }, + { 3, 1, "book@serenity.com", "Shepherd", "Book", "3333333333" }, + { 4, 1, "vera@serenity.com", "Jayne", "Cobb", "4444444444" }, + { 5, 1, "mechanic@serenity.com", "Kaylee", "Frye", "5555555555" }, + { 6, 1, "awesomeDoctor@serenity.com", "Simon", "Tam", "6666666666" }, + { 7, 1, "miranda@serenity.com", "River", "Tam", "7777777777" } + }); + + migrationBuilder.CreateIndex( + name: "IX_Contacts_CategoryId", + table: "Contacts", + column: "CategoryId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Contacts"); + + migrationBuilder.DropTable( + name: "ContactCategories"); + } + } +} diff --git a/phonebook.jzhartman/PhoneBook/Migrations/20260613032625_UpdatedSeedData.Designer.cs b/phonebook.jzhartman/PhoneBook/Migrations/20260613032625_UpdatedSeedData.Designer.cs new file mode 100644 index 00000000..92a20119 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Migrations/20260613032625_UpdatedSeedData.Designer.cs @@ -0,0 +1,192 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PhoneBook.Infrastructure; + +#nullable disable + +namespace PhoneBook.Migrations +{ + [DbContext(typeof(PhoneBookDbContext))] + [Migration("20260613032625_UpdatedSeedData")] + partial class UpdatedSeedData + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); + + modelBuilder.Entity("PhoneBook.Domain.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Contacts", (string)null); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + Email = "browncoat@serenity.com", + FirstName = "Malcolm", + LastName = "Reynolds", + PhoneNumber = "1111111111" + }, + new + { + Id = 2, + CategoryId = 1, + Email = "companion@serenity.com", + FirstName = "Inara", + LastName = "Serra", + PhoneNumber = "2222222222" + }, + new + { + Id = 3, + CategoryId = 1, + Email = "book@serenity.com", + FirstName = "Shepherd", + LastName = "Book", + PhoneNumber = "3333333333" + }, + new + { + Id = 4, + CategoryId = 1, + Email = "vera@serenity.com", + FirstName = "Jayne", + LastName = "Cobb", + PhoneNumber = "4444444444" + }, + new + { + Id = 5, + CategoryId = 1, + Email = "mechanic@serenity.com", + FirstName = "Kaylee", + LastName = "Frye", + PhoneNumber = "5555555555" + }, + new + { + Id = 6, + CategoryId = 1, + Email = "awesomeDoctor@serenity.com", + FirstName = "Simon", + LastName = "Tam", + PhoneNumber = "6666666666" + }, + new + { + Id = 7, + CategoryId = 1, + Email = "miranda@serenity.com", + FirstName = "River", + LastName = "Tam", + PhoneNumber = "7777777777" + }, + new + { + Id = 8, + CategoryId = 1, + Email = "leafOnTheWind@serenity.com", + FirstName = "Hoban", + LastName = "Washburne", + PhoneNumber = "8888888" + }, + new + { + Id = 9, + CategoryId = 1, + Email = "browncoats@serenity.com", + FirstName = "Zoe", + LastName = "Washburne", + PhoneNumber = "99999999" + }); + }); + + modelBuilder.Entity("PhoneBook.Domain.Entities.ContactCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ContactCategories", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "Uncategorized" + }, + new + { + Id = 2, + Name = "Family" + }, + new + { + Id = 3, + Name = "Friends" + }, + new + { + Id = 4, + Name = "Work" + }); + }); + + modelBuilder.Entity("PhoneBook.Domain.Entities.Contact", b => + { + b.HasOne("PhoneBook.Domain.Entities.ContactCategory", "Category") + .WithMany("Contacts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("PhoneBook.Domain.Entities.ContactCategory", b => + { + b.Navigation("Contacts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/phonebook.jzhartman/PhoneBook/Migrations/20260613032625_UpdatedSeedData.cs b/phonebook.jzhartman/PhoneBook/Migrations/20260613032625_UpdatedSeedData.cs new file mode 100644 index 00000000..41b8d42c --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Migrations/20260613032625_UpdatedSeedData.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace PhoneBook.Migrations +{ + /// + public partial class UpdatedSeedData : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData( + table: "Contacts", + columns: new[] { "Id", "CategoryId", "Email", "FirstName", "LastName", "PhoneNumber" }, + values: new object[,] + { + { 8, 1, "leafOnTheWind@serenity.com", "Hoban", "Washburne", "8888888" }, + { 9, 1, "browncoats@serenity.com", "Zoe", "Washburne", "99999999" } + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "Contacts", + keyColumn: "Id", + keyValue: 8); + + migrationBuilder.DeleteData( + table: "Contacts", + keyColumn: "Id", + keyValue: 9); + } + } +} diff --git a/phonebook.jzhartman/PhoneBook/Migrations/PhoneBookDbContextModelSnapshot.cs b/phonebook.jzhartman/PhoneBook/Migrations/PhoneBookDbContextModelSnapshot.cs new file mode 100644 index 00000000..5de8cd34 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Migrations/PhoneBookDbContextModelSnapshot.cs @@ -0,0 +1,189 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PhoneBook.Infrastructure; + +#nullable disable + +namespace PhoneBook.Migrations +{ + [DbContext(typeof(PhoneBookDbContext))] + partial class PhoneBookDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); + + modelBuilder.Entity("PhoneBook.Domain.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Contacts", (string)null); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + Email = "browncoat@serenity.com", + FirstName = "Malcolm", + LastName = "Reynolds", + PhoneNumber = "1111111111" + }, + new + { + Id = 2, + CategoryId = 1, + Email = "companion@serenity.com", + FirstName = "Inara", + LastName = "Serra", + PhoneNumber = "2222222222" + }, + new + { + Id = 3, + CategoryId = 1, + Email = "book@serenity.com", + FirstName = "Shepherd", + LastName = "Book", + PhoneNumber = "3333333333" + }, + new + { + Id = 4, + CategoryId = 1, + Email = "vera@serenity.com", + FirstName = "Jayne", + LastName = "Cobb", + PhoneNumber = "4444444444" + }, + new + { + Id = 5, + CategoryId = 1, + Email = "mechanic@serenity.com", + FirstName = "Kaylee", + LastName = "Frye", + PhoneNumber = "5555555555" + }, + new + { + Id = 6, + CategoryId = 1, + Email = "awesomeDoctor@serenity.com", + FirstName = "Simon", + LastName = "Tam", + PhoneNumber = "6666666666" + }, + new + { + Id = 7, + CategoryId = 1, + Email = "miranda@serenity.com", + FirstName = "River", + LastName = "Tam", + PhoneNumber = "7777777777" + }, + new + { + Id = 8, + CategoryId = 1, + Email = "leafOnTheWind@serenity.com", + FirstName = "Hoban", + LastName = "Washburne", + PhoneNumber = "8888888" + }, + new + { + Id = 9, + CategoryId = 1, + Email = "browncoats@serenity.com", + FirstName = "Zoe", + LastName = "Washburne", + PhoneNumber = "99999999" + }); + }); + + modelBuilder.Entity("PhoneBook.Domain.Entities.ContactCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ContactCategories", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "Uncategorized" + }, + new + { + Id = 2, + Name = "Family" + }, + new + { + Id = 3, + Name = "Friends" + }, + new + { + Id = 4, + Name = "Work" + }); + }); + + modelBuilder.Entity("PhoneBook.Domain.Entities.Contact", b => + { + b.HasOne("PhoneBook.Domain.Entities.ContactCategory", "Category") + .WithMany("Contacts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("PhoneBook.Domain.Entities.ContactCategory", b => + { + b.Navigation("Contacts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/phonebook.jzhartman/PhoneBook/PhoneBook.csproj b/phonebook.jzhartman/PhoneBook/PhoneBook.csproj new file mode 100644 index 00000000..e4248611 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/PhoneBook.csproj @@ -0,0 +1,33 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + Always + + + + + + + + diff --git a/phonebook.jzhartman/PhoneBook/Properties/launchSettings.json b/phonebook.jzhartman/PhoneBook/Properties/launchSettings.json new file mode 100644 index 00000000..281e9b5f --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "PhoneBook": { + "commandName": "Project", + "workingDirectory": "C:\\Programming\\C Sharp Academy\\Phonebook\\PhoneBookApp\\PhoneBook" + } + } +} \ No newline at end of file diff --git a/phonebook.jzhartman/PhoneBook/appsettings.json b/phonebook.jzhartman/PhoneBook/appsettings.json new file mode 100644 index 00000000..0c899c66 --- /dev/null +++ b/phonebook.jzhartman/PhoneBook/appsettings.json @@ -0,0 +1,15 @@ +{ + "ConnectionStrings": { + "PhoneBook": "Data Source=phonebook.db" + }, + + "SmtpSettings": { + "Host": "smtp.gmail.com", + "Port": 587, + "UseSsl": false, + "Username": "your-username", + "Password": "your-password", + "FromName": "Phonebook App", + "FromEmail": "youremail@email.com" + } +} \ No newline at end of file diff --git a/phonebook.jzhartman/PhoneBookApp.slnx b/phonebook.jzhartman/PhoneBookApp.slnx new file mode 100644 index 00000000..4cfe2feb --- /dev/null +++ b/phonebook.jzhartman/PhoneBookApp.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/phonebook.jzhartman/README.md b/phonebook.jzhartman/README.md new file mode 100644 index 00000000..c119be5e --- /dev/null +++ b/phonebook.jzhartman/README.md @@ -0,0 +1 @@ +# PhoneBookApp \ No newline at end of file From 5e24b7d21677b2c79a662f55a2360edadb9b7551 Mon Sep 17 00:00:00 2001 From: Jason Hartman <129107535+jzhartman@users.noreply.github.com> Date: Sat, 13 Jun 2026 23:13:51 -0400 Subject: [PATCH 2/2] Added Readme Expanded README with detailed application features, configuration, and usage instructions. --- phonebook.jzhartman/README.md | 61 ++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/phonebook.jzhartman/README.md b/phonebook.jzhartman/README.md index c119be5e..cd4241b7 100644 --- a/phonebook.jzhartman/README.md +++ b/phonebook.jzhartman/README.md @@ -1 +1,60 @@ -# PhoneBookApp \ No newline at end of file +# PhoneBookApp + +A simple, console-based Phone Book application built with .NET 10 using a layered clean architecture (Domain → Application → Infrastructure → Presentation). Uses Entity Framework Core with SQLite and includes an SMTP-backed email service. + +## Description +This is a basic Phone Book application. It allows the user to add contacts to a database. Contacts consist of First Name, Last Name, Phone Number, Email Address and Category. Categories are user-defined and help to increase organization and simplify searches. Users are able to create, edit, or delete these contacts and categories. The user can configure SMTP email settings to allow them to directly send an email to a contact from the list. +This was built following The C# Academy [Phone Book project guidelines](https://www.thecsharpacademy.com/project/16/phonebook). + +## Features +- Clean Architecture (layered as Domain → Application → Infrastructure → Presentation) +- Entity Framework Core +- SQLite database for data persistence +- Console-based UI using Spectre.Console +- Email sending to a selected contact via configurable SMTP + +## Configuration +- App settings live in `appsettings.json`. + - Connection string name: `PhoneBook` — recommended SQLite example: + - `Data Source=phonebook.db` + - SMTP settings section: `SmtpSettings` matching the `SmtpSettings` class: + - `Host`, `Port`, `UseSsl`, `Username`, `Password`, `FromName`, `FromEmail` + +## Usage +### Main Menu +- Opens with a Main Menu providing the options: + image + + +### Add Contact +- Selecting Add Contact will prompt the user to input the relevant data for the contact. All fields are required: + image + +- A confirmation message is provided, after which the contact will be added or discarded. + + +### View Contact +- Selecting View Contact will first print the list of Categories. The user selects from the list to see only contacts in that category: + image + +- Once the Category is selected, the relevant contact list is printed. The user selects the contact to view their details: + image + +- Contact details are displayed below, as well as the relevant options: + image + +- Options: + - Delete Contact: Deleted the currently displayed contact and returns to the Main Menu (requires confirmation) + - Edit Contact: Allows any of the paramters for the contact to be changed + - Send Email: Sends an email to the contact at their displayed email address (requires proper SMTP configuration in appsettings.json) + - Return to Main Menu: Returns to the Main Menu + +### Manage Categories +- Selecting Manage Categories displays the following submenu: + image + +- Options: + - Add Category: Adds a new category to the list (must have a unique name -- case insensitive) + - Delete Category: Select a category from the list and delete it -- This will set all contacts within that category to Uncategorized (requires confirmation) + - Rename Category: Rename the category to whatevery you want (case sensitive) + - Return to Main Menu: Returns to the Main Menu