├── Web ├── BeautyBooking.Web │ ├── Views │ │ ├── _ViewStart.cshtml │ │ ├── _ViewImports.cshtml │ │ ├── Shared │ │ │ ├── _MenuPartial.cshtml │ │ │ ├── Error404.cshtml │ │ │ ├── Components │ │ │ │ ├── SalonServiceDetails │ │ │ │ │ └── Default.cshtml │ │ │ │ ├── CategoriesSimpleList │ │ │ │ │ └── Default.cshtml │ │ │ │ └── LatestBlogPosts │ │ │ │ │ └── Default.cshtml │ │ │ ├── Error.cshtml │ │ │ ├── _SearchBarPartial.cshtml │ │ │ ├── _MenuItemsPartial.cshtml │ │ │ ├── _CookieConsentPartial.cshtml │ │ │ └── _LoginPartial.cshtml │ │ └── Appointments │ │ │ ├── UnavailableService.cshtml │ │ │ └── CancelAppointment.cshtml │ ├── Areas │ │ ├── Manager │ │ │ ├── Views │ │ │ │ ├── _ViewStart.cshtml │ │ │ │ ├── _ViewImports.cshtml │ │ │ │ ├── Dashboard │ │ │ │ │ └── Index.cshtml │ │ │ │ └── Shared │ │ │ │ │ ├── Components │ │ │ │ │ └── SalonsSimpleList │ │ │ │ │ │ └── Default.cshtml │ │ │ │ │ ├── _ManagerSidebarPartial.cshtml │ │ │ │ │ ├── _ManagerMenuPartial.cshtml │ │ │ │ │ └── _ManagerHeaderPartial.cshtml │ │ │ └── Controllers │ │ │ │ ├── DashboardController.cs │ │ │ │ └── ManagerBaseController.cs │ │ ├── Administration │ │ │ ├── Views │ │ │ │ ├── _ViewStart.cshtml │ │ │ │ ├── _ViewImports.cshtml │ │ │ │ ├── Shared │ │ │ │ │ ├── _AdminMenuPartial.cshtml │ │ │ │ │ ├── _AdminHeaderPartial.cshtml │ │ │ │ │ └── _AdminSidebarPartial.cshtml │ │ │ │ ├── Cities │ │ │ │ │ ├── AddCity.cshtml │ │ │ │ │ └── Index.cshtml │ │ │ │ └── Appointments │ │ │ │ │ └── Index.cshtml │ │ │ └── Controllers │ │ │ │ ├── DashboardController.cs │ │ │ │ ├── AdministrationController.cs │ │ │ │ ├── AppointmentsController.cs │ │ │ │ └── CitiesController.cs │ │ └── Identity │ │ │ ├── Pages │ │ │ ├── Account │ │ │ │ ├── _ViewImports.cshtml │ │ │ │ ├── Manage │ │ │ │ │ ├── _ViewImports.cshtml │ │ │ │ │ ├── DownloadPersonalData.cshtml │ │ │ │ │ ├── _StatusMessage.cshtml │ │ │ │ │ ├── _Layout.cshtml │ │ │ │ │ ├── Disable2fa.cshtml │ │ │ │ │ ├── ShowRecoveryCodes.cshtml │ │ │ │ │ ├── ShowRecoveryCodes.cshtml.cs │ │ │ │ │ ├── PersonalData.cshtml │ │ │ │ │ ├── ResetAuthenticator.cshtml │ │ │ │ │ ├── GenerateRecoveryCodes.cshtml │ │ │ │ │ ├── DeletePersonalData.cshtml │ │ │ │ │ ├── Index.cshtml │ │ │ │ │ ├── PersonalData.cshtml.cs │ │ │ │ │ ├── _ManageNav.cshtml │ │ │ │ │ ├── SetPassword.cshtml │ │ │ │ │ ├── ChangePassword.cshtml │ │ │ │ │ └── Email.cshtml │ │ │ │ ├── ConfirmEmail.cshtml │ │ │ │ ├── ConfirmEmailChange.cshtml │ │ │ │ ├── Logout.cshtml │ │ │ │ ├── ForgotPasswordConfirmation.cshtml │ │ │ │ ├── AccessDenied.cshtml │ │ │ │ ├── Lockout.cshtml │ │ │ │ ├── ResetPasswordConfirmation.cshtml │ │ │ │ ├── AccessDenied.cshtml.cs │ │ │ │ ├── _StatusMessage.cshtml │ │ │ │ ├── Lockout.cshtml.cs │ │ │ │ ├── ForgotPasswordConfirmation.cshtml.cs │ │ │ │ ├── ResetPasswordConfirmation.cshtml.cs │ │ │ │ ├── RegisterConfirmation.cshtml │ │ │ │ ├── ForgotPassword.cshtml │ │ │ │ ├── ResendEmailConfirmation.cshtml │ │ │ │ ├── LoginWithRecoveryCode.cshtml │ │ │ │ ├── ExternalLogin.cshtml │ │ │ │ ├── Logout.cshtml.cs │ │ │ │ ├── ResetPassword.cshtml │ │ │ │ ├── Register.cshtml │ │ │ │ ├── ConfirmEmail.cshtml.cs │ │ │ │ └── LoginWith2fa.cshtml │ │ │ ├── _ViewStart.cshtml │ │ │ ├── _ViewImports.cshtml │ │ │ ├── Error.cshtml.cs │ │ │ ├── Error.cshtml │ │ │ └── _ValidationScriptsPartial.cshtml │ │ │ └── IdentityHostingStartup.cs │ ├── wwwroot │ │ ├── favicon.ico │ │ ├── plugins │ │ │ ├── OwlCarousel2-2.2.1 │ │ │ │ ├── ajax-loader.gif │ │ │ │ ├── owl.video.play.png │ │ │ │ └── owl.theme.default.css │ │ │ ├── font-awesome-4.7.0 │ │ │ │ ├── fonts │ │ │ │ │ ├── FontAwesome.otf │ │ │ │ │ ├── fontawesome-webfont.eot │ │ │ │ │ ├── fontawesome-webfont.ttf │ │ │ │ │ ├── fontawesome-webfont.woff │ │ │ │ │ └── fontawesome-webfont.woff2 │ │ │ │ ├── less │ │ │ │ │ ├── screen-reader.less │ │ │ │ │ ├── fixed-width.less │ │ │ │ │ ├── larger.less │ │ │ │ │ ├── list.less │ │ │ │ │ ├── core.less │ │ │ │ │ ├── stacked.less │ │ │ │ │ ├── font-awesome.less │ │ │ │ │ ├── bordered-pulled.less │ │ │ │ │ ├── rotated-flipped.less │ │ │ │ │ ├── path.less │ │ │ │ │ ├── animated.less │ │ │ │ │ └── mixins.less │ │ │ │ └── scss │ │ │ │ │ ├── _fixed-width.scss │ │ │ │ │ ├── _screen-reader.scss │ │ │ │ │ ├── _larger.scss │ │ │ │ │ ├── _list.scss │ │ │ │ │ ├── _core.scss │ │ │ │ │ ├── font-awesome.scss │ │ │ │ │ ├── _stacked.scss │ │ │ │ │ ├── _bordered-pulled.scss │ │ │ │ │ ├── _rotated-flipped.scss │ │ │ │ │ ├── _path.scss │ │ │ │ │ ├── _animated.scss │ │ │ │ │ └── _mixins.scss │ │ │ └── greensock │ │ │ │ └── animation.gsap.min.js │ │ ├── js │ │ │ ├── rating-static.js │ │ │ ├── date-picker.js │ │ │ ├── rating-responsive.js │ │ │ └── upload-image.js │ │ └── css │ │ │ └── sidebar.css │ ├── Controllers │ │ ├── BaseController.cs │ │ ├── CategoriesController.cs │ │ └── BlogPostsController.cs │ ├── appsettings.json │ ├── Program.cs │ ├── bundleconfig.json │ ├── Properties │ │ └── launchSettings.json │ └── libman.json ├── BeautyBooking.Web.ViewModels │ ├── ErrorViewModel.cs │ ├── Home │ │ ├── IndexViewModel.cs │ │ └── IndexCategoryViewModel.cs │ ├── Cities │ │ ├── CitiesListViewModel.cs │ │ ├── CityViewModel.cs │ │ └── CityInputModel.cs │ ├── Common │ │ ├── Pagination │ │ │ ├── PageSizesConstants.cs │ │ │ └── PaginatedList.cs │ │ ├── SelectLists │ │ │ ├── CitySelectListViewModel.cs │ │ │ └── CategorySelectListViewModel.cs │ │ └── CustomValidationAttributes │ │ │ ├── ValidateTimeStringAttribute.cs │ │ │ ├── ValidateImageFileAttribute.cs │ │ │ └── ValidateDateStringAttribute.cs │ ├── Salons │ │ ├── SalonsListViewModel.cs │ │ ├── SalonsSimpleListViewModel.cs │ │ ├── SalonsPaginatedListViewModel.cs │ │ ├── SalonSimpleViewModel.cs │ │ ├── SalonServiceViewModel.cs │ │ ├── SalonViewModel.cs │ │ ├── SalonWithServicesViewModel.cs │ │ └── SalonInputModel.cs │ ├── Services │ │ ├── ServicesListViewModel.cs │ │ ├── ServiceViewModel.cs │ │ └── ServiceInputModel.cs │ ├── BlogPosts │ │ ├── BlogPostsListViewModel.cs │ │ ├── BlogPostsPaginatedListViewModel.cs │ │ ├── BlogPostViewModel.cs │ │ └── BlogPostInputModel.cs │ ├── Categories │ │ ├── CategoriesListViewModel.cs │ │ ├── CategoriesSimpleListViewModel.cs │ │ ├── CategorySimpleViewModel.cs │ │ ├── CategoryViewModel.cs │ │ └── CategoryInputModel.cs │ ├── Appointments │ │ ├── AppointmentsListViewModel.cs │ │ ├── AppointmentInputModel.cs │ │ ├── AppointmentViewModel.cs │ │ └── AppointmentRatingViewModel.cs │ ├── SalonServices │ │ ├── SalonServiceSimpleViewModel.cs │ │ └── SalonServiceDetailsViewModel.cs │ └── BeautyBooking.Web.ViewModels.csproj └── BeautyBooking.Web.Infrastructure │ ├── ViewComponents │ ├── LatestBlogPostsViewComponent.cs │ ├── SalonServiceDetailsViewComponent.cs │ ├── CategoriesSimpleListViewComponent.cs │ ├── AllAppointmentsBySalonViewComponent.cs │ ├── SalonsSimpleListViewComponent.cs │ └── PastAppointmentsViewComponent.cs │ └── BeautyBooking.Web.Infrastructure.csproj ├── Tests ├── Sandbox │ ├── appsettings.json │ └── SandboxOptions.cs ├── BeautyBooking.Web.Tests │ ├── Properties │ │ └── launchSettings.json │ ├── SeleniumTests.cs │ ├── BeautyBooking.Web.Tests.csproj │ └── WebTests.cs └── BeautyBooking.Services.Data.Tests │ └── BeautyBooking.Services.Data.Tests.csproj ├── Data ├── BeautyBooking.Data │ ├── appsettings.json │ ├── Seeding │ │ ├── ISeeder.cs │ │ ├── CustomSeeders │ │ │ ├── CitiesSeeder.cs │ │ │ └── SalonServicesSeeder.cs │ │ ├── RolesSeeder.cs │ │ └── ApplicationDbContextSeeder.cs │ ├── Configurations │ │ ├── SalonServiceConfiguration.cs │ │ ├── ApplicationUserConfiguration.cs │ │ └── AppointmentConfiguration.cs │ ├── IdentityOptionsProvider.cs │ ├── Migrations │ │ ├── 20200423154439_AddRaterCountToSalons.cs │ │ ├── 20200423023315_AddOwnerToSalons.cs │ │ └── 20200425045559_RemoveEntitySetting.cs │ ├── EntityIndexesConfiguration.cs │ ├── DesignTimeDbContextFactory.cs │ ├── DbQueryRunner.cs │ └── Repositories │ │ ├── EfDeletableEntityRepository.cs │ │ └── EfRepository.cs ├── BeautyBooking.Data.Common │ ├── Models │ │ ├── IAuditInfo.cs │ │ ├── IDeletableEntity.cs │ │ ├── BaseDeletableModel.cs │ │ └── BaseModel.cs │ ├── IDbQueryRunner.cs │ ├── Repositories │ │ ├── IRepository.cs │ │ └── IDeletableEntityRepository.cs │ └── BeautyBooking.Data.Common.csproj └── BeautyBooking.Data.Models │ ├── City.cs │ ├── ApplicationRole.cs │ ├── SalonService.cs │ ├── BlogPost.cs │ ├── Category.cs │ ├── BeautyBooking.Data.Models.csproj │ ├── Service.cs │ ├── Appointment.cs │ ├── ApplicationUser.cs │ └── Salon.cs ├── Services ├── BeautyBooking.Services.Mapping │ ├── IMapTo.cs │ ├── IMapFrom.cs │ ├── IHaveCustomMappings.cs │ ├── BeautyBooking.Services.Mapping.csproj │ └── QueryableMappingExtensions.cs ├── BeautyBooking.Services │ ├── DateTimeParser │ │ ├── IDateTimeParserService.cs │ │ └── DateTimeParserService.cs │ ├── Cloudinary │ │ ├── ICloudinaryService.cs │ │ └── CloudinaryService.cs │ └── BeautyBooking.Services.csproj ├── BeautyBooking.Services.Messaging │ ├── EmailAttachment.cs │ ├── IEmailSender.cs │ ├── NullMessageSender.cs │ └── BeautyBooking.Services.Messaging.csproj └── BeautyBooking.Services.Data │ ├── Cities │ ├── ICitiesService.cs │ └── CitiesService.cs │ ├── Categories │ └── ICategoriesService.cs │ ├── Services │ └── IServicesService.cs │ ├── SalonServicesServices │ └── ISalonServicesService.cs │ ├── BlogPosts │ └── IBlogPostsService.cs │ ├── Appointments │ └── IAppointmentsService.cs │ ├── Salons │ └── ISalonsService.cs │ └── BeautyBooking.Services.Data.csproj ├── BeautyBooking.Common └── BeautyBooking.Common.csproj ├── stylecop.json └── LICENSE /Web/BeautyBooking.Web/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | this.Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Manager/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | this.Layout = "_ManagerLayout"; 3 | } 4 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Administration/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | this.Layout = "_AdminLayout"; 3 | } 4 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using BeautyBooking.Web.Areas.Identity.Pages.Account -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | this.Layout = "/Views/Shared/_Layout.cshtml"; 3 | } 4 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using BeautyBooking.Web.Areas.Identity.Pages.Account.Manage 2 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinakolova/BeautyBooking/HEAD/Web/BeautyBooking.Web/wwwroot/favicon.ico -------------------------------------------------------------------------------- /Tests/Sandbox/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Server=.\\SQLEXPRESS;Database=BeautyBooking;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/OwlCarousel2-2.2.1/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinakolova/BeautyBooking/HEAD/Web/BeautyBooking.Web/wwwroot/plugins/OwlCarousel2-2.2.1/ajax-loader.gif -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using BeautyBooking.Web 2 | @using BeautyBooking.Web.ViewModels 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | @addTagHelper *, BeautyBooking.Web 5 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/OwlCarousel2-2.2.1/owl.video.play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinakolova/BeautyBooking/HEAD/Web/BeautyBooking.Web/wwwroot/plugins/OwlCarousel2-2.2.1/owl.video.play.png -------------------------------------------------------------------------------- /Data/BeautyBooking.Data/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Server=.\\SQLEXPRESS;Database=BeautyBooking;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services.Mapping/IMapTo.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Services.Mapping 2 | { 3 | // ReSharper disable once UnusedTypeParameter 4 | public interface IMapTo 5 | { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinakolova/BeautyBooking/HEAD/Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/less/screen-reader.less: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { .sr-only(); } 5 | .sr-only-focusable { .sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services.Mapping/IMapFrom.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Services.Mapping 2 | { 3 | // ReSharper disable once UnusedTypeParameter 4 | public interface IMapFrom 5 | { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Tests/Sandbox/SandboxOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Sandbox 2 | { 3 | using CommandLine; 4 | 5 | [Verb("sandbox", HelpText = "Run sandbox code.")] 6 | public class SandboxOptions 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Controllers/BaseController.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.Controllers 2 | { 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | public class BaseController : Controller 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/less/fixed-width.less: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .@{fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/scss/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Manager/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using BeautyBooking.Web 2 | @using BeautyBooking.Web.ViewModels 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | @addTagHelper *, BeautyBooking.Web 5 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Administration/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using BeautyBooking.Web 2 | @using BeautyBooking.Web.ViewModels 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | @addTagHelper *, BeautyBooking.Web 5 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinakolova/BeautyBooking/HEAD/Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinakolova/BeautyBooking/HEAD/Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinakolova/BeautyBooking/HEAD/Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinakolova/BeautyBooking/HEAD/Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/scss/_screen-reader.scss: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { @include sr-only(); } 5 | .sr-only-focusable { @include sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/ConfirmEmail.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ConfirmEmailModel 3 | @{ 4 | ViewData["Title"] = "Confirm email"; 5 | } 6 | 7 |

@ViewData["Title"]

8 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ConfirmEmailChangeModel 3 | @{ 4 | ViewData["Title"] = "Confirm email change"; 5 | } 6 | 7 |

@ViewData["Title"]

8 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services.Mapping/IHaveCustomMappings.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Services.Mapping 2 | { 3 | using AutoMapper; 4 | 5 | public interface IHaveCustomMappings 6 | { 7 | void CreateMappings(IProfileExpression configuration); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Logout.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model LogoutModel 3 | @{ 4 | ViewData["Title"] = "Log out"; 5 | } 6 | 7 |
8 |

@ViewData["Title"]

9 |

You have successfully logged out of the application.

10 |
-------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using BeautyBooking.Web.Areas.Identity 3 | @using BeautyBooking.Web.Areas.Identity.Pages 4 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 5 | @using BeautyBooking.Data.Models 6 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data.Common/Models/IAuditInfo.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Common.Models 2 | { 3 | using System; 4 | 5 | public interface IAuditInfo 6 | { 7 | DateTime CreatedOn { get; set; } 8 | 9 | DateTime? ModifiedOn { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services/DateTimeParser/IDateTimeParserService.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Services.DateTimeParser 2 | { 3 | using System; 4 | 5 | public interface IDateTimeParserService 6 | { 7 | DateTime ConvertStrings(string date, string time); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels 2 | { 3 | public class ErrorViewModel 4 | { 5 | public string RequestId { get; set; } 6 | 7 | public bool ShowRequestId => !string.IsNullOrEmpty(this.RequestId); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Home/IndexViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Home 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class IndexViewModel 6 | { 7 | public IEnumerable Categories { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data.Common/Models/IDeletableEntity.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Common.Models 2 | { 3 | using System; 4 | 5 | public interface IDeletableEntity 6 | { 7 | bool IsDeleted { get; set; } 8 | 9 | DateTime? DeletedOn { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Cities/CitiesListViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Cities 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class CitiesListViewModel 6 | { 7 | public IEnumerable Cities { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Common/Pagination/PageSizesConstants.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Common.Pagination 2 | { 3 | public static class PageSizesConstants 4 | { 5 | public const int Salons = 8; 6 | 7 | public const int BlogPosts = 1; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Salons/SalonsListViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Salons 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class SalonsListViewModel 6 | { 7 | public IEnumerable Salons { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data/Seeding/ISeeder.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Seeding 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | public interface ISeeder 7 | { 8 | Task SeedAsync(ApplicationDbContext dbContext, IServiceProvider serviceProvider); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Services/ServicesListViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Services 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class ServicesListViewModel 6 | { 7 | public IEnumerable Services { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data.Common/IDbQueryRunner.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Common 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | public interface IDbQueryRunner : IDisposable 7 | { 8 | Task RunQueryAsync(string query, params object[] parameters); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/BlogPosts/BlogPostsListViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.BlogPosts 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class BlogPostsListViewModel 6 | { 7 | public IEnumerable BlogPosts { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Salons/SalonsSimpleListViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Salons 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class SalonsSimpleListViewModel 6 | { 7 | public IEnumerable Salons { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Categories/CategoriesListViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Categories 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class CategoriesListViewModel 6 | { 7 | public IEnumerable Categories { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ForgotPasswordConfirmation 3 | @{ 4 | ViewData["Title"] = "Forgot password confirmation"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |

9 | Please check your email to reset your password. 10 |

11 | 12 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/AccessDenied.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model AccessDeniedModel 3 | @{ 4 | ViewData["Title"] = "Access denied"; 5 | } 6 | 7 |
8 |

@ViewData["Title"]

9 |

You do not have access to this resource.

10 |
11 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services.Messaging/EmailAttachment.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Services.Messaging 2 | { 3 | public class EmailAttachment 4 | { 5 | public byte[] Content { get; set; } 6 | 7 | public string FileName { get; set; } 8 | 9 | public string MimeType { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Appointments/AppointmentsListViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Appointments 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class AppointmentsListViewModel 6 | { 7 | public IEnumerable Appointments { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Lockout.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model LockoutModel 3 | @{ 4 | ViewData["Title"] = "Locked out"; 5 | } 6 | 7 |
8 |

@ViewData["Title"]

9 |

This account has been locked out, please try again later.

10 |
11 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Categories/CategoriesSimpleListViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Categories 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class CategoriesSimpleListViewModel 6 | { 7 | public IEnumerable Categories { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Salons/SalonsPaginatedListViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Salons 2 | { 3 | using BeautyBooking.Web.ViewModels.Common.Pagination; 4 | 5 | public class SalonsPaginatedListViewModel 6 | { 7 | public PaginatedList Salons { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/BlogPosts/BlogPostsPaginatedListViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.BlogPosts 2 | { 3 | using BeautyBooking.Web.ViewModels.Common.Pagination; 4 | 5 | public class BlogPostsPaginatedListViewModel 6 | { 7 | public PaginatedList BlogPosts { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ResetPasswordConfirmationModel 3 | @{ 4 | ViewData["Title"] = "Reset password confirmation"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |

9 | Your password has been reset. Please click here to log in. 10 |

11 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data.Common/Models/BaseDeletableModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Common.Models 2 | { 3 | using System; 4 | 5 | public abstract class BaseDeletableModel : BaseModel, IDeletableEntity 6 | { 7 | public bool IsDeleted { get; set; } 8 | 9 | public DateTime? DeletedOn { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services/Cloudinary/ICloudinaryService.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Services.Cloudinary 2 | { 3 | using System.Threading.Tasks; 4 | 5 | using Microsoft.AspNetCore.Http; 6 | 7 | public interface ICloudinaryService 8 | { 9 | Task UploadPictureAsync(IFormFile pictureFile, string fileName); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model DownloadPersonalDataModel 3 | @{ 4 | ViewData["Title"] = "Download Your Data"; 5 | ViewData["ActivePage"] = ManageNavPages.PersonalData; 6 | } 7 | 8 |

@ViewData["Title"]

9 | 10 | @section Scripts { 11 | 12 | } -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Manager/Controllers/DashboardController.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.Areas.Manager.Controllers 2 | { 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | public class DashboardController : ManagerBaseController 6 | { 7 | public IActionResult Index() 8 | { 9 | return this.View(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Views/Shared/_MenuPartial.cshtml: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Manager/Views/Dashboard/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model BeautyBooking.Web.ViewModels.Salons.SalonsListViewModel 2 | @{ 3 | this.ViewData["Title"] = "Salon Manager Dashboard"; 4 | } 5 | 6 |

Welcome to Salon Manager Dashboard!

7 |

Here are the salons you manage:

8 | 9 |
10 | @await Component.InvokeAsync("SalonsSimpleList") 11 |
12 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Salons/SalonSimpleViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Salons 2 | { 3 | using BeautyBooking.Data.Models; 4 | using BeautyBooking.Services.Mapping; 5 | 6 | public class SalonSimpleViewModel : IMapFrom 7 | { 8 | public string Id { get; set; } 9 | 10 | public string Name { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Administration/Controllers/DashboardController.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.Areas.Administration.Controllers 2 | { 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | public class DashboardController : AdministrationController 6 | { 7 | public IActionResult Index() 8 | { 9 | return this.View(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services.Data/Cities/ICitiesService.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Services.Data.Cities 2 | { 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | public interface ICitiesService 7 | { 8 | Task> GetAllAsync(); 9 | 10 | Task AddAsync(string name); 11 | 12 | Task DeleteAsync(int id); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Common/SelectLists/CitySelectListViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Common.SelectLists 2 | { 3 | using BeautyBooking.Data.Models; 4 | using BeautyBooking.Services.Mapping; 5 | 6 | public class CitySelectListViewModel : IMapFrom 7 | { 8 | public int Id { get; set; } 9 | 10 | public string Name { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Cities/CityViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Cities 2 | { 3 | using BeautyBooking.Data.Models; 4 | using BeautyBooking.Services.Mapping; 5 | 6 | public class CityViewModel : IMapFrom 7 | { 8 | public int Id { get; set; } 9 | 10 | public string Name { get; set; } 11 | 12 | public int SalonsCount { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Common/SelectLists/CategorySelectListViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Common.SelectLists 2 | { 3 | using BeautyBooking.Data.Models; 4 | using BeautyBooking.Services.Mapping; 5 | 6 | public class CategorySelectListViewModel : IMapFrom 7 | { 8 | public int Id { get; set; } 9 | 10 | public string Name { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Server=.\\SQLEXPRESS;Database=BeautyBooking;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Information", 8 | "Microsoft": "Warning", 9 | "Microsoft.Hosting.Lifetime": "Information" 10 | } 11 | }, 12 | "AllowedHosts": "*" 13 | } 14 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data.Common/Models/BaseModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Common.Models 2 | { 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | public abstract class BaseModel : IAuditInfo 7 | { 8 | [Key] 9 | public TKey Id { get; set; } 10 | 11 | public DateTime CreatedOn { get; set; } 12 | 13 | public DateTime? ModifiedOn { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Categories/CategorySimpleViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Categories 2 | { 3 | using BeautyBooking.Data.Models; 4 | using BeautyBooking.Services.Mapping; 5 | 6 | public class CategorySimpleViewModel : IMapFrom 7 | { 8 | public int Id { get; set; } 9 | 10 | public string Name { get; set; } 11 | 12 | public int SalonsCount { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Home/IndexCategoryViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Home 2 | { 3 | using BeautyBooking.Data.Models; 4 | using BeautyBooking.Services.Mapping; 5 | 6 | public class IndexCategoryViewModel : IMapFrom 7 | { 8 | public string Name { get; set; } 9 | 10 | public string Description { get; set; } 11 | 12 | public string ImageUrl { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/AccessDenied.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | 7 | namespace BeautyBooking.Web.Areas.Identity.Pages.Account 8 | { 9 | public class AccessDeniedModel : PageModel 10 | { 11 | public void OnGet() 12 | { 13 | 14 | } 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Manager/Views/Shared/Components/SalonsSimpleList/Default.cshtml: -------------------------------------------------------------------------------- 1 | @model BeautyBooking.Web.ViewModels.Salons.SalonsSimpleListViewModel 2 | @{ 3 | this.ViewData["Title"] = "Default"; 4 | } 5 | 6 | @foreach (var salon in Model.Salons) 7 | { 8 | @salon.Name 10 | } 11 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Manager/Views/Shared/_ManagerSidebarPartial.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Manage/_StatusMessage.cshtml: -------------------------------------------------------------------------------- 1 | @model string 2 | 3 | @if (!String.IsNullOrEmpty(Model)) 4 | { 5 | var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; 6 | 10 | } -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/_StatusMessage.cshtml: -------------------------------------------------------------------------------- 1 | @model string 2 | 3 | @if (!String.IsNullOrEmpty(Model)) 4 | { 5 | var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; 6 | 10 | } 11 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/SalonServices/SalonServiceSimpleViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.SalonServices 2 | { 3 | using BeautyBooking.Data.Models; 4 | using BeautyBooking.Services.Mapping; 5 | 6 | public class SalonServiceSimpleViewModel : IMapFrom 7 | { 8 | public string SalonId { get; set; } 9 | 10 | public int ServiceId { get; set; } 11 | 12 | public bool Available { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Manager/Controllers/ManagerBaseController.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.Areas.Manager.Controllers 2 | { 3 | using BeautyBooking.Common; 4 | using BeautyBooking.Web.Controllers; 5 | 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | [Authorize(Roles = GlobalConstants.SalonManagerRoleName)] 10 | [Area("Manager")] 11 | public class ManagerBaseController : BaseController 12 | { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/less/larger.less: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .@{fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .@{fa-css-prefix}-2x { font-size: 2em; } 11 | .@{fa-css-prefix}-3x { font-size: 3em; } 12 | .@{fa-css-prefix}-4x { font-size: 4em; } 13 | .@{fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/scss/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Lockout.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | 8 | namespace BeautyBooking.Web.Areas.Identity.Pages.Account 9 | { 10 | [AllowAnonymous] 11 | public class LockoutModel : PageModel 12 | { 13 | public void OnGet() 14 | { 15 | 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services.Messaging/IEmailSender.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Services.Messaging 2 | { 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | public interface IEmailSender 7 | { 8 | Task SendEmailAsync( 9 | string from, 10 | string fromName, 11 | string to, 12 | string subject, 13 | string htmlContent, 14 | IEnumerable attachments = null); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | 7 | namespace BeautyBooking.Web.Areas.Identity.Pages.Account 8 | { 9 | [AllowAnonymous] 10 | public class ForgotPasswordConfirmation : PageModel 11 | { 12 | public void OnGet() 13 | { 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Administration/Controllers/AdministrationController.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.Areas.Administration.Controllers 2 | { 3 | using BeautyBooking.Common; 4 | using BeautyBooking.Web.Controllers; 5 | 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | [Authorize(Roles = GlobalConstants.AdministratorRoleName)] 10 | [Area("Administration")] 11 | public class AdministrationController : BaseController 12 | { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services.Data/Categories/ICategoriesService.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Services.Data.Categories 2 | { 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | public interface ICategoriesService 7 | { 8 | Task> GetAllAsync(int? count = null); 9 | 10 | Task GetByIdAsync(int id); 11 | 12 | Task AddAsync(string name, string description, string imageUrl); 13 | 14 | Task DeleteAsync(int id); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/js/rating-static.js: -------------------------------------------------------------------------------- 1 | var $star_rating = $('.star-rating .fa'); 2 | 3 | var SetRatingStar = function () { 4 | return $star_rating.each(function () { 5 | if (parseInt($star_rating.siblings('input.rating-value').val()) >= parseInt($(this).data('rating'))) { 6 | return $(this).removeClass('fa-star-o').addClass('fa-star'); 7 | } else { 8 | return $(this).removeClass('fa-star').addClass('fa-star-o'); 9 | } 10 | }); 11 | }; 12 | 13 | SetRatingStar(); 14 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/less/list.less: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: @fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .@{fa-css-prefix}-li { 11 | position: absolute; 12 | left: -@fa-li-width; 13 | width: @fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.@{fa-css-prefix}-lg { 17 | left: (-@fa-li-width + (4em / 14)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/scss/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/IdentityHostingStartup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | 3 | [assembly: HostingStartup(typeof(BeautyBooking.Web.Areas.Identity.IdentityHostingStartup))] 4 | namespace BeautyBooking.Web.Areas.Identity 5 | { 6 | public class IdentityHostingStartup : IHostingStartup 7 | { 8 | public void Configure(IWebHostBuilder builder) 9 | { 10 | builder.ConfigureServices((context, services) => 11 | { 12 | }); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services.Data/Services/IServicesService.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Services.Data.Services 2 | { 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | public interface IServicesService 7 | { 8 | Task> GetAllAsync(); 9 | 10 | Task> GetAllIdsByCategoryAsync(int categoryId); 11 | 12 | Task AddAsync(string name, int categoryId, string description); 13 | 14 | Task DeleteAsync(int id); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | 8 | namespace BeautyBooking.Web.Areas.Identity.Pages.Account 9 | { 10 | [AllowAnonymous] 11 | public class ResetPasswordConfirmationModel : PageModel 12 | { 13 | public void OnGet() 14 | { 15 | 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Views/Appointments/UnavailableService.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | this.ViewData["Title"] = "Unavailable Salon/Service"; 3 | } 4 | 5 |

Sorry, no such salon/service available.

6 | 7 |
8 | 16 |
-------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/less/core.less: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data/Configurations/SalonServiceConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Configurations 2 | { 3 | using BeautyBooking.Data.Models; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | public class SalonServiceConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder salonService) 10 | { 11 | salonService.HasKey(ss => new { ss.SalonId, ss.ServiceId }); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Manage/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "/Views/Shared/_Layout.cshtml"; 3 | } 4 | 5 |

Manage your account

6 | 7 |
8 |

Change your account settings

9 |
10 |
11 |
12 | 13 |
14 |
15 | @RenderBody() 16 |
17 |
18 |
19 | 20 | @section Scripts { 21 | @RenderSection("Scripts", required: false) 22 | } 23 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/scss/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Cities/CityInputModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Cities 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | using BeautyBooking.Common; 6 | 7 | public class CityInputModel 8 | { 9 | [Required] 10 | [StringLength( 11 | GlobalConstants.DataValidations.NameMaxLength, 12 | ErrorMessage = GlobalConstants.ErrorMessages.Name, 13 | MinimumLength = GlobalConstants.DataValidations.NameMinLength)] 14 | public string Name { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Salons/SalonServiceViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Salons 2 | { 3 | using BeautyBooking.Data.Models; 4 | using BeautyBooking.Services.Mapping; 5 | 6 | public class SalonServiceViewModel : IMapFrom 7 | { 8 | public string SalonId { get; set; } 9 | 10 | public int ServiceId { get; set; } 11 | 12 | public string ServiceName { get; set; } 13 | 14 | public string ServiceDescription { get; set; } 15 | 16 | public bool Available { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/scss/font-awesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "animated"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | @import "screen-reader"; 19 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data/IdentityOptionsProvider.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data 2 | { 3 | using Microsoft.AspNetCore.Identity; 4 | 5 | public static class IdentityOptionsProvider 6 | { 7 | public static void GetIdentityOptions(IdentityOptions options) 8 | { 9 | options.Password.RequireDigit = false; 10 | options.Password.RequireLowercase = false; 11 | options.Password.RequireUppercase = false; 12 | options.Password.RequireNonAlphanumeric = false; 13 | options.Password.RequiredLength = 6; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services.Data/SalonServicesServices/ISalonServicesService.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Services.Data.SalonServicesServices 2 | { 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | public interface ISalonServicesService 7 | { 8 | Task GetByIdAsync(string salonId, int serviceId); 9 | 10 | Task AddAsync(string salonId, IEnumerable servicesIds); 11 | 12 | Task AddAsync(IEnumerable salonsIds, int serviceId); 13 | 14 | Task ChangeAvailableStatusAsync(string salonId, int serviceId); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services.Messaging/NullMessageSender.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Services.Messaging 2 | { 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | public class NullMessageSender : IEmailSender 7 | { 8 | public Task SendEmailAsync( 9 | string from, 10 | string fromName, 11 | string to, 12 | string subject, 13 | string htmlContent, 14 | IEnumerable attachments = null) 15 | { 16 | return Task.CompletedTask; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Manager/Views/Shared/_ManagerMenuPartial.cshtml: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data.Common/Repositories/IRepository.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Common.Repositories 2 | { 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | public interface IRepository : IDisposable 8 | where TEntity : class 9 | { 10 | IQueryable All(); 11 | 12 | IQueryable AllAsNoTracking(); 13 | 14 | Task AddAsync(TEntity entity); 15 | 16 | void Update(TEntity entity); 17 | 18 | void Delete(TEntity entity); 19 | 20 | Task SaveChangesAsync(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Categories/CategoryViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Categories 2 | { 3 | using BeautyBooking.Data.Models; 4 | using BeautyBooking.Services.Mapping; 5 | 6 | public class CategoryViewModel : IMapFrom 7 | { 8 | public int Id { get; set; } 9 | 10 | public string Name { get; set; } 11 | 12 | public string Description { get; set; } 13 | 14 | public string ImageUrl { get; set; } 15 | 16 | public int SalonsCount { get; set; } 17 | 18 | public int ServicesCount { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Services/ServiceViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Services 2 | { 3 | using BeautyBooking.Data.Models; 4 | using BeautyBooking.Services.Mapping; 5 | 6 | public class ServiceViewModel : IMapFrom 7 | { 8 | public int Id { get; set; } 9 | 10 | public string Name { get; set; } 11 | 12 | public string Description { get; set; } 13 | 14 | public string CategoryName { get; set; } 15 | 16 | public int SalonsCount { get; set; } 17 | 18 | public int AppointmentsCount { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/js/date-picker.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | 3 | $('.datepicker').datepicker({ 4 | format: 'dd-mm-yyyy', 5 | autoclose: true, 6 | startDate: '0d' 7 | }); 8 | 9 | $('.cell').click(function () { 10 | $('.cell').removeClass('select'); 11 | $(this).addClass('select'); 12 | $("#selectedTime").text(this.innerHTML); 13 | $("#Time").val(this.innerHTML); 14 | }); 15 | 16 | $("#dp1").on('change', function () { 17 | $("#selectedDate").text(this.value); 18 | $("#Date").val(this.value); 19 | }); 20 | }); -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/less/stacked.less: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .@{fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .@{fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .@{fa-css-prefix}-inverse { color: @fa-inverse; } 21 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/BlogPosts/BlogPostViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.BlogPosts 2 | { 3 | using System; 4 | 5 | using BeautyBooking.Data.Models; 6 | using BeautyBooking.Services.Mapping; 7 | 8 | public class BlogPostViewModel : IMapFrom 9 | { 10 | public int Id { get; set; } 11 | 12 | public string Title { get; set; } 13 | 14 | public string Author { get; set; } 15 | 16 | public string Content { get; set; } 17 | 18 | public string ImageUrl { get; set; } 19 | 20 | public DateTime CreatedOn { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/scss/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/less/font-awesome.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables.less"; 7 | @import "mixins.less"; 8 | @import "path.less"; 9 | @import "core.less"; 10 | @import "larger.less"; 11 | @import "fixed-width.less"; 12 | @import "list.less"; 13 | @import "bordered-pulled.less"; 14 | @import "animated.less"; 15 | @import "rotated-flipped.less"; 16 | @import "stacked.less"; 17 | @import "icons.less"; 18 | @import "screen-reader.less"; 19 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/SalonServices/SalonServiceDetailsViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.SalonServices 2 | { 3 | using BeautyBooking.Data.Models; 4 | using BeautyBooking.Services.Mapping; 5 | 6 | public class SalonServiceDetailsViewModel : IMapFrom 7 | { 8 | public string SalonId { get; set; } 9 | 10 | public string SalonName { get; set; } 11 | 12 | public string SalonCityName { get; set; } 13 | 14 | public string SalonAddress { get; set; } 15 | 16 | public int ServiceId { get; set; } 17 | 18 | public string ServiceName { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Program.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web 2 | { 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Hosting; 5 | 6 | public static class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => 16 | { 17 | webBuilder.UseStartup(); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Views/Shared/Error404.cshtml: -------------------------------------------------------------------------------- 1 | @using BeautyBooking.Common 2 | @{ 3 | this.ViewData["Title"] = "Error 404"; 4 | } 5 | 6 |
7 |
8 |
9 |
10 | 11 |
12 |
13 |

Oops!

14 |

404

15 |

Page Not Found!

16 |
17 |
18 |
19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data.Models/City.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Models 2 | { 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | using BeautyBooking.Common; 7 | using BeautyBooking.Data.Common.Models; 8 | 9 | public class City : BaseDeletableModel 10 | { 11 | public City() 12 | { 13 | this.Salons = new HashSet(); 14 | } 15 | 16 | [Required] 17 | [MaxLength(GlobalConstants.DataValidations.NameMaxLength)] 18 | public string Name { get; set; } 19 | 20 | public virtual ICollection Salons { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/js/rating-responsive.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | $("span.fa-star-o").on('click', function () { 3 | let $this = $(this), 4 | rating = $this.data('rating'); 5 | $("span.fa").each(function () { 6 | let $star = $(this); 7 | if ($star.data('rating') <= rating) { 8 | $star.removeClass('fa-star-o').addClass('fa-star') 9 | } 10 | else { 11 | $star.removeClass('fa-star').addClass('fa-star-o'); 12 | } 13 | }); 14 | 15 | $("#selectedRating").prop('value', rating); 16 | $("#RateValue").val(rating); 17 | }); 18 | }); -------------------------------------------------------------------------------- /Data/BeautyBooking.Data.Common/Repositories/IDeletableEntityRepository.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Common.Repositories 2 | { 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | using BeautyBooking.Data.Common.Models; 7 | 8 | public interface IDeletableEntityRepository : IRepository 9 | where TEntity : class, IDeletableEntity 10 | { 11 | IQueryable AllWithDeleted(); 12 | 13 | IQueryable AllAsNoTrackingWithDeleted(); 14 | 15 | Task GetByIdWithDeletedAsync(params object[] id); 16 | 17 | void HardDelete(TEntity entity); 18 | 19 | void Undelete(TEntity entity); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services/DateTimeParser/DateTimeParserService.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Services.DateTimeParser 2 | { 3 | using System; 4 | using System.Globalization; 5 | 6 | using BeautyBooking.Common; 7 | 8 | public class DateTimeParserService : IDateTimeParserService 9 | { 10 | public DateTime ConvertStrings(string date, string time) 11 | { 12 | string dateString = date + " " + time; 13 | string format = GlobalConstants.DateTimeFormats.DateTimeFormat; 14 | 15 | DateTime dateTime = DateTime.ParseExact(dateString, format, CultureInfo.InvariantCulture); 16 | 17 | return dateTime; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/bundleconfig.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "outputFileName": "wwwroot/css/site.min.css", 4 | "inputFiles": [ 5 | "wwwroot/css/site.css" 6 | ] 7 | }, 8 | { 9 | "outputFileName": "wwwroot/js/site.min.js", 10 | "inputFiles": [ 11 | "wwwroot/js/site.js" 12 | ], 13 | "minify": { 14 | "enabled": true, 15 | "renameLocals": true 16 | } 17 | }, 18 | { 19 | "outputFileName": "wwwroot/js/popper.min.js", 20 | "inputFiles": [ 21 | "wwwroot/js/popper.js" 22 | ] 23 | }, 24 | { 25 | "outputFileName": "wwwroot/css/responsive.min.css", 26 | "inputFiles": [ 27 | "wwwroot/css/responsive.css" 28 | ] 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /BeautyBooking.Common/BeautyBooking.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | latest 6 | 7 | 8 | 9 | ..\Rules.ruleset 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model RegisterConfirmationModel 3 | @{ 4 | ViewData["Title"] = "Register confirmation"; 5 | } 6 | 7 |

@ViewData["Title"]

8 | @{ 9 | if (@Model.DisplayConfirmAccountLink) 10 | { 11 |

12 | This app does not currently have a real email sender registered, see these docs for how to configure a real email sender. 13 | Normally this would be emailed: Click here to confirm your account 14 |

15 | } 16 | else 17 | { 18 |

19 | Please check your email to confirm your account. 20 |

21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.RazorPages; 5 | 6 | namespace BeautyBooking.Web.Areas.Identity.Pages 7 | { 8 | [AllowAnonymous] 9 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 10 | public class ErrorModel : PageModel 11 | { 12 | public string RequestId { get; set; } 13 | 14 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 15 | 16 | public void OnGet() 17 | { 18 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services.Data/BlogPosts/IBlogPostsService.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Services.Data.BlogPosts 2 | { 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | public interface IBlogPostsService 7 | { 8 | Task> GetAllAsync(int? count = null); 9 | 10 | Task> GetAllWithPagingAsync( 11 | int? sortId, 12 | int pageSize, 13 | int pageIndex); 14 | 15 | Task GetCountForPaginationAsync(); 16 | 17 | Task GetByIdAsync(int id); 18 | 19 | Task AddAsync(string title, string content, string author, string imageUrl); 20 | 21 | Task DeleteAsync(int id); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/less/bordered-pulled.less: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em @fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .@{fa-css-prefix}-pull-left { float: left; } 11 | .@{fa-css-prefix}-pull-right { float: right; } 12 | 13 | .@{fa-css-prefix} { 14 | &.@{fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.@{fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .@{fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/scss/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .#{$fa-css-prefix}-pull-left { float: left; } 11 | .#{$fa-css-prefix}-pull-right { float: right; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .#{$fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/less/rotated-flipped.less: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } 5 | .@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } 6 | .@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } 7 | 8 | .@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } 9 | .@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .@{fa-css-prefix}-rotate-90, 15 | :root .@{fa-css-prefix}-rotate-180, 16 | :root .@{fa-css-prefix}-rotate-270, 17 | :root .@{fa-css-prefix}-flip-horizontal, 18 | :root .@{fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /Tests/BeautyBooking.Web.Tests/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:61093/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "BeautyBooking.Web.Tests": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:61094/" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Data/BeautyBooking.Data/Migrations/20200423154439_AddRaterCountToSalons.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Migrations 2 | { 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | 5 | public partial class AddRaterCountToSalons : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.AddColumn( 10 | name: "RatersCount", 11 | table: "Salons", 12 | nullable: false, 13 | defaultValue: 0); 14 | } 15 | 16 | protected override void Down(MigrationBuilder migrationBuilder) 17 | { 18 | migrationBuilder.DropColumn( 19 | name: "RatersCount", 20 | table: "Salons"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:19901", 7 | "sslPort": 44319 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "BeautyBooking.Web": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/js/upload-image.js: -------------------------------------------------------------------------------- 1 | $(".image-upload").on('change', function () { 2 | let id = $(this).parent().attr('id'); 3 | console.log(id); 4 | var files = !!this.files ? this.files : []; 5 | if (!files.length || !window.FileReader) return; // Check if File is selected, or no FileReader support 6 | if (/^image/.test(files[0].type)) { // Allow only image upload 7 | var ReaderObj = new FileReader(); // Create instance of the FileReader 8 | ReaderObj.readAsDataURL(files[0]); // read the file uploaded 9 | ReaderObj.onloadend = function () { // set uploaded image data as background of div 10 | $("#" + id).css("background-image", "url(" + this.result + ")"); 11 | } 12 | } else { 13 | alert("Upload an image"); 14 | } 15 | }); -------------------------------------------------------------------------------- /Services/BeautyBooking.Services.Messaging/BeautyBooking.Services.Messaging.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | latest 6 | 7 | 8 | 9 | ..\..\Rules.ruleset 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Salons/SalonViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Salons 2 | { 3 | using BeautyBooking.Data.Models; 4 | using BeautyBooking.Services.Mapping; 5 | 6 | public class SalonViewModel : IMapFrom 7 | { 8 | public string Id { get; set; } 9 | 10 | public string Name { get; set; } 11 | 12 | public string ImageUrl { get; set; } 13 | 14 | public int CategoryId { get; set; } 15 | 16 | public string CategoryName { get; set; } 17 | 18 | public string CityName { get; set; } 19 | 20 | public string Address { get; set; } 21 | 22 | public double Rating { get; set; } 23 | 24 | public int RatersCount { get; set; } 25 | 26 | public int AppointmentsCount { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data.Common/BeautyBooking.Data.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | latest 6 | 7 | 8 | 9 | ..\..\Rules.ruleset 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services.Mapping/BeautyBooking.Services.Mapping.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | latest 6 | 7 | 8 | 9 | ..\..\Rules.ruleset 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/scss/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .#{$fa-css-prefix}-rotate-90, 15 | :root .#{$fa-css-prefix}-rotate-180, 16 | :root .#{$fa-css-prefix}-rotate-270, 17 | :root .#{$fa-css-prefix}-flip-horizontal, 18 | :root .#{$fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Appointments/AppointmentInputModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Appointments 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | using BeautyBooking.Common; 6 | using BeautyBooking.Web.ViewModels.Common.CustomValidationAttributes; 7 | 8 | public class AppointmentInputModel 9 | { 10 | [Required] 11 | public string SalonId { get; set; } 12 | 13 | [Required] 14 | public int ServiceId { get; set; } 15 | 16 | [Required] 17 | [ValidateDateString(ErrorMessage = GlobalConstants.ErrorMessages.DateTime)] 18 | public string Date { get; set; } 19 | 20 | [Required] 21 | [ValidateTimeString(ErrorMessage = GlobalConstants.ErrorMessages.DateTime)] 22 | public string Time { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Salons/SalonWithServicesViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Salons 2 | { 3 | using System.Collections.Generic; 4 | 5 | using BeautyBooking.Data.Models; 6 | using BeautyBooking.Services.Mapping; 7 | 8 | public class SalonWithServicesViewModel : IMapFrom 9 | { 10 | public string Id { get; set; } 11 | 12 | public string Name { get; set; } 13 | 14 | public string ImageUrl { get; set; } 15 | 16 | public string CategoryName { get; set; } 17 | 18 | public string CityName { get; set; } 19 | 20 | public string Address { get; set; } 21 | 22 | public double Rating { get; set; } 23 | 24 | public int RatersCount { get; set; } 25 | 26 | public virtual ICollection Services { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data.Models/ApplicationRole.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable VirtualMemberCallInConstructor 2 | namespace BeautyBooking.Data.Models 3 | { 4 | using System; 5 | 6 | using BeautyBooking.Data.Common.Models; 7 | 8 | using Microsoft.AspNetCore.Identity; 9 | 10 | public class ApplicationRole : IdentityRole, IAuditInfo, IDeletableEntity 11 | { 12 | public ApplicationRole() 13 | : this(null) 14 | { 15 | } 16 | 17 | public ApplicationRole(string name) 18 | : base(name) 19 | { 20 | this.Id = Guid.NewGuid().ToString(); 21 | } 22 | 23 | public DateTime CreatedOn { get; set; } 24 | 25 | public DateTime? ModifiedOn { get; set; } 26 | 27 | public bool IsDeleted { get; set; } 28 | 29 | public DateTime? DeletedOn { get; set; } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data/EntityIndexesConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data 2 | { 3 | using System.Linq; 4 | 5 | using BeautyBooking.Data.Common.Models; 6 | 7 | using Microsoft.EntityFrameworkCore; 8 | 9 | internal static class EntityIndexesConfiguration 10 | { 11 | public static void Configure(ModelBuilder modelBuilder) 12 | { 13 | // IDeletableEntity.IsDeleted index 14 | var deletableEntityTypes = modelBuilder.Model 15 | .GetEntityTypes() 16 | .Where(et => et.ClrType != null && typeof(IDeletableEntity).IsAssignableFrom(et.ClrType)); 17 | foreach (var deletableEntityType in deletableEntityTypes) 18 | { 19 | modelBuilder.Entity(deletableEntityType.ClrType).HasIndex(nameof(IDeletableEntity.IsDeleted)); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Views/Shared/Components/SalonServiceDetails/Default.cshtml: -------------------------------------------------------------------------------- 1 | @model BeautyBooking.Web.ViewModels.SalonServices.SalonServiceDetailsViewModel 2 | @{ 3 | ViewData["Title"] = "Default"; 4 | } 5 | 6 |
7 | 8 |
9 |
@Model.SalonName
10 |
11 |
12 |
13 | 14 |
15 |
@Model.SalonCityName, @Model.SalonAddress
16 |
17 |
18 |
19 | 20 |
21 |
@Model.ServiceName
22 |
23 |
24 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/less/path.less: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); 7 | src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), 8 | url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), 9 | url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), 10 | url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), 11 | url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/scss/_path.scss: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); 7 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), 8 | url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), 9 | url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), 10 | url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), 11 | url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/ForgotPassword.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ForgotPasswordModel 3 | @{ 4 | ViewData["Title"] = "Forgot your password?"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |

Enter your email.

9 |
10 |
11 |
12 |
13 |
14 |
15 | 16 | 17 | 18 |
19 | 20 |
21 |
22 |
23 | 24 | @section Scripts { 25 | 26 | } 27 | -------------------------------------------------------------------------------- /stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "layoutRules": { 5 | "newlineAtEndOfFile": "require" 6 | }, 7 | "namingRules": { 8 | "allowCommonHungarianPrefixes": true, 9 | "allowedHungarianPrefixes": [ "db", "at", "or", "up", "it", "un", "x", "y", "id", "ip", "bg" ] 10 | }, 11 | "documentationRules": { 12 | "companyName": "BeautyBooking", 13 | "copyrightText": "Copyright (c) {companyName}. All Rights Reserved.", 14 | "documentInterfaces": false, 15 | "documentInternalElements": false 16 | }, 17 | "orderingRules": { 18 | "usingDirectivesPlacement": "insideNamespace", 19 | "systemUsingDirectivesFirst": true, 20 | "blankLinesBetweenUsingGroups": "require" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ResendEmailConfirmationModel 3 | @{ 4 | ViewData["Title"] = "Resend email confirmation"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |

Enter your email.

9 |
10 |
11 |
12 |
13 |
14 |
15 | 16 | 17 | 18 |
19 | 20 |
21 |
22 |
23 | 24 | @section Scripts { 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/less/animated.less: -------------------------------------------------------------------------------- 1 | // Animated Icons 2 | // -------------------------- 3 | 4 | .@{fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .@{fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/scss/_animated.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .#{$fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data.Models/SalonService.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Models 2 | { 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | using BeautyBooking.Data.Common.Models; 7 | 8 | public class SalonService : BaseDeletableModel 9 | { 10 | public SalonService() 11 | { 12 | this.Appointments = new HashSet(); 13 | } 14 | 15 | [Required] 16 | public string SalonId { get; set; } 17 | 18 | public virtual Salon Salon { get; set; } 19 | 20 | public int ServiceId { get; set; } 21 | 22 | public virtual Service Service { get; set; } 23 | 24 | // Each Salon gets all Services from its Category, but may not provide them all 25 | public bool Available { get; set; } 26 | 27 | public virtual ICollection Appointments { get; set; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Disable2faModel 3 | @{ 4 | ViewData["Title"] = "Disable two-factor authentication (2FA)"; 5 | ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; 6 | } 7 | 8 | 9 |

@ViewData["Title"]

10 | 11 | 20 | 21 |
22 |
23 | 24 |
25 |
26 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services.Data/Appointments/IAppointmentsService.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Services.Data.Appointments 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | 7 | public interface IAppointmentsService 8 | { 9 | Task GetByIdAsync(string id); 10 | 11 | Task> GetAllAsync(); 12 | 13 | Task> GetAllBySalonAsync(string salonId); 14 | 15 | Task> GetUpcomingByUserAsync(string userId); 16 | 17 | Task> GetPastByUserAsync(string userId); 18 | 19 | Task AddAsync(string userId, string salonId, int serviceId, DateTime dateTime); 20 | 21 | Task DeleteAsync(string id); 22 | 23 | Task ConfirmAsync(string id); 24 | 25 | Task DeclineAsync(string id); 26 | 27 | Task RateAppointmentAsync(string id); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Controllers/CategoriesController.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.Controllers 2 | { 3 | using System.Threading.Tasks; 4 | 5 | using BeautyBooking.Services.Data.Categories; 6 | using BeautyBooking.Web.ViewModels.Categories; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | public class CategoriesController : BaseController 10 | { 11 | private readonly ICategoriesService categoriesService; 12 | 13 | public CategoriesController(ICategoriesService categoriesService) 14 | { 15 | this.categoriesService = categoriesService; 16 | } 17 | 18 | public async Task Index() 19 | { 20 | var viewModel = new CategoriesListViewModel 21 | { 22 | Categories = await this.categoriesService.GetAllAsync(), 23 | }; 24 | return this.View(viewModel); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data.Models/BlogPost.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Models 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | using BeautyBooking.Common; 6 | using BeautyBooking.Data.Common.Models; 7 | 8 | public class BlogPost : BaseDeletableModel 9 | { 10 | [Required] 11 | [MaxLength(GlobalConstants.DataValidations.TitleMaxLength)] 12 | public string Title { get; set; } 13 | 14 | [Required] 15 | [MaxLength(GlobalConstants.DataValidations.ContentMaxLength)] 16 | public string Content { get; set; } 17 | 18 | // BlogPost can be created only in the Admin Dashboard 19 | // so the Author is not a User, just a string for name 20 | [Required] 21 | [MaxLength(GlobalConstants.DataValidations.NameMaxLength)] 22 | public string Author { get; set; } 23 | 24 | [Required] 25 | public string ImageUrl { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ShowRecoveryCodesModel 3 | @{ 4 | ViewData["Title"] = "Recovery codes"; 5 | ViewData["ActivePage"] = "TwoFactorAuthentication"; 6 | } 7 | 8 | 9 |

@ViewData["Title"]

10 | 18 |
19 |
20 | @for (var row = 0; row < Model.RecoveryCodes.Length; row += 2) 21 | { 22 | @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
23 | } 24 |
25 |
-------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/css/sidebar.css: -------------------------------------------------------------------------------- 1 | #sidebar-wrapper { 2 | min-height: 100vh; 3 | margin-left: -15rem; 4 | -webkit-transition: margin .25s ease-out; 5 | -moz-transition: margin .25s ease-out; 6 | -o-transition: margin .25s ease-out; 7 | transition: margin .25s ease-out; 8 | } 9 | 10 | #sidebar-wrapper .sidebar-heading { 11 | padding: 0.875rem 1.25rem; 12 | font-size: 1.2rem; 13 | } 14 | 15 | #sidebar-wrapper .list-group { 16 | width: 15rem; 17 | } 18 | 19 | #page-content-wrapper { 20 | min-width: 100vw; 21 | } 22 | 23 | #wrapper.toggled #sidebar-wrapper { 24 | margin-left: 0; 25 | } 26 | 27 | @media (min-width: 768px) { 28 | #sidebar-wrapper { 29 | margin-left: 0; 30 | } 31 | 32 | #page-content-wrapper { 33 | min-width: 0; 34 | width: 100%; 35 | } 36 | 37 | #wrapper.toggled #sidebar-wrapper { 38 | margin-left: -15rem; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services.Data/Salons/ISalonsService.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Services.Data.Salons 2 | { 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | public interface ISalonsService 7 | { 8 | Task> GetAllAsync(); 9 | 10 | Task> GetAllWithSortingFilteringAndPagingAsync( 11 | string searchString, 12 | int? sortId, 13 | int pageSize, 14 | int pageIndex); 15 | 16 | Task GetCountForPaginationAsync(string searchString, int? sortId); 17 | 18 | Task> GetAllIdsByCategoryAsync(int categoryId); 19 | 20 | Task GetByIdAsync(string id); 21 | 22 | Task AddAsync(string name, int categoryId, int cityId, string address, string imageUrl); 23 | 24 | Task DeleteAsync(string id); 25 | 26 | Task RateSalonAsync(string id, int rateValue); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Services/ServiceInputModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Services 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | using BeautyBooking.Common; 6 | 7 | public class ServiceInputModel 8 | { 9 | [Required] 10 | [StringLength( 11 | GlobalConstants.DataValidations.NameMaxLength, 12 | ErrorMessage = GlobalConstants.ErrorMessages.Name, 13 | MinimumLength = GlobalConstants.DataValidations.NameMinLength)] 14 | public string Name { get; set; } 15 | 16 | [Required] 17 | public int CategoryId { get; set; } 18 | 19 | [Required] 20 | [StringLength( 21 | GlobalConstants.DataValidations.DescriptionMaxLength, 22 | ErrorMessage = GlobalConstants.ErrorMessages.Description, 23 | MinimumLength = GlobalConstants.DataValidations.DescriptionMinLength)] 24 | public string Description { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Appointments/AppointmentViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Appointments 2 | { 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | using BeautyBooking.Data.Models; 7 | using BeautyBooking.Services.Mapping; 8 | 9 | public class AppointmentViewModel : IMapFrom 10 | { 11 | public string Id { get; set; } 12 | 13 | public DateTime DateTime { get; set; } 14 | 15 | public string UserEmail { get; set; } 16 | 17 | public string SalonId { get; set; } 18 | 19 | public string SalonName { get; set; } 20 | 21 | public string SalonCityName { get; set; } 22 | 23 | public string SalonAddress { get; set; } 24 | 25 | public int ServiceId { get; set; } 26 | 27 | public string ServiceName { get; set; } 28 | 29 | public bool? Confirmed { get; set; } 30 | 31 | public bool? IsSalonRatedByTheUser { get; set; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ErrorModel 3 | @{ 4 | ViewData["Title"] = "Error"; 5 | } 6 | 7 |

Error.

8 |

An error occurred while processing your request.

9 | 10 | 26 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using BeautyBooking.Data.Models; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace BeautyBooking.Web.Areas.Identity.Pages.Account.Manage 12 | { 13 | public class ShowRecoveryCodesModel : PageModel 14 | { 15 | [TempData] 16 | public string[] RecoveryCodes { get; set; } 17 | 18 | [TempData] 19 | public string StatusMessage { get; set; } 20 | 21 | public IActionResult OnGet() 22 | { 23 | if (RecoveryCodes == null || RecoveryCodes.Length == 0) 24 | { 25 | return RedirectToPage("./TwoFactorAuthentication"); 26 | } 27 | 28 | return Page(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.Infrastructure/ViewComponents/LatestBlogPostsViewComponent.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.Infrastructure.ViewComponents 2 | { 3 | using System.Threading.Tasks; 4 | 5 | using BeautyBooking.Services.Data.BlogPosts; 6 | using BeautyBooking.Web.ViewModels.BlogPosts; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | public class LatestBlogPostsViewComponent : ViewComponent 10 | { 11 | private readonly IBlogPostsService blogPostsService; 12 | 13 | public LatestBlogPostsViewComponent(IBlogPostsService blogPostsService) 14 | { 15 | this.blogPostsService = blogPostsService; 16 | } 17 | 18 | public async Task InvokeAsync(int? count) 19 | { 20 | var viewModel = new BlogPostsListViewModel 21 | { 22 | BlogPosts = await this.blogPostsService.GetAllAsync(count), 23 | }; 24 | 25 | return this.View(viewModel); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Appointments/AppointmentRatingViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Appointments 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | using BeautyBooking.Common; 6 | using BeautyBooking.Data.Models; 7 | using BeautyBooking.Services.Mapping; 8 | 9 | public class AppointmentRatingViewModel : IMapFrom 10 | { 11 | public string Id { get; set; } 12 | 13 | public string SalonId { get; set; } 14 | 15 | public string SalonName { get; set; } 16 | 17 | public string SalonCategoryName { get; set; } 18 | 19 | public string SalonCityName { get; set; } 20 | 21 | public string SalonAddress { get; set; } 22 | 23 | public string SalonImageUrl { get; set; } 24 | 25 | public bool? IsSalonRatedByTheUser { get; set; } 26 | 27 | [Required] 28 | [Range(1, 5, ErrorMessage = GlobalConstants.ErrorMessages.Rating)] 29 | public int RateValue { get; set; } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model ErrorViewModel 2 | @{ 3 | this.ViewData["Title"] = "Error"; 4 | } 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | 28 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services/BeautyBooking.Services.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | latest 6 | 7 | 8 | 9 | ..\..\Rules.ruleset 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.Infrastructure/ViewComponents/SalonServiceDetailsViewComponent.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.Infrastructure.ViewComponents 2 | { 3 | using System.Threading.Tasks; 4 | 5 | using BeautyBooking.Services.Data.SalonServicesServices; 6 | using BeautyBooking.Web.ViewModels.SalonServices; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | public class SalonServiceDetailsViewComponent : ViewComponent 10 | { 11 | private readonly ISalonServicesService salonServicesService; 12 | 13 | public SalonServiceDetailsViewComponent(ISalonServicesService salonServicesService) 14 | { 15 | this.salonServicesService = salonServicesService; 16 | } 17 | 18 | public async Task InvokeAsync(string salonId, int serviceId) 19 | { 20 | var viewModel = 21 | await this.salonServicesService.GetByIdAsync(salonId, serviceId); 22 | 23 | return this.View(viewModel); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data.Models/Category.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Models 2 | { 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | using BeautyBooking.Common; 7 | using BeautyBooking.Data.Common.Models; 8 | 9 | public class Category : BaseDeletableModel 10 | { 11 | public Category() 12 | { 13 | this.Services = new HashSet(); 14 | this.Salons = new HashSet(); 15 | } 16 | 17 | [Required] 18 | [MaxLength(GlobalConstants.DataValidations.NameMaxLength)] 19 | public string Name { get; set; } 20 | 21 | [Required] 22 | [MaxLength(GlobalConstants.DataValidations.DescriptionMaxLength)] 23 | public string Description { get; set; } 24 | 25 | [Required] 26 | public string ImageUrl { get; set; } 27 | 28 | public virtual ICollection Services { get; set; } 29 | 30 | public virtual ICollection Salons { get; set; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data/DesignTimeDbContextFactory.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data 2 | { 3 | using System.IO; 4 | 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Design; 7 | using Microsoft.Extensions.Configuration; 8 | 9 | public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory 10 | { 11 | public ApplicationDbContext CreateDbContext(string[] args) 12 | { 13 | var configuration = new ConfigurationBuilder() 14 | .SetBasePath(Directory.GetCurrentDirectory()) 15 | .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) 16 | .Build(); 17 | 18 | var builder = new DbContextOptionsBuilder(); 19 | var connectionString = configuration.GetConnectionString("DefaultConnection"); 20 | builder.UseSqlServer(connectionString); 21 | 22 | return new ApplicationDbContext(builder.Options); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.Infrastructure/ViewComponents/CategoriesSimpleListViewComponent.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.Infrastructure.ViewComponents 2 | { 3 | using System.Threading.Tasks; 4 | 5 | using BeautyBooking.Services.Data.Categories; 6 | using BeautyBooking.Web.ViewModels.Categories; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | public class CategoriesSimpleListViewComponent : ViewComponent 10 | { 11 | private readonly ICategoriesService categoriesService; 12 | 13 | public CategoriesSimpleListViewComponent(ICategoriesService categoriesService) 14 | { 15 | this.categoriesService = categoriesService; 16 | } 17 | 18 | public async Task InvokeAsync() 19 | { 20 | var viewModel = new CategoriesSimpleListViewModel 21 | { 22 | Categories = await this.categoriesService.GetAllAsync(), 23 | }; 24 | 25 | return this.View(viewModel); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Administration/Controllers/AppointmentsController.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.Areas.Administration.Controllers 2 | { 3 | using System.Threading.Tasks; 4 | 5 | using BeautyBooking.Services.Data.Appointments; 6 | using BeautyBooking.Web.ViewModels.Appointments; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | public class AppointmentsController : AdministrationController 10 | { 11 | private readonly IAppointmentsService appointmentsService; 12 | 13 | public AppointmentsController(IAppointmentsService appointmentsService) 14 | { 15 | this.appointmentsService = appointmentsService; 16 | } 17 | 18 | public async Task Index() 19 | { 20 | var viewModel = new AppointmentsListViewModel 21 | { 22 | Appointments = 23 | await this.appointmentsService.GetAllAsync(), 24 | }; 25 | return this.View(viewModel); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model PersonalDataModel 3 | @{ 4 | ViewData["Title"] = "Personal Data"; 5 | ViewData["ActivePage"] = ManageNavPages.PersonalData; 6 | } 7 | 8 |

@ViewData["Title"]

9 | 10 |
11 |
12 |

Your account contains personal data that you have given us. This page allows you to download or delete that data.

13 |

14 | Deleting this data will permanently remove your account, and this cannot be recovered. 15 |

16 |
17 | 18 |
19 |

20 | Delete 21 |

22 |
23 |
24 | 25 | @section Scripts { 26 | 27 | } -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Views/Shared/_SearchBarPartial.cshtml: -------------------------------------------------------------------------------- 1 |
2 |
3 | 5 | 6 |
7 |
8 | 9 | 17 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Views/Shared/Components/CategoriesSimpleList/Default.cshtml: -------------------------------------------------------------------------------- 1 | @model BeautyBooking.Web.ViewModels.Categories.CategoriesSimpleListViewModel 2 | @{ 3 | this.ViewData["Title"] = "Default"; 4 | } 5 | 6 |
7 | 10 | 21 |
22 | 23 |
24 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data.Models/BeautyBooking.Data.Models.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | latest 6 | 7 | 8 | 9 | ..\..\Rules.ruleset 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ResetAuthenticatorModel 3 | @{ 4 | ViewData["Title"] = "Reset authenticator key"; 5 | ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; 6 | } 7 | 8 | 9 |

@ViewData["Title"]

10 | 20 |
21 |
22 | 23 |
24 |
-------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/BeautyBooking.Web.ViewModels.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | latest 6 | 7 | 8 | 9 | ..\..\Rules.ruleset 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data.Models/Service.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Models 2 | { 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | using BeautyBooking.Common; 7 | using BeautyBooking.Data.Common.Models; 8 | 9 | public class Service : BaseDeletableModel 10 | { 11 | public Service() 12 | { 13 | this.Salons = new HashSet(); 14 | this.Appointments = new HashSet(); 15 | } 16 | 17 | [Required] 18 | [MaxLength(GlobalConstants.DataValidations.NameMaxLength)] 19 | public string Name { get; set; } 20 | 21 | public int CategoryId { get; set; } 22 | 23 | public virtual Category Category { get; set; } 24 | 25 | [Required] 26 | [MaxLength(GlobalConstants.DataValidations.DescriptionMaxLength)] 27 | public string Description { get; set; } 28 | 29 | public virtual ICollection Salons { get; set; } 30 | 31 | public virtual ICollection Appointments { get; set; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data/DbQueryRunner.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | using BeautyBooking.Data.Common; 7 | 8 | using Microsoft.EntityFrameworkCore; 9 | 10 | public class DbQueryRunner : IDbQueryRunner 11 | { 12 | public DbQueryRunner(ApplicationDbContext context) 13 | { 14 | this.Context = context ?? throw new ArgumentNullException(nameof(context)); 15 | } 16 | 17 | public ApplicationDbContext Context { get; set; } 18 | 19 | public Task RunQueryAsync(string query, params object[] parameters) 20 | { 21 | return this.Context.Database.ExecuteSqlRawAsync(query, parameters); 22 | } 23 | 24 | public void Dispose() 25 | { 26 | this.Dispose(true); 27 | GC.SuppressFinalize(this); 28 | } 29 | 30 | protected virtual void Dispose(bool disposing) 31 | { 32 | if (disposing) 33 | { 34 | this.Context?.Dispose(); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.Infrastructure/ViewComponents/AllAppointmentsBySalonViewComponent.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.Infrastructure.ViewComponents 2 | { 3 | using System.Threading.Tasks; 4 | 5 | using BeautyBooking.Services.Data.Appointments; 6 | using BeautyBooking.Web.ViewModels.Appointments; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | public class AllAppointmentsBySalonViewComponent : ViewComponent 10 | { 11 | private readonly IAppointmentsService appointmentsService; 12 | 13 | public AllAppointmentsBySalonViewComponent(IAppointmentsService appointmentsService) 14 | { 15 | this.appointmentsService = appointmentsService; 16 | } 17 | 18 | public async Task InvokeAsync(string salonId) 19 | { 20 | var viewModel = new AppointmentsListViewModel 21 | { 22 | Appointments = 23 | await this.appointmentsService.GetAllBySalonAsync(salonId), 24 | }; 25 | 26 | return this.View(viewModel); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model LoginWithRecoveryCodeModel 3 | @{ 4 | ViewData["Title"] = "Recovery code verification"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |
9 |

10 | You have requested to log in with a recovery code. This login will not be remembered until you provide 11 | an authenticator app code at log in or disable 2FA and log in again. 12 |

13 |
14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 |
22 | 23 |
24 |
25 |
26 | 27 | @section Scripts { 28 | 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Marina Kolova 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data.Models/Appointment.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Models 2 | { 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | using BeautyBooking.Data.Common.Models; 7 | 8 | public class Appointment : BaseDeletableModel 9 | { 10 | public DateTime DateTime { get; set; } 11 | 12 | [Required] 13 | public string UserId { get; set; } 14 | 15 | public virtual ApplicationUser User { get; set; } 16 | 17 | [Required] 18 | public string SalonId { get; set; } 19 | 20 | public virtual Salon Salon { get; set; } 21 | 22 | public int ServiceId { get; set; } 23 | 24 | public virtual Service Service { get; set; } 25 | 26 | public virtual SalonService SalonService { get; set; } 27 | 28 | // The Salon can Confirm or Decline an appointment 29 | public bool? Confirmed { get; set; } 30 | 31 | // For every past (and confirmed) appointment the User can Rate the Salon 32 | // But rating can be given only once for each appointment 33 | public bool? IsSalonRatedByTheUser { get; set; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Views/Shared/_MenuItemsPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using BeautyBooking.Common 2 | @using BeautyBooking.Data.Models 3 | @using Microsoft.AspNetCore.Identity 4 | @inject SignInManager SignInManager 5 | @inject UserManager UserManager 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | @if (this.SignInManager.IsSignedIn(this.User) && this.User.IsInRole(GlobalConstants.SalonManagerRoleName)) 16 | { 17 | 20 | } 21 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/libman.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "defaultProvider": "jsdelivr", 4 | "libraries": [ 5 | { 6 | "library": "jquery@3.4.1", 7 | "destination": "wwwroot/lib/jquery/", 8 | "files": [ 9 | "dist/jquery.min.js", 10 | "dist/jquery.js" 11 | ] 12 | }, 13 | { 14 | "library": "bootstrap@4.4.1", 15 | "destination": "wwwroot/lib/bootstrap/", 16 | "files": [ 17 | "dist/css/bootstrap.css", 18 | "dist/css/bootstrap.min.css", 19 | "dist/js/bootstrap.js", 20 | "dist/js/bootstrap.min.js" 21 | ] 22 | }, 23 | { 24 | "library": "jquery-validation@1.19.1", 25 | "destination": "wwwroot/lib/jquery-validation/", 26 | "files": [ 27 | "dist/jquery.validate.js", 28 | "dist/jquery.validate.min.js" 29 | ] 30 | }, 31 | { 32 | "library": "jquery-validation-unobtrusive@3.2.11", 33 | "destination": "wwwroot/lib/jquery-validation-unobtrusive/", 34 | "files": [ 35 | "dist/jquery.validate.unobtrusive.js", 36 | "dist/jquery.validate.unobtrusive.min.js" 37 | ] 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /Data/BeautyBooking.Data/Seeding/CustomSeeders/CitiesSeeder.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Seeding 2 | { 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | using BeautyBooking.Data.Models; 8 | 9 | public class CitiesSeeder : ISeeder 10 | { 11 | public async Task SeedAsync(ApplicationDbContext dbContext, IServiceProvider serviceProvider) 12 | { 13 | if (dbContext.Cities.Any()) 14 | { 15 | return; 16 | } 17 | 18 | var cities = new City[] 19 | { 20 | new City // Id = 1 21 | { 22 | Name = "Sofia", 23 | }, 24 | new City // Id = 2 25 | { 26 | Name = "Varna", 27 | }, 28 | }; 29 | 30 | // Need them in particular order 31 | foreach (var city in cities) 32 | { 33 | await dbContext.AddAsync(city); 34 | await dbContext.SaveChangesAsync(); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Common/CustomValidationAttributes/ValidateTimeStringAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Common.CustomValidationAttributes 2 | { 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | using System.Globalization; 6 | 7 | using BeautyBooking.Common; 8 | 9 | public class ValidateTimeStringAttribute : RequiredAttribute 10 | { 11 | public override bool IsValid(object value) 12 | { 13 | var timeString = value as string; 14 | 15 | if (string.IsNullOrEmpty(timeString)) 16 | { 17 | return false; 18 | } 19 | 20 | bool parsed = DateTime.TryParseExact( 21 | timeString, 22 | GlobalConstants.DateTimeFormats.TimeFormat, 23 | CultureInfo.InvariantCulture, 24 | style: DateTimeStyles.AssumeUniversal, 25 | result: out _); 26 | if (!parsed) 27 | { 28 | return false; 29 | } 30 | 31 | return true; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model GenerateRecoveryCodesModel 3 | @{ 4 | ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes"; 5 | ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; 6 | } 7 | 8 | 9 |

@ViewData["Title"]

10 | 23 |
24 |
25 | 26 |
27 |
-------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Views/Shared/_CookieConsentPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Http.Features 2 | 3 | @{ 4 | var consentFeature = this.Context.Features.Get(); 5 | var showBanner = !consentFeature?.CanTrack ?? false; 6 | var cookieString = consentFeature?.CreateConsentCookie(); 7 | } 8 | 9 | @if (showBanner) 10 | { 11 | 17 | 25 | } 26 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model DeletePersonalDataModel 3 | @{ 4 | ViewData["Title"] = "Delete Personal Data"; 5 | ViewData["ActivePage"] = ManageNavPages.PersonalData; 6 | } 7 | 8 |

@ViewData["Title"]

9 | 10 | 15 | 16 |
17 |
18 |
19 | @if (Model.RequirePassword) 20 | { 21 |
22 | 23 | 24 | 25 |
26 | } 27 | 28 |
29 |
30 | 31 | @section Scripts { 32 | 33 | } -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Manage/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IndexModel 3 | @{ 4 | ViewData["Title"] = "Profile"; 5 | ViewData["ActivePage"] = ManageNavPages.Index; 6 | } 7 | 8 |

@ViewData["Title"]

9 | 10 |
11 |
12 |
13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 | 24 |
25 |
26 |
27 | 28 | @section Scripts { 29 | 30 | } -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using BeautyBooking.Data.Models; 3 | using Microsoft.AspNetCore.Identity; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace BeautyBooking.Web.Areas.Identity.Pages.Account.Manage 9 | { 10 | public class PersonalDataModel : PageModel 11 | { 12 | private readonly UserManager _userManager; 13 | private readonly ILogger _logger; 14 | 15 | public PersonalDataModel( 16 | UserManager userManager, 17 | ILogger logger) 18 | { 19 | _userManager = userManager; 20 | _logger = logger; 21 | } 22 | 23 | public async Task OnGet() 24 | { 25 | var user = await _userManager.GetUserAsync(User); 26 | if (user == null) 27 | { 28 | return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 29 | } 30 | 31 | return Page(); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /Services/BeautyBooking.Services.Data/BeautyBooking.Services.Data.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | latest 6 | 7 | 8 | 9 | ..\..\Rules.ruleset 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Categories/CategoryInputModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Categories 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | using BeautyBooking.Common; 6 | using BeautyBooking.Web.ViewModels.Common.CustomValidationAttributes; 7 | using Microsoft.AspNetCore.Http; 8 | 9 | public class CategoryInputModel 10 | { 11 | [Required] 12 | [StringLength( 13 | GlobalConstants.DataValidations.NameMaxLength, 14 | ErrorMessage = GlobalConstants.ErrorMessages.Name, 15 | MinimumLength = GlobalConstants.DataValidations.NameMinLength)] 16 | public string Name { get; set; } 17 | 18 | [Required] 19 | [StringLength( 20 | GlobalConstants.DataValidations.DescriptionMaxLength, 21 | ErrorMessage = GlobalConstants.ErrorMessages.Description, 22 | MinimumLength = GlobalConstants.DataValidations.DescriptionMinLength)] 23 | public string Description { get; set; } 24 | 25 | [DataType(DataType.Upload)] 26 | [ValidateImageFile(ErrorMessage = GlobalConstants.ErrorMessages.Image)] 27 | public IFormFile Image { get; set; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data/Configurations/ApplicationUserConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Configurations 2 | { 3 | using BeautyBooking.Data.Models; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | public class ApplicationUserConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder appUser) 10 | { 11 | appUser 12 | .HasMany(e => e.Claims) 13 | .WithOne() 14 | .HasForeignKey(e => e.UserId) 15 | .IsRequired() 16 | .OnDelete(DeleteBehavior.Restrict); 17 | 18 | appUser 19 | .HasMany(e => e.Logins) 20 | .WithOne() 21 | .HasForeignKey(e => e.UserId) 22 | .IsRequired() 23 | .OnDelete(DeleteBehavior.Restrict); 24 | 25 | appUser 26 | .HasMany(e => e.Roles) 27 | .WithOne() 28 | .HasForeignKey(e => e.UserId) 29 | .IsRequired() 30 | .OnDelete(DeleteBehavior.Restrict); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data/Configurations/AppointmentConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Configurations 2 | { 3 | using BeautyBooking.Data.Models; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | public class AppointmentConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder appointment) 10 | { 11 | appointment 12 | .HasOne(a => a.User) 13 | .WithMany(u => u.Appointments) 14 | .HasForeignKey(a => a.UserId); 15 | 16 | appointment 17 | .HasOne(a => a.Salon) 18 | .WithMany(s => s.Appointments) 19 | .HasForeignKey(a => a.SalonId); 20 | 21 | appointment 22 | .HasOne(a => a.Service) 23 | .WithMany(s => s.Appointments) 24 | .HasForeignKey(a => a.ServiceId); 25 | 26 | appointment 27 | .HasOne(a => a.SalonService) 28 | .WithMany(ss => ss.Appointments) 29 | .HasForeignKey(a => new { a.SalonId, a.ServiceId }); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/ExternalLogin.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ExternalLoginModel 3 | @{ 4 | ViewData["Title"] = "Register"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |

Associate your @Model.LoginProvider account.

9 |
10 | 11 |

12 | You've successfully authenticated with @Model.LoginProvider. 13 | Please enter an email address for this site below and click the Register button to finish 14 | logging in. 15 |

16 | 17 |
18 |
19 |
20 |
21 |
22 | 23 | 24 | 25 |
26 | 27 |
28 |
29 |
30 | 31 | @section Scripts { 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Views/Shared/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using BeautyBooking.Common 2 | @using BeautyBooking.Data.Models 3 | @using Microsoft.AspNetCore.Identity 4 | @inject SignInManager SignInManager 5 | @inject UserManager UserManager 6 | 7 | @if (this.SignInManager.IsSignedIn(this.User)) 8 | { 9 |
  • 10 | Hello @(this.User.Identity.Name)! 11 |
  • 12 | if (this.User.IsInRole(GlobalConstants.AdministratorRoleName)) 13 | { 14 |
  • 15 | Admin 16 |
  • 17 | } 18 |
  • 19 |
    20 | 21 |
    22 |
  • 23 | } 24 | else 25 | { 26 |
  • 27 | Register 28 |
  • 29 |
  • 30 | Login 31 |
  • 32 | } 33 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Administration/Views/Shared/_AdminMenuPartial.cshtml: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Views/Shared/Components/LatestBlogPosts/Default.cshtml: -------------------------------------------------------------------------------- 1 | @model BeautyBooking.Web.ViewModels.BlogPosts.BlogPostsListViewModel 2 | @{ 3 | this.ViewData["Title"] = "Default"; 4 | } 5 | 6 | @foreach (var blogPost in Model.BlogPosts) 7 | { 8 | 9 |
    10 |
    11 |
    12 |
    13 | @blogPost.Title 14 |
    15 | 21 |
    22 | Read 23 |
    24 |
    25 |
    26 | } 27 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Common/CustomValidationAttributes/ValidateImageFileAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Common.CustomValidationAttributes 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | using Microsoft.AspNetCore.Http; 6 | 7 | public class ValidateImageFileAttribute : RequiredAttribute 8 | { 9 | private const int MaxFileLengthInBytes = 1048576; // = (1 * 1024 * 1024) = 1 MB; 10 | 11 | public override bool IsValid(object value) 12 | { 13 | // Represents the file sent with the HttpRequest 14 | IFormFile file = value as IFormFile; 15 | 16 | if (file == null) 17 | { 18 | return false; 19 | } 20 | 21 | if (file.Length > MaxFileLengthInBytes) 22 | { 23 | return false; 24 | } 25 | 26 | // Check the image mime types 27 | if (file.ContentType.ToLower() != "image/jpg" 28 | && file.ContentType.ToLower() != "image/jpeg" 29 | && file.ContentType.ToLower() != "image/png") 30 | { 31 | return false; 32 | } 33 | 34 | return true; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Administration/Views/Cities/AddCity.cshtml: -------------------------------------------------------------------------------- 1 | @model BeautyBooking.Web.ViewModels.Cities.CityInputModel 2 | @{ 3 | this.ViewData["Title"] = "Add City"; 4 | } 5 | 6 |
    7 |

    New City

    8 |
    9 |
    10 | 11 |
    12 | 13 | 14 |
    15 |
    16 |
    17 |
    18 | 22 |
    23 |
    24 | Cancel 25 |
    26 |
    27 |
    28 |
    29 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services.Mapping/QueryableMappingExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Services.Mapping 2 | { 3 | using System; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | 7 | using AutoMapper.QueryableExtensions; 8 | 9 | public static class QueryableMappingExtensions 10 | { 11 | public static IQueryable To( 12 | this IQueryable source, 13 | params Expression>[] membersToExpand) 14 | { 15 | if (source == null) 16 | { 17 | throw new ArgumentNullException(nameof(source)); 18 | } 19 | 20 | return source.ProjectTo(AutoMapperConfig.MapperInstance.ConfigurationProvider, null, membersToExpand); 21 | } 22 | 23 | public static IQueryable To( 24 | this IQueryable source, 25 | object parameters) 26 | { 27 | if (source == null) 28 | { 29 | throw new ArgumentNullException(nameof(source)); 30 | } 31 | 32 | return source.ProjectTo(AutoMapperConfig.MapperInstance.ConfigurationProvider, parameters); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.Infrastructure/BeautyBooking.Web.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | latest 6 | 7 | 8 | 9 | ..\..\Rules.ruleset 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.Infrastructure/ViewComponents/SalonsSimpleListViewComponent.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.Infrastructure.ViewComponents 2 | { 3 | using System.Threading.Tasks; 4 | 5 | using BeautyBooking.Services.Data.Salons; 6 | using BeautyBooking.Web.ViewModels.Salons; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | public class SalonsSimpleListViewComponent : ViewComponent 10 | { 11 | private readonly ISalonsService salonsService; 12 | 13 | public SalonsSimpleListViewComponent(ISalonsService salonsService) 14 | { 15 | this.salonsService = salonsService; 16 | } 17 | 18 | public async Task InvokeAsync() 19 | { 20 | // This is used as a Menu in Salon Manager Area 21 | // Now only the Admin can Add Salons and only the seeded Manager can manage all of them 22 | // When Registering a Salon becomes an option for every user, UserId (OwnerId for Salons) would be checked here 23 | var viewModel = new SalonsSimpleListViewModel 24 | { 25 | Salons = await this.salonsService.GetAllAsync(), 26 | }; 27 | 28 | return this.View(viewModel); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/greensock/animation.gsap.min.js: -------------------------------------------------------------------------------- 1 | /*! ScrollMagic v2.0.5 | (c) 2015 Jan Paepke (@janpaepke) | license & info: http://scrollmagic.io */ 2 | !function(e,n){"function"==typeof define&&define.amd?define(["ScrollMagic","TweenMax","TimelineMax"],n):"object"==typeof exports?(require("gsap"),n(require("scrollmagic"),TweenMax,TimelineMax)):n(e.ScrollMagic||e.jQuery&&e.jQuery.ScrollMagic,e.TweenMax||e.TweenLite,e.TimelineMax||e.TimelineLite)}(this,function(e,n,r){"use strict";e.Scene.addOption("tweenChanges",!1,function(e){return!!e}),e.Scene.extend(function(){var e,t=this;t.on("progress.plugin_gsap",function(){i()}),t.on("destroy.plugin_gsap",function(e){t.removeTween(e.reset)});var i=function(){if(e){var n=t.progress(),r=t.state();e.repeat&&-1===e.repeat()?"DURING"===r&&e.paused()?e.play():"DURING"===r||e.paused()||e.pause():n!=e.progress()&&(0===t.duration()?n>0?e.play():e.reverse():t.tweenChanges()&&e.tweenTo?e.tweenTo(n*e.duration()):e.progress(n).pause())}};t.setTween=function(o,a,s){var u;arguments.length>1&&(arguments.length<3&&(s=a,a=1),o=n.to(o,a,s));try{u=r?new r({smoothChildTiming:!0}).add(o):o,u.pause()}catch(p){return t}return e&&t.removeTween(),e=u,o.repeat&&-1===o.repeat()&&(e.repeat(-1),e.yoyo(o.yoyo())),i(),t},t.removeTween=function(n){return e&&(n&&e.progress(0).pause(),e.kill(),e=void 0),t}})}); -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Common/Pagination/PaginatedList.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Common.Pagination 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | using Microsoft.EntityFrameworkCore; 9 | 10 | public class PaginatedList : List 11 | { 12 | public PaginatedList(List items, int count, int pageIndex, int pageSize) 13 | { 14 | this.PageIndex = pageIndex; 15 | this.TotalPages = (int)Math.Ceiling(count / (double)pageSize); 16 | 17 | this.AddRange(items); 18 | } 19 | 20 | public int PageIndex { get; private set; } 21 | 22 | public int TotalPages { get; private set; } 23 | 24 | public bool HasPreviousPage => this.PageIndex > 1; 25 | 26 | public bool HasNextPage => this.PageIndex < this.TotalPages; 27 | 28 | public static async Task> CreateAsync(IQueryable source, int pageIndex, int pageSize) 29 | { 30 | var count = await source.CountAsync(); 31 | var items = await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(); 32 | return new PaginatedList(items, count, pageIndex, pageSize); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml: -------------------------------------------------------------------------------- 1 | @inject SignInManager SignInManager 2 | @{ 3 | var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); 4 | } 5 | 16 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Common/CustomValidationAttributes/ValidateDateStringAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Common.CustomValidationAttributes 2 | { 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | using System.Globalization; 6 | 7 | using BeautyBooking.Common; 8 | 9 | public class ValidateDateStringAttribute : RequiredAttribute 10 | { 11 | public override bool IsValid(object value) 12 | { 13 | var dateString = value as string; 14 | 15 | if (string.IsNullOrEmpty(dateString)) 16 | { 17 | return false; 18 | } 19 | 20 | DateTime dt; 21 | bool parsed = DateTime.TryParseExact( 22 | dateString, 23 | GlobalConstants.DateTimeFormats.DateFormat, 24 | CultureInfo.InvariantCulture, 25 | style: DateTimeStyles.AssumeUniversal, 26 | result: out dt); 27 | if (!parsed) 28 | { 29 | return false; 30 | } 31 | 32 | if (dt < DateTime.UtcNow.Date) 33 | { 34 | return false; 35 | } 36 | 37 | return true; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/Salons/SalonInputModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.Salons 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | using BeautyBooking.Common; 6 | using BeautyBooking.Web.ViewModels.Common.CustomValidationAttributes; 7 | using Microsoft.AspNetCore.Http; 8 | 9 | public class SalonInputModel 10 | { 11 | [Required] 12 | [StringLength( 13 | GlobalConstants.DataValidations.NameMaxLength, 14 | ErrorMessage = GlobalConstants.ErrorMessages.Name, 15 | MinimumLength = GlobalConstants.DataValidations.NameMinLength)] 16 | public string Name { get; set; } 17 | 18 | [Required] 19 | public int CategoryId { get; set; } 20 | 21 | [Required] 22 | public int CityId { get; set; } 23 | 24 | [Required] 25 | [StringLength( 26 | GlobalConstants.DataValidations.AddressMaxLength, 27 | ErrorMessage = GlobalConstants.ErrorMessages.Address, 28 | MinimumLength = GlobalConstants.DataValidations.AddressMinLength)] 29 | public string Address { get; set; } 30 | 31 | [DataType(DataType.Upload)] 32 | [ValidateImageFile(ErrorMessage = GlobalConstants.ErrorMessages.Image)] 33 | public IFormFile Image { get; set; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Manager/Views/Shared/_ManagerHeaderPartial.cshtml: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Administration/Views/Shared/_AdminHeaderPartial.cshtml: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /Tests/BeautyBooking.Web.Tests/SeleniumTests.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.Tests 2 | { 3 | using OpenQA.Selenium; 4 | using OpenQA.Selenium.Chrome; 5 | using OpenQA.Selenium.Remote; 6 | 7 | using Xunit; 8 | 9 | public class SeleniumTests : IClassFixture> 10 | { 11 | private readonly SeleniumServerFactory server; 12 | private readonly IWebDriver browser; 13 | 14 | // Be sure that selenium-server-standalone-3.141.59.jar is running 15 | public SeleniumTests(SeleniumServerFactory server) 16 | { 17 | this.server = server; 18 | server.CreateClient(); 19 | var opts = new ChromeOptions(); 20 | opts.AddArgument("--headless"); // Optional, comment this out if you want to SEE the browser window 21 | opts.AddArgument("no-sandbox"); 22 | this.browser = new RemoteWebDriver(opts); 23 | } 24 | 25 | /* 26 | [Fact(Skip = "Example test. Disabled for CI.")] 27 | public void FooterOfThePageContainsPrivacyLink() 28 | { 29 | this.browser.Navigate().GoToUrl(this.server.RootUri); 30 | Assert.Contains( 31 | this.browser.FindElements(By.CssSelector("footer a")), 32 | x => x.GetAttribute("href").EndsWith("/Home/Privacy")); 33 | } 34 | */ 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data/Seeding/RolesSeeder.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Seeding 2 | { 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | using BeautyBooking.Common; 8 | using BeautyBooking.Data.Models; 9 | 10 | using Microsoft.AspNetCore.Identity; 11 | using Microsoft.Extensions.DependencyInjection; 12 | 13 | internal class RolesSeeder : ISeeder 14 | { 15 | public async Task SeedAsync(ApplicationDbContext dbContext, IServiceProvider serviceProvider) 16 | { 17 | var roleManager = serviceProvider.GetRequiredService>(); 18 | 19 | await SeedRoleAsync(roleManager, GlobalConstants.AdministratorRoleName); 20 | await SeedRoleAsync(roleManager, GlobalConstants.SalonManagerRoleName); 21 | } 22 | 23 | private static async Task SeedRoleAsync(RoleManager roleManager, string roleName) 24 | { 25 | var role = await roleManager.FindByNameAsync(roleName); 26 | if (role == null) 27 | { 28 | var result = await roleManager.CreateAsync(new ApplicationRole(roleName)); 29 | if (!result.Succeeded) 30 | { 31 | throw new Exception(string.Join(Environment.NewLine, result.Errors.Select(e => e.Description))); 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Logout.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authorization; 6 | using BeautyBooking.Data.Models; 7 | using Microsoft.AspNetCore.Identity; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.AspNetCore.Mvc.RazorPages; 10 | using Microsoft.Extensions.Logging; 11 | 12 | namespace BeautyBooking.Web.Areas.Identity.Pages.Account 13 | { 14 | [AllowAnonymous] 15 | public class LogoutModel : PageModel 16 | { 17 | private readonly SignInManager _signInManager; 18 | private readonly ILogger _logger; 19 | 20 | public LogoutModel(SignInManager signInManager, ILogger logger) 21 | { 22 | _signInManager = signInManager; 23 | _logger = logger; 24 | } 25 | 26 | public void OnGet() 27 | { 28 | } 29 | 30 | public async Task OnPost(string returnUrl = null) 31 | { 32 | await _signInManager.SignOutAsync(); 33 | _logger.LogInformation("User logged out."); 34 | if (returnUrl != null) 35 | { 36 | return LocalRedirect(returnUrl); 37 | } 38 | else 39 | { 40 | return RedirectToPage(); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data/Seeding/CustomSeeders/SalonServicesSeeder.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Seeding 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | using BeautyBooking.Data.Models; 9 | 10 | public class SalonServicesSeeder : ISeeder 11 | { 12 | public async Task SeedAsync(ApplicationDbContext dbContext, IServiceProvider serviceProvider) 13 | { 14 | if (dbContext.SalonServices.Any()) 15 | { 16 | return; 17 | } 18 | 19 | var salonServices = new List(); 20 | 21 | // For each Salon add all Services from its Category 22 | foreach (var salon in dbContext.Salons) 23 | { 24 | var salonId = salon.Id; 25 | var categoryId = salon.CategoryId; 26 | 27 | foreach (var service in dbContext.Services.Where(x => x.CategoryId == categoryId)) 28 | { 29 | var serviceId = service.Id; 30 | 31 | salonServices.Add(new SalonService 32 | { 33 | SalonId = salonId, 34 | ServiceId = serviceId, 35 | Available = true, 36 | }); 37 | } 38 | } 39 | 40 | await dbContext.AddRangeAsync(salonServices); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services/Cloudinary/CloudinaryService.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Services.Cloudinary 2 | { 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | 6 | using CloudinaryDotNet; 7 | using CloudinaryDotNet.Actions; 8 | using Microsoft.AspNetCore.Http; 9 | 10 | public class CloudinaryService : ICloudinaryService 11 | { 12 | private readonly Cloudinary cloudinary; 13 | 14 | public CloudinaryService(Cloudinary cloudinary) 15 | { 16 | this.cloudinary = cloudinary; 17 | } 18 | 19 | public async Task UploadPictureAsync(IFormFile pictureFile, string fileName) 20 | { 21 | byte[] destinationData; 22 | 23 | using (var ms = new MemoryStream()) 24 | { 25 | await pictureFile.CopyToAsync(ms); 26 | destinationData = ms.ToArray(); 27 | } 28 | 29 | UploadResult uploadResult = null; 30 | 31 | using (var ms = new MemoryStream(destinationData)) 32 | { 33 | ImageUploadParams uploadParams = new ImageUploadParams 34 | { 35 | Folder = "images", 36 | File = new FileDescription(fileName, ms), 37 | }; 38 | 39 | uploadResult = this.cloudinary.Upload(uploadParams); 40 | } 41 | 42 | return uploadResult?.SecureUri.AbsoluteUri; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tests/BeautyBooking.Web.Tests/BeautyBooking.Web.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | latest 6 | 7 | 8 | 9 | ..\..\Rules.ruleset 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers 24 | 25 | 26 | runtime; build; native; contentfiles; analyzers 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Tests/BeautyBooking.Web.Tests/WebTests.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.Tests 2 | { 3 | using System.Net; 4 | using System.Threading.Tasks; 5 | 6 | using Microsoft.AspNetCore.Mvc.Testing; 7 | 8 | using Xunit; 9 | 10 | public class WebTests : IClassFixture> 11 | { 12 | private readonly WebApplicationFactory server; 13 | 14 | public WebTests(WebApplicationFactory server) 15 | { 16 | this.server = server; 17 | } 18 | 19 | /* 20 | [Fact(Skip = "Example test. Disabled for CI.")] 21 | public async Task IndexPageShouldReturnStatusCode200WithTitle() 22 | { 23 | var client = this.server.CreateClient(); 24 | var response = await client.GetAsync("/"); 25 | response.EnsureSuccessStatusCode(); 26 | var responseContent = await response.Content.ReadAsStringAsync(); 27 | Assert.Contains("", responseContent); 28 | } 29 | 30 | [Fact(Skip = "Example test. Disabled for CI.")] 31 | public async Task AccountManagePageRequiresAuthorization() 32 | { 33 | var client = this.server.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); 34 | var response = await client.GetAsync("Identity/Account/Manage"); 35 | Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); 36 | } 37 | */ 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model SetPasswordModel 3 | @{ 4 | ViewData["Title"] = "Set password"; 5 | ViewData["ActivePage"] = ManageNavPages.ChangePassword; 6 | } 7 | 8 | <h4>Set your password</h4> 9 | <partial name="_StatusMessage" for="StatusMessage" /> 10 | <p class="text-info"> 11 | You do not have a local username/password for this site. Add a local 12 | account so you can log in without an external login. 13 | </p> 14 | <div class="row"> 15 | <div class="col-md-6"> 16 | <form id="set-password-form" method="post"> 17 | <div asp-validation-summary="All" class="text-danger"></div> 18 | <div class="form-group"> 19 | <label asp-for="Input.NewPassword"></label> 20 | <input asp-for="Input.NewPassword" class="form-control" /> 21 | <span asp-validation-for="Input.NewPassword" class="text-danger"></span> 22 | </div> 23 | <div class="form-group"> 24 | <label asp-for="Input.ConfirmPassword"></label> 25 | <input asp-for="Input.ConfirmPassword" class="form-control" /> 26 | <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span> 27 | </div> 28 | <button type="submit" class="btn btn-primary">Set password</button> 29 | </form> 30 | </div> 31 | </div> 32 | 33 | @section Scripts { 34 | <partial name="_ValidationScriptsPartial" /> 35 | } -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.Infrastructure/ViewComponents/PastAppointmentsViewComponent.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.Infrastructure.ViewComponents 2 | { 3 | using System.Threading.Tasks; 4 | 5 | using BeautyBooking.Data.Models; 6 | using BeautyBooking.Services.Data.Appointments; 7 | using BeautyBooking.Web.ViewModels.Appointments; 8 | using Microsoft.AspNetCore.Identity; 9 | using Microsoft.AspNetCore.Mvc; 10 | 11 | public class PastAppointmentsViewComponent : ViewComponent 12 | { 13 | private readonly IAppointmentsService appointmentsService; 14 | private readonly UserManager<ApplicationUser> userManager; 15 | 16 | public PastAppointmentsViewComponent( 17 | IAppointmentsService appointmentsService, 18 | UserManager<ApplicationUser> userManager) 19 | { 20 | this.appointmentsService = appointmentsService; 21 | this.userManager = userManager; 22 | } 23 | 24 | public async Task<IViewComponentResult> InvokeAsync() 25 | { 26 | var user = await this.userManager.GetUserAsync(this.HttpContext.User); 27 | var userId = await this.userManager.GetUserIdAsync(user); 28 | 29 | var viewModel = new AppointmentsListViewModel 30 | { 31 | Appointments = 32 | await this.appointmentsService.GetPastByUserAsync<AppointmentViewModel>(userId), 33 | }; 34 | 35 | return this.View(viewModel); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web.ViewModels/BlogPosts/BlogPostInputModel.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.ViewModels.BlogPosts 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | using BeautyBooking.Common; 6 | using BeautyBooking.Web.ViewModels.Common.CustomValidationAttributes; 7 | using Microsoft.AspNetCore.Http; 8 | 9 | public class BlogPostInputModel 10 | { 11 | [Required] 12 | [StringLength( 13 | GlobalConstants.DataValidations.TitleMaxLength, 14 | ErrorMessage = GlobalConstants.ErrorMessages.Title, 15 | MinimumLength = GlobalConstants.DataValidations.TitleMinLength)] 16 | public string Title { get; set; } 17 | 18 | [Required] 19 | [StringLength( 20 | GlobalConstants.DataValidations.ContentMaxLength, 21 | ErrorMessage = GlobalConstants.ErrorMessages.Content, 22 | MinimumLength = GlobalConstants.DataValidations.ContentMinLength)] 23 | public string Content { get; set; } 24 | 25 | [Required] 26 | [StringLength( 27 | GlobalConstants.DataValidations.NameMaxLength, 28 | ErrorMessage = GlobalConstants.ErrorMessages.Author, 29 | MinimumLength = GlobalConstants.DataValidations.NameMinLength)] 30 | public string Author { get; set; } 31 | 32 | [Required] 33 | [DataType(DataType.Upload)] 34 | [ValidateImageFile(ErrorMessage = GlobalConstants.ErrorMessages.Image)] 35 | public IFormFile Image { get; set; } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data/Migrations/20200423023315_AddOwnerToSalons.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Migrations 2 | { 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | 5 | public partial class AddOwnerToSalons : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.AddColumn<string>( 10 | name: "OwnerId", 11 | table: "Salons", 12 | nullable: true); 13 | 14 | migrationBuilder.CreateIndex( 15 | name: "IX_Salons_OwnerId", 16 | table: "Salons", 17 | column: "OwnerId"); 18 | 19 | migrationBuilder.AddForeignKey( 20 | name: "FK_Salons_AspNetUsers_OwnerId", 21 | table: "Salons", 22 | column: "OwnerId", 23 | principalTable: "AspNetUsers", 24 | principalColumn: "Id", 25 | onDelete: ReferentialAction.Restrict); 26 | } 27 | 28 | protected override void Down(MigrationBuilder migrationBuilder) 29 | { 30 | migrationBuilder.DropForeignKey( 31 | name: "FK_Salons_AspNetUsers_OwnerId", 32 | table: "Salons"); 33 | 34 | migrationBuilder.DropIndex( 35 | name: "IX_Salons_OwnerId", 36 | table: "Salons"); 37 | 38 | migrationBuilder.DropColumn( 39 | name: "OwnerId", 40 | table: "Salons"); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/ResetPassword.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ResetPasswordModel 3 | @{ 4 | ViewData["Title"] = "Reset password"; 5 | } 6 | 7 | <h1>@ViewData["Title"]</h1> 8 | <h4>Reset your password.</h4> 9 | <hr /> 10 | <div class="row"> 11 | <div class="col-md-4"> 12 | <form method="post"> 13 | <div asp-validation-summary="All" class="text-danger"></div> 14 | <input asp-for="Input.Code" type="hidden" /> 15 | <div class="form-group"> 16 | <label asp-for="Input.Email"></label> 17 | <input asp-for="Input.Email" class="form-control" /> 18 | <span asp-validation-for="Input.Email" class="text-danger"></span> 19 | </div> 20 | <div class="form-group"> 21 | <label asp-for="Input.Password"></label> 22 | <input asp-for="Input.Password" class="form-control" /> 23 | <span asp-validation-for="Input.Password" class="text-danger"></span> 24 | </div> 25 | <div class="form-group"> 26 | <label asp-for="Input.ConfirmPassword"></label> 27 | <input asp-for="Input.ConfirmPassword" class="form-control" /> 28 | <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span> 29 | </div> 30 | <button type="submit" class="btn btn-primary">Reset</button> 31 | </form> 32 | </div> 33 | </div> 34 | 35 | @section Scripts { 36 | <partial name="_ValidationScriptsPartial" /> 37 | } 38 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Register.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model RegisterModel 3 | @{ 4 | ViewData["Title"] = "Register"; 5 | } 6 | 7 | <h1 class="ml-5">@ViewData["Title"]</h1> 8 | 9 | <div class="row ml-5"> 10 | <div class="col-md-4"> 11 | <form asp-route-returnUrl="@Model.ReturnUrl" method="post"> 12 | <h4>Create a new account.</h4> 13 | <hr /> 14 | <div asp-validation-summary="All" class="text-danger"></div> 15 | <div class="form-group"> 16 | <label asp-for="Input.Email"></label> 17 | <input asp-for="Input.Email" class="form-control" /> 18 | <span asp-validation-for="Input.Email" class="text-danger"></span> 19 | </div> 20 | <div class="form-group"> 21 | <label asp-for="Input.Password"></label> 22 | <input asp-for="Input.Password" class="form-control" /> 23 | <span asp-validation-for="Input.Password" class="text-danger"></span> 24 | </div> 25 | <div class="form-group"> 26 | <label asp-for="Input.ConfirmPassword"></label> 27 | <input asp-for="Input.ConfirmPassword" class="form-control" /> 28 | <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span> 29 | </div> 30 | <button type="submit" class="btn btn-primary">Register</button> 31 | </form> 32 | </div> 33 | </div> 34 | 35 | @section Scripts { 36 | <partial name="_ValidationScriptsPartial" /> 37 | } 38 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Administration/Views/Shared/_AdminSidebarPartial.cshtml: -------------------------------------------------------------------------------- 1 | <!-- Sidebar --> 2 | <div class="bg-light border border-secondary" id="sidebar-wrapper"> 3 | <div class="sidebar-heading"><strong><u>Admin</u></strong></div> 4 | <div class="list-group list-group-flush"> 5 | 6 | <a asp-area="Administration" asp-controller="Dashboard" asp-action="Index" 7 | class="list-group-item list-group-item-action bg-light">Dashboard</a> 8 | 9 | <a asp-area="Administration" asp-controller="BlogPosts" asp-action="Index" 10 | class="list-group-item list-group-item-action bg-light">Blog Posts</a> 11 | 12 | <a asp-area="Administration" asp-controller="Categories" asp-action="Index" 13 | class="list-group-item list-group-item-action bg-light">Categories</a> 14 | 15 | <a asp-area="Administration" asp-controller="Services" asp-action="Index" 16 | class="list-group-item list-group-item-action bg-light">Services</a> 17 | 18 | <a asp-area="Administration" asp-controller="Cities" asp-action="Index" 19 | class="list-group-item list-group-item-action bg-light">Cities</a> 20 | 21 | <a asp-area="Administration" asp-controller="Salons" asp-action="Index" 22 | class="list-group-item list-group-item-action bg-light">Salons</a> 23 | 24 | <a asp-area="Administration" asp-controller="Appointments" asp-action="Index" 25 | class="list-group-item list-group-item-action bg-light">Appointments</a> 26 | 27 | </div> 28 | </div> 29 | <!-- /#sidebar-wrapper --> 30 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data.Models/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable VirtualMemberCallInConstructor 2 | namespace BeautyBooking.Data.Models 3 | { 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | using BeautyBooking.Data.Common.Models; 8 | 9 | using Microsoft.AspNetCore.Identity; 10 | 11 | public class ApplicationUser : IdentityUser, IAuditInfo, IDeletableEntity 12 | { 13 | public ApplicationUser() 14 | { 15 | this.Id = Guid.NewGuid().ToString(); 16 | this.Roles = new HashSet<IdentityUserRole<string>>(); 17 | this.Claims = new HashSet<IdentityUserClaim<string>>(); 18 | this.Logins = new HashSet<IdentityUserLogin<string>>(); 19 | this.Appointments = new HashSet<Appointment>(); 20 | this.Salons = new HashSet<Salon>(); 21 | } 22 | 23 | // Audit info 24 | public DateTime CreatedOn { get; set; } 25 | 26 | public DateTime? ModifiedOn { get; set; } 27 | 28 | // Deletable entity 29 | public bool IsDeleted { get; set; } 30 | 31 | public DateTime? DeletedOn { get; set; } 32 | 33 | public virtual ICollection<IdentityUserRole<string>> Roles { get; set; } 34 | 35 | public virtual ICollection<IdentityUserClaim<string>> Claims { get; set; } 36 | 37 | public virtual ICollection<IdentityUserLogin<string>> Logins { get; set; } 38 | 39 | public virtual ICollection<Appointment> Appointments { get; set; } 40 | 41 | public virtual ICollection<Salon> Salons { get; set; } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/BeautyBooking.Services.Data.Tests/BeautyBooking.Services.Data.Tests.csproj: -------------------------------------------------------------------------------- 1 | <Project Sdk="Microsoft.NET.Sdk"> 2 | 3 | <PropertyGroup> 4 | <TargetFramework>netcoreapp3.1</TargetFramework> 5 | <LangVersion>latest</LangVersion> 6 | </PropertyGroup> 7 | 8 | <PropertyGroup> 9 | <CodeAnalysisRuleSet>..\..\Rules.ruleset</CodeAnalysisRuleSet> 10 | </PropertyGroup> 11 | <ItemGroup> 12 | <AdditionalFiles Include="..\..\stylecop.json" /> 13 | </ItemGroup> 14 | 15 | <ItemGroup> 16 | <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.2" /> 17 | <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> 18 | <PackageReference Include="Moq" Version="4.13.1" /> 19 | <PackageReference Include="NLipsum" Version="1.1.0" /> 20 | <PackageReference Include="xunit" Version="2.4.1" /> 21 | <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1"> 22 | <PrivateAssets>all</PrivateAssets> 23 | <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> 24 | </PackageReference> 25 | <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.164" PrivateAssets="all"> 26 | <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> 27 | </PackageReference> 28 | </ItemGroup> 29 | 30 | <ItemGroup> 31 | <ProjectReference Include="..\..\Data\BeautyBooking.Data\BeautyBooking.Data.csproj" /> 32 | <ProjectReference Include="..\..\Services\BeautyBooking.Services.Data\BeautyBooking.Services.Data.csproj" /> 33 | </ItemGroup> 34 | 35 | </Project> 36 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ChangePasswordModel 3 | @{ 4 | ViewData["Title"] = "Change password"; 5 | ViewData["ActivePage"] = ManageNavPages.ChangePassword; 6 | } 7 | 8 | <h4>@ViewData["Title"]</h4> 9 | <partial name="_StatusMessage" for="StatusMessage" /> 10 | <div class="row"> 11 | <div class="col-md-6"> 12 | <form id="change-password-form" method="post"> 13 | <div asp-validation-summary="All" class="text-danger"></div> 14 | <div class="form-group"> 15 | <label asp-for="Input.OldPassword"></label> 16 | <input asp-for="Input.OldPassword" class="form-control" /> 17 | <span asp-validation-for="Input.OldPassword" class="text-danger"></span> 18 | </div> 19 | <div class="form-group"> 20 | <label asp-for="Input.NewPassword"></label> 21 | <input asp-for="Input.NewPassword" class="form-control" /> 22 | <span asp-validation-for="Input.NewPassword" class="text-danger"></span> 23 | </div> 24 | <div class="form-group"> 25 | <label asp-for="Input.ConfirmPassword"></label> 26 | <input asp-for="Input.ConfirmPassword" class="form-control" /> 27 | <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span> 28 | </div> 29 | <button type="submit" class="btn btn-primary">Update password</button> 30 | </form> 31 | </div> 32 | </div> 33 | 34 | @section Scripts { 35 | <partial name="_ValidationScriptsPartial" /> 36 | } -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Administration/Views/Appointments/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model BeautyBooking.Web.ViewModels.Appointments.AppointmentsListViewModel 2 | @{ 3 | this.ViewData["Title"] = "Appointments"; 4 | } 5 | 6 | <h2>All Appointments (@Model.Appointments.Count())</h2> 7 | 8 | <table class="table table-bordered table-striped"> 9 | <thead class="thead-light"> 10 | <tr> 11 | <th scope="col">DateTime</th> 12 | <th scope="col">User</th> 13 | <th scope="col">Salon</th> 14 | <th scope="col">Service</th> 15 | <th scope="col">Status</th> 16 | </tr> 17 | </thead> 18 | <tbody> 19 | @foreach (var appointment in this.Model.Appointments) 20 | { 21 | <tr> 22 | <th scope="row">@appointment.DateTime.ToString("f")</th> 23 | <td>@appointment.UserEmail</td> 24 | <td>@appointment.SalonName</td> 25 | <td>@appointment.ServiceName</td> 26 | <td> 27 | @if (appointment.Confirmed == true) 28 | { 29 | <span class="badge badge-success">Confirmed</span> 30 | } 31 | else if (appointment.Confirmed == false) 32 | { 33 | <span class="badge badge-danger">Declined</span> 34 | } 35 | else 36 | { 37 | <span class="badge badge-secondary">Pending</span> 38 | } 39 | </td> 40 | </tr> 41 | } 42 | </tbody> 43 | </table> 44 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/OwlCarousel2-2.2.1/owl.theme.default.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Owl Carousel v2.2.1 3 | * Copyright 2013-2017 David Deutsch 4 | * Licensed under () 5 | */ 6 | /* 7 | * Default theme - Owl Carousel CSS File 8 | */ 9 | .owl-theme .owl-nav 10 | { 11 | margin-top: 10px; 12 | text-align: center; 13 | -webkit-tap-highlight-color: transparent; 14 | z-index: 10000; 15 | background: red; 16 | } 17 | .owl-theme .owl-nav [class*='owl-'] 18 | { 19 | color: #FFF; 20 | font-size: 14px; 21 | margin: 5px; 22 | padding: 4px 7px; 23 | background: #D6D6D6; 24 | display: inline-block; 25 | cursor: pointer; 26 | border-radius: 3px; 27 | } 28 | .owl-theme .owl-nav [class*='owl-']:hover 29 | { 30 | background: #869791; 31 | color: #FFF; 32 | text-decoration: none; 33 | } 34 | .owl-theme .owl-nav .disabled 35 | { 36 | opacity: 0.5; 37 | cursor: default; 38 | } 39 | 40 | .owl-theme .owl-nav.disabled + .owl-dots 41 | { 42 | margin-top: 10px; 43 | } 44 | 45 | .owl-theme .owl-dots 46 | { 47 | text-align: center; 48 | -webkit-tap-highlight-color: transparent; 49 | } 50 | .owl-theme .owl-dots .owl-dot 51 | { 52 | display: inline-block; 53 | zoom: 1; 54 | *display: inline; 55 | } 56 | .owl-theme .owl-dots .owl-dot span 57 | { 58 | width: 10px; 59 | height: 10px; 60 | margin: 5px 7px; 61 | background: #CFCFD0; 62 | display: block; 63 | -webkit-backface-visibility: visible; 64 | transition: opacity 200ms ease; 65 | border-radius: 30px; 66 | } 67 | .owl-theme .owl-dots .owl-dot.active span, .owl-theme .owl-dots .owl-dot:hover span 68 | { 69 | background: #A19799; 70 | } 71 | 72 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Authorization; 7 | using BeautyBooking.Data.Models; 8 | using Microsoft.AspNetCore.Identity; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.AspNetCore.Mvc.RazorPages; 11 | using Microsoft.AspNetCore.WebUtilities; 12 | 13 | namespace BeautyBooking.Web.Areas.Identity.Pages.Account 14 | { 15 | [AllowAnonymous] 16 | public class ConfirmEmailModel : PageModel 17 | { 18 | private readonly UserManager<ApplicationUser> _userManager; 19 | 20 | public ConfirmEmailModel(UserManager<ApplicationUser> userManager) 21 | { 22 | _userManager = userManager; 23 | } 24 | 25 | [TempData] 26 | public string StatusMessage { get; set; } 27 | 28 | public async Task<IActionResult> OnGetAsync(string userId, string code) 29 | { 30 | if (userId == null || code == null) 31 | { 32 | return RedirectToPage("/Index"); 33 | } 34 | 35 | var user = await _userManager.FindByIdAsync(userId); 36 | if (user == null) 37 | { 38 | return NotFound($"Unable to load user with ID '{userId}'."); 39 | } 40 | 41 | code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code)); 42 | var result = await _userManager.ConfirmEmailAsync(user, code); 43 | StatusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email."; 44 | return Page(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Services/BeautyBooking.Services.Data/Cities/CitiesService.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Services.Data.Cities 2 | { 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | using BeautyBooking.Data.Common.Repositories; 8 | using BeautyBooking.Data.Models; 9 | using BeautyBooking.Services.Mapping; 10 | using Microsoft.EntityFrameworkCore; 11 | 12 | public class CitiesService : ICitiesService 13 | { 14 | private readonly IDeletableEntityRepository<City> citiesRepository; 15 | 16 | public CitiesService(IDeletableEntityRepository<City> citiesRepository) 17 | { 18 | this.citiesRepository = citiesRepository; 19 | } 20 | 21 | public async Task<IEnumerable<T>> GetAllAsync<T>() 22 | { 23 | var cities = 24 | await this.citiesRepository 25 | .All() 26 | .OrderBy(x => x.Id) 27 | .To<T>().ToListAsync(); 28 | return cities; 29 | } 30 | 31 | public async Task AddAsync(string name) 32 | { 33 | await this.citiesRepository.AddAsync(new City 34 | { 35 | Name = name, 36 | }); 37 | await this.citiesRepository.SaveChangesAsync(); 38 | } 39 | 40 | public async Task DeleteAsync(int id) 41 | { 42 | var city = 43 | await this.citiesRepository 44 | .AllAsNoTracking() 45 | .Where(x => x.Id == id) 46 | .FirstOrDefaultAsync(); 47 | this.citiesRepository.Delete(city); 48 | await this.citiesRepository.SaveChangesAsync(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/LoginWith2fa.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model LoginWith2faModel 3 | @{ 4 | ViewData["Title"] = "Two-factor authentication"; 5 | } 6 | 7 | <h1>@ViewData["Title"]</h1> 8 | <hr /> 9 | <p>Your login is protected with an authenticator app. Enter your authenticator code below.</p> 10 | <div class="row"> 11 | <div class="col-md-4"> 12 | <form method="post" asp-route-returnUrl="@Model.ReturnUrl"> 13 | <input asp-for="RememberMe" type="hidden" /> 14 | <div asp-validation-summary="All" class="text-danger"></div> 15 | <div class="form-group"> 16 | <label asp-for="Input.TwoFactorCode"></label> 17 | <input asp-for="Input.TwoFactorCode" class="form-control" autocomplete="off" /> 18 | <span asp-validation-for="Input.TwoFactorCode" class="text-danger"></span> 19 | </div> 20 | <div class="form-group"> 21 | <div class="checkbox"> 22 | <label asp-for="Input.RememberMachine"> 23 | <input asp-for="Input.RememberMachine" /> 24 | @Html.DisplayNameFor(m => m.Input.RememberMachine) 25 | </label> 26 | </div> 27 | </div> 28 | <div class="form-group"> 29 | <button type="submit" class="btn btn-primary">Log in</button> 30 | </div> 31 | </form> 32 | </div> 33 | </div> 34 | <p> 35 | Don't have access to your authenticator device? You can 36 | <a id="recovery-code-login" asp-page="./LoginWithRecoveryCode" asp-route-returnUrl="@Model.ReturnUrl">log in with a recovery code</a>. 37 | </p> 38 | 39 | @section Scripts { 40 | <partial name="_ValidationScriptsPartial" /> 41 | } 42 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data/Migrations/20200425045559_RemoveEntitySetting.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Migrations 2 | { 3 | using System; 4 | 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | 7 | public partial class RemoveEntitySetting : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.DropTable( 12 | name: "Settings"); 13 | } 14 | 15 | protected override void Down(MigrationBuilder migrationBuilder) 16 | { 17 | migrationBuilder.CreateTable( 18 | name: "Settings", 19 | columns: table => new 20 | { 21 | Id = table.Column<int>(type: "int", nullable: false) 22 | .Annotation("SqlServer:Identity", "1, 1"), 23 | CreatedOn = table.Column<DateTime>(type: "datetime2", nullable: false), 24 | DeletedOn = table.Column<DateTime>(type: "datetime2", nullable: true), 25 | IsDeleted = table.Column<bool>(type: "bit", nullable: false), 26 | ModifiedOn = table.Column<DateTime>(type: "datetime2", nullable: true), 27 | Name = table.Column<string>(type: "nvarchar(max)", nullable: true), 28 | Value = table.Column<string>(type: "nvarchar(max)", nullable: true), 29 | }, 30 | constraints: table => 31 | { 32 | table.PrimaryKey("PK_Settings", x => x.Id); 33 | }); 34 | 35 | migrationBuilder.CreateIndex( 36 | name: "IX_Settings_IsDeleted", 37 | table: "Settings", 38 | column: "IsDeleted"); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Administration/Views/Cities/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model BeautyBooking.Web.ViewModels.Cities.CitiesListViewModel 2 | @using BeautyBooking.Common 3 | @{ 4 | this.ViewData["Title"] = "Cities"; 5 | } 6 | 7 | <h2> 8 | All Cities (@Model.Cities.Count()) 9 | <span> 10 | <a class="btn btn-info" asp-area="Administration" asp-controller="Cities" asp-action="AddCity">Add New City</a> 11 | </span> 12 | </h2> 13 | 14 | <table class="table table-bordered table-striped"> 15 | <thead class="thead-light"> 16 | <tr> 17 | <th scope="col">Name</th> 18 | <th scope="col">Salons</th> 19 | <th scope="col">Delete Button</th> 20 | </tr> 21 | </thead> 22 | <tbody> 23 | @foreach (var city in this.Model.Cities) 24 | { 25 | <tr> 26 | <th scope="row">@city.Name</th> 27 | <td>@city.SalonsCount</td> 28 | <td> 29 | @if (city.Id <= GlobalConstants.SeededDataCounts.Cities) 30 | { 31 | <div class="text-muted" style="font-size:smaller"> 32 | Seeded Data <br />Cannot Be Deleted 33 | </div> 34 | } 35 | else 36 | { 37 | <form method="post"> 38 | <button type="submit" class="btn btn-danger btn-sm" 39 | asp-area="Administration" asp-controller="Cities" asp-action="DeleteCity" asp-route-id="@city.Id"> 40 | Delete 41 | </button> 42 | </form> 43 | } 44 | </td> 45 | </tr> 46 | } 47 | </tbody> 48 | </table> 49 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data.Models/Salon.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Models 2 | { 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | using BeautyBooking.Common; 7 | using BeautyBooking.Data.Common.Models; 8 | 9 | public class Salon : BaseDeletableModel<string> 10 | { 11 | public Salon() 12 | { 13 | this.Appointments = new HashSet<Appointment>(); 14 | this.Services = new HashSet<SalonService>(); 15 | } 16 | 17 | [Required] 18 | [MaxLength(GlobalConstants.DataValidations.NameMaxLength)] 19 | public string Name { get; set; } 20 | 21 | [Required] 22 | public string ImageUrl { get; set; } 23 | 24 | // Not Required! Allows testing/showing functionality with seeded data in Admin/Manager Area 25 | // For now Salons can be created only in the AdminDashboard and all of them are managed by one seeded ManagerAccount 26 | // This will be used when Registering a Salon becomes an option for every user 27 | public string OwnerId { get; set; } 28 | 29 | public virtual ApplicationUser Owner { get; set; } 30 | 31 | public int CategoryId { get; set; } 32 | 33 | public virtual Category Category { get; set; } 34 | 35 | public int CityId { get; set; } 36 | 37 | public virtual City City { get; set; } 38 | 39 | [Required] 40 | [MaxLength(GlobalConstants.DataValidations.AddressMaxLength)] 41 | public string Address { get; set; } 42 | 43 | public double Rating { get; set; } 44 | 45 | public int RatersCount { get; set; } 46 | 47 | public virtual ICollection<SalonService> Services { get; set; } 48 | 49 | public virtual ICollection<Appointment> Appointments { get; set; } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Views/Appointments/CancelAppointment.cshtml: -------------------------------------------------------------------------------- 1 | @model BeautyBooking.Web.ViewModels.Appointments.AppointmentViewModel 2 | @{ 3 | this.ViewData["Title"] = "Cancel Appointment"; 4 | } 5 | 6 | <h3><strong>Are you sure you want to cancel this appointment?</strong></h3> 7 | <form class="col-6 mt-5"> 8 | <div class="form-group row"> 9 | <label class="col-sm-3 col-form-label">Date and Time</label> 10 | <div class="col-sm-7"> 11 | <div class="form-control">@Model.DateTime</div> 12 | </div> 13 | </div> 14 | <div class="form-group row"> 15 | <label class="col-sm-3 col-form-label">Salon</label> 16 | <div class="col-sm-7"> 17 | <div class="form-control">@Model.SalonName</div> 18 | </div> 19 | </div> 20 | <div class="form-group row"> 21 | <label class="col-sm-3 col-form-label">Address</label> 22 | <div class="col-sm-7"> 23 | <div class="form-control">@Model.SalonCityName, @Model.SalonAddress</div> 24 | </div> 25 | </div> 26 | <div class="form-group row"> 27 | <label class="col-sm-3 col-form-label">Service</label> 28 | <div class="col-sm-7"> 29 | <div class="form-control">@Model.ServiceName</div> 30 | </div> 31 | </div> 32 | </form> 33 | 34 | <div class="col-6 mt-5 ml-5"> 35 | <div class="row ml-5"> 36 | <form method="post"> 37 | <button type="submit" class="btn btn-outline-success" 38 | asp-area="" asp-controller="Appointments" asp-action="DeleteAppointment" asp-route-id="@Model.Id"> 39 | Yes, Cancel it! 40 | </button> 41 | </form> 42 | <div class="ml-5"> 43 | <a class="btn btn-danger" asp-area="" asp-controller="Appointments" asp-action="Index">No! Go Back</a> 44 | </div> 45 | </div> 46 | </div> 47 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Administration/Controllers/CitiesController.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.Areas.Administration.Controllers 2 | { 3 | using System.Threading.Tasks; 4 | 5 | using BeautyBooking.Common; 6 | using BeautyBooking.Services.Data.Cities; 7 | using BeautyBooking.Web.ViewModels.Cities; 8 | using Microsoft.AspNetCore.Mvc; 9 | 10 | public class CitiesController : AdministrationController 11 | { 12 | private readonly ICitiesService citiesService; 13 | 14 | public CitiesController(ICitiesService citiesService) 15 | { 16 | this.citiesService = citiesService; 17 | } 18 | 19 | public async Task<IActionResult> Index() 20 | { 21 | var viewModel = new CitiesListViewModel 22 | { 23 | Cities = await this.citiesService.GetAllAsync<CityViewModel>(), 24 | }; 25 | return this.View(viewModel); 26 | } 27 | 28 | public IActionResult AddCity() 29 | { 30 | return this.View(); 31 | } 32 | 33 | [HttpPost] 34 | public async Task<IActionResult> AddCity(CityInputModel input) 35 | { 36 | if (!this.ModelState.IsValid) 37 | { 38 | return this.View(input); 39 | } 40 | 41 | await this.citiesService.AddAsync(input.Name); 42 | 43 | return this.RedirectToAction("Index"); 44 | } 45 | 46 | [HttpPost] 47 | public async Task<IActionResult> DeleteCity(int id) 48 | { 49 | if (id <= GlobalConstants.SeededDataCounts.Cities) 50 | { 51 | return this.RedirectToAction("Index"); 52 | } 53 | 54 | await this.citiesService.DeleteAsync(id); 55 | 56 | return this.RedirectToAction("Index"); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data/Seeding/ApplicationDbContextSeeder.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Seeding 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Logging; 9 | 10 | public class ApplicationDbContextSeeder : ISeeder 11 | { 12 | public async Task SeedAsync(ApplicationDbContext dbContext, IServiceProvider serviceProvider) 13 | { 14 | if (dbContext == null) 15 | { 16 | throw new ArgumentNullException(nameof(dbContext)); 17 | } 18 | 19 | if (serviceProvider == null) 20 | { 21 | throw new ArgumentNullException(nameof(serviceProvider)); 22 | } 23 | 24 | var logger = serviceProvider.GetService<ILoggerFactory>().CreateLogger(typeof(ApplicationDbContextSeeder)); 25 | 26 | var seeders = new List<ISeeder> 27 | { 28 | new RolesSeeder(), 29 | new AccountsSeeder(), 30 | new BlogPostsSeeder(), 31 | new CategoriesSeeder(), 32 | new ServicesSeeder(), 33 | new CitiesSeeder(), 34 | new SalonsSeeder(), 35 | new SalonServicesSeeder(), 36 | new AppointmentsSeeder(), 37 | }; 38 | 39 | foreach (var seeder in seeders) 40 | { 41 | await seeder.SeedAsync(dbContext, serviceProvider); 42 | await dbContext.SaveChangesAsync(); 43 | logger.LogInformation($"Seeder {seeder.GetType().Name} done."); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/less/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | .fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | .fa-icon-rotate(@degrees, @rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})"; 16 | -webkit-transform: rotate(@degrees); 17 | -ms-transform: rotate(@degrees); 18 | transform: rotate(@degrees); 19 | } 20 | 21 | .fa-icon-flip(@horiz, @vert, @rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)"; 23 | -webkit-transform: scale(@horiz, @vert); 24 | -ms-transform: scale(@horiz, @vert); 25 | transform: scale(@horiz, @vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | .sr-only() { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | .sr-only-focusable() { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data/Repositories/EfDeletableEntityRepository.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Repositories 2 | { 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | using BeautyBooking.Data.Common.Models; 8 | using BeautyBooking.Data.Common.Repositories; 9 | 10 | using Microsoft.EntityFrameworkCore; 11 | 12 | public class EfDeletableEntityRepository<TEntity> : EfRepository<TEntity>, IDeletableEntityRepository<TEntity> 13 | where TEntity : class, IDeletableEntity 14 | { 15 | public EfDeletableEntityRepository(ApplicationDbContext context) 16 | : base(context) 17 | { 18 | } 19 | 20 | public override IQueryable<TEntity> All() => base.All().Where(x => !x.IsDeleted); 21 | 22 | public override IQueryable<TEntity> AllAsNoTracking() => base.AllAsNoTracking().Where(x => !x.IsDeleted); 23 | 24 | public IQueryable<TEntity> AllWithDeleted() => base.All().IgnoreQueryFilters(); 25 | 26 | public IQueryable<TEntity> AllAsNoTrackingWithDeleted() => base.AllAsNoTracking().IgnoreQueryFilters(); 27 | 28 | public Task<TEntity> GetByIdWithDeletedAsync(params object[] id) 29 | { 30 | var getByIdPredicate = EfExpressionHelper.BuildByIdPredicate<TEntity>(this.Context, id); 31 | return this.AllWithDeleted().FirstOrDefaultAsync(getByIdPredicate); 32 | } 33 | 34 | public void HardDelete(TEntity entity) => base.Delete(entity); 35 | 36 | public void Undelete(TEntity entity) 37 | { 38 | entity.IsDeleted = false; 39 | entity.DeletedOn = null; 40 | this.Update(entity); 41 | } 42 | 43 | public override void Delete(TEntity entity) 44 | { 45 | entity.IsDeleted = true; 46 | entity.DeletedOn = DateTime.UtcNow; 47 | this.Update(entity); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Areas/Identity/Pages/Account/Manage/Email.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model EmailModel 3 | @{ 4 | ViewData["Title"] = "Manage Email"; 5 | ViewData["ActivePage"] = ManageNavPages.Email; 6 | } 7 | 8 | <h4>@ViewData["Title"]</h4> 9 | <partial name="_StatusMessage" model="Model.StatusMessage" /> 10 | <div class="row"> 11 | <div class="col-md-6"> 12 | <form id="email-form" method="post"> 13 | <div asp-validation-summary="All" class="text-danger"></div> 14 | <div class="form-group"> 15 | <label asp-for="Email"></label> 16 | @if (Model.IsEmailConfirmed) 17 | { 18 | <div class="input-group"> 19 | <input asp-for="Email" class="form-control" disabled /> 20 | <div class="input-group-append"> 21 | <span class="input-group-text text-success font-weight-bold">✓</span> 22 | </div> 23 | </div> 24 | } 25 | else 26 | { 27 | <input asp-for="Email" class="form-control" disabled /> 28 | <button id="email-verification" type="submit" asp-page-handler="SendVerificationEmail" class="btn btn-link">Send verification email</button> 29 | } 30 | </div> 31 | <div class="form-group"> 32 | <label asp-for="Input.NewEmail"></label> 33 | <input asp-for="Input.NewEmail" class="form-control" /> 34 | <span asp-validation-for="Input.NewEmail" class="text-danger"></span> 35 | </div> 36 | <button id="change-email-button" type="submit" asp-page-handler="ChangeEmail" class="btn btn-primary">Change email</button> 37 | </form> 38 | </div> 39 | </div> 40 | 41 | @section Scripts { 42 | <partial name="_ValidationScriptsPartial" /> 43 | } 44 | -------------------------------------------------------------------------------- /Data/BeautyBooking.Data/Repositories/EfRepository.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Data.Repositories 2 | { 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | using BeautyBooking.Data.Common.Repositories; 8 | 9 | using Microsoft.EntityFrameworkCore; 10 | 11 | public class EfRepository<TEntity> : IRepository<TEntity> 12 | where TEntity : class 13 | { 14 | public EfRepository(ApplicationDbContext context) 15 | { 16 | this.Context = context ?? throw new ArgumentNullException(nameof(context)); 17 | this.DbSet = this.Context.Set<TEntity>(); 18 | } 19 | 20 | protected DbSet<TEntity> DbSet { get; set; } 21 | 22 | protected ApplicationDbContext Context { get; set; } 23 | 24 | public virtual IQueryable<TEntity> All() => this.DbSet; 25 | 26 | public virtual IQueryable<TEntity> AllAsNoTracking() => this.DbSet.AsNoTracking(); 27 | 28 | public virtual Task AddAsync(TEntity entity) => this.DbSet.AddAsync(entity).AsTask(); 29 | 30 | public virtual void Update(TEntity entity) 31 | { 32 | var entry = this.Context.Entry(entity); 33 | if (entry.State == EntityState.Detached) 34 | { 35 | this.DbSet.Attach(entity); 36 | } 37 | 38 | entry.State = EntityState.Modified; 39 | } 40 | 41 | public virtual void Delete(TEntity entity) => this.DbSet.Remove(entity); 42 | 43 | public Task<int> SaveChangesAsync() => this.Context.SaveChangesAsync(); 44 | 45 | public void Dispose() 46 | { 47 | this.Dispose(true); 48 | GC.SuppressFinalize(this); 49 | } 50 | 51 | protected virtual void Dispose(bool disposing) 52 | { 53 | if (disposing) 54 | { 55 | this.Context?.Dispose(); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/wwwroot/plugins/font-awesome-4.7.0/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | @mixin fa-icon-rotate($degrees, $rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; 16 | -webkit-transform: rotate($degrees); 17 | -ms-transform: rotate($degrees); 18 | transform: rotate($degrees); 19 | } 20 | 21 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; 23 | -webkit-transform: scale($horiz, $vert); 24 | -ms-transform: scale($horiz, $vert); 25 | transform: scale($horiz, $vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | @mixin sr-only { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | @mixin sr-only-focusable { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Web/BeautyBooking.Web/Controllers/BlogPostsController.cs: -------------------------------------------------------------------------------- 1 | namespace BeautyBooking.Web.Controllers 2 | { 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | using BeautyBooking.Services.Data.BlogPosts; 7 | using BeautyBooking.Web.ViewModels.BlogPosts; 8 | using BeautyBooking.Web.ViewModels.Common.Pagination; 9 | using Microsoft.AspNetCore.Mvc; 10 | 11 | public class BlogPostsController : BaseController 12 | { 13 | private readonly IBlogPostsService blogPostsService; 14 | 15 | public BlogPostsController(IBlogPostsService blogPostsService) 16 | { 17 | this.blogPostsService = blogPostsService; 18 | } 19 | 20 | public async Task<IActionResult> Index( 21 | int? sortId, 22 | int? pageNumber) // blogPostId 23 | { 24 | if (sortId != null) 25 | { 26 | var blogPost = await this.blogPostsService 27 | .GetByIdAsync<BlogPostViewModel>(sortId.Value); 28 | if (blogPost == null) 29 | { 30 | return new StatusCodeResult(404); 31 | } 32 | } 33 | 34 | this.ViewData["CurrentSort"] = sortId; 35 | 36 | int pageSize = PageSizesConstants.BlogPosts; 37 | var pageIndex = pageNumber ?? 1; 38 | 39 | var blogPosts = await this.blogPostsService 40 | .GetAllWithPagingAsync<BlogPostViewModel>(sortId, pageSize, pageIndex); 41 | var blogPostsList = blogPosts.ToList(); 42 | 43 | var count = await this.blogPostsService.GetCountForPaginationAsync(); 44 | 45 | var viewModel = new BlogPostsPaginatedListViewModel 46 | { 47 | BlogPosts = new PaginatedList<BlogPostViewModel>(blogPostsList, count, pageIndex, pageSize), 48 | }; 49 | 50 | return this.View(viewModel); 51 | } 52 | } 53 | } 54 | --------------------------------------------------------------------------------