diff --git a/IRaCIS.Core.API.sln b/IRaCIS.Core.API.sln new file mode 100644 index 00000000..16d41fdc --- /dev/null +++ b/IRaCIS.Core.API.sln @@ -0,0 +1,95 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRaCIS.Core.API", "IRaCIS.Core.API\IRaCIS.Core.API.csproj", "{F15CE209-6039-46A6-AE7F-E81ADA795F28}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRaCIS.Core.Domain", "IRaCIS.Core.Domain\IRaCIS.Core.Domain.csproj", "{D4DF27AC-3739-4264-BFB8-AED6DC2B84C7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRaCIS.Core.Application", "IRaCIS.Core.Application\IRaCIS.Core.Application.csproj", "{037E7DE9-0AE2-4987-8C69-F51D5B9CA19D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRaCIS.Core.Domain.Share", "IRaCIS.Core.Domain.Share\IRaCIS.Core.Domain.Share.csproj", "{7CBC76F5-3817-46B7-8D9D-79253A89B578}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRaCIS.JWT.RS256", "ZhiZhunAuthenticationCenter\IRaCIS.JWT.RS256.csproj", "{3EF210EE-D5D1-4C93-A8FA-E0DB1852BD39}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRaCIS.Core.Test", "IRaCIS.Core.Test\IRaCIS.Core.Test.csproj", "{3292B2B4-6E8A-43AA-84C0-AB4A391E8A2A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRaCIS.Core.Infra.EFCore", "IRaCIS.Core.Infra.EFCore\IRaCIS.Core.Infra.EFCore.csproj", "{6D8115E5-84D6-424B-8F8D-0C2D40347A8C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRaCIS.Core.HttpReports", "IRaCIS.Core.HttpReports\IRaCIS.Core.HttpReports.csproj", "{09A6A5BE-FE36-4822-A405-432876B284A3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRaCIS.Core.Infrastructure", "IRaCIS.Core.Infrastructure\IRaCIS.Core.Infrastructure.csproj", "{07EED0F8-08E6-46F3-ACBE-17BC1391BD4C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IRaCIS.AuthenticationCenter", "IRaCIS.AuthenticationCenter", "{481329D6-B8A0-491F-A398-1DF66A0FBB62}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRaCIS.Core.IdentityServer4", "IRaCIS.Core.IdentityServer4\IRaCIS.Core.IdentityServer4.csproj", "{C3DD48CF-B8B3-40F6-9BDB-B7C7F0851674}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRaCIS.Core.IdentityServer4.MVC", "IRaCIS.Core.IdentityServer4.MVC\IRaCIS.Core.IdentityServer4.MVC.csproj", "{F621ADD6-94E8-4A4B-998E-25B8EF15D39C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LearningTest", "LearningTest", "{520F583F-FB33-4CA5-AF4E-577BF40389CE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F15CE209-6039-46A6-AE7F-E81ADA795F28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F15CE209-6039-46A6-AE7F-E81ADA795F28}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F15CE209-6039-46A6-AE7F-E81ADA795F28}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F15CE209-6039-46A6-AE7F-E81ADA795F28}.Release|Any CPU.Build.0 = Release|Any CPU + {D4DF27AC-3739-4264-BFB8-AED6DC2B84C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4DF27AC-3739-4264-BFB8-AED6DC2B84C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4DF27AC-3739-4264-BFB8-AED6DC2B84C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4DF27AC-3739-4264-BFB8-AED6DC2B84C7}.Release|Any CPU.Build.0 = Release|Any CPU + {037E7DE9-0AE2-4987-8C69-F51D5B9CA19D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {037E7DE9-0AE2-4987-8C69-F51D5B9CA19D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {037E7DE9-0AE2-4987-8C69-F51D5B9CA19D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {037E7DE9-0AE2-4987-8C69-F51D5B9CA19D}.Release|Any CPU.Build.0 = Release|Any CPU + {7CBC76F5-3817-46B7-8D9D-79253A89B578}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CBC76F5-3817-46B7-8D9D-79253A89B578}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CBC76F5-3817-46B7-8D9D-79253A89B578}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CBC76F5-3817-46B7-8D9D-79253A89B578}.Release|Any CPU.Build.0 = Release|Any CPU + {3EF210EE-D5D1-4C93-A8FA-E0DB1852BD39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3EF210EE-D5D1-4C93-A8FA-E0DB1852BD39}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3EF210EE-D5D1-4C93-A8FA-E0DB1852BD39}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3EF210EE-D5D1-4C93-A8FA-E0DB1852BD39}.Release|Any CPU.Build.0 = Release|Any CPU + {3292B2B4-6E8A-43AA-84C0-AB4A391E8A2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3292B2B4-6E8A-43AA-84C0-AB4A391E8A2A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3292B2B4-6E8A-43AA-84C0-AB4A391E8A2A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3292B2B4-6E8A-43AA-84C0-AB4A391E8A2A}.Release|Any CPU.Build.0 = Release|Any CPU + {6D8115E5-84D6-424B-8F8D-0C2D40347A8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D8115E5-84D6-424B-8F8D-0C2D40347A8C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D8115E5-84D6-424B-8F8D-0C2D40347A8C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D8115E5-84D6-424B-8F8D-0C2D40347A8C}.Release|Any CPU.Build.0 = Release|Any CPU + {09A6A5BE-FE36-4822-A405-432876B284A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09A6A5BE-FE36-4822-A405-432876B284A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09A6A5BE-FE36-4822-A405-432876B284A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09A6A5BE-FE36-4822-A405-432876B284A3}.Release|Any CPU.Build.0 = Release|Any CPU + {07EED0F8-08E6-46F3-ACBE-17BC1391BD4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07EED0F8-08E6-46F3-ACBE-17BC1391BD4C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07EED0F8-08E6-46F3-ACBE-17BC1391BD4C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07EED0F8-08E6-46F3-ACBE-17BC1391BD4C}.Release|Any CPU.Build.0 = Release|Any CPU + {C3DD48CF-B8B3-40F6-9BDB-B7C7F0851674}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3DD48CF-B8B3-40F6-9BDB-B7C7F0851674}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3DD48CF-B8B3-40F6-9BDB-B7C7F0851674}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3DD48CF-B8B3-40F6-9BDB-B7C7F0851674}.Release|Any CPU.Build.0 = Release|Any CPU + {F621ADD6-94E8-4A4B-998E-25B8EF15D39C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F621ADD6-94E8-4A4B-998E-25B8EF15D39C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F621ADD6-94E8-4A4B-998E-25B8EF15D39C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F621ADD6-94E8-4A4B-998E-25B8EF15D39C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {3EF210EE-D5D1-4C93-A8FA-E0DB1852BD39} = {481329D6-B8A0-491F-A398-1DF66A0FBB62} + {09A6A5BE-FE36-4822-A405-432876B284A3} = {520F583F-FB33-4CA5-AF4E-577BF40389CE} + {C3DD48CF-B8B3-40F6-9BDB-B7C7F0851674} = {481329D6-B8A0-491F-A398-1DF66A0FBB62} + {F621ADD6-94E8-4A4B-998E-25B8EF15D39C} = {481329D6-B8A0-491F-A398-1DF66A0FBB62} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BCC2EB19-3914-489B-B1D7-B7303E0218A3} + EndGlobalSection +EndGlobal diff --git a/IRaCIS.Core.API/.config/dotnet-tools.json b/IRaCIS.Core.API/.config/dotnet-tools.json new file mode 100644 index 00000000..06b86828 --- /dev/null +++ b/IRaCIS.Core.API/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "5.0.9", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.API/.preview.jpg b/IRaCIS.Core.API/.preview.jpg new file mode 100644 index 00000000..e69de29b diff --git a/IRaCIS.Core.API/AnonymizeTagSetting.json b/IRaCIS.Core.API/AnonymizeTagSetting.json new file mode 100644 index 00000000..6576ca04 --- /dev/null +++ b/IRaCIS.Core.API/AnonymizeTagSetting.json @@ -0,0 +1,106 @@ +{ + "needAnonymizeTag": [ + { //PatientsName + "Group": "0010", + "Element": "0010", + "ReplaceValue": "", + "Enable": true + }, + { // PatientID + "Group": "0010", + "Element": "0020", + "ReplaceValue": "", + "Enable": true + }, + { // IssuerOfPatientID + "Group": "0010", + "Element": "0021", + "ReplaceValue": "", + "Enable": true + }, + { // PatientsBirthDate + "Group": "0010", + "Element": "0030", + "ReplaceValue": "", + "Enable": true + }, + { // PatientsBirthTime + "Group": "0010", + "Element": "0032", + "ReplaceValue": "", + "Enable": false + }, + { // PatientsSex + "Group": "0010", + "Element": "0040", + "ReplaceValue": "", + "Enable": false + }, + { // OtherPatientIDs + "Group": "0010", + "Element": "1000", + "ReplaceValue": "", + "Enable": false + }, + { // OtherPatientNames + "Group": "0010", + "Element": "1001", + "ReplaceValue": "", + "Enable": false + }, + { // OtherPatientNames + "Group": "0010", + "Element": "1005", + "ReplaceValue": "", + "Enable": true + }, + { // PatientBirthName + "Group": "0010", + "Element": "1005", + "ReplaceValue": "", + "Enable": true + }, + { // PatientsAge + "Group": "0010", + "Element": "1010", + "ReplaceValue": "", + "Enable": true + }, + { // PatientsAddress + "Group": "0010", + "Element": "1040", + "ReplaceValue": "", + "Enable": true + }, + { // PatientsMothersBirthName + "Group": "0010", + "Element": "1060", + "ReplaceValue": "", + "Enable": true + }, + { + "Group": "0010", + "Element": "2150", + "ReplaceValue": "", + "Enable": true + }, + { + "Group": "0010", + "Element": "2152", + "ReplaceValue": "", + "Enable": true + }, + { + "Group": "0010", + "Element": "2154", + "ReplaceValue": "", + "Enable": true + }, + { + "Group": "0012", + "Element": "0040", + "ReplaceValue": "XXX", + "Enable": true + } + ] +} \ No newline at end of file diff --git a/IRaCIS.Core.API/Controllers/DownLoadController.cs b/IRaCIS.Core.API/Controllers/DownLoadController.cs new file mode 100644 index 00000000..5f0506fe --- /dev/null +++ b/IRaCIS.Core.API/Controllers/DownLoadController.cs @@ -0,0 +1,74 @@ +//using AutoMapper; +//using AutoMapper.QueryableExtensions; +//using IRaCIS.Application.ViewModels; +//using IRaCIS.Core.Application.Contracts; +//using IRaCIS.Core.Application.Filter; +//using IRaCIS.Core.Application.MediatR.CommandAndQueries; +//using IRaCIS.Core.Domain.Models; +//using IRaCIS.Core.Domain.Share; +//using IRaCIS.Core.Infra.EFCore; +//using IRaCIS.Core.Infrastructure.Extention; +//using Magicodes.ExporterAndImporter.Core; +//using Magicodes.ExporterAndImporter.Excel; +//using MediatR; +//using Microsoft.AspNetCore.Hosting; +//using Microsoft.AspNetCore.Http; +//using Microsoft.AspNetCore.Mvc; +//using Microsoft.AspNetCore.WebUtilities; +//using Microsoft.Net.Http.Headers; +//using System; +//using System.IO; +//using System.Linq; +//using System.Threading.Tasks; + +//namespace IRaCIS.Core.API.Controllers +//{ +// [ApiExplorerSettings(GroupName = "Image")] +// [ApiController] +// public class DownLoadController : ControllerBase +// { +// public IMapper _mapper { get; set; } +// public IUserInfo _userInfo { get; set; } +// private readonly IMediator _mediator; + +// private readonly IWebHostEnvironment _hostEnvironment; + +// private readonly IServiceProvider _serviceProvider; + + +// public DownLoadController(IMapper mapper, IUserInfo userInfo, IMediator mediator, IWebHostEnvironment hostEnvironment, IServiceProvider serviceProvider) +// { +// _serviceProvider = serviceProvider; +// _hostEnvironment = hostEnvironment; +// _mediator = mediator; +// _mapper = mapper; +// _userInfo = userInfo; +// } + +// [HttpGet("VisitPlan/DownloadInflunceStudyList{trialId:guid}/{createTime:dateTime}")] +// public async Task DownloadInflunceStudyList(Guid trialId, DateTime createTime, [FromServices] IRepository _influnceRepository) +// { +// var list = _influnceRepository.Where(t => t.TrialId == trialId && t.CreateTime == createTime) +// .ProjectTo(_mapper.ConfigurationProvider).ToList(); + +// if(list.Count == 0) +// { +// list.Add(new VisitPlanInfluenceSubjectVisitDTO() { CreateTime=DateTime.Now,SubjectCode="test",StudyTime=DateTime.Now,IsDicomStudy=false,HistoryWindow="test"}); +// } + +// IExporter exporter = new ExcelExporter(); + +// var result = await exporter.ExportAsByteArray(list); + + +// return new XlsxFileResult(bytes: bytes); + +// //return File(result, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $"检查导出_{DateTime.Now}.xlsx"); + +// } + + + + +// } +//} diff --git a/IRaCIS.Core.API/Controllers/ErrorController.cs b/IRaCIS.Core.API/Controllers/ErrorController.cs new file mode 100644 index 00000000..c273d3e5 --- /dev/null +++ b/IRaCIS.Core.API/Controllers/ErrorController.cs @@ -0,0 +1,39 @@ +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Mvc; +using Panda.DynamicWebApi.Attributes; + +namespace EasyCaching.Demo.Interceptors.Controllers +{ + + [NonDynamicWebApi] + public class ErrorController : ControllerBase + { + /// + /// 主要处理 前端404等错误 全局业务异常已统一处理了,非业务错误会来到这里 + /// + /// + /// + [Route("error/{code:int}")] + [HttpGet] + public IResponseOutput Error(int code) + { + + if (code < 500) + { + //LogDashboard 要求返回码必须是401不能覆盖,否则 认证有问题 + if (code == 401) + { + ControllerContext.HttpContext.Response.StatusCode = 401; + } + + return ResponseOutput.NotOk($"Client error, actual request error status code({code})"); + } + else + { + + return ResponseOutput.NotOk($"Server error , actual request error status code({code})"); + } + + } + } +} diff --git a/IRaCIS.Core.API/Controllers/ExtraController.cs b/IRaCIS.Core.API/Controllers/ExtraController.cs new file mode 100644 index 00000000..74409fb4 --- /dev/null +++ b/IRaCIS.Core.API/Controllers/ExtraController.cs @@ -0,0 +1,241 @@ +using System; +using System.Net.Http; +using EasyCaching.Core; +using gRPC.ZHiZHUN.AuthServer.protos; +using Grpc.Net.Client; +using Grpc.Net.Client.Configuration; +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Application.Auth; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Http; +using IRaCIS.Core.Application.Interfaces; +using System.Threading.Tasks; +using IRaCIS.Application.Services; + +namespace IRaCIS.Api.Controllers +{ + /// + /// 医生基本信息 、工作信息 专业信息、审核状态 + /// + [ApiController, ApiExplorerSettings(GroupName = "Reviewer")] + public class ExtraController : ControllerBase + { + + + /// + /// 获取医生详情 + /// + /// + /// + /// + /// + /// + /// + /// + /// + [HttpGet, Route("doctor/getDetail/{doctorId:guid}")] + + public async Task> GetDoctorDetail([FromServices] IAttachmentService attachmentService, [FromServices] IDoctorService _doctorService, + [FromServices] IEducationService _educationService, [FromServices] ITrialExperienceService _trialExperienceService, + [FromServices] IResearchPublicationService _researchPublicationService, [FromServices] IVacationService _vacationService, Guid doctorId) + { + var education = await _educationService.GetEducation(doctorId); + + var sowList = _doctorService.GetDoctorSowList(doctorId); + var ackSowList = _doctorService.GetDoctorAckSowList(doctorId); + + var doctorDetail = new DoctorDetailDTO + { + AuditView =await _doctorService.GetAuditState(doctorId), + BasicInfoView = await _doctorService.GetBasicInfo(doctorId), + EmploymentView = await _doctorService.GetEmploymentInfo(doctorId), + //AttachmentList = attachmentService.GetAttachments(doctorId), + + EducationList = education.EducationList, + PostgraduateList = education.PostgraduateList, + + TrialExperienceView = await _trialExperienceService.GetTrialExperience(doctorId), + ResearchPublicationView = await _researchPublicationService.GetResearchPublication(doctorId), + + SpecialtyView =await _doctorService.GetSpecialtyInfo(doctorId), + InHoliday = (await _vacationService.OnVacation(doctorId)).IsSuccess, + IntoGroupInfo = _doctorService.GetDoctorIntoGroupInfo(doctorId), + SowList = sowList, + AckSowList = ackSowList + }; + + return ResponseOutput.Ok(doctorDetail); + } + + + + + + [HttpPost, Route("enroll/downloadResume/{trialId:guid}/{language}")] + [TypeFilter(typeof(TrialResourceFilter))] + [AllowAnonymous] + public async Task> DownloadResume([FromServices] IFileService _fileService, int language, Guid trialId, Guid[] doctorIdArray) + { + var userId = Guid.Parse(User.FindFirst("id").Value); + var zipPath = await _fileService.CreateOfficialResumeZip(language, doctorIdArray); + + return ResponseOutput.Ok(zipPath); + } + + + + + /// 系统用户登录接口[New] + [HttpPost, Route("user/login")] + [AllowAnonymous] + public async Task> Login(UserLoginDTO loginUser, [FromServices] IEasyCachingProvider provider, [FromServices] IUserService _userService, + [FromServices] ITokenService _tokenService, [FromServices] IConfiguration configuration) + { + + var returnModel = await _userService.Login(loginUser.UserName, loginUser.Password); + + if (returnModel.IsSuccess) + { + #region GRPC 调用鉴权中心,因为服务器IIS问题 http/2 故而没法使用 + + ////重试策略 + //var defaultMethodConfig = new MethodConfig + //{ + // Names = { MethodName.Default }, + // RetryPolicy = new RetryPolicy + // { + // MaxAttempts = 3, + // InitialBackoff = TimeSpan.FromSeconds(1), + // MaxBackoff = TimeSpan.FromSeconds(5), + // BackoffMultiplier = 1.5, + // RetryableStatusCodes = { Grpc.Core.StatusCode.Unavailable } + // } + //}; + + //#region unable to trust the certificate then the gRPC client can be configured to ignore the invalid certificate + + //var httpHandler = new HttpClientHandler(); + //// Return `true` to allow certificates that are untrusted/invalid + //httpHandler.ServerCertificateCustomValidationCallback = + // HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + + + //////这一句是让grpc支持本地 http 如果本地访问部署在服务器上,那么是访问不成功的 + //AppContext.SetSwitch( + // "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + + //#endregion + + + + //var grpcAdress = configuration.GetValue("GrpcAddress"); + ////var grpcAdress = "http://localhost:7200"; + + //var channel = GrpcChannel.ForAddress(grpcAdress, new GrpcChannelOptions + //{ + // HttpHandler = httpHandler, + // ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } } + + //}); + ////var channel = GrpcChannel.ForAddress(grpcAdress); + //var grpcClient = new TokenGrpcService.TokenGrpcServiceClient(channel); + + //var userInfo = returnModel.Data.BasicInfo; + + //var tokenResponse = grpcClient.GetUserToken(new GetTokenReuqest() + //{ + // Id = userInfo.Id.ToString(), + // ReviewerCode = userInfo.ReviewerCode, + // IsAdmin = userInfo.IsAdmin, + // RealName = userInfo.RealName, + // UserTypeEnumInt = (int)userInfo.UserTypeEnum, + // UserTypeShortName = userInfo.UserTypeShortName, + // UserName = userInfo.UserName + //}); + + //returnModel.Data.JWTStr = tokenResponse.Token; + + #endregion + + returnModel.Data.JWTStr = _tokenService.GetToken(IRaCISClaims.Create(returnModel.Data.BasicInfo)); + + } + + var userId = returnModel.Data.BasicInfo.Id.ToString(); + provider.Set(userId, userId, TimeSpan.FromMinutes(AppSettings.LoginExpiredTimeSpan)); + return returnModel; + } + + + + [HttpGet, Route("imageShare/ShareImage")] + public IResponseOutput ShareImage([FromServices] ITokenService _tokenService) + { + var token = _tokenService.GetToken(IRaCISClaims.Create(new UserBasicInfo() + { + Id = Guid.Empty, + IsReviewer = false, + IsAdmin = false, + RealName = "Share001", + UserName = "Share001", + Sex = 0, + //UserType = "ShareType", + UserTypeEnum = UserTypeEnum.ShareImage, + Code = "ShareCode001", + })); + return ResponseOutput.Ok("/#/preview?studyId=d8e7ed1c-4b59-a483-563d-4f05bd4f8921&token=" + token); + } + + + + + //外部用户 邮件链接调用 以及跳转逻辑 + + [HttpGet("trialExternalUser/ExternalUserJoinTrial")] + [AllowAnonymous] + public async Task ExternalUserJoinTrial([FromServices] ITrialExternalUserService _trialExternalUserService, Guid trialId, Guid trialExternalUserId, string url) + { + await _trialExternalUserService.UserConfirmJoinTrial(trialId, trialExternalUserId); + + + var decodeUrl = System.Web.HttpUtility.UrlDecode(url); + + + return Redirect(decodeUrl); + } + + + + + + [HttpGet, Route("ip")] + [AllowAnonymous] + public IResponseOutput Get([FromServices] IHttpContextAccessor _context, [FromServices] IUserService _userService) + { + + StringBuilder sb = new StringBuilder(); + sb.AppendLine($"RemoteIpAddress:{_context.HttpContext.Connection.RemoteIpAddress}"); + + if (Request.Headers.ContainsKey("X-Real-IP")) + { + sb.AppendLine($"X-Real-IP:{Request.Headers["X-Real-IP"].ToString()}"); + } + + if (Request.Headers.ContainsKey("X-Forwarded-For")) + { + sb.AppendLine($"X-Forwarded-For:{Request.Headers["X-Forwarded-For"].ToString()}"); + } + return ResponseOutput.Ok(sb.ToString()); + } + + + } +} diff --git a/IRaCIS.Core.API/Controllers/FileController.cs b/IRaCIS.Core.API/Controllers/FileController.cs new file mode 100644 index 00000000..985be4e9 --- /dev/null +++ b/IRaCIS.Core.API/Controllers/FileController.cs @@ -0,0 +1,532 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading.Tasks; +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.API.Utility; +using IRaCIS.Core.Application.Contracts.Image; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Application.Contracts; + +namespace IRaCIS.Api.Controllers +{ + /// + /// 文件上传 + /// + [Route("file")] + [ApiController, Authorize, ApiExplorerSettings(GroupName = "Common")] + public class FileController : ControllerBase + { + private readonly IFileService _fileService; + private readonly IWebHostEnvironment _webHostEnvironment; + private readonly IHostEnvironment _hostEnvironment; + private ILogger _logger; + + private string _targetFilePath; + private readonly long _fileSizeLimit; + private readonly string[] _permittedExtensions = { ".pdf", ".doc", ".docx" }; + + public string trustedFileNameForFileStorage = ""; + + // Get the default form options so that we can use them to set the default + // limits for request body data. + private static readonly FormOptions _defaultFormOptions = new FormOptions(); + private string defaultUploadFilePath = string.Empty; + public FileController(IFileService fileService, Microsoft.Extensions.Configuration.IConfiguration config, + IHostEnvironment hostEnvironment, ILogger logger, + IWebHostEnvironment webHostEnvironment) + { + _fileService = fileService; + _hostEnvironment = hostEnvironment; + _webHostEnvironment = webHostEnvironment; + _fileSizeLimit = config.GetValue("FileSizeLimit"); + defaultUploadFilePath = Directory.GetParent(_hostEnvironment.ContentRootPath.TrimEnd('\\')).FullName; + + _logger = logger; + + _logger.LogWarning("File Path:" + defaultUploadFilePath); + + } + + + /// + /// 上传文件[FileUpload] + /// + /// 附件类型 + /// 医生Id + /// 返回文件信息 + [HttpPost, Route("uploadFile/{attachmentType}/{doctorId}")] + [DisableFormValueModelBinding] + public async Task UploadOrdinaryFile(string attachmentType, Guid doctorId) + { + #region 官方文档方式 + + + if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType)) + { + ModelState.AddModelError("File", + $"The request couldn't be processed (Error 1)."); + // Log error + return BadRequest(ModelState); + } + + var boundary = MultipartRequestHelper.GetBoundary( + MediaTypeHeaderValue.Parse(Request.ContentType), + _defaultFormOptions.MultipartBoundaryLengthLimit); + var reader = new MultipartReader(boundary, HttpContext.Request.Body); + var section = await reader.ReadNextSectionAsync(); + + while (section != null) + { + var hasContentDispositionHeader = + ContentDispositionHeaderValue.TryParse( + section.ContentDisposition, out var contentDisposition); + + if (hasContentDispositionHeader) + { + // This check assumes that there's a file + // present without form data. If form data + // is present, this method immediately fails + // and returns the model error. + if (!MultipartRequestHelper + .HasFileContentDisposition(contentDisposition)) + { + ModelState.AddModelError("File", + $"The request couldn't be processed (Error 2)."); + // Log error + return BadRequest(ModelState); + } + else + { + // Don't trust the file name sent by the client. To display + // the file name, HTML-encode the value. + var trustedFileNameForDisplay = WebUtility.HtmlEncode( + contentDisposition.FileName.Value); + + //var trustedFileNameForFileStorage = Path.GetRandomFileName(); + trustedFileNameForFileStorage = contentDisposition.FileName.Value; + + // **WARNING!** + // In the following example, the file is saved without + // scanning the file's contents. In most production + // scenarios, an anti-virus/anti-malware scanner API + // is used on the file before making the file available + // for download or for use by other systems. + // For more information, see the topic that accompanies + // this sample. + + var streamedFileContent = await FileHelpers.ProcessStreamedFile( + section, contentDisposition, ModelState, + _permittedExtensions, _fileSizeLimit); + + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + + //实际文件处理 + string uploadFolderPath = Path.Combine(defaultUploadFilePath, "UploadFile/" + doctorId + "/"); + + var doctorAttachmentUploadFolder = Path.Combine(uploadFolderPath, attachmentType); + + _targetFilePath = doctorAttachmentUploadFolder; + + if (!Directory.Exists(doctorAttachmentUploadFolder)) Directory.CreateDirectory(doctorAttachmentUploadFolder); + + using (var targetStream = System.IO.File.Create( + Path.Combine(_targetFilePath, trustedFileNameForFileStorage))) + { + await targetStream.WriteAsync(streamedFileContent); + + var attachmentPath = $"/UploadFile/{doctorId}/{attachmentType}/{trustedFileNameForFileStorage}"; + + return new JsonResult(ResponseOutput.Ok(new UploadFileInfoDTO() { FilePath = attachmentPath, FullFilePath = attachmentPath + "?access_token=" + HttpContext.Request.Headers["Authorization"].ToString().Substring(7) })); + + } + + + } + } + + // Drain any remaining section body that hasn't been consumed and + // read the headers for the next section. + section = await reader.ReadNextSectionAsync(); + } + + + return Created(nameof(FileController), null); + + #endregion + } + + + /// + /// 上传文件( 不是医生个人的文件)[FileUpload] + /// 例如:阅片章程等 + /// + /// 文件类型 + /// + + [HttpPost, Route("uploadNonDoctorFile/{type}")] + [DisableFormValueModelBinding] + public async Task UploadNonDoctorFile(string type) + { + #region New Test 实测OK + + //上传根路径 + string uploadFolderPath = Path.Combine(defaultUploadFilePath, "UploadFile"); + + + if (uploadFolderPath != null) + { + //文件类型路径处理 + var uploadTypePath = Path.Combine(uploadFolderPath, type); + if (!Directory.Exists(uploadTypePath)) Directory.CreateDirectory(uploadTypePath); + + + ////实际文件处理 + + _targetFilePath = uploadTypePath; + + //获取boundary + + #region 方式二 + + var boundary = MultipartRequestHelper.GetBoundary( + MediaTypeHeaderValue.Parse(Request.ContentType), + _defaultFormOptions.MultipartBoundaryLengthLimit); + + #endregion + + #region 方式一 + //var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(Request.ContentType).Boundary).Value; + + #endregion + + + //得到reader + var reader = new MultipartReader(boundary, HttpContext.Request.Body); + //{ BodyLengthLimit = 2000 };// + var section = await reader.ReadNextSectionAsync(); + + //读取section + while (section != null) + { + var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition); + if (hasContentDispositionHeader) + { + trustedFileNameForFileStorage = contentDisposition.FileName.Value; + + + await WriteFileAsync(section.Body, Path.Combine(_targetFilePath, trustedFileNameForFileStorage)); + + #region 方式二 + + //using (var targetStream = System.IO.File.Create( + // Path.Combine(_targetFilePath, trustedFileNameForFileStorage))) + //{ + + // using (var memoryStream = new MemoryStream()) + // { + // await section.Body.CopyToAsync(memoryStream); + + // await targetStream.WriteAsync(memoryStream.ToArray()); + // } + //} + + + #endregion + + + var attachmentPath = $"/UploadFile/{type}/{trustedFileNameForFileStorage}"; + + return new JsonResult(ResponseOutput.Ok(new UploadFileInfoDTO() { FilePath = attachmentPath, FullFilePath = attachmentPath + "?access_token=" + HttpContext.Request.Headers["Authorization"].ToString().Substring(7) })); + } + + section = await reader.ReadNextSectionAsync(); + } + return Created(nameof(FileController), null); + + + } + return new JsonResult(ResponseOutput.NotOk("服务器端映射路径操作失败")); + #endregion + } + + + + /// + /// 写文件导到磁盘 + /// + /// 流 + /// 文件保存路径 + /// + public static async Task WriteFileAsync(System.IO.Stream stream, string path) + { + const int FILE_WRITE_SIZE = 84975;//写出缓冲区大小 + int writeCount = 0; + using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Write, FILE_WRITE_SIZE, true)) + { + byte[] byteArr = new byte[FILE_WRITE_SIZE]; + int readCount = 0; + while ((readCount = await stream.ReadAsync(byteArr, 0, byteArr.Length)) > 0) + { + await fileStream.WriteAsync(byteArr, 0, readCount); + writeCount += readCount; + } + } + return writeCount; + + + } + + + + + #region 上传无影响部分 + + /// + /// 下载多个医生的所有附件 + /// + /// + /// + + [HttpPost, Route("downloadDoctorAttachments")] + public async Task> DownloadAttachment(Guid[] doctorIds) + { + + var path = await _fileService.CreateDoctorsAllAttachmentZip(doctorIds); + + return ResponseOutput.Ok(new UploadFileInfoDTO + { + FilePath = path, + FullFilePath = path + "?access_token=" + HttpContext.Request.Headers["Authorization"].ToString().Substring(7) + + }); + } + + /// + /// 下载医生官方简历 + /// + /// + /// + /// + [HttpPost, Route("downloadOfficialCV/{language}")] + public async Task> DownloadOfficialResume(int language, Guid[] doctorIds) + { + + var path = _fileService.CreateDoctorsAllAttachmentZip(doctorIds); + return ResponseOutput.Ok(new UploadFileInfoDTO + { + FilePath = await _fileService.CreateOfficialResumeZip(language, doctorIds), + FullFilePath = path + "?access_token=" + HttpContext.Request.Headers["Authorization"].ToString().Substring(7) + }); + } + + /// + /// 下载指定医生的指定附件 + /// + /// 医生Id + /// 要下载的附件Id + /// + [HttpPost, Route("downloadByAttachmentId/{doctorId}")] + public async Task> DownloadAttachmentById(Guid doctorId, Guid[] attachmentIds) + { + var path = await _fileService.CreateZipPackageByAttachment(doctorId, attachmentIds); + return ResponseOutput.Ok(new UploadFileInfoDTO + { + FilePath = await _fileService.CreateZipPackageByAttachment(doctorId, attachmentIds), + FullFilePath = path + "?access_token=" + HttpContext.Request.Headers["Authorization"].ToString().Substring(7) + }); + } + + #endregion + + + #region 废弃 + + public class UploadDTFCommand + { + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public Guid SubjectId { get; set; } + public Guid SubjectVisitId { get; set; } + + } + /// + /// 流式上传 临时文件 + /// + /// + [HttpPost("UploadDTF/{trialId:guid}/{siteId:guid}/{subjectId:guid}/{subjectVisitId:guid}/{studyId:guid}")] + [DisableFormValueModelBinding] + [DisableRequestSizeLimit] + [Obsolete] + public async Task UploadingStream(Guid trialId, Guid siteId, Guid subjectId, Guid subjectVisitId, Guid studyId, [FromServices] IStudyDTFService studyDTFService) + { + + //上传根路径 + string uploadFolderPath = Path.Combine(defaultUploadFilePath, "Dicom"); + + + var dtfPath = Path.Combine(uploadFolderPath, DateTime.Now.Year.ToString(), trialId.ToString(), + siteId.ToString(), subjectId.ToString(), subjectVisitId.ToString()); + + if (!Directory.Exists(dtfPath)) + { + Directory.CreateDirectory(dtfPath); + } + + + #region 之前DTF 先上传临时文件,再拷贝 + ////上传根路径 + //string uploadFolderPath = Path.Combine(defaultUploadFilePath, "UploadFile"); + ////文件类型路径处理 + //var uploadTempFilePath = Path.Combine(uploadFolderPath, "TempFile"); + //if (!Directory.Exists(uploadTempFilePath)) Directory.CreateDirectory(uploadTempFilePath); + + + //var trustedFileNameForFileStorage = Guid.NewGuid().ToString() + fileNameEX; + + //await WriteFileAsync(section.Body, Path.Combine(uploadTempFilePath, trustedFileNameForFileStorage)); + + //var attachmentPath = $"{uploadTempFilePath}/{trustedFileNameForFileStorage}"; + + ////多个文件上传,在这里返回就不合适,需要在外层,返回所有的文件路径,实际需要时再更改 + //return ResponseOutput.Ok(new UploadFileInfoDTO() { FilePath = attachmentPath }); + + #endregion + + + //获取boundary + var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(Request.ContentType).Boundary).Value; + //得到reader + var reader = new MultipartReader(boundary, HttpContext.Request.Body); + + var section = await reader.ReadNextSectionAsync(); + + //读取section + while (section != null) + { + var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition); + if (hasContentDispositionHeader) + { + var realName = contentDisposition.FileName.Value; + + var fileNameEX = Path.GetExtension(contentDisposition.FileName.Value); + var trustedFileNameForFileStorage = Guid.NewGuid().ToString() + fileNameEX; + + + var relativePath = $"/Dicom/{DateTime.Now.Year.ToString()}/{trialId}/{siteId}/{subjectId}/{subjectVisitId}/{trustedFileNameForFileStorage}"; + studyDTFService.AddStudyDTF(new Core.Application.Contracts.Dicom.DTO.StudyDTFAddOrUpdateCommand() + { + StudyId = studyId, + + FileName = realName, + + Path = relativePath + }); + + await WriteFileAsync(section.Body, Path.Combine(dtfPath, trustedFileNameForFileStorage)); + + //仅仅返回一个文件,如果多文件上传 在最后返回多个路径 + return ResponseOutput.Ok(new UploadFileInfoDTO() { FilePath = relativePath, FullFilePath = relativePath + "?access_token=" + HttpContext.Request.Headers["Authorization"].ToString().Substring(7) }); + } + section = await reader.ReadNextSectionAsync(); + } + return ResponseOutput.NotOk("Upload error"); + } + + + /// + /// 流式上传 非Dicom文件 + /// + /// + [HttpPost("UploadNoneDICOM")] + [DisableFormValueModelBinding] + [DisableRequestSizeLimit] + [Obsolete] + public async Task UploadingNoneDicomStream([FromForm] ArchiveStudyCommand archiveStudyCommand, [FromServices] IStudyService studyService) + { + //上传根路径 + string uploadFolderPath = Path.Combine(defaultUploadFilePath, "Dicom"); + + //文件类型路径处理 + //var noneDicomPath = Path.Combine(uploadFolderPath, DateTime.Now.Year.ToString(), archiveStudyCommand.TrialId.ToString(), + // archiveStudyCommand.SiteId.ToString(), archiveStudyCommand.SubjectId.ToString(), archiveStudyCommand.SubjectVisitId.ToString(), "Data"); + + //var dtfPath = Path.Combine(uploadFolderPath, DateTime.Now.Year.ToString(), archiveStudyCommand.TrialId.ToString(), + // archiveStudyCommand.SiteId.ToString(), archiveStudyCommand.SubjectId.ToString(), archiveStudyCommand.SubjectVisitId.ToString()); + + var noneDicomPath = string.Empty; + + var dtfPath = string.Empty; + + var tempDtfPath = Path.Combine(defaultUploadFilePath/*, archiveStudyCommand.DTFPath*/); + + + if (!Directory.Exists(noneDicomPath)) + { + Directory.CreateDirectory(noneDicomPath); + } + if (!System.IO.File.Exists(tempDtfPath)) + { + return ResponseOutput.NotOk("DTF file can not found"); + } + + System.IO.File.Move(tempDtfPath, dtfPath); + + var saveFileDic = new Dictionary(); + + //获取boundary + var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(Request.ContentType).Boundary).Value; + //得到reader + var reader = new MultipartReader(boundary, HttpContext.Request.Body); + + var section = await reader.ReadNextSectionAsync(); + + //读取section + while (section != null) + { + var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition); + if (hasContentDispositionHeader) + { + var fileName = contentDisposition.FileName.Value; + var fileNameEX = Path.GetExtension(contentDisposition.FileName.Value); + var trustedFileNameForFileStorage = Guid.NewGuid() + fileNameEX; + + + + await WriteFileAsync(section.Body, Path.Combine(noneDicomPath, trustedFileNameForFileStorage)); + + var attachmentPath = $"{noneDicomPath}/{trustedFileNameForFileStorage}"; + + saveFileDic.Add(attachmentPath, fileName); + + + } + section = await reader.ReadNextSectionAsync(); + } + + //处理数据库操作 + + //studyService.DealNonDicomFile(saveFileDic, archiveStudyCommand); + return ResponseOutput.Ok(saveFileDic); + } + #endregion + + } +} diff --git a/IRaCIS.Core.API/Controllers/FinancialChangeController.cs b/IRaCIS.Core.API/Controllers/FinancialChangeController.cs new file mode 100644 index 00000000..13ef7628 --- /dev/null +++ b/IRaCIS.Core.API/Controllers/FinancialChangeController.cs @@ -0,0 +1,303 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Authorization; +using System.Threading.Tasks; +using IRaCIS.Application.Services; + +namespace IRaCIS.Core.API.Controllers.Special +{ + //谨慎修改 涉及到财务模块 + + [ApiController, Authorize, ApiExplorerSettings(GroupName = "Financial")] + public class FinancialChangeController : ControllerBase + { + private readonly ITrialService _trialService; + private readonly ICalculateService _calculateService; + + public FinancialChangeController(ITrialService trialService, ICalculateService calculateService) + { + _trialService = trialService; + _calculateService = calculateService; + } + + /// 添加实验项目-返回新增Id[AUTH] + /// + /// 新记录Id + + //[TrialAudit(AuditType.TrialAudit, AuditOptType.AddOrUpdateTrial)] + + [HttpPost, Route("trial/addOrUpdateTrial")] + + public async Task AddOrUpdateTrial(TrialCommand param) + { + var userId = Guid.Parse(User.FindFirst("id").Value); + var result = await _trialService.AddOrUpdateTrial(param); + + if (_trialService.TrialExpeditedChange) + { + var needCalReviewerIds = await _trialService.GetTrialEnrollmentReviewerIds(param.Id.Value); + var calcList = await _calculateService.GetNeedCalculateReviewerList(Guid.Empty, string.Empty); + + calcList.ForEach(t => + { + if (needCalReviewerIds.Contains(t.DoctorId)) + { + _calculateService.CalculateMonthlyPayment(new CalculateDoctorAndMonthDTO() + { + NeedCalculateReviewers = new List() + { + t.DoctorId + }, + CalculateMonth = DateTime.Parse(t.YearMonth) + }, User.FindFirst("id").Value); + + } + }); + } + + return result; + } + + + /// + /// 添加或更新工作量[AUTH] + /// + /// + /// + /// + + [HttpPost, Route("doctorWorkload/workLoadAddOrUpdate")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task WorkLoadAddOrUpdate([FromServices] IDoctorWorkloadService _trialWorkloadService, WorkloadCommand workLoadAddOrUpdateModel) + { + var userId = Guid.Parse(User.FindFirst("id").Value); + var result = await _trialWorkloadService.AddOrUpdateWorkload(workLoadAddOrUpdateModel, userId); + if (result.IsSuccess && workLoadAddOrUpdateModel.DataFrom == 2) + { + await _calculateService.CalculateMonthlyPayment(new CalculateDoctorAndMonthDTO() + { + NeedCalculateReviewers = new List() + { + workLoadAddOrUpdateModel.DoctorId + }, + CalculateMonth = workLoadAddOrUpdateModel.WorkTime + }, User.FindFirst("id").Value); + } + return result; + } + + + + [HttpDelete, Route("doctorWorkload/deleteWorkLoad/{id:guid}/{trialId:guid}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task DeleteWorkLoad([FromServices] IDoctorWorkloadService _trialWorkloadService, Guid id) + { + //先判断该工作量的费用是否被锁定,如果被锁定,则不能删除 + var workload = await _trialWorkloadService.GetWorkloadDetailById(id); + var yearMonth = workload.WorkTime.ToString("yyyy-MM"); + var isLock = await _calculateService.IsLock(workload.DoctorId, yearMonth); + + if (isLock) + { + return ResponseOutput.NotOk("Expenses have been settled and workload can not be reset."); + } + + var deleteResult = await _trialWorkloadService.DeleteWorkload(id); + if (workload.DataFrom == (int)Domain.Share.WorkLoadFromStatus.FinalConfirm) + { + await _calculateService.CalculateMonthlyPayment(new CalculateDoctorAndMonthDTO() + { + NeedCalculateReviewers = new List() + { + workload.DoctorId + }, + CalculateMonth = workload.WorkTime + }, User.FindFirst("id").Value); + } + return deleteResult; + } + + + /// + /// 添加或更新汇率(会触发没有对锁定的费用计算) + /// + + [HttpPost, Route("exchangeRate/addOrUpdateExchangeRate")] + public async Task AddOrUpdateExchangeRate([FromServices] IExchangeRateService _exchangeRateService, [FromServices] IPaymentAdjustmentService _costAdjustmentService, ExchangeRateCommand addOrUpdateModel) + { + var result = await _exchangeRateService.AddOrUpdateExchangeRate(addOrUpdateModel); + var calcList = await _calculateService.GetNeedCalculateReviewerList(Guid.Empty, addOrUpdateModel.YearMonth); + foreach (var item in calcList) + { + if (item != null) + { + await _calculateService.CalculateMonthlyPayment(new CalculateDoctorAndMonthDTO() + { + NeedCalculateReviewers = new List() + { + item.DoctorId + }, + CalculateMonth = DateTime.Parse(item.YearMonth) + }, User.FindFirst("id").Value); + } + } + _costAdjustmentService.CalculateCNY(addOrUpdateModel.YearMonth, addOrUpdateModel.Rate); + return result; + } + + + /// + /// 添加或更新 职称单价[AUTH] + /// + + [HttpPost, Route("rankPrice/addOrUpdateRankPrice")] + public async Task AddOrUpdateRankPrice([FromServices] IReviewerPayInfoService _reviewerPayInfoService, [FromServices] IRankPriceService _rankPriceService, RankPriceCommand addOrUpdateModel) + { + if (addOrUpdateModel.Id != Guid.Empty && addOrUpdateModel.Id != null) + { + var needCalReviewerIds =await _reviewerPayInfoService.GetReviewerIdByRankId(Guid.Parse(addOrUpdateModel.Id.ToString())); + var calcList = await _calculateService.GetNeedCalculateReviewerList(Guid.Empty, string.Empty); + + foreach (var item in calcList) + { + if (item != null && needCalReviewerIds.Contains(item.DoctorId)) + { + await _calculateService.CalculateMonthlyPayment(new CalculateDoctorAndMonthDTO() + { + NeedCalculateReviewers = new List() + { + item.DoctorId + }, + CalculateMonth = DateTime.Parse(item.YearMonth) + }, User.FindFirst("id").Value); + } + } + } + var userId = Guid.Parse(User.FindFirst("id").Value); + return await _rankPriceService.AddOrUpdateRankPrice(addOrUpdateModel, userId); + } + + + + /// + /// 添加或更新(替换)医生支付展信息[AUTH] + /// + + [HttpPost, Route("reviewerPayInfo/addOrUpdateReviewerPayInfo")] + public async Task AddOrUpdateReviewerPayInfo([FromServices] IReviewerPayInfoService _doctorPayInfoService, ReviewerPayInfoCommand addOrUpdateModel) + { + var userId = Guid.Parse(User.FindFirst("id").Value); + var result =await _doctorPayInfoService.AddOrUpdateReviewerPayInfo(addOrUpdateModel, userId); + var calcList = await _calculateService.GetNeedCalculateReviewerList(addOrUpdateModel.DoctorId, string.Empty); + foreach (var item in calcList) + { + if (item != null) + { + await _calculateService.CalculateMonthlyPayment(new CalculateDoctorAndMonthDTO() + { + NeedCalculateReviewers = new List() + { + item.DoctorId + }, + CalculateMonth = DateTime.Parse(item.YearMonth) + }, User.FindFirst("id").Value); + } + } + return result; + } + + + + /// + /// 保存(替换)项目支付价格信息(会触发没有被锁定的费用计算)[AUTH] + /// + + [HttpPost, Route("trialPaymentPrice/addOrUpdateTrialPaymentPrice")] + public async Task AddOrUpdateTrialPaymentPrice([FromServices] ITrialPaymentPriceService _trialPaymentPriceService, TrialPaymentPriceCommand addOrUpdateModel) + { + var userId = Guid.Parse(User.FindFirst("id").Value); + var result =await _trialPaymentPriceService.AddOrUpdateTrialPaymentPrice(addOrUpdateModel); + var needCalReviewerIds = await _trialService.GetTrialEnrollmentReviewerIds(addOrUpdateModel.TrialId); + var calcList = await _calculateService.GetNeedCalculateReviewerList(Guid.Empty, string.Empty); + + foreach (var item in calcList) + { + if (item != null && needCalReviewerIds.Contains(item.DoctorId)) + { + await _calculateService.CalculateMonthlyPayment(new CalculateDoctorAndMonthDTO() + { + NeedCalculateReviewers = new List() + { + item.DoctorId + }, + CalculateMonth = DateTime.Parse(item.YearMonth) + }, User.FindFirst("id").Value); + } + } + return result; + } + + /// + /// 批量更新奖励费用[AUTH] + /// + + [HttpPost, Route("volumeReward/addOrUpdatevolumeRewardPriceList")] + public async Task AddOrUpdateAwardPriceList([FromServices] IVolumeRewardService _volumeRewardService, IEnumerable addOrUpdateModel) + { + + var result =await _volumeRewardService.AddOrUpdateVolumeRewardPriceList(addOrUpdateModel); + + var calcList = await _calculateService.GetNeedCalculateReviewerList(Guid.Empty, string.Empty); + foreach (var item in calcList) + { + if (item != null) + { + await _calculateService.CalculateMonthlyPayment(new CalculateDoctorAndMonthDTO() + { + NeedCalculateReviewers = new List() + { + item.DoctorId + }, + CalculateMonth = DateTime.Parse(item.YearMonth) + }, User.FindFirst("id").Value); + } + } + return result; + } + + + + /// + /// 计算医生月度费用,并将计算的结果存入费用表 + /// + [HttpPost, Route("financial/calculateMonthlyPayment")] + public async Task CalculateMonthlyPayment(CalculateDoctorAndMonthDTO param) + { + if (!ModelState.IsValid) + { + return ResponseOutput.NotOk("Invalid parameter."); + } + return await _calculateService.CalculateMonthlyPayment(param, User.FindFirst("id").Value); + } + + /// + /// Financials /Monthly Payment 列表查询接口 + /// + [HttpPost, Route("financial/getMonthlyPaymentList")] + public async Task> GetMonthlyPaymentList([FromServices] IPaymentService _paymentService, [FromServices] IExchangeRateService _exchangeRateService, MonthlyPaymentQueryDTO queryParam) + { + return ResponseOutput.Ok(new PaymentDTO + { + CostList = await _paymentService.GetMonthlyPaymentList(queryParam), + ExchangeRate = await _exchangeRateService.GetExchangeRateByMonth(queryParam.StatisticsDate.ToString("yyyy-MM")) + }); + } + + + } +} diff --git a/IRaCIS.Core.API/Controllers/InspectionController.cs b/IRaCIS.Core.API/Controllers/InspectionController.cs new file mode 100644 index 00000000..d4955225 --- /dev/null +++ b/IRaCIS.Core.API/Controllers/InspectionController.cs @@ -0,0 +1,113 @@ +using System; +using System.Threading.Tasks; +using AutoMapper; +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Core.API.Controllers +{ + + [ApiController, ApiExplorerSettings(GroupName = "Reviewer")] + public class InspectionController : ControllerBase + { + private readonly IRepository _repository; + private readonly IMapper _mapper; + private readonly IUserInfo _userInfo; + + public InspectionController(IRepository repository, IMapper mapper, IUserInfo userInfo) + { + _repository = repository; + _mapper = mapper; + _userInfo = userInfo; + } + + [HttpPost, Route("trialDocument/userConfirm")] + public async Task UserConfirm(TrialDocumentConfirmDTO opt, [FromServices] ITrialDocumentService _trialDocumentService) + { + var verifyResult = await VerifySignatureAsync(opt.SignInfo); + + if (verifyResult.IsSuccess == false) + { + return verifyResult; + } + + var bResult = await _trialDocumentService.UserConfirm(opt.OptCommand); + + if (bResult.IsSuccess == false) + { + return bResult; + } + + //SiteId SubjectId SubjectVisitId TrialId 最开始没有 需要特殊处理 + //表冗余字段 前端只传递一次的话 后台模型就需要单独处理 + + + if (opt.AuditInfo.IsSign) + { + var signId = await AddSignRecordAsync(opt.SignInfo); + + await AddInspectionRecordAsync(opt.AuditInfo, signId); + } + else + { + await AddInspectionRecordAsync(opt.AuditInfo, null); + } + + return bResult; + } + + + + /// 验证用户签名信息 /// + private async Task VerifySignatureAsync(SignDTO signDTO) + { + var user = await _repository.FirstOrDefaultAsync(u => u.UserName == signDTO.UserName && u.Password == signDTO.PassWord); + if (user == null) + { + return ResponseOutput.NotOk("password error"); + } + else if (user.Status == UserStateEnum.Disable) + { + return ResponseOutput.NotOk("The user has been disabled!"); + } + return ResponseOutput.Ok(); + + } + + + /// 添加签名记录 /// + private async Task AddSignRecordAsync(SignDTO signDTO) + { + var add = await _repository.AddAsync(_mapper.Map(signDTO)); + + var success = await _repository.SaveChangesAsync(); + + return add.Id; + + } + + /// 添加稽查记录( 有的会签名,有的不会签名) /// + private async Task AddInspectionRecordAsync(DataInspectionAddDTO addDto, Guid? signId) + { + + var add = await _repository.AddAsync(_mapper.Map(addDto)); + + add.SignId = signId; + add.IP = _userInfo.IP; + + var success = await _repository.SaveChangesAsync(); + + + } + + } + + + + +} diff --git a/IRaCIS.Core.API/Controllers/StudyController.cs b/IRaCIS.Core.API/Controllers/StudyController.cs new file mode 100644 index 00000000..87ad8c7a --- /dev/null +++ b/IRaCIS.Core.API/Controllers/StudyController.cs @@ -0,0 +1,408 @@ +using IRaCIS.Application.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using IRaCIS.Core.Application.Contracts.Dicom.DTO; +using Microsoft.Net.Http.Headers; +using Microsoft.AspNetCore.WebUtilities; +using System.Threading.Tasks; +using IRaCIS.Core.Application.Contracts.Dicom; +using System.IO; +using System.IO.Compression; +using IRaCIS.Core.Application.Dicom; +using Microsoft.Extensions.Logging; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Infrastructure.Extention; +using EasyCaching.Core; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Domain.Models; + +namespace IRaCIS.Api.Controllers +{ + /// + /// Study + /// + [Route("study")] + [ApiController, Authorize, ApiExplorerSettings(GroupName = "Image")] + public class StudyController : ControllerBase + { + private readonly IStudyService _studyService; + private readonly IDicomArchiveService _dicomArchiveService; + private readonly ILogger _logger; + + private IEasyCachingProvider _provider; + private IUserInfo _userInfo; + private static object _locker = new object(); + + + public StudyController(IStudyService studyService, + IDicomArchiveService dicomArchiveService, + ILogger logger, + IEasyCachingProvider provider, IUserInfo userInfo + ) + { + _userInfo = userInfo; + _provider = provider; + _studyService = studyService; + _dicomArchiveService = dicomArchiveService; + _logger = logger; + + } + + + + + + + /// 归档 + [HttpPost, Route("archiveStudy/{trialId:guid}")] + [DisableFormValueModelBinding] + [DisableRequestSizeLimit] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task ArchiveStudy([FromForm] ArchiveStudyCommand archiveStudyCommand) + { + //Stopwatch sw = new Stopwatch(); + var startTime = DateTime.Now; + //sw.Start(); + + if (_provider.Exists("StudyUid_" + archiveStudyCommand.StudyInstanceUid)) + { + return ResponseOutput.NotOk("当前已有人正在上传和归档该检查!"); + } + else + { + _provider.Set("StudyUid_" + archiveStudyCommand.StudyInstanceUid, _userInfo.Id, TimeSpan.FromMinutes(30)); + } + + var archiveResult = new DicomArchiveResult(); + var archivedStudyIds = new List(); + var seriesInstanceUidList = new List(); + var instanceUidList = new List(); + + //重传的时候,找出当前检查已经上传的series instance + if (archiveStudyCommand.AbandonStudyId != null) + { + _studyService.GetHasUploadSeriesAndInstance(archiveStudyCommand.AbandonStudyId.Value, ref seriesInstanceUidList, ref instanceUidList); + } + + var savedInfo = _studyService.GetSaveToDicomInfo(archiveStudyCommand.SubjectVisitId); + + + var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(Request.ContentType).Boundary).Value; + var reader = new MultipartReader(boundary, HttpContext.Request.Body); + var section = await reader.ReadNextSectionAsync(); + while (section != null) + { + //采用post方式 这里多加一个判断 过滤其他参数 + if (string.IsNullOrEmpty(section.ContentType)) + { + section = await reader.ReadNextSectionAsync(); + continue; + } + + var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition); + if (hasContentDispositionHeader) + { + string fileName = contentDisposition.FileName.Value; + try + { + + string mediaType = section.ContentType; + + if (mediaType.Contains("zip")) + { + var partStream = section.Body; + using (var zipArchive = new ZipArchive(partStream, ZipArchiveMode.Read)) + { + foreach (var entry in zipArchive.Entries) + { + if (entry.FullName.EndsWith("/")) continue; + try + { + ++archiveResult.ReceivedFileCount; + using (var memoryStream = new MemoryStream()) + { + await section.Body.CopyToAsync(memoryStream); + + memoryStream.Seek(0, SeekOrigin.Begin); + + var archiveStudyId = await _dicomArchiveService.ArchiveDicomStreamAsync(memoryStream, savedInfo, seriesInstanceUidList, instanceUidList); + + if (!archivedStudyIds.Contains(archiveStudyId)) + archivedStudyIds.Add(archiveStudyId); + } + } + catch + { + archiveResult.ErrorFiles.Add($"{fileName}/{entry.FullName}"); + } + } + } + } + + ++archiveResult.ReceivedFileCount; + + + if (mediaType.Contains("octet-stream")) + { + using (var memoryStream = new MemoryStream()) + { + await section.Body.CopyToAsync(memoryStream); + + memoryStream.Seek(0, SeekOrigin.Begin); + + var archiveStudyId = await _dicomArchiveService.ArchiveDicomStreamAsync(memoryStream, savedInfo, seriesInstanceUidList, instanceUidList); + if (!archivedStudyIds.Contains(archiveStudyId)) + archivedStudyIds.Add(archiveStudyId); + } + } + } + catch (Exception e) + { + _logger.LogError(e.Message + e.StackTrace); + + archiveResult.ErrorFiles.Add(fileName); + + _provider.Remove("StudyUid_" + archiveStudyCommand.StudyInstanceUid); + + } + } + section = await reader.ReadNextSectionAsync(); + } + + if (archivedStudyIds.Count > 0) // 上传成功,处理逻辑 + { + + + // 同一个访视 多个线程上传处理 批量保存 可能造成死锁 https://www.cnblogs.com/johnblogs/p/9945767.html + + await _dicomArchiveService.DicomDBDataSaveChange(); + + //sw.Stop(); + _studyService.UploadOrReUploadNeedTodo(archiveStudyCommand, archivedStudyIds, ref archiveResult, new StudyMonitor() + { + TrialId = savedInfo.TrialId, + SiteId = savedInfo.SiteId, + SubjectId = savedInfo.SubjectId, + SubjectVisitId = savedInfo.SubjectVisitId, + StudyId = archivedStudyIds[0], + UploadStartTime = startTime, + UploadFinishedTime = DateTime.Now, + FileSize = (decimal)HttpContext.Request.ContentLength, + FileCount = archiveResult.ReceivedFileCount, + IsDicom = true, + IsDicomReUpload = archiveStudyCommand.AbandonStudyId!=null, + IP =_userInfo.IP + }); + + _provider.Remove("StudyUid_" + archiveStudyCommand.StudyInstanceUid); + + } + else + { + return ResponseOutput.NotOk("未完成该检查的归档", archiveResult); + } + + return ResponseOutput.Ok(archiveResult); + + } + + + + + + + + + #region 2021.12.14 整理废弃 + + //[Obsolete] + //[HttpGet, Route("forwardStudy/{studyId:guid}/{trialId:guid}")] + //[TrialAudit(AuditType.StudyAudit, AuditOptType.Forwarded)] + //[TypeFilter(typeof(TrialResourceFilter))] + + //public IResponseOutput ForwardStudy(Guid studyId) + //{ + // return _studyService.ForwardStudy(studyId); + //} + + ///// 指定资源Id,获取Dicom检查信息 + ///// Dicom检查的Id + //[HttpGet, Route("item/{studyId:guid}")] + //[Obsolete] + //public IResponseOutput GetStudyItem(Guid studyId) + //{ + // return ResponseOutput.Ok(_studyService.GetStudyItem(studyId)); + //} + + //[Obsolete] + //[HttpDelete, Route("deleteStudy/{id:guid}/{trialId:guid}")] + //public IResponseOutput DeleteStudy(Guid id) + //{ + // return _studyService.DeleteStudy(id); + //} + + //[Obsolete] + //[HttpPost, Route("getStudyList")] + //public IResponseOutput> GetStudyList(StudyQueryDTO queryDto) + //{ + // return ResponseOutput.Ok(_studyService.GetStudyList(queryDto)); + //} + + + /////// 指定资源Id,渲染Dicom检查的Jpeg预览图像 + /////// Dicom检查的Id + ////[HttpGet, Route("preview/{studyId:guid}")] + ////[AllowAnonymous] + ////public FileContentResult GetStudyPreview(Guid studyId) + ////{ + //// string path = _studyService.GetStudyPreview(studyId); + //// using (var sw = DicomRenderingHelper.RenderPreviewJpeg(path)) + //// { + //// var bytes = new byte[sw.Length]; + //// sw.Read(bytes, 0, bytes.Length); + //// sw.Close(); + //// return new FileContentResult(bytes, "image/jpeg"); + //// } + ////} + + ///// + ///// Dicom匿名化 + ///// + ///// 需要匿名化的检查Id + ///// + //[HttpPost, Route("dicomAnonymize/{studyId:guid}/{trialId:guid}")] + //[TrialAudit(AuditType.StudyAudit, AuditOptType.Anonymized)] + //[Obsolete] + //[TypeFilter(typeof(TrialResourceFilter))] + //public async Task DicomAnonymize(Guid studyId) + //{ + // string userName = User.FindFirst("realName").Value; ; + // return await _studyService.DicomAnonymize(studyId, userName); + //} + + + ///// + ///// 获取受试者 这次访视 对应的study modality 列表 + ///// + ///// + ///// + ///// + ///// + ///// + //[HttpPost, Route("getSubjectVisitStudyList/{trialId:guid}/{siteId:guid}/{subjectId:guid}/{subjectVisitId:guid}")] + //[Obsolete] + //[AllowAnonymous] + //public IResponseOutput> GetSubjectVisitStudyList(Guid trialId, Guid siteId, Guid subjectId, + // Guid subjectVisitId) + //{ + // return ResponseOutput.Ok(_studyService.GetSubjectVisitStudyList(trialId, siteId, subjectId, subjectVisitId)); + //} + + //[HttpPost, Route("getDistributeStudyList")] + //[Obsolete] + //public IResponseOutput> GetDistributeStudyList(StudyStatusQueryDTO studyStatusQueryDto) + //{ + // return ResponseOutput.Ok(_studyService.GetDistributeStudyList(studyStatusQueryDto)); + //} + + /////// 删除检查 + ////[HttpDelete, Route("deleteStudy/{id:guid}/{trialId:guid}")] + //// + ////[TypeFilter(typeof(TrialResourceFilter))] + ////public IResponseOutput DeleteStudy(Guid id) + ////{ + //// return _studyService.DeleteStudy(id); + ////} + + ///// 更新Study状态,并保存状态变更信息 + ///// + + //[HttpPost, Route("updateStudyStatus/{trialId:guid}")] + + //[TrialAudit(AuditType.StudyAudit, AuditOptType.ChangeStudyStatus)] + //[Obsolete] + //[TypeFilter(typeof(TrialResourceFilter))] + //public IResponseOutput UpdateStudyStatus(StudyStatusDetailCommand studyStatusDetailCommand) + //{ + // return _studyService.UpdateStudyStatus(studyStatusDetailCommand); + //} + + ///// + ///// 根据项目Id 获取可选医生列表 + ///// + ///// + ///// + //[HttpGet, Route("GetReviewerList/{trialId:guid}")] + //[Obsolete] + //public IResponseOutput> GetReviewerListByTrialId(Guid trialId) + //{ + // var result = _studyService.GetReviewerListByTrialId(trialId); + // return ResponseOutput.Ok(result); + //} + + ///// + ///// 根据StudyId获取该Study的操作记录,时间倒序 + ///// + ///// + ///// + //[HttpGet, Route("getStudyStatusDetailList/{studyId:guid}")] + //[Obsolete] + //public IResponseOutput> GetStudyStatusDetailList(Guid studyId) + //{ + // var result = _studyService.GetStudyStatusDetailList(studyId); + // return ResponseOutput.Ok(result); + //} + + ///// + ///// 获取某个访视的关联访视 + ///// 用于获取关联影像(调用之前的接口:/series/list/,根据StudyId,获取访视的序列列表) + ///// + ///// + ///// + ///// + //[HttpGet, Route("getRelationVisitList/{visitNum}/{tpCode}")] + //[Obsolete] + //[AllowAnonymous] + //public IResponseOutput> GetRelationVisitList(decimal visitNum, string tpCode) + //{ + // return ResponseOutput.Ok(_studyService.GetRelationVisitList(visitNum, tpCode)); + //} + + ///// + ///// 保存标记(跟删除合并,每次保存最新的标记),会删除替换之前的标记 + ///// 外层的TPcode 必须传,里面的标记数组可为空数组,表示删除该Study的所有标记 + ///// + ///// + ///// + //[HttpPost, Route("saveImageLabelList")] + //[AllowAnonymous] + //[Obsolete] + + //public IResponseOutput SaveImageLabelList(ImageLabelCommand imageLabelCommand) + //{ + // return ResponseOutput.Result(_studyService.SaveImageLabelList(imageLabelCommand)); + //} + + ///// + ///// 根据TPCode 获取所有的标记 + ///// + ///// + ///// + //[HttpGet, Route("getImageLabelList/{tpCode}")] + //[Obsolete] + //[AllowAnonymous] + //public IResponseOutput> GetImageLabelList(string tpCode) + //{ + // return ResponseOutput.Ok(_studyService.GetImageLabel(tpCode)); + //} + + #endregion + + + + + } +} diff --git a/IRaCIS.Core.API/Controllers/UploadController.cs b/IRaCIS.Core.API/Controllers/UploadController.cs new file mode 100644 index 00000000..30e373e3 --- /dev/null +++ b/IRaCIS.Core.API/Controllers/UploadController.cs @@ -0,0 +1,280 @@ +using AutoMapper; +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Infrastructure.Extention; +using MediatR; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Net.Http.Headers; +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace IRaCIS.Core.API.Controllers +{ + [ApiExplorerSettings(GroupName = "Trial")] + [ApiController] + public class UploadController : ControllerBase + { + public IMapper _mapper { get; set; } + public IUserInfo _userInfo { get; set; } + private readonly IMediator _mediator; + + private readonly IWebHostEnvironment _hostEnvironment; + + private readonly IServiceProvider _serviceProvider; + + + public UploadController(IMapper mapper, IUserInfo userInfo, IMediator mediator, IWebHostEnvironment hostEnvironment, IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + _hostEnvironment = hostEnvironment; + _mediator = mediator; + _mapper = mapper; + _userInfo = userInfo; + } + + [HttpPost("TrialDocument/UploadTrialDoc/{trialId:guid}/{type}")] + [DisableRequestSizeLimit] + [DisableFormValueModelBinding] + public async Task UploadTrialDoc(Guid trialId,string type) + { + var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(Request.ContentType).Boundary).Value; + + var reader = new MultipartReader(boundary, HttpContext.Request.Body); + + var section = await reader.ReadNextSectionAsync(); + + while (section != null) + { + var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition); + + if (hasContentDispositionHeader) + { + + DealTrialStorePath(trialId, type, contentDisposition.FileName.Value, out string serverFilePath, out string relativePath ); + + await WriteFileAsync(section.Body, serverFilePath); + + //仅仅返回一个文件,如果多文件上传 在最后返回多个路径 + return ResponseOutput.Ok(new + { + FilePath = relativePath, + FullFilePath = relativePath + "?access_token=" + _userInfo.UserToken + }); + + } + section = await reader.ReadNextSectionAsync(); + } + return ResponseOutput.Ok(); + } + + + [HttpPost("TrialDocument/UploadSystemDoc/{type}")] + [DisableRequestSizeLimit] + [DisableFormValueModelBinding] + public async Task UploadSysTemDoc( string type) + { + var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(Request.ContentType).Boundary).Value; + + var reader = new MultipartReader(boundary, HttpContext.Request.Body); + + var section = await reader.ReadNextSectionAsync(); + + while (section != null) + { + var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition); + + if (hasContentDispositionHeader) + { + + DealSysTemStorePath( type, contentDisposition.FileName.Value, out string serverFilePath, out string relativePath); + + await WriteFileAsync(section.Body, serverFilePath); + + //仅仅返回一个文件,如果多文件上传 在最后返回多个路径 + return ResponseOutput.Ok(new + { + FilePath = relativePath, + FullFilePath = relativePath + "?access_token=" + _userInfo.UserToken + }); + + } + section = await reader.ReadNextSectionAsync(); + } + return ResponseOutput.Ok(); + } + + private void DealSysTemStorePath( string type, string fileRealName, out string serverFilePath, out string relativePath) + { + var rootPath = Directory.GetParent(_hostEnvironment.ContentRootPath.TrimEnd('\\')).FullName; + //上传根路径 + var _fileStorePath = Path.Combine(rootPath, StaticData.TrialDataFolder); + + //文件类型路径处理 + var uploadFolderPath = Path.Combine(_fileStorePath, "SysTemDocument", type); + if (!Directory.Exists(uploadFolderPath)) Directory.CreateDirectory(uploadFolderPath); + + + var fileNameEX = Path.GetExtension(fileRealName); + var trustedFileNameForFileStorage = Guid.NewGuid().ToString() + fileNameEX; + + relativePath = $"/{StaticData.TrialDataFolder}/SysTemDocument/{type}/{trustedFileNameForFileStorage}"; + + serverFilePath = Path.Combine(uploadFolderPath, trustedFileNameForFileStorage); + } + + + private void DealTrialStorePath(Guid trialId,string type,string fileRealName, out string serverFilePath, out string relativePath) + { + var rootPath = Directory.GetParent(_hostEnvironment.ContentRootPath.TrimEnd('\\')).FullName; + //上传根路径 + var _fileStorePath = Path.Combine(rootPath, StaticData.TrialDataFolder); + + //文件类型路径处理 + var uploadFolderPath = Path.Combine(_fileStorePath, trialId.ToString(), type); + if (!Directory.Exists(uploadFolderPath)) Directory.CreateDirectory(uploadFolderPath); + + + var fileNameEX = Path.GetExtension(fileRealName); + var trustedFileNameForFileStorage = Guid.NewGuid().ToString() + fileNameEX; + + relativePath = $"/{StaticData.TrialDataFolder}/{trialId}/{type}/{trustedFileNameForFileStorage}"; + + serverFilePath = Path.Combine(uploadFolderPath, trustedFileNameForFileStorage); + } + + + + /// + /// 上传临床数据 + /// + /// + /// 1:DICOM DTF 2:非DIOM DTF 3: 受试者临床数据 + /// + /// + [HttpPost("ClinicalData/UploadVisitClinicalData/{trialId:guid}/{subjectVisitId:guid}/{type}")] + [DisableRequestSizeLimit] + [DisableFormValueModelBinding] + [Obsolete] + public async Task UploadVisitData(Guid subjectVisitId, [FromRoute] UploadFileTypeEnum uploadType, [FromServices] IRepository _subjectVisitRepository) + { + + var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(Request.ContentType).Boundary).Value; + + var reader = new MultipartReader(boundary, HttpContext.Request.Body); + + var section = await reader.ReadNextSectionAsync(); + while (section != null) + { + var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition); + + if (hasContentDispositionHeader) + { + + DealStorePath(subjectVisitId, contentDisposition.FileName.Value, uploadType, out string serverFilePath, out string relativePath, _subjectVisitRepository); + + await WriteFileAsync(section.Body, serverFilePath); + + //仅仅返回一个文件,如果多文件上传 在最后返回多个路径 + return ResponseOutput.Ok(new + { + FilePath = relativePath, + FullFilePath = relativePath + "?access_token=" + _userInfo.UserToken + }); + + } + section = await reader.ReadNextSectionAsync(); + } + return ResponseOutput.Ok(); + + } + + public enum UploadFileTypeEnum + { + DICOM_DTF = 1, + + NonDICOM_DTF = 2, + + SubjectTreatement = 3 + } + + private void DealStorePath(Guid subjectVisitId, string realName, UploadFileTypeEnum typeEnum, out string serverFilePath, out string relativePath, IRepository _subjectVisitRepository) + { + + var rootPath = Directory.GetParent(_hostEnvironment.ContentRootPath.TrimEnd('\\')).FullName; + + //上传根路径 + var _fileStorePath = Path.Combine(rootPath, StaticData.TrialDataFolder); + + var sv = _subjectVisitRepository.Where(t => t.Id == subjectVisitId).Select(t => new { t.TrialId, t.SiteId, t.SubjectId }).FirstOrDefault(); + + //处理存储的文件夹 + var typeFolder = typeEnum == UploadFileTypeEnum.SubjectTreatement ? StaticData.TreatmenthistoryFolder + : typeEnum == UploadFileTypeEnum.NonDICOM_DTF ? StaticData.NoneDicomFolder + : /*typeEnum == UploadFileTypeEnum.DICOM_DTF ?*/ StaticData.DicomFolder; + + string uploadFolderPath = Path.Combine(_fileStorePath, sv.TrialId.ToString(), sv.SiteId.ToString(), sv.SubjectId.ToString(), subjectVisitId.ToString(), typeFolder); + + if (!Directory.Exists(uploadFolderPath)) + { + Directory.CreateDirectory(uploadFolderPath); + } + + var fileNameEX = Path.GetExtension(realName); + + var trustedFileNameForFileStorage = Guid.NewGuid().ToString() + fileNameEX; + + relativePath = $"/{StaticData.TrialDataFolder}/{sv.TrialId}/{sv.SiteId}/{sv.SubjectId}/{subjectVisitId}/{typeFolder}/{trustedFileNameForFileStorage}"; + + serverFilePath = Path.Combine(uploadFolderPath, trustedFileNameForFileStorage); + + //处理 是上传文件返回路径 还是上传文件后,需要保存数据库 + + + if (typeEnum == UploadFileTypeEnum.SubjectTreatement) + { + var repository = _serviceProvider.GetService(typeof(IRepository)) as IRepository; + + _= repository.InsertOrUpdateAsync(new PreviousPDFAddOrEdit() { FileName = realName, Path = relativePath, SubjectVisitId = subjectVisitId }, true).Result; + } + else if (typeEnum == UploadFileTypeEnum.NonDICOM_DTF) + { + + } + else if (typeEnum == UploadFileTypeEnum.NonDICOM_DTF) + { + + } + } + + /// + /// 写文件导到磁盘 + /// + /// 流 + /// 文件保存路径 + /// + private static async Task WriteFileAsync(System.IO.Stream stream, string path) + { + const int FILE_WRITE_SIZE = 84975;//写出缓冲区大小 + int writeCount = 0; + using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Write, FILE_WRITE_SIZE, true)) + { + byte[] byteArr = new byte[FILE_WRITE_SIZE]; + int readCount = 0; + while ((readCount = await stream.ReadAsync(byteArr, 0, byteArr.Length)) > 0) + { + await fileStream.WriteAsync(byteArr, 0, readCount); + writeCount += readCount; + } + } + return writeCount; + } + } +} diff --git a/IRaCIS.Core.API/GrpcToken.proto b/IRaCIS.Core.API/GrpcToken.proto new file mode 100644 index 00000000..ace5d424 --- /dev/null +++ b/IRaCIS.Core.API/GrpcToken.proto @@ -0,0 +1,69 @@ +// ʹõproto3汾 +syntax = "proto3"; +// ռ䣬ɴʱͻɶӦռ +option csharp_namespace = "gRPC.ZHiZHUN.AuthServer.protos"; + +/* +ÿһҪ÷ֺŽβ +message ͷݸʽ +tag messageִֵֶεıʶ(tag),Ǹֵ +*/ + + +// ûʱҪϢ Ϊһ +message GetTokenReuqest{ + string id=1; + string userName=2; + string realName=3; + string reviewerCode=4; + int32 userTypeEnumInt=5; + string userTypeShortName=6; + bool isAdmin=7; + +} + +// ʱصϢʽ +message GetTokenResponse { + int32 code=1; + string token =2; +} + + +// service ñʶģдӦķ +service TokenGrpcService{ + // ȡtoken + rpc GetUserToken(GetTokenReuqest) returns (GetTokenResponse); + +} + +/* +// ûʱҪϢ Ϊһ +message AddUserReuqest{ + string name=1; + int32 age=2; + bool isBoy=3; +} +// ʱصϢʽ +message ResultResponse { + int32 code=1; + string msg =2; +} +//ݵIJѯϢʽΪƽʱIJѯ +message QueryUserReuqest{ + string name=1; +} +//ѯصûϢʽΪص +message UserInfoResponse { + string name=1; + int32 age=2; + string gender=3; +} + +// service ñʶģдӦķ +service UserService{ + // û + rpc AddUser(AddUserReuqest) returns (ResultResponse); + // ѯû + rpc GetAllUser(QueryUserReuqest) returns (UserInfoResponse); +} +*/ diff --git a/IRaCIS.Core.API/IRaCIS - Backup.Core.API.csproj b/IRaCIS.Core.API/IRaCIS - Backup.Core.API.csproj new file mode 100644 index 00000000..b14f8c9b --- /dev/null +++ b/IRaCIS.Core.API/IRaCIS - Backup.Core.API.csproj @@ -0,0 +1,107 @@ + + + + net6.0 + false + 354572d4-9e15-4099-807c-63a2d29ff9f2 + default + Linux + + + + .\IRaCIS.Core.API.xml + 1701;1702;1591; + ..\bin\ + + + + bin\Release\IRaCIS.Core.API.xml + bin\Release\ + 1701;1702;1591 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Client + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + + + + diff --git a/IRaCIS.Core.API/IRaCIS.Core.API.csproj b/IRaCIS.Core.API/IRaCIS.Core.API.csproj new file mode 100644 index 00000000..30fb9224 --- /dev/null +++ b/IRaCIS.Core.API/IRaCIS.Core.API.csproj @@ -0,0 +1,110 @@ + + + + net6.0 + false + 354572d4-9e15-4099-807c-63a2d29ff9f2 + default + Linux + + + + .\IRaCIS.Core.API.xml + 1701;1702;1591; + ..\bin\ + + + + bin\Release\IRaCIS.Core.API.xml + bin\Release\ + 1701;1702;1591 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + Always + + + + + Client + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + + + + diff --git a/IRaCIS.Core.API/IRaCIS.Core.API.xml b/IRaCIS.Core.API/IRaCIS.Core.API.xml new file mode 100644 index 00000000..f8ffea41 --- /dev/null +++ b/IRaCIS.Core.API/IRaCIS.Core.API.xml @@ -0,0 +1,345 @@ + + + + IRaCIS.Core.API + + + + + 主要处理 前端404等错误 全局业务异常已统一处理了,非业务错误会来到这里 + + + + + + + 医生基本信息 、工作信息 专业信息、审核状态 + + + + + 获取医生详情 + + + + + + + + + + + + 系统用户登录接口[New] + + + + 文件上传 + + + + + 上传文件[FileUpload] + + 附件类型 + 医生Id + 返回文件信息 + + + + 上传文件( 不是医生个人的文件)[FileUpload] + 例如:阅片章程等 + + 文件类型 + + + + + 写文件导到磁盘 + + 流 + 文件保存路径 + + + + + 下载多个医生的所有附件 + + + + + + + 下载医生官方简历 + + + + + + + + 下载指定医生的指定附件 + + 医生Id + 要下载的附件Id + + + + + 流式上传 临时文件 + + + + + + 流式上传 非Dicom文件 + + + + + + Study + + + + 归档 + + + 添加实验项目-返回新增Id[AUTH] + + 新记录Id + + + + 添加或更新工作量[AUTH] + + + + + + + + 添加或更新汇率(会触发没有对锁定的费用计算) + + + + + 添加或更新 职称单价[AUTH] + + + + + 添加或更新(替换)医生支付展信息[AUTH] + + + + + 保存(替换)项目支付价格信息(会触发没有被锁定的费用计算)[AUTH] + + + + + 批量更新奖励费用[AUTH] + + + + + 计算医生月度费用,并将计算的结果存入费用表 + + + + + Financials /Monthly Payment 列表查询接口 + + + + 验证用户签名信息 /// + + + 添加签名记录 /// + + + 添加稽查记录( 有的会签名,有的不会签名) /// + + + + 上传临床数据 + + + 1:DICOM DTF 2:非DIOM DTF 3: 受试者临床数据 + + + + + + 写文件导到磁盘 + + 流 + 文件保存路径 + + + + + IPLimit限流 启动服务 + + + + + 创建属性 + + 类型 + 序列化成员 + + + + + 为了前端 一段时间无操作,需要重新登陆 + + + + + + + 对称可逆加密 + + + + + 用户登录成功以后,用来生成Token的方法 + + + + + + + + 非对称可逆加密 + + + + + 从本地文件中读取用来签发 Token 的 RSA Key + + 存放密钥的文件夹路径 + + + + + + + 生成并保存 RSA 公钥与私钥 + + + + + + + Holder for reflection information generated from Protos/GrpcToken.proto + + + File descriptor for Protos/GrpcToken.proto + + + + 新增用户时需要传递数据消息, 可理解为一个类 + + + + Field number for the "id" field. + + + Field number for the "userName" field. + + + Field number for the "realName" field. + + + Field number for the "reviewerCode" field. + + + Field number for the "userTypeEnumInt" field. + + + Field number for the "userTypeShortName" field. + + + Field number for the "isAdmin" field. + + + + 新增时返回的消息格式 + + + + Field number for the "code" field. + + + Field number for the "token" field. + + + + service 用标识定义服务的,里面写对应的方法 + + + + Service descriptor + + + Client for TokenGrpcService + + + Creates a new client for TokenGrpcService + The channel to use to make remote calls. + + + Creates a new client for TokenGrpcService that uses a custom CallInvoker. + The callInvoker to use to make remote calls. + + + Protected parameterless constructor to allow creation of test doubles. + + + Protected constructor to allow creation of configured clients. + The client configuration. + + + + 获取token + + The request to send to the server. + The initial metadata to send with the call. This parameter is optional. + An optional deadline for the call. The call will be cancelled if deadline is hit. + An optional token for canceling the call. + The response received from the server. + + + + 获取token + + The request to send to the server. + The options for the call. + The response received from the server. + + + + 获取token + + The request to send to the server. + The initial metadata to send with the call. This parameter is optional. + An optional deadline for the call. The call will be cancelled if deadline is hit. + An optional token for canceling the call. + The call object. + + + + 获取token + + The request to send to the server. + The options for the call. + The call object. + + + Creates a new instance of client from given ClientBaseConfiguration. + + + diff --git a/IRaCIS.Core.API/NLog.config b/IRaCIS.Core.API/NLog.config new file mode 100644 index 00000000..bf1dbbb1 --- /dev/null +++ b/IRaCIS.Core.API/NLog.config @@ -0,0 +1,18 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/IRaCIS.Core.API/Program.cs b/IRaCIS.Core.API/Program.cs new file mode 100644 index 00000000..278a1dd4 --- /dev/null +++ b/IRaCIS.Core.API/Program.cs @@ -0,0 +1,142 @@ +using System; +using System.Linq; +using Autofac.Extensions.DependencyInjection; +using Dicom.Imaging; +using EasyCaching.Core; +using IRaCIS.Core.Domain.Models; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.Extensions.Configuration; +using Serilog; +using System.Collections.Generic; +using IRaCIS.Core.Domain.Share; +using MediatR; +using IRaCIS.Core.Application.MediatR.Handlers; +using System.Threading.Tasks; + +namespace IRaCIS.Core.API +{ + public class Program + { + public readonly string environment; + public static async Task Main(string[] args) + { + try + { + var index = Array.IndexOf(args, "--env"); + var environment = index > -1 + ? args[index + 1] + : "Development"; + + //Dicom + //ImageManager.SetImplementation(WinFormsImageManager.Instance); + + var host = CreateHostBuilder(args) + .UseEnvironment(environment) //д뻷 + .ConfigureAppConfiguration((hostContext, config) => + { + + //Console.WriteLine(hostContext.HostingEnvironment.EnvironmentName); + config.AddJsonFile("appsettings.json", false, true) + .AddJsonFile($"appsettings.{environment}.json", false, true); + }) + .Build(); + + //// Serilog + SerilogExtension.AddSerilogSetup(environment, host.Services); + + //Ŀ״̬ + await InitCache(host); + + //Log.Logger.Error("Ŀ״̬"); + + host.Run(); + } + catch (Exception e) + { + + Log.Logger.Error(e.InnerException is null ? e.Message + e.StackTrace : e.InnerException?.Message + e.InnerException?.StackTrace); + } + + + + #region Nlog + //var logger = NLog.Web.NLogBuilder.ConfigureNLog("NLog.config").GetCurrentClassLogger(); + //try + //{ + // var ihostBuilder = CreateHostBuilder(args); + + // ihostBuilder.UseEnvironment(environment); + + // ihostBuilder.ConfigureAppConfiguration((hostContext, config) => + // { + + // //Console.WriteLine(hostContext.HostingEnvironment.EnvironmentName); + // config.AddJsonFile("appsettings.json", false, true) + // .AddJsonFile($"appsettings.{environment}.json", false, true); + // }); + + // var host = ihostBuilder.Build(); + + // CacheTrialStatus(host); + + // host.Run(); + + //} + //catch (Exception exception) + //{ + // //NLog: catch setup errors + // logger.Error(exception, "Stopped program because of exception"); + //} + //finally + //{ + // // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) + // NLog.LogManager.Shutdown(); + //} + + #endregion + + + + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseWindowsService() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureKestrel((context, options) => + { + //Ӧ÷KestrelΪ1GB // if don't set default value is: 30 MB + options.Limits.MaxRequestBodySize = long.MaxValue; + options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(30); + options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(20); + + }); + webBuilder.UseSerilog();//ʱserilog,΢ILogger + webBuilder.UseStartup(); + }) + .UseServiceProviderFactory(new AutofacServiceProviderFactory());//ʹAutofac + //ConfigureLogging(logging => + //{ + // logging.ClearProviders(); + // logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace); + //}) + //.UseNLog() + // NLog: Setup NLog for Dependency injection; + + + + + private static async Task InitCache(IHost host) + { + var _mediator = host.Services.GetService(typeof(IMediator)) as IMediator; + + await _mediator.Send(new AnonymizeCacheRequest()); + + await _mediator.Send(new TrialStateCacheRequest()); + } + + } +} diff --git a/IRaCIS.Core.API/Properties/launchSettings.json b/IRaCIS.Core.API/Properties/launchSettings.json new file mode 100644 index 00000000..1c960e4c --- /dev/null +++ b/IRaCIS.Core.API/Properties/launchSettings.json @@ -0,0 +1,34 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:57302", + "sslPort": 0 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IRaCIS.Core.API": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:6100" + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "publishAllPorts": true + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.API/Protos/GrpcToken.proto b/IRaCIS.Core.API/Protos/GrpcToken.proto new file mode 100644 index 00000000..ace5d424 --- /dev/null +++ b/IRaCIS.Core.API/Protos/GrpcToken.proto @@ -0,0 +1,69 @@ +// ʹõproto3汾 +syntax = "proto3"; +// ռ䣬ɴʱͻɶӦռ +option csharp_namespace = "gRPC.ZHiZHUN.AuthServer.protos"; + +/* +ÿһҪ÷ֺŽβ +message ͷݸʽ +tag messageִֵֶεıʶ(tag),Ǹֵ +*/ + + +// ûʱҪϢ Ϊһ +message GetTokenReuqest{ + string id=1; + string userName=2; + string realName=3; + string reviewerCode=4; + int32 userTypeEnumInt=5; + string userTypeShortName=6; + bool isAdmin=7; + +} + +// ʱصϢʽ +message GetTokenResponse { + int32 code=1; + string token =2; +} + + +// service ñʶģдӦķ +service TokenGrpcService{ + // ȡtoken + rpc GetUserToken(GetTokenReuqest) returns (GetTokenResponse); + +} + +/* +// ûʱҪϢ Ϊһ +message AddUserReuqest{ + string name=1; + int32 age=2; + bool isBoy=3; +} +// ʱصϢʽ +message ResultResponse { + int32 code=1; + string msg =2; +} +//ݵIJѯϢʽΪƽʱIJѯ +message QueryUserReuqest{ + string name=1; +} +//ѯصûϢʽΪص +message UserInfoResponse { + string name=1; + int32 age=2; + string gender=3; +} + +// service ñʶģдӦķ +service UserService{ + // û + rpc AddUser(AddUserReuqest) returns (ResultResponse); + // ѯû + rpc GetAllUser(QueryUserReuqest) returns (UserInfoResponse); +} +*/ diff --git a/IRaCIS.Core.API/Startup.cs b/IRaCIS.Core.API/Startup.cs new file mode 100644 index 00000000..7c635014 --- /dev/null +++ b/IRaCIS.Core.API/Startup.cs @@ -0,0 +1,194 @@ +using Autofac; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using IRaCIS.Core.Application.Filter; +using LogDashboard; +using MediatR; +using IRaCIS.Core.Application.MediatR.Handlers; +using Microsoft.Extensions.Logging; +using AspNetCoreRateLimit; +using Microsoft.AspNetCore.HttpOverrides; +using IRaCIS.Core.Infra.EFCore; +using System.Globalization; +using Microsoft.AspNetCore.Localization; +using Localization; + +namespace IRaCIS.Core.API +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + _configuration = configuration; + } + public ILogger _logger { get; } + + public IConfiguration _configuration { get; } + + //// ConfigureContainer is where you can register things directly + //// with Autofac. This runs after ConfigureServices so the things + //// here will override registrations made in ConfigureServices. + //// Don't build the container; that gets done for you by the factory. + // for castle + public void ConfigureContainer(ContainerBuilder containerBuilder) + { + containerBuilder.RegisterModule(); + + #region Test + //containerBuilder.RegisterType().PropertiesAutowired().InstancePerLifetimeScope();//עִ + + //var container = containerBuilder.Build(); + + //// Now you can resolve services using Autofac. For example, + //// this line will execute the lambda expression registered + //// to the IConfigReader service. + //using (var scope = container.BeginLifetimeScope()) + //{ + // var reader = scope.Resolve(); + + // var test = scope.Resolve(); + // var test2 = scope.Resolve(); + + // var test3 = scope.Resolve>(); + //} + #endregion + } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + //ػ + services.AddJsonLocalization(options => options.ResourcesPath = "Resources"); + + // 쳣ͳһ֤JsonлáַͳһTrim() + services.AddControllers(options => + { + //options.Filters.Add(); + options.Filters.Add(); + options.Filters.Add(); + options.Filters.Add(); + }) + .AddDataAnnotationsLocalization(options => { + options.DataAnnotationLocalizerProvider = (type, factory) => + factory.Create(typeof(SharedResource)); + }) + .AddNewtonsoftJsonSetup(); // NewtonsoftJson л + + //̬WebApi + UnifiedApiResultFilter ʡ + services.AddDynamicWebApiSetup(); + //AutoMapper + services.AddAutoMapperSetup(); + //EF ORM QueryWithNoLock + services.AddEFSetup(_configuration); + //Http Ӧѹ + services.AddResponseCompressionSetup(); + //Swagger Api ĵ + services.AddSwaggerSetup(); + //JWT Token ֤ + services.AddJWTAuthSetup(_configuration); + // MediatR Ϣ ¼ ӳ עhandlerӦϵ + services.AddMediatR(typeof(ConsistencyVerificationHandler).Assembly); + // EasyCaching + services.AddEasyCachingSetup(); + // hangfire ʱ н棬Ѻ~ + services.AddhangfireSetup(_configuration); + // QuartZ ʱ ʹhangfire ʱãҪԴ򿪣Ѿ + services.AddQuartZSetup(_configuration); + // ϴļ + services.AddStaticFileAuthorizationSetup(); + ////HttpReports ʱ + //services.AddHttpReports().AddHttpTransport(); + //Serilog ־ӻ LogDashboard־ + services.AddLogDashboardSetup(); + //ϴ + services.Configure(options => + { + options.MultipartBodyLengthLimit = int.MaxValue; + options.ValueCountLimit = int.MaxValue; + options.ValueLengthLimit = int.MaxValue; + }); + //IP ð ߺ + //services.AddIpPolicyRateLimitSetup(_configuration); + // û Ȩ + services.AddAuthorizationPolicySetup(_configuration); + //תͷ ȡʵIP + services.Configure(options => + { + options.ForwardedHeaders = + ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + }); + //DicomӰȾͼƬ ƽ̨ + services.AddDicomSetup(); + + + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + //ػ + app.UseLocalization(); + + app.UseForwardedHeaders(); + + //Ҫ token ʵľ̬ļ wwwroot css, JavaScript, and images don't require authentication. + app.UseStaticFiles(); + + //LogDashboard + app.UseLogDashboard("/LogDashboard"); + + //hangfire + app.UseHangfireConfig(env); + + // //ʱ + //app.UseHttpReports(); + + // м + //app.UseIpRateLimiting(); + + //Ӧѹ + app.UseResponseCompression(); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + //app.UseHsts(); + } + Console.WriteLine("ǰ " + env.EnvironmentName); + + //app.UseMiddleware(); + + // 쳣 404 + app.UseStatusCodePagesWithReExecute("/Error/{0}"); + + SwaggerSetup.Configure(app, env); + + + + //serilog ¼ûϢ + app.UseSerilogConfig(env); + + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + //ļŷ Token + app.UseIRacisHostStaticFileStore(env); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + } + } +} diff --git a/IRaCIS.Core.API/Test.cs b/IRaCIS.Core.API/Test.cs new file mode 100644 index 00000000..f7567c3c --- /dev/null +++ b/IRaCIS.Core.API/Test.cs @@ -0,0 +1,99 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Microsoft.EntityFrameworkCore; + +namespace EFSaving.Concurrency +{ + public class Sample + { + public static void Run() + { + // Ensure database is created and has a person in it + using (var setupContext = new PersonContext()) + { + setupContext.Database.EnsureDeleted(); + setupContext.Database.EnsureCreated(); + + setupContext.People.Add(new Person { FirstName = "John", LastName = "Doe" }); + setupContext.SaveChanges(); + } + + #region ConcurrencyHandlingCode + using var context = new PersonContext(); + // Fetch a person from database and change phone number + var person = context.People.Single(p => p.PersonId == 1); + person.PhoneNumber = "555-555-5555"; + + // Change the person's name in the database to simulate a concurrency conflict + context.Database.ExecuteSqlRaw( + "UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 1"); + + var saved = false; + while (!saved) + { + try + { + // Attempt to save changes to the database + context.SaveChanges(); + saved = true; + } + catch (DbUpdateConcurrencyException ex) + { + foreach (var entry in ex.Entries) + { + if (entry.Entity is Person) + { + var proposedValues = entry.CurrentValues; + var databaseValues = entry.GetDatabaseValues(); + + foreach (var property in proposedValues.Properties) + { + var proposedValue = proposedValues[property]; + var databaseValue = databaseValues[property]; + + // TODO: decide which value should be written to database + // proposedValues[property] = ; + } + + // Refresh original values to bypass next concurrency check + entry.OriginalValues.SetValues(databaseValues); + } + else + { + throw new NotSupportedException( + "Don't know how to handle concurrency conflicts for " + + entry.Metadata.Name); + } + } + } + } + #endregion + } + + public class PersonContext : DbContext + { + public DbSet People { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + // Requires NuGet package Microsoft.EntityFrameworkCore.SqlServer + optionsBuilder.UseSqlServer( + @"Server=(localdb)\mssqllocaldb;Database=EFSaving.Concurrency;Trusted_Connection=True"); + } + } + + public class Person + { + public int PersonId { get; set; } + + [ConcurrencyCheck] + public string FirstName { get; set; } + + [ConcurrencyCheck] + public string LastName { get; set; } + + public string PhoneNumber { get; set; } + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.API/Utility/EnableBufferingAttribute .cs b/IRaCIS.Core.API/Utility/EnableBufferingAttribute .cs new file mode 100644 index 00000000..35d698cd --- /dev/null +++ b/IRaCIS.Core.API/Utility/EnableBufferingAttribute .cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using System; + +namespace IRaCIS.Core.API.Filter +{ + public class EnableBufferingAttribute : Attribute, IResourceFilter + { + public void OnResourceExecuting(ResourceExecutingContext context) + { + context.HttpContext.Request.EnableBuffering(); + } + + public void OnResourceExecuted(ResourceExecutedContext context) + { + } + } +} diff --git a/IRaCIS.Core.API/Utility/FileHelpers.cs b/IRaCIS.Core.API/Utility/FileHelpers.cs new file mode 100644 index 00000000..a77ac047 --- /dev/null +++ b/IRaCIS.Core.API/Utility/FileHelpers.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Net.Http.Headers; + +namespace IRaCIS.Core.API.Utility +{ + public static class FileHelpers + { + private static readonly byte[] _allowedChars = { }; + // For more file signatures, see the File Signatures Database (https://www.filesignatures.net/) + // and the official specifications for the file types you wish to add. + private static readonly Dictionary> _fileSignature = new Dictionary> + { + { ".gif", new List { new byte[] { 0x47, 0x49, 0x46, 0x38 } } }, + { ".png", new List { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } } }, + { ".jpeg", new List + { + new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, + new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 }, + new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 }, + } + }, + { ".jpg", new List + { + new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, + new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 }, + new byte[] { 0xFF, 0xD8, 0xFF, 0xE8 }, + } + }, + { ".zip", new List + { + new byte[] { 0x50, 0x4B, 0x03, 0x04 }, + new byte[] { 0x50, 0x4B, 0x4C, 0x49, 0x54, 0x45 }, + new byte[] { 0x50, 0x4B, 0x53, 0x70, 0x58 }, + new byte[] { 0x50, 0x4B, 0x05, 0x06 }, + new byte[] { 0x50, 0x4B, 0x07, 0x08 }, + new byte[] { 0x57, 0x69, 0x6E, 0x5A, 0x69, 0x70 }, + } + }, + }; + + // **WARNING!** + // In the following file processing methods, the file's content isn't scanned. + // In most production scenarios, an anti-virus/anti-malware scanner API is + // used on the file before making the file available to users or other + // systems. For more information, see the topic that accompanies this sample + // app. + + public static async Task ProcessFormFile(IFormFile formFile, + ModelStateDictionary modelState, string[] permittedExtensions, + long sizeLimit) + { + var fieldDisplayName = string.Empty; + + // Use reflection to obtain the display name for the model + // property associated with this IFormFile. If a display + // name isn't found, error messages simply won't show + // a display name. + MemberInfo property = + typeof(T).GetProperty( + formFile.Name.Substring(formFile.Name.IndexOf(".", + StringComparison.Ordinal) + 1)); + + if (property != null) + { + if (property.GetCustomAttribute(typeof(DisplayAttribute)) is + DisplayAttribute displayAttribute) + { + fieldDisplayName = $"{displayAttribute.Name} "; + } + } + + // Don't trust the file name sent by the client. To display + // the file name, HTML-encode the value. + var trustedFileNameForDisplay = WebUtility.HtmlEncode( + formFile.FileName); + + // Check the file length. This check doesn't catch files that only have + // a BOM as their content. + if (formFile.Length == 0) + { + modelState.AddModelError(formFile.Name, + $"{fieldDisplayName}({trustedFileNameForDisplay}) is empty."); + + return new byte[0]; + } + + if (formFile.Length > sizeLimit) + { + var megabyteSizeLimit = sizeLimit / 1048576; + modelState.AddModelError(formFile.Name, + $"{fieldDisplayName}({trustedFileNameForDisplay}) exceeds " + + $"{megabyteSizeLimit:N1} MB."); + + return new byte[0]; + } + + try + { + using (var memoryStream = new MemoryStream()) + { + await formFile.CopyToAsync(memoryStream); + + // Check the content length in case the file's only + // content was a BOM and the content is actually + // empty after removing the BOM. + if (memoryStream.Length == 0) + { + modelState.AddModelError(formFile.Name, + $"{fieldDisplayName}({trustedFileNameForDisplay}) is empty."); + } + + if (!IsValidFileExtensionAndSignature( + formFile.FileName, memoryStream, permittedExtensions)) + { + modelState.AddModelError(formFile.Name, + $"{fieldDisplayName}({trustedFileNameForDisplay}) file " + + "type isn't permitted or the file's signature " + + "doesn't match the file's extension."); + } + else + { + return memoryStream.ToArray(); + } + } + } + catch (Exception ex) + { + modelState.AddModelError(formFile.Name, + $"{fieldDisplayName}({trustedFileNameForDisplay}) upload failed. " + + $"Please contact the Help Desk for support. Error: {ex.HResult}"); + } + return new byte[0]; + } + + public static async Task ProcessStreamedFile( + MultipartSection section, ContentDispositionHeaderValue contentDisposition, + ModelStateDictionary modelState, string[] permittedExtensions, long sizeLimit) + { + try + { + using (var memoryStream = new MemoryStream()) + { + await section.Body.CopyToAsync(memoryStream); + + // Check if the file is empty or exceeds the size limit. + if (memoryStream.Length == 0) + { + modelState.AddModelError("File", "The file is empty."); + } + else if (memoryStream.Length > sizeLimit) + { + var megabyteSizeLimit = sizeLimit / 1048576; + modelState.AddModelError("File", + $"The file exceeds {megabyteSizeLimit:N1} MB."); + } + else if (!IsValidFileExtensionAndSignature( + contentDisposition.FileName.Value, memoryStream, + permittedExtensions)) + { + modelState.AddModelError("File", + "The file type isn't permitted or the file's " + + "signature doesn't match the file's extension."); + } + else + { + return memoryStream.ToArray(); + } + } + } + catch (Exception ex) + { + modelState.AddModelError("File", + "The upload failed. Please contact the Help Desk " + + $" for support. Error: {ex.HResult}"); + // Log the exception + } + + return new byte[0]; + } + + private static bool IsValidFileExtensionAndSignature(string fileName, Stream data, string[] permittedExtensions) + { + if (string.IsNullOrEmpty(fileName) || data == null || data.Length == 0) + { + return false; + } + + var ext = Path.GetExtension(fileName).ToLowerInvariant(); + + if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext)) + { + return false; + } + + data.Position = 0; + + using (var reader = new BinaryReader(data)) + { + if (ext.Equals(".txt") || ext.Equals(".csv") || ext.Equals(".prn")) + { + if (_allowedChars.Length == 0) + { + // Limits characters to ASCII encoding. + for (var i = 0; i < data.Length; i++) + { + if (reader.ReadByte() > sbyte.MaxValue) + { + return false; + } + } + } + else + { + // Limits characters to ASCII encoding and + // values of the _allowedChars array. + for (var i = 0; i < data.Length; i++) + { + var b = reader.ReadByte(); + if (b > sbyte.MaxValue || + !_allowedChars.Contains(b)) + { + return false; + } + } + } + + return true; + } + + // Uncomment the following code block if you must permit + // files whose signature isn't provided in the _fileSignature + // dictionary. We recommend that you add file signatures + // for files (when possible) for all file types you intend + // to allow on the system and perform the file signature + // check. + + //if (!_fileSignature.ContainsKey(ext)) + //{ + // return true; + //} + + + // File signature check + // -------------------- + // With the file signatures provided in the _fileSignature + // dictionary, the following code tests the input content's + // file signature. + + //var signatures = _fileSignature[ext]; + //var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length)); + + //return signatures.Any(signature => + // headerBytes.Take(signature.Length).SequenceEqual(signature)); + + //test + return true; + } + } + } +} diff --git a/IRaCIS.Core.API/Utility/Jwt/CustomHSJWTService.cs b/IRaCIS.Core.API/Utility/Jwt/CustomHSJWTService.cs new file mode 100644 index 00000000..26e61286 --- /dev/null +++ b/IRaCIS.Core.API/Utility/Jwt/CustomHSJWTService.cs @@ -0,0 +1,65 @@ +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; + +namespace ZhaoXi._001.NET5Demo.Practice.WebApi.Utility.Jwt +{ + /// + /// 对称可逆加密 + /// + public class CustomHSJWTService : ICustomJWTService + { + #region Option注入 + private readonly JWTTokenOptions _JWTTokenOptions; + public CustomHSJWTService(IOptionsMonitor jwtTokenOptions) + { + this._JWTTokenOptions = jwtTokenOptions.CurrentValue; + } + #endregion + + /// + /// 用户登录成功以后,用来生成Token的方法 + /// + /// + /// + /// + public string GetToken(string UserName, string password) + { + #region 有效载荷,大家可以自己写,爱写多少写多少;尽量避免敏感信息 + var claims = new[] + { + new Claim(ClaimTypes.Name, UserName), + new Claim("NickName",UserName), + new Claim("Role","Administrator"),//传递其他信息 + new Claim("ABCC","ABCC"), + new Claim("ABCCDDDDD","ABCCDDDDD"), + new Claim("Student","甜酱油") + }; + + //需要加密:需要加密key: + //Nuget引入:Microsoft.IdentityModel.Tokens + SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_JWTTokenOptions.SecurityKey)); + + SigningCredentials creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + //Nuget引入:System.IdentityModel.Tokens.Jwt + JwtSecurityToken token = new JwtSecurityToken( + issuer: _JWTTokenOptions.Issuer, + audience: _JWTTokenOptions.Audience, + claims: claims, + expires: DateTime.Now.AddMinutes(5),//5分钟有效期 + signingCredentials: creds); + + string returnToken = new JwtSecurityTokenHandler().WriteToken(token); + return returnToken; + #endregion + + } + } +} diff --git a/IRaCIS.Core.API/Utility/Jwt/CustomRSSJWTervice.cs b/IRaCIS.Core.API/Utility/Jwt/CustomRSSJWTervice.cs new file mode 100644 index 00000000..b93b444d --- /dev/null +++ b/IRaCIS.Core.API/Utility/Jwt/CustomRSSJWTervice.cs @@ -0,0 +1,60 @@ +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Threading.Tasks; + +namespace ZhaoXi._001.NET5Demo.Practice.WebApi.Utility.Jwt +{ + /// + /// 非对称可逆加密 + /// + public class CustomRSSJWTervice : ICustomJWTService + + { + #region Option注入 + private readonly JWTTokenOptions _JWTTokenOptions; + public CustomRSSJWTervice(IOptionsMonitor jwtTokenOptions) + { + this._JWTTokenOptions = jwtTokenOptions.CurrentValue; + } + #endregion + + public string GetToken(string userName, string password) + { + #region 使用加密解密Key 非对称 + string keyDir = Directory.GetCurrentDirectory(); + if (RSAHelper.TryGetKeyParameters(keyDir, true, out RSAParameters keyParams) == false) + { + keyParams = RSAHelper.GenerateAndSaveKey(keyDir); + } + #endregion + + //string jtiCustom = Guid.NewGuid().ToString();//用来标识 Token + Claim[] claims = new[] + { + new Claim(ClaimTypes.Name, userName), + new Claim(ClaimTypes.Role,"admin"), + new Claim("password",password) + }; + + SigningCredentials credentials = new SigningCredentials(new RsaSecurityKey(keyParams), SecurityAlgorithms.RsaSha256Signature); + + var token = new JwtSecurityToken( + issuer: this._JWTTokenOptions.Issuer, + audience: this._JWTTokenOptions.Audience, + claims: claims, + expires: DateTime.Now.AddMinutes(60),//5分钟有效期 + signingCredentials: credentials); + + var handler = new JwtSecurityTokenHandler(); + string tokenString = handler.WriteToken(token); + return tokenString; + } + } +} diff --git a/IRaCIS.Core.API/Utility/Jwt/ICustomJWTService.cs b/IRaCIS.Core.API/Utility/Jwt/ICustomJWTService.cs new file mode 100644 index 00000000..ac1af958 --- /dev/null +++ b/IRaCIS.Core.API/Utility/Jwt/ICustomJWTService.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ZhaoXi._001.NET5Demo.Practice.WebApi.Utility.Jwt +{ + public interface ICustomJWTService + { + string GetToken(string UserName, string password); + } +} diff --git a/IRaCIS.Core.API/Utility/Jwt/JWTTokenOptions.cs b/IRaCIS.Core.API/Utility/Jwt/JWTTokenOptions.cs new file mode 100644 index 00000000..e8a08768 --- /dev/null +++ b/IRaCIS.Core.API/Utility/Jwt/JWTTokenOptions.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ZhaoXi._001.NET5Demo.Practice.WebApi.Utility.Jwt +{ + public class JWTTokenOptions + { + public string Audience + { + get; + set; + } + public string SecurityKey + { + get; + set; + } + //public SigningCredentials Credentials + //{ + // get; + // set; + //} + public string Issuer + { + get; + set; + } + } +} diff --git a/IRaCIS.Core.API/Utility/Jwt/RSAHelper.cs b/IRaCIS.Core.API/Utility/Jwt/RSAHelper.cs new file mode 100644 index 00000000..707ba463 --- /dev/null +++ b/IRaCIS.Core.API/Utility/Jwt/RSAHelper.cs @@ -0,0 +1,61 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Threading.Tasks; + +namespace ZhaoXi._001.NET5Demo.Practice.WebApi.Utility.Jwt +{ + public class RSAHelper + { + /// + /// 从本地文件中读取用来签发 Token 的 RSA Key + /// + /// 存放密钥的文件夹路径 + /// + /// + /// + public static bool TryGetKeyParameters(string filePath, bool withPrivate, out RSAParameters keyParameters) + { + string filename = withPrivate ? "key.json" : "key.public.json"; + string fileTotalPath = Path.Combine(filePath, filename); + keyParameters = default(RSAParameters); + if (!File.Exists(fileTotalPath)) + { + return false; + } + else + { + keyParameters = JsonConvert.DeserializeObject(File.ReadAllText(fileTotalPath)); + return true; + } + } + /// + /// 生成并保存 RSA 公钥与私钥 + /// + /// + /// + /// + public static RSAParameters GenerateAndSaveKey(string filePath, bool withPrivate = true) + { + RSAParameters publicKeys, privateKeys; + using (var rsa = new RSACryptoServiceProvider(2048))//即时生成 + { + try + { + privateKeys = rsa.ExportParameters(true); + publicKeys = rsa.ExportParameters(false); + } + finally + { + rsa.PersistKeyInCsp = false; + } + } + File.WriteAllText(Path.Combine(filePath, "key.json"), JsonConvert.SerializeObject(privateKeys)); + File.WriteAllText(Path.Combine(filePath, "key.public.json"), JsonConvert.SerializeObject(publicKeys)); + return withPrivate ? privateKeys : publicKeys; + } + } +} diff --git a/IRaCIS.Core.API/Utility/MultipartRequestHelper.cs b/IRaCIS.Core.API/Utility/MultipartRequestHelper.cs new file mode 100644 index 00000000..6be5c7bb --- /dev/null +++ b/IRaCIS.Core.API/Utility/MultipartRequestHelper.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; +using Microsoft.Net.Http.Headers; + +namespace IRaCIS.Core.API.Utility +{ + public static class MultipartRequestHelper + { + // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq" + // The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit. + public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit) + { + var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value; + + if (string.IsNullOrWhiteSpace(boundary)) + { + throw new InvalidDataException("Missing content-type boundary."); + } + + if (boundary.Length > lengthLimit) + { + throw new InvalidDataException( + $"Multipart boundary length limit {lengthLimit} exceeded."); + } + + return boundary; + } + + public static bool IsMultipartContentType(string contentType) + { + return !string.IsNullOrEmpty(contentType) + && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0; + } + + public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition) + { + // Content-Disposition: form-data; name="key"; + return contentDisposition != null + && contentDisposition.DispositionType.Equals("form-data") + && string.IsNullOrEmpty(contentDisposition.FileName.Value) + && string.IsNullOrEmpty(contentDisposition.FileNameStar.Value); + } + + public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition) + { + // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg" + return contentDisposition != null + && contentDisposition.DispositionType.Equals("form-data") + && (!string.IsNullOrEmpty(contentDisposition.FileName.Value) + || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value)); + } + } +} diff --git a/IRaCIS.Core.API/_PipelineExtensions/AuthMiddleware.cs b/IRaCIS.Core.API/_PipelineExtensions/AuthMiddleware.cs new file mode 100644 index 00000000..7308942e --- /dev/null +++ b/IRaCIS.Core.API/_PipelineExtensions/AuthMiddleware.cs @@ -0,0 +1,66 @@ +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Net; +using System.Threading.Tasks; +using EasyCaching.Core; +using IRaCIS.Core.Domain.Share; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http; + +namespace IRaCIS.WX.CoreApi.Auth +{ + public class AuthMiddleware + { + private readonly RequestDelegate _next; + private readonly IEasyCachingProvider _provider; + public AuthMiddleware(RequestDelegate next, IEasyCachingProvider provider) + { + _next = next; + _provider = provider; + } + /// + ///为了前端 一段时间无操作,需要重新登陆 + /// + /// + /// + public async Task Invoke(HttpContext httpContext) + { + + var isLogin = httpContext.Request.Path.ToString().ToLower().Contains("login"); + + var result = await httpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); + + if (!isLogin) + { + if (!result.Succeeded) + { + httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + await httpContext.Response.WriteAsync("Unauthorized"); + } + else + { + var toekn = result.Properties.Items[".Token.access_token"]; + var jwtHandler = new JwtSecurityTokenHandler(); + JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(toekn); + object userId; + jwtToken.Payload.TryGetValue("id", out userId); + + var cacheValueExist = await _provider.ExistsAsync(userId.ToString()); //Get(userId.ToString()).ToString(); + if (!cacheValueExist) + { + httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + await httpContext.Response.WriteAsync("Unauthorized"); + } + else + { + await _provider.SetAsync(userId.ToString(), userId.ToString(), TimeSpan.FromMinutes(AppSettings.LoginExpiredTimeSpan)); + httpContext.User = result.Principal; + await _next.Invoke(httpContext); + } + } + } + else await _next.Invoke(httpContext); + } + } +} diff --git a/IRaCIS.Core.API/_PipelineExtensions/Hangfire/HangfireConfig.cs b/IRaCIS.Core.API/_PipelineExtensions/Hangfire/HangfireConfig.cs new file mode 100644 index 00000000..06ef8054 --- /dev/null +++ b/IRaCIS.Core.API/_PipelineExtensions/Hangfire/HangfireConfig.cs @@ -0,0 +1,39 @@ +using Hangfire; +using Hangfire.Dashboard; +using IRaCIS.Application.Services.BackGroundJob; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; + +namespace IRaCIS.Core.API +{ + + + public static class HangfireConfig + { + + public static void UseHangfireConfig(this IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseHangfireDashboard("/api/hangfire", new DashboardOptions() + { + //直接访问,没有带token 获取不到用户身份信息,所以这种自定义授权暂时没法使用 + //Authorization = new[] { new hangfireAuthorizationFilter() } + + //本地请求 才能看 + Authorization = new[] { new LocalRequestsOnlyAuthorizationFilter() } + + }); + + #region hangfire + //// 延迟任务执行 1秒之后执行 有时启动没运行 换成添加到队列中 + //BackgroundJob.Schedule(t => t.MemoryCacheTrialStatus(), TimeSpan.FromSeconds(1)); + ////添加到后台任务队列, + //BackgroundJob.Enqueue(t => t.MemoryCacheTrialStatus()); + + //周期性任务,1天执行一次 + RecurringJob.AddOrUpdate(t => t.MemoryCacheTrialStatus(), Cron.Daily); + + #endregion + + } + } +} diff --git a/IRaCIS.Core.API/_PipelineExtensions/Hangfire/hangfireAuthorizationFilter.cs b/IRaCIS.Core.API/_PipelineExtensions/Hangfire/hangfireAuthorizationFilter.cs new file mode 100644 index 00000000..707b5523 --- /dev/null +++ b/IRaCIS.Core.API/_PipelineExtensions/Hangfire/hangfireAuthorizationFilter.cs @@ -0,0 +1,17 @@ +using Hangfire.Dashboard; + +namespace IRaCIS.Core.API.Filter +{ + public class hangfireAuthorizationFilter : IDashboardAuthorizationFilter + { + public bool Authorize(DashboardContext context) + { + var httpContext = context.GetHttpContext(); + + // Allow all authenticated users to see the Dashboard (potentially dangerous). + return httpContext.User.Identity.IsAuthenticated; + + //return true; + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.API/_PipelineExtensions/IRacisHostFileStoreConfig.cs b/IRaCIS.Core.API/_PipelineExtensions/IRacisHostFileStoreConfig.cs new file mode 100644 index 00000000..60827a51 --- /dev/null +++ b/IRaCIS.Core.API/_PipelineExtensions/IRacisHostFileStoreConfig.cs @@ -0,0 +1,44 @@ +using IRaCIS.Core.Domain.Share; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.FileProviders; +using System.IO; + +namespace IRaCIS.Core.API +{ + public static class IRacisHostFileStore + { + + public static void UseIRacisHostStaticFileStore(this IApplicationBuilder app, IWebHostEnvironment env) + { + + var uploadPath = Path.Combine(Directory.GetParent(env.ContentRootPath.TrimEnd('\\')).FullName, StaticData.UploadFileFolder); + var dicomPath = Path.Combine(Directory.GetParent(env.ContentRootPath.TrimEnd('\\')).FullName, StaticData.TrialDataFolder); + + + if (!Directory.Exists(uploadPath)) + { + Directory.CreateDirectory(uploadPath); + } + + if (!Directory.Exists(dicomPath)) + { + Directory.CreateDirectory(dicomPath); + } + + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider(uploadPath), + RequestPath = $"/{StaticData.UploadFileFolder}" + }); + + + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider(dicomPath), + RequestPath = $"/{StaticData.TrialDataFolder}" + }); + + } + } +} diff --git a/IRaCIS.Core.API/_PipelineExtensions/LocalizationConfig.cs b/IRaCIS.Core.API/_PipelineExtensions/LocalizationConfig.cs new file mode 100644 index 00000000..45ab1ec1 --- /dev/null +++ b/IRaCIS.Core.API/_PipelineExtensions/LocalizationConfig.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Localization; +using System.Collections.Generic; +using System.Globalization; + +namespace IRaCIS.Core.API +{ + public static class LocalizationConfig + { + + public static void UseLocalization(this IApplicationBuilder app) + { + var supportedCultures = new List + { + new CultureInfo("en-US"), + new CultureInfo("zh-CN") + }; + + var options = new RequestLocalizationOptions + { + //DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = supportedCultures, + SupportedUICultures = supportedCultures, + + ApplyCurrentCultureToResponseHeaders = true + }; + + //options.RequestCultureProviders.RemoveAt(0); + //options.RequestCultureProviders.RemoveAt(1); + + app.UseRequestLocalization(options); + } + } +} diff --git a/IRaCIS.Core.API/_PipelineExtensions/LogDashboard/LogDashBoardAuthFilter.cs b/IRaCIS.Core.API/_PipelineExtensions/LogDashboard/LogDashBoardAuthFilter.cs new file mode 100644 index 00000000..2fd35eae --- /dev/null +++ b/IRaCIS.Core.API/_PipelineExtensions/LogDashboard/LogDashBoardAuthFilter.cs @@ -0,0 +1,15 @@ +using LogDashboard; +using LogDashboard.Authorization; + +namespace IRaCIS.Core.API.Filter +{ + + public class LogDashBoardAuthFilter : ILogDashboardAuthorizationFilter + { + //在此可以利用 本系统的UerTypeEnum 判断 + public bool Authorization(LogDashboardContext context) + { + return context.HttpContext.User.Identity.IsAuthenticated; + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.API/_PipelineExtensions/Serilog/RequestResponseLoggingMiddleware.cs b/IRaCIS.Core.API/_PipelineExtensions/Serilog/RequestResponseLoggingMiddleware.cs new file mode 100644 index 00000000..720b0d95 --- /dev/null +++ b/IRaCIS.Core.API/_PipelineExtensions/Serilog/RequestResponseLoggingMiddleware.cs @@ -0,0 +1,104 @@ +using Microsoft.AspNetCore.Http; +using Serilog; +using Serilog.Context; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace IRaCIS.Core.API._PipelineExtensions.Serilog +{ + public class RequestResponseLoggingMiddleware + { + private readonly RequestDelegate _next; + + public RequestResponseLoggingMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext context) + { + var ContentType = context.Request.ContentType; + + var isUploadRelation = false; + + if (ContentType != null) + { + isUploadRelation = context.Request.ContentType.Contains("multipart/form-data") || context.Request.ContentType.Contains("boundary"); + } + + + if (isUploadRelation) + { + using (LogContext.PushProperty("RequestBody", string.Empty)) + { + await _next.Invoke(context); + } + } + else + { + var requestBodyPayload = await ReadRequestBody(context.Request); + + using (LogContext.PushProperty("RequestBody", requestBodyPayload)) + { + await _next.Invoke(context); + } + } + + //var request = context.Request; + + + // Read and log request body data + + //SerilogHelper.RequestPayload = requestBodyPayload; + + #region ResponseBody + + //// Read and log response body data + //// Copy a pointer to the original response body stream + //var originalResponseBodyStream = context.Response.Body; + + //// Create a new memory stream... + //using (var responseBody = new MemoryStream()) + //{ + // // ...and use that for the temporary response body + // context.Response.Body = responseBody; + + // // Continue down the Middleware pipeline, eventually returning to this class + //await _next(context); + + // // Copy the contents of the new memory stream (which contains the response) to the original stream, which is then returned to the client. + // await responseBody.CopyToAsync(originalResponseBodyStream); + //} + //string responseBodyPayload = await ReadResponseBody(context.Response); + //_diagnosticContext.Set("ResponseBody", responseBodyPayload); + + #endregion + + + } + + private async Task ReadRequestBody(HttpRequest request) + { + // Ensure the request's body can be read multiple times (for the next middlewares in the pipeline). + request.EnableBuffering(); + + using var streamReader = new StreamReader(request.Body, leaveOpen: true); + var requestBody = await streamReader.ReadToEndAsync(); + + // Reset the request's body stream position for next middleware in the pipeline. + request.Body.Position = 0; + return requestBody == null ? String.Empty : requestBody.Trim(); + } + + private static async Task ReadResponseBody(HttpResponse response) + { + response.Body.Seek(0, SeekOrigin.Begin); + string responseBody = await new StreamReader(response.Body).ReadToEndAsync(); + response.Body.Seek(0, SeekOrigin.Begin); + + return $"{responseBody}"; + } + + } +} diff --git a/IRaCIS.Core.API/_PipelineExtensions/Serilog/SerilogConfig.cs b/IRaCIS.Core.API/_PipelineExtensions/Serilog/SerilogConfig.cs new file mode 100644 index 00000000..c5aab64f --- /dev/null +++ b/IRaCIS.Core.API/_PipelineExtensions/Serilog/SerilogConfig.cs @@ -0,0 +1,30 @@ +using Hangfire; +using Hangfire.Dashboard; +using IRaCIS.Core.API._PipelineExtensions.Serilog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Serilog; + +namespace IRaCIS.Core.API +{ + + + public static class SerilogConfig + { + + public static void UseSerilogConfig(this IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseMiddleware(); + + app.UseSerilogRequestLogging(opts + => + { + opts.MessageTemplate = "{TokenUserRealName} {TokenUserType} {ClientIp} {RequestIP} {Host} {Protocol} {RequestMethod} {RequestPath} {RequestBody} responded {StatusCode} in {Elapsed:0.0000} ms"; + opts.EnrichDiagnosticContext = SerilogHelper.EnrichFromRequest; + }); + + + + } + } +} diff --git a/IRaCIS.Core.API/_PipelineExtensions/Serilog/SerilogHelper.cs b/IRaCIS.Core.API/_PipelineExtensions/Serilog/SerilogHelper.cs new file mode 100644 index 00000000..1bb30e7b --- /dev/null +++ b/IRaCIS.Core.API/_PipelineExtensions/Serilog/SerilogHelper.cs @@ -0,0 +1,58 @@ +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Http; +using Serilog; +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace IRaCIS.Core.API +{ + public class SerilogHelper + { + //public static string RequestPayload = ""; + + public static void EnrichFromRequest(IDiagnosticContext diagnosticContext, HttpContext httpContext) + { + var request = httpContext.Request; + + // Set all the common properties available for every request + diagnosticContext.Set("Host", request.Host); + + //这种获取的Ip不准 配置服务才行 + diagnosticContext.Set("RequestIP", httpContext.Connection.RemoteIpAddress.ToString()); + + //这种方式可以,但是serilog提供了 就不用了 + //diagnosticContext.Set("TestIP", httpContext.GetUserIp()); + + diagnosticContext.Set("Protocol", request.Protocol); + diagnosticContext.Set("Scheme", request.Scheme); + + //这种方式不行 读取的body为空字符串 必须在中间件中读取 + //diagnosticContext.Set("RequestBody", await ReadRequestBody(httpContext.Request)); + //diagnosticContext.Set("RequestBody", RequestPayload); + + // Only set it if available. You're not sending sensitive data in a querystring right?! + if (request.QueryString.HasValue) + { + diagnosticContext.Set("QueryString", request.QueryString.Value); + } + + // Set the content-type of the Response at this point + diagnosticContext.Set("ContentType", httpContext.Response.ContentType); + + diagnosticContext.Set("TokenUserRealName", httpContext?.User?.FindFirst("realName")?.Value); + + diagnosticContext.Set("TokenUserType", httpContext?.User?.FindFirst("userTypeEnumName")?.Value); + + // Retrieve the IEndpointFeature selected for the request + var endpoint = httpContext.GetEndpoint(); + if (endpoint is object) // endpoint != null + { + diagnosticContext.Set("EndpointName", endpoint.DisplayName); + } + } + + + } +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/Authorization/ApiResponseHandler.cs b/IRaCIS.Core.API/_ServiceExtensions/Authorization/ApiResponseHandler.cs new file mode 100644 index 00000000..921c156e --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/Authorization/ApiResponseHandler.cs @@ -0,0 +1,38 @@ +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using System; +using System.Text.Encodings.Web; +using System.Threading.Tasks; + +namespace IRaCIS.Core.API +{ + public class ApiResponseHandler : AuthenticationHandler + { + public ApiResponseHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + { + } + + protected override Task HandleAuthenticateAsync() + { + throw new NotImplementedException(); + } + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) + { + Response.ContentType = "application/json"; + Response.StatusCode = StatusCodes.Status401Unauthorized; + await Response.WriteAsync(JsonConvert.SerializeObject(ResponseOutput.NotOk("您无权访问该接口"))); + } + + protected override async Task HandleForbiddenAsync(AuthenticationProperties properties) + { + Response.ContentType = "application/json"; + Response.StatusCode = StatusCodes.Status403Forbidden; + await Response.WriteAsync(JsonConvert.SerializeObject(ResponseOutput.NotOk("您的权限不允许进行该操作"))); + } + + } +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/Authorization/AuthorizationPolicySetup.cs b/IRaCIS.Core.API/_ServiceExtensions/Authorization/AuthorizationPolicySetup.cs new file mode 100644 index 00000000..f26461f2 --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/Authorization/AuthorizationPolicySetup.cs @@ -0,0 +1,32 @@ +using IRaCIS.Core.Domain.Share; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace IRaCIS.Core.API +{ + public static class AuthorizationPolicySetup + { + + public static void AddAuthorizationPolicySetup(this IServiceCollection services, IConfiguration configuration) + { + services.AddAuthorization(options => + { + //影像质控策略 只允许 CRC QA进行操作 + options.AddPolicy("ImageQCPolicy", policyBuilder => + { + policyBuilder.RequireClaim("userTypeEnumInt", ((int)UserTypeEnum.ClinicalResearchCoordinator).ToString(), ((int)UserTypeEnum.IQC).ToString()); + }); + + //一致性核查策略 只允许 CRC PM APM 进行操作 + options.AddPolicy("ImageCheckPolicy", policyBuilder => + { + policyBuilder.RequireClaim("userTypeEnumInt", ((int)UserTypeEnum.ProjectManager).ToString(), ((int)UserTypeEnum.ClinicalResearchCoordinator).ToString(), ((int)UserTypeEnum.APM).ToString()); + }); + + + + + }); + } + } +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/Authorization/JWTAuthSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/Authorization/JWTAuthSetup.cs new file mode 100644 index 00000000..ff0a92b7 --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/Authorization/JWTAuthSetup.cs @@ -0,0 +1,91 @@ +using Invio.Extensions.Authentication.JwtBearer; +using IRaCIS.Core.Application.Auth; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Text; + +namespace IRaCIS.Core.API +{ + public static class JWTAuthSetup + { + public static void AddJWTAuthSetup(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection("JwtSetting")); + + var jwtSetting = new JwtSetting(); + configuration.Bind("JwtSetting", jwtSetting); + + services + .AddAuthentication(o=> { + o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultChallengeScheme = nameof(ApiResponseHandler); + o.DefaultForbidScheme = nameof(ApiResponseHandler); + }) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidIssuer = jwtSetting.Issuer, + ValidAudience = jwtSetting.Audience, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSetting.SecurityKey)), + // 默认 300s + ClockSkew = TimeSpan.Zero + }; + + // OPTION 1: use `Invio.Extensions.Authentication.JwtBearer` + + options.AddQueryStringAuthentication(); + + // OPTION 2: do it manually + + #region + //options.Events = new JwtBearerEvents + //{ + // OnMessageReceived = (context) => { + + // if (!context.Request.Query.TryGetValue("access_token", out StringValues values)) + // { + // return Task.CompletedTask; + // } + + // if (values.Count > 1) + // { + // context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + // context.Fail( + // "Only one 'access_token' query string parameter can be defined. " + + // $"However, {values.Count:N0} were included in the request." + // ); + + // return Task.CompletedTask; + // } + + // var token = values.Single(); + + // if (String.IsNullOrWhiteSpace(token)) + // { + // context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + // context.Fail( + // "The 'access_token' query string parameter was defined, " + + // "but a value to represent the token was not included." + // ); + + // return Task.CompletedTask; + // } + + // context.Token = token; + + // return Task.CompletedTask; + // } + //}; + #endregion + + }) + .AddScheme(nameof(ApiResponseHandler), o => { }); + + } + } +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/AutoMapperSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/AutoMapperSetup.cs new file mode 100644 index 00000000..5797d890 --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/AutoMapperSetup.cs @@ -0,0 +1,36 @@ +using AutoMapper.EquivalencyExpression; +using IRaCIS.Core.Application.Service; +using Microsoft.Extensions.DependencyInjection; + +namespace IRaCIS.Core.API +{ + public static class AutoMapperSetup + { + public static void AddAutoMapperSetup(this IServiceCollection services) + { + + services.AddAutoMapper(automapper => + { + //AutoMapper.Collection.EntityFrameworkCore + automapper.AddCollectionMappers(); + + #region 会使 IncludeMembers 失效 不能全局使用 + //mapping an EntityFramework Core DbContext-object. + //automapper.UseEntityFrameworkCoreModel(services); + + + //automapper.ForAllMaps((a, b) => b.ForAllMembers(opt => opt.Condition((src, dest, srcMember, desMenber) => + //{ + // //// Can test When Guid? -> Guid if source is null will change to Guid.Empty + // //Console.WriteLine("srcMember:" + srcMember + "desMenber:" + desMenber); + // return srcMember != null && srcMember.ToString() != Guid.Empty.ToString(); + // // not want to map a null Guid? value to db Guid value + //}))); + #endregion + + }, typeof(QCConfig).Assembly); + + + } + } +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/AutofacModuleSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/AutofacModuleSetup.cs new file mode 100644 index 00000000..9e65f3c6 --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/AutofacModuleSetup.cs @@ -0,0 +1,79 @@ +using Autofac; +using Autofac.Extras.DynamicProxy; +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Services; +using IRaCIS.Core.API.Utility.AOP; +using IRaCIS.Core.Application; +using IRaCIS.Core.Application.AOP; +using IRaCIS.Core.Application.BackGroundJob; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Infra.EFCore.AuthUser; +using Microsoft.AspNetCore.Http; +using Panda.DynamicWebApi; +using System; +using System.Linq; +using System.Reflection; + + +namespace IRaCIS.Core.API +{ + // ReSharper disable once IdentifierTypo + public class AutofacModuleSetup : Autofac.Module + { + protected override void Load(ContainerBuilder containerBuilder) + { + + #region byzhouhang 20210917 此处注册泛型仓储 可以减少Domain层 和Infra.EFcore 两层 空的仓储接口定义和 仓储文件定义 + + containerBuilder.RegisterGeneric(typeof(Repository<>)) + .As(typeof(IRepository<>)).InstancePerLifetimeScope();//注册泛型仓储 + + containerBuilder.RegisterGeneric(typeof(EFUnitOfWork<>)) + .As(typeof(IEFUnitOfWork<>)).InstancePerLifetimeScope();//注册仓储 + + #endregion + + #region 指定控制器也由autofac 来进行实例获取 https://www.cnblogs.com/xwhqwer/p/15320838.html + + //获取所有控制器类型并使用属性注入 + containerBuilder.RegisterAssemblyTypes(typeof(BaseService).Assembly) + .Where(type => typeof(IDynamicWebApi).IsAssignableFrom(type)) + .PropertiesAutowired(); + + //var controllerBaseType = typeof(ControllerBase); + //containerBuilder.RegisterAssemblyTypes(typeof(BaseService).Assembly) + // .Where(t => controllerBaseType.IsAssignableFrom(t) && t != controllerBaseType) + // .PropertiesAutowired(); + + + #endregion + + Assembly application = Assembly.LoadFrom(AppDomain.CurrentDomain.BaseDirectory + "IRaCIS.Core.Application.dll"); + containerBuilder.RegisterAssemblyTypes(application).Where(t => t.FullName.Contains("Service")) + .PropertiesAutowired().AsImplementedInterfaces().EnableClassInterceptors(); + + Assembly infrastructure = Assembly.Load("IRaCIS.Core.Infra.EFCore"); + containerBuilder.RegisterAssemblyTypes(infrastructure).AsImplementedInterfaces(); + + containerBuilder.RegisterType().As().SingleInstance(); + + + + containerBuilder.RegisterType().SingleInstance(); + + //Autofac 注册拦截器 需要注意的是生成api上服务上的动态代理AOP失效 间接掉用不影响 + containerBuilder.RegisterType(); + containerBuilder.RegisterType(); + //containerBuilder.RegisterType(); + //containerBuilder.RegisterType().As().SingleInstance(); + + + //注册hangfire任务 依赖注入 + containerBuilder.RegisterType().As().InstancePerDependency(); + + + + + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.API/_ServiceExtensions/DicomSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/DicomSetup.cs new file mode 100644 index 00000000..c32dbd4c --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/DicomSetup.cs @@ -0,0 +1,20 @@ +using FellowOakDicom; +using FellowOakDicom.Imaging; +using Microsoft.Extensions.DependencyInjection; + +namespace IRaCIS.Core.API +{ + public static class DicomSetup + { + public static void AddDicomSetup(this IServiceCollection services) + { + new DicomSetupBuilder() + .RegisterServices(s => s.AddFellowOakDicom() + .AddTranscoderManager() + .AddImageManager() + ) + .SkipValidation() + .Build(); + } + } +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/DynamicWebApiSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/DynamicWebApiSetup.cs new file mode 100644 index 00000000..787c3918 --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/DynamicWebApiSetup.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.DependencyInjection; +using Panda.DynamicWebApi; + +namespace IRaCIS.Core.API +{ + public static class DynamicWebApiSetup + { + //20210910 避免冗余的控制器层代码编写,仅仅包了一层前后台定义的格式 这里采用动态webAPi+IResultFilter 替代大部分情况 + public static void AddDynamicWebApiSetup(this IServiceCollection services) + { + //动态webApi 目前存在的唯一小坑是生成api上服务上的动态代理AOP失效 间接掉用不影响 + services.AddDynamicWebApi(dynamicWebApiOption => + { + //默认是 api + dynamicWebApiOption.DefaultApiPrefix = ""; + //首字母小写 + dynamicWebApiOption.GetRestFulActionName = (actionName) => char.ToLower(actionName[0]) + actionName.Substring(1); + //删除 Service后缀 + dynamicWebApiOption.RemoveControllerPostfixes.Add("Service"); + + }); + } + } +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/EFSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/EFSetup.cs new file mode 100644 index 00000000..f9c75ef1 --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/EFSetup.cs @@ -0,0 +1,30 @@ +using IRaCIS.Core.Infra.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace IRaCIS.Core.API +{ + public static class EFSetup + { + public static void AddEFSetup( this IServiceCollection services, IConfiguration configuration) + { + //services.AddScoped(); + + //这个注入没有成功--注入是没问题的,构造函数也只是支持参数就好,错在注入的地方不能写DbContext + //Web程序中通过重用池中DbContext实例可提高高并发场景下的吞吐量, 这在概念上类似于ADO.NET Provider原生的连接池操作方式,具有节省DbContext实例化成本的优点 + services.AddDbContext(options => + { + options.UseSqlServer(configuration.GetSection("ConnectionStrings:RemoteNew").Value, + contextOptionsBuilder => contextOptionsBuilder.EnableRetryOnFailure()); + + options.EnableSensitiveDataLogging(); + + options.AddInterceptors(new QueryWithNoLockDbCommandInterceptor()); + + options.AddInterceptors(new AuditingInterceptor(configuration.GetSection("ConnectionStrings:RemoteNew").Value)); + + }); + } + } +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/EasyCachingSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/EasyCachingSetup.cs new file mode 100644 index 00000000..7d7a28c6 --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/EasyCachingSetup.cs @@ -0,0 +1,18 @@ +using EasyCaching.Core; +using EasyCaching.Interceptor.Castle; +using Microsoft.Extensions.DependencyInjection; + +namespace IRaCIS.Core.API +{ + public static class EasyCachingSetup + { + public static void AddEasyCachingSetup(this IServiceCollection services) + { + services.AddEasyCaching(options => + { + options.UseInMemory(); + }); + services.ConfigureCastleInterceptor(options => options.CacheProviderName = EasyCachingConstValue.DefaultInMemoryName); + } + } +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/IpPolicyRateLimitSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/IpPolicyRateLimitSetup.cs new file mode 100644 index 00000000..74ea2228 --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/IpPolicyRateLimitSetup.cs @@ -0,0 +1,38 @@ +using AspNetCoreRateLimit; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace IRaCIS.Core.API +{ + /// + /// IPLimit限流 启动服务 + /// + public static class IpPolicyRateLimitSetup + { + public static void AddIpPolicyRateLimitSetup(this IServiceCollection services, IConfiguration Configuration) + { + + // needed to store rate limit counters and ip rules + services.AddMemoryCache(); + + //load general configuration from appsettings.json + services.Configure(Configuration.GetSection("IpRateLimiting")); + + //load ip rules from appsettings.json + //services.Configure(Configuration.GetSection("IpRateLimitPolicies")); + + // inject counter and rules stores + services.AddInMemoryRateLimiting(); + //services.AddDistributedRateLimiting(); + //services.AddDistributedRateLimiting(); + //services.AddRedisRateLimiting(); + + + + // configuration (resolvers, counter key builders) + services.AddSingleton(); + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.API/_ServiceExtensions/LogDashboardSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/LogDashboardSetup.cs new file mode 100644 index 00000000..4042dfef --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/LogDashboardSetup.cs @@ -0,0 +1,26 @@ + +using LogDashboard; +using LogDashboard.Authorization.Filters; +using Microsoft.Extensions.DependencyInjection; + +namespace IRaCIS.Core.API +{ + public static class LogDashboardSetup + { + public static void AddLogDashboardSetup(this IServiceCollection services) + { + //IIS 配置虚拟路径部署,会出现IIS静态文件404 + services.AddLogDashboard(opt => + { + //opt.PathMatch = "/api/LogDashboard"; + opt.PathMatch = "/LogDashboard"; + + //opt.AddAuthorizationFilter(new LogDashboardBasicAuthFilter("admin", "zhizhun2018")); + + //opt.AddAuthorizationFilter(new LogDashBoardAuthFilter()); + + }); + + } + } +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/MiniProfilerConfigure.cs b/IRaCIS.Core.API/_ServiceExtensions/MiniProfilerConfigure.cs new file mode 100644 index 00000000..f08e27aa --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/MiniProfilerConfigure.cs @@ -0,0 +1,81 @@ +//using System; +//using Microsoft.Extensions.DependencyInjection; +//using StackExchange.Profiling.Storage; + +//namespace IRaCIS.Core.API +//{ +// public class MiniProfilerConfigure +// { +// public static void ConfigureMiniProfiler(IServiceCollection services) +// { + +// services.AddMiniProfiler(options => +// { +// // All of this is optional. You can simply call .AddMiniProfiler() for all defaults + +// // (Optional) Path to use for profiler URLs, default is /mini-profiler-resources +// options.RouteBasePath = "/profiler"; + +// //// (Optional) Control storage +// //// (default is 30 minutes in MemoryCacheStorage) +// (options.Storage as MemoryCacheStorage).CacheDuration = TimeSpan.FromMinutes(10); + +// //// (Optional) Control which SQL formatter to use, InlineFormatter is the default +// //options.SqlFormatter = new StackExchange.Profiling.SqlFormatters.InlineFormatter(); + +// //// (Optional) To control authorization, you can use the Func options: +// //// (default is everyone can access profilers) +// //options.ResultsAuthorize = request => MyGetUserFunction(request).CanSeeMiniProfiler; +// //options.ResultsListAuthorize = request => MyGetUserFunction(request).CanSeeMiniProfiler; +// //// Or, there are async versions available: +// //options.ResultsAuthorizeAsync = async request => (await MyGetUserFunctionAsync(request)).CanSeeMiniProfiler; +// //options.ResultsAuthorizeListAsync = async request => (await MyGetUserFunctionAsync(request)).CanSeeMiniProfilerLists; + +// //// (Optional) To control which requests are profiled, use the Func option: +// //// (default is everything should be profiled) +// //options.ShouldProfile = request => MyShouldThisBeProfiledFunction(request); + +// //// (Optional) Profiles are stored under a user ID, function to get it: +// //// (default is null, since above methods don't use it by default) +// //options.UserIdProvider = request => MyGetUserIdFunction(request); + +// //// (Optional) Swap out the entire profiler provider, if you want +// //// (default handles async and works fine for almost all applications) +// //options.ProfilerProvider = new MyProfilerProvider(); + +// //// (Optional) You can disable "Connection Open()", "Connection Close()" (and async variant) tracking. +// //// (defaults to true, and connection opening/closing is tracked) +// //options.TrackConnectionOpenClose = true; + +// //// (Optional) Use something other than the "light" color scheme. +// //// (defaults to "light") +// //options.ColorScheme = StackExchange.Profiling.ColorScheme.Auto; + +// //// The below are newer options, available in .NET Core 3.0 and above: + +// //// (Optional) You can disable MVC filter profiling +// //// (defaults to true, and filters are profiled) +// //options.EnableMvcFilterProfiling = true; +// //// ...or only save filters that take over a certain millisecond duration (including their children) +// //// (defaults to null, and all filters are profiled) +// //// options.MvcFilterMinimumSaveMs = 1.0m; + +// //// (Optional) You can disable MVC view profiling +// //// (defaults to true, and views are profiled) +// //options.EnableMvcViewProfiling = true; +// //// ...or only save views that take over a certain millisecond duration (including their children) +// //// (defaults to null, and all views are profiled) +// //// options.MvcViewMinimumSaveMs = 1.0m; + +// //// (Optional) listen to any errors that occur within MiniProfiler itself +// //// options.OnInternalError = e => MyExceptionLogger(e); + +// //// (Optional - not recommended) You can enable a heavy debug mode with stacks and tooltips when using memory storage +// //// It has a lot of overhead vs. normal profiling and should only be used with that in mind +// //// (defaults to false, debug/heavy mode is off) +// ////options.EnableDebugMode = true; +// }); +// //.AddEntityFramework(); +// } +// } +//} \ No newline at end of file diff --git a/IRaCIS.Core.API/_ServiceExtensions/NewtonsoftJson/NewtonsoftJsonSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/NewtonsoftJson/NewtonsoftJsonSetup.cs new file mode 100644 index 00000000..2cc7bc03 --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/NewtonsoftJson/NewtonsoftJsonSetup.cs @@ -0,0 +1,46 @@ + +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; + +namespace IRaCIS.Core.API +{ + public static class NewtonsoftJsonSetup + { + public static void AddNewtonsoftJsonSetup(this IMvcBuilder builder) + { + + builder.AddNewtonsoftJson(options => + { + //options.SerializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.Objects; + // 忽略循环引用 + options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + //options.SerializerSettings.TypeNameHandling = TypeNameHandling.All; + + //处理返回给前端 可空类型 给出默认值 比如in? 为null 设置 默认值0 + options.SerializerSettings.ContractResolver = new NullToEmptyStringResolver(); //new DefaultContractResolver();// new NullToEmptyStringResolver(); + // 设置时间格式 + options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss"; + + //options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + + }).AddControllersAsServices()//动态webApi属性注入需要 + .ConfigureApiBehaviorOptions(o => + { + o.SuppressModelStateInvalidFilter = true; //自己写验证 + + ////这里是自定义验证结果和返回状态码 因为这里是在[ApiController]控制器层校验,动态webApi的不会校验 所以需要单独写一个Filter + //o.InvalidModelStateResponseFactory = (context) => + //{ + // var error = context.ModelState .Keys + // .SelectMany(k => context.ModelState[k].Errors) + // .Select(e => e.ErrorMessage) + // .ToArray(); + + //return new JsonResult(ResponseOutput.NotOk("The inputs supplied to the API are invalid. " + JsonConvert.SerializeObject( error))); + //}; + + }); + + } + } +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/NewtonsoftJson/NullToEmptyStringResolver.cs b/IRaCIS.Core.API/_ServiceExtensions/NewtonsoftJson/NullToEmptyStringResolver.cs new file mode 100644 index 00000000..6b013066 --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/NewtonsoftJson/NullToEmptyStringResolver.cs @@ -0,0 +1,54 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace IRaCIS.Core.API +{ + public class NullToEmptyStringResolver : DefaultContractResolver + { + /// + /// 创建属性 + /// + /// 类型 + /// 序列化成员 + /// + //protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) + //{ + // IList properties = base.CreateProperties(type, memberSerialization); + + + // foreach (var jsonProperty in properties) + // { + // jsonProperty.DefaultValue = new NullToEmptyStringValueProvider(jsonProperty); + // } + + // return properties; + + //} + + protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) + { + IList properties = base.CreateProperties(type, memberSerialization); + + var list= type.GetProperties() + .Select(p => + { + var jp = base.CreateProperty(p, memberSerialization); + jp.ValueProvider = new NullToEmptyStringValueProvider(p); + return jp; + }).ToList(); + + var uu = list.Select(t => t.PropertyName).ToList(); + + //获取复杂对象属性 + properties = properties.TakeWhile(t => !uu.Contains(t.PropertyName)).ToList(); + + list.AddRange(properties); + return list; + } + + + } +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/NewtonsoftJson/NullToEmptyStringValueProvider.cs b/IRaCIS.Core.API/_ServiceExtensions/NewtonsoftJson/NullToEmptyStringValueProvider.cs new file mode 100644 index 00000000..512efb7a --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/NewtonsoftJson/NullToEmptyStringValueProvider.cs @@ -0,0 +1,42 @@ +using System; +using System.Reflection; +using Newtonsoft.Json.Serialization; + +namespace IRaCIS.Core.API +{ + + public class NullToEmptyStringValueProvider : IValueProvider + { + PropertyInfo _MemberInfo; + public NullToEmptyStringValueProvider(PropertyInfo memberInfo) + { + _MemberInfo = memberInfo; + } + public object GetValue(object target) + { + object result = _MemberInfo.GetValue(target); + if (_MemberInfo.PropertyType == typeof(string) && result == null) result = ""; + else if (_MemberInfo.PropertyType == typeof(String[]) && result == null) result = new string[] { }; + //else if (_MemberInfo.PropertyType == typeof(Nullable) && result == null) result = 0; + else if (_MemberInfo.PropertyType == typeof(Nullable) && result == null) result = 0.00M; + + return result; + } + public void SetValue(object target, object value) + { + + if(_MemberInfo.PropertyType == typeof(string)) + { + //去掉前后空格 + _MemberInfo.SetValue(target, value==null?string.Empty: value.ToString()==string.Empty? value:value.ToString().Trim()); + + } + else + { + _MemberInfo.SetValue(target, value); + } + + } + } + +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/QuartZSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/QuartZSetup.cs new file mode 100644 index 00000000..bd48c49a --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/QuartZSetup.cs @@ -0,0 +1,45 @@ + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace IRaCIS.Core.API +{ + public static class QuartZSetup + { + public static void AddQuartZSetup(this IServiceCollection services, IConfiguration configuration) + { + //services.AddTransient(); + + //services.AddQuartz(q => + //{ + // // base quartz scheduler, job and trigger configuration + + // // as of 3.3.2 this also injects scoped services (like EF DbContext) without problems + // q.UseMicrosoftDependencyInjectionJobFactory(); + + // // 基本Quartz调度器、作业和触发器配置 + // var jobKey = new JobKey("RegularTrialWork", "regularWorkGroup"); + // q.AddJob(jobKey, j => j + // .WithDescription("Trial regular work") + // ); + // q.AddTrigger(t => t + // .WithIdentity("TrialStatusTrigger") + // .ForJob(jobKey) + // //.StartNow() + // //.WithSimpleSchedule(x => x.WithInterval(TimeSpan.FromSeconds(15))//开始秒数 15s + // // .RepeatForever())//持续工作 + // .WithCronSchedule("0 0 0/2 * * ?")//每小时执行一次 + // .WithDescription("My regular trial work trigger") + // ); + //}); + + //// ASP.NET Core hosting + //services.AddQuartzServer(options => + //{ + // // when shutting down we want jobs to complete gracefully + // options.WaitForJobsToComplete = true; + //}); + + } + } +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/ResponseCompressionSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/ResponseCompressionSetup.cs new file mode 100644 index 00000000..6037273d --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/ResponseCompressionSetup.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.ResponseCompression; +using Microsoft.Extensions.DependencyInjection; + +namespace IRaCIS.Core.API +{ + public static class ResponseCompressionSetup + { + public static void AddResponseCompressionSetup(this IServiceCollection services) + { + services.AddResponseCompression(options => + { + options.Providers.Add(); + options.Providers.Add(); + }); + } + } +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/Serilog/EnricherExtensions.cs b/IRaCIS.Core.API/_ServiceExtensions/Serilog/EnricherExtensions.cs new file mode 100644 index 00000000..a6f10b4e --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/Serilog/EnricherExtensions.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Http; +using Serilog; +using Serilog.Configuration; +using Serilog.Core; +using Serilog.Events; +using System; + +namespace IRaCIS.Core.API +{ + public static class EnricherExtensions + { + public static LoggerConfiguration WithHttpContextInfo(this LoggerEnrichmentConfiguration enrich, IServiceProvider serviceProvider) + { + if (enrich == null) + throw new ArgumentNullException(nameof(enrich)); + + return enrich.With(new HttpContextEnricher(serviceProvider)); + } + public static LoggerConfiguration WithHttpContextInfo(this LoggerEnrichmentConfiguration enrich, IServiceProvider serviceProvider, Action enrichAction) + { + if (enrich == null) + throw new ArgumentNullException(nameof(enrich)); + + return enrich.With(new HttpContextEnricher(serviceProvider, enrichAction)); + } + + } + +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/Serilog/HttpContextEnricher.cs b/IRaCIS.Core.API/_ServiceExtensions/Serilog/HttpContextEnricher.cs new file mode 100644 index 00000000..b4ca797d --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/Serilog/HttpContextEnricher.cs @@ -0,0 +1,86 @@ +using IRaCIS.Core.Infra.EFCore.AuthUser; +using IRaCIS.Core.Infrastructure; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Serilog.Core; +using Serilog.Events; +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace IRaCIS.Core.API +{ + public class HttpContextEnricher : ILogEventEnricher + { + private readonly IServiceProvider _serviceProvider; + private readonly Action _enrichAction; + + public HttpContextEnricher(IServiceProvider serviceProvider) : this(serviceProvider, null) + { } + + public HttpContextEnricher(IServiceProvider serviceProvider, Action enrichAction) + { + _serviceProvider = serviceProvider; + if (enrichAction == null) + { + _enrichAction = (logEvent, propertyFactory, httpContext) => + { + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("RequestIP", httpContext.Connection.RemoteIpAddress.ToString())); + + //这样读取没用 + //logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("RequestBody", await ReadRequestBody(httpContext.Request))); + //logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("RequestIP", IPHelper.GetIP(httpContext.Request) )); + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("TokenUserRealName", httpContext?.User?.FindFirst(ClaimAttributes.RealName)?.Value)); + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("TokenUserType", httpContext?.User?.FindFirst("userTypeEnumName")?.Value)); + + //logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Referer", httpContext.Request.Headers["Referer"].ToString())); + //logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("request_path", httpContext.Request.Path)); + //logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("request_method", httpContext.Request.Method)); + //if (httpContext.Response.HasStarted) + //{ + // logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("response_status", httpContext.Response.StatusCode)); + //} + }; + } + else + { + _enrichAction = enrichAction; + } + } + + + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + var httpContext = _serviceProvider.GetService()?.HttpContext; + if (null != httpContext) + { + _enrichAction.Invoke(logEvent, propertyFactory, httpContext); + } + } + + private async Task ReadRequestBody(HttpRequest request) + { + // Ensure the request's body can be read multiple times (for the next middlewares in the pipeline). + request.EnableBuffering(); + + using var streamReader = new StreamReader(request.Body, leaveOpen: true); + var requestBody = await streamReader.ReadToEndAsync(); + + // Reset the request's body stream position for next middleware in the pipeline. + request.Body.Position = 0; + return requestBody==null?String.Empty: requestBody.Trim(); + } + + private async Task ReadResponseBody(HttpResponse response) + { + response.Body.Seek(0, SeekOrigin.Begin); + string responseBody = await new StreamReader(response.Body).ReadToEndAsync(); + response.Body.Seek(0, SeekOrigin.Begin); + + return $"{responseBody}"; + } + } + +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/Serilog/SerilogSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/Serilog/SerilogSetup.cs new file mode 100644 index 00000000..5dc92875 --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/Serilog/SerilogSetup.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Builder; +using Serilog; +using Serilog.Events; +using Serilog.Sinks.Email; +using System; +using System.Net; + +namespace IRaCIS.Core.API +{ + public class SerilogExtension + { + + public static void AddSerilogSetup(string environment, IServiceProvider serviceProvider) + { + + var config = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + // Filter out ASP.NET Core infrastructre logs that are Information and below 日志太多了 一个请求 记录好几条 + .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) + .MinimumLevel.Override("Hangfire", LogEventLevel.Warning) + .MinimumLevel.Override("System.Net.Http.HttpClient.HttpReports", LogEventLevel.Warning) + .Enrich.WithClientIp() + .Enrich.WithClientAgent() + .Enrich.FromLogContext() + + //控制台 方便调试 问题 我们显示记录日志 时 获取上下文的ip 和用户名 用户类型 + .WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Warning, + outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3} ] {ClientIp} {TokenUserRealName} {TokenUserType} {Message:lj} {Properties:j}{NewLine} {Exception}") + .WriteTo.File($"{AppContext.BaseDirectory}Serilogs/.log", rollingInterval: RollingInterval.Day, + outputTemplate: "{Timestamp:HH:mm:ss} || {Level} || {SourceContext:l} || {Message} ||{Exception} ||end {NewLine}"); + //.WriteTo.MSSqlServer("Data Source=DESKTOP-4TU9A6M;Initial Catalog=CoreFrame;User ID=sa;Password=123456", "logs", autoCreateSqlTable: true, restrictedToMinimumLevel: LogEventLevel.Information)//从左至右四个参数分别是数据库连接字符串、表名、如果表不存在是否创建、最低等级。Serilog会默认创建一些列。 + + if (environment == "Production") + { + config.WriteTo.Email(new EmailConnectionInfo() + { + EmailSubject = "系统警告,请速速查看!",//邮件标题 + FromEmail = "iracis_grr@163.com",//发件人邮箱 + MailServer = "smtp.163.com",//smtp服务器地址 + NetworkCredentials = new NetworkCredential("iracis_grr@163.com", "XLWVQKZAEKLDWOAH"),//两个参数分别是发件人邮箱与客户端授权码 + Port = 25,//端口号 + ToEmail = "872297557@qq.com"//收件人 + }, restrictedToMinimumLevel: LogEventLevel.Error, + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [ {Level} {ClientIp} {ClientAgent} {TokenUserRealName} {TokenUserType} ] || [path: {RequestPath} arguments: {RequestBody}] {SourceContext:l} || {Message} || {Exception} ||end {NewLine})"); + } + + //扩展方法 获取上下文的ip 用户名 用户类型 + Log.Logger = config.Enrich.WithHttpContextInfo(serviceProvider).CreateLogger(); + } + + } +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/StaticFileAuthorizationSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/StaticFileAuthorizationSetup.cs new file mode 100644 index 00000000..0ec0aa44 --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/StaticFileAuthorizationSetup.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; + +namespace IRaCIS.Core.API +{ + public static class StaticFileAuthorizationSetup + { + public static void AddStaticFileAuthorizationSetup(this IServiceCollection services) + { + services.AddAuthorization(options => + { + options.FallbackPolicy = new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .Build(); + }); + } + } +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/Swagger/JsonPatchDocumentFilter.cs b/IRaCIS.Core.API/_ServiceExtensions/Swagger/JsonPatchDocumentFilter.cs new file mode 100644 index 00000000..9f356207 --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/Swagger/JsonPatchDocumentFilter.cs @@ -0,0 +1,55 @@ +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using System.Collections.Generic; +using System.Linq; + +namespace IRaCIS.Core.API +{ + public class JsonPatchDocumentFilter : IDocumentFilter + { + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + var schemas = swaggerDoc.Components.Schemas.ToList(); + foreach (var item in schemas) + { + if (item.Key.StartsWith("Operation") || item.Key.StartsWith("JsonPatchDocument")) + swaggerDoc.Components.Schemas.Remove(item.Key); + } + + swaggerDoc.Components.Schemas.Add("Operation", new OpenApiSchema + { + Type = "object", + Properties = new Dictionary + { + { "op", new OpenApiSchema { Type = "string" } }, + {"value", new OpenApiSchema{ Type = "object", Nullable = true } }, + { "path", new OpenApiSchema { Type = "string" } } + } + }); + + swaggerDoc.Components.Schemas.Add("JsonPatchDocument", new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "Operation" } + }, + Description = "Array of operations to perform" + }); + + foreach (var path in swaggerDoc.Paths.SelectMany(p => p.Value.Operations) + .Where(p => p.Key == Microsoft.OpenApi.Models.OperationType.Patch)) + { + foreach (var item in path.Value.RequestBody.Content.Where(c => c.Key != "application/json-patch+json")) + path.Value.RequestBody.Content.Remove(item.Key); + + var response = path.Value.RequestBody.Content.SingleOrDefault(c => c.Key == "application/json-patch+json"); + + response.Value.Schema = new OpenApiSchema + { + Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "JsonPatchDocument" } + }; + } + } + } +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/Swagger/SwaggerSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/Swagger/SwaggerSetup.cs new file mode 100644 index 00000000..09096b72 --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/Swagger/SwaggerSetup.cs @@ -0,0 +1,136 @@ +using IRaCIS.Core.Application.Contracts; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.Filters; +using Swashbuckle.AspNetCore.SwaggerGen; +using Swashbuckle.AspNetCore.SwaggerUI; +using System; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace IRaCIS.Core.API +{ + public static class SwaggerSetup + { + public static void AddSwaggerSetup(this IServiceCollection services) + { + services.AddSwaggerExamplesFromAssemblyOf(); + + services.AddSwaggerGen(options => + { + //此处的Name 是控制器上分组的名称 Title是界面的大标题 + //分组 + options.SwaggerDoc("Reviewer", new OpenApiInfo {Title = "医生模块",Version = "Reviewer", }); + options.SwaggerDoc("Trial", new OpenApiInfo { Title = "项目模块", Version = "Trial" }); + options.SwaggerDoc("Enroll", new OpenApiInfo { Title = "入组模块", Version = "Enroll" }); + options.SwaggerDoc("Workload", new OpenApiInfo { Title = "工作量模块", Version = "Workload" }); + options.SwaggerDoc("Common", new OpenApiInfo { Title = "通用信息获取", Version = "Common" }); + options.SwaggerDoc("Institution", new OpenApiInfo { Title = "机构信息模块", Version = "Institution" }); + options.SwaggerDoc("Dashboard&Statistics", new OpenApiInfo { Title = "统计模块", Version = "Dashboard&Statistics" }); + + options.SwaggerDoc("Financial", new OpenApiInfo { Title = "财务模块", Version = "Financial" }); + options.SwaggerDoc("Management", new OpenApiInfo { Title = "管理模块", Version = "Management" }); + options.SwaggerDoc("Image", new OpenApiInfo { Title = "影像模块", Version = "Image" }); + options.SwaggerDoc("Reading", new OpenApiInfo { Title = "读片模块", Version = "Reading" }); + + // 接口排序 + options.OrderActionsBy(o => o.GroupName); + + options.DocInclusionPredicate((docName, apiDes) => + { + if (!apiDes.TryGetMethodInfo(out MethodInfo methodInfo)) return false; + var versions = methodInfo.DeclaringType.GetCustomAttributes(true) + .OfType() + .Select(attr => attr.GroupName); + + return versions.Any(v => v.ToString() == docName); + }); + + var xmlPath = Path.Combine(AppContext.BaseDirectory, "IRaCIS.Core.API.xml");//这个就是刚刚配置的xml文件名 + options.IncludeXmlComments(xmlPath, true); + + var xmlPath2 = Path.Combine(AppContext.BaseDirectory, "IRaCIS.Core.Application.xml");//这个就是刚刚配置的xml文件名 + options.IncludeXmlComments(xmlPath2, true); + //默认的第二个参数是false,这个是controller的注释,记得修改 + + + // 在header中添加token,传递到后台 + options.OperationFilter(); + + options.DocumentFilter(); + + // 添加登录按钮 + options.AddSecurityDefinition("bearerAuth", new OpenApiSecurityScheme() + { + Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"", + Name = "Authorization", + + //In = "header", + //Type = "apiKey" + }); + + + //// Bearer + //options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme + //{ + // Description = "JWT Authorization header using the Bearer scheme.", + // Name = "Authorization", + // In = ParameterLocation.Header, + // Scheme = "bearer", + // Type = SecuritySchemeType.Http, + // BearerFormat = "JWT" + //}); + }); + } + + public static void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(options => + { + //此处的Name 是页面 选择文档下拉框 显示的名称 + options.SwaggerEndpoint($"swagger/Reviewer/swagger.json", "医生模块"); + options.SwaggerEndpoint($"swagger/Trial/swagger.json", "项目模块"); + options.SwaggerEndpoint($"swagger/Enroll/swagger.json", "入组模块"); + options.SwaggerEndpoint($"swagger/Workload/swagger.json", "工作量模块"); + options.SwaggerEndpoint($"swagger/Dashboard&Statistics/swagger.json", "统计模块"); + options.SwaggerEndpoint($"swagger/Common/swagger.json", "通用模块"); + + options.SwaggerEndpoint($"swagger/Financial/swagger.json", "财务模块"); + options.SwaggerEndpoint($"swagger/Institution/swagger.json", "机构信息模块"); + options.SwaggerEndpoint($"swagger/Management/swagger.json", "管理模块"); + options.SwaggerEndpoint($"swagger/Image/swagger.json", "影像模块"); + options.SwaggerEndpoint($"swagger/Reading/swagger.json", "读片模块"); + + //路径配置,设置为空,表示直接在根域名(localhost:8001)访问该文件, + //注意localhost:8001/swagger是访问不到的,去launchSettings.json把launchUrl去掉,如果你想换一个路径,直接写名字即可,比如直接写c.Route = "doc"; + //options.RoutePrefix = string.Empty; + + var data = Assembly.GetExecutingAssembly().Location; + options.IndexStream = () => Assembly.GetExecutingAssembly() + .GetManifestResourceStream("IRaCIS.Core.API.wwwroot.swagger.ui.Index.html"); + + options.RoutePrefix = string.Empty; + + //DocExpansion设置为none可折叠所有方法 + options.DocExpansion(DocExpansion.None); + //DefaultModelsExpandDepth设置为 - 1 可不显示models + options.DefaultModelsExpandDepth(-1); + + + // 引入静态文件添加登录功能 + // 清除静态文件缓存 + // options.IndexStream = () => null; + + + }); + + + } + } +} diff --git a/IRaCIS.Core.API/_ServiceExtensions/hangfireSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/hangfireSetup.cs new file mode 100644 index 00000000..a5dd1fc0 --- /dev/null +++ b/IRaCIS.Core.API/_ServiceExtensions/hangfireSetup.cs @@ -0,0 +1,39 @@ +using Hangfire; +using Hangfire.SqlServer; +using Hangfire.Tags.SqlServer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace IRaCIS.Core.API +{ + public static class hangfireSetup + { + public static void AddhangfireSetup(this IServiceCollection services, IConfiguration configuration) + { + var hangFireConnStr = configuration.GetSection("ConnectionStrings:Hangfire").Value; + + services.AddHangfire(hangFireConfig => + { + //指定存储介质 + hangFireConfig.UseSqlServerStorage(hangFireConnStr, new SqlServerStorageOptions() + { + SchemaName = "hangfire", + CommandBatchMaxTimeout = TimeSpan.FromMinutes(5), + SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5), + QueuePollInterval = TimeSpan.Zero, + UseRecommendedIsolationLevel = true, + UsePageLocksOnDequeue = true, + DisableGlobalLocks = true + }); + + hangFireConfig.UseTagsWithSql(); //nuget引入Hangfire.Tags.SqlServer + //.UseHangfireHttpJob(); + + }); + + services.AddHangfireServer(); + + } + } +} diff --git a/IRaCIS.Core.API/appsettings.Development.json b/IRaCIS.Core.API/appsettings.Development.json new file mode 100644 index 00000000..b8fcdda5 --- /dev/null +++ b/IRaCIS.Core.API/appsettings.Development.json @@ -0,0 +1,30 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ConnectionStrings": { + "RemoteNew": "Server=123.56.181.144,14333\\MSSQLExpress14;Database=IRaCIS_New;User ID=sa;Password=dev123456DEV;" + //,"hang": "Server=ZHOU;Database=IRaCIS;User ID=sa;Password=sa123456;", + + }, + "HttpReports": { + "Transport": { + "CollectorAddress": "http://123.56.181.144:6099/", + "DeferSecond": 10, + "DeferThreshold": 100 + }, + "Server": "http://localhost:6100/", + "Service": "IRaCIS", + "Switch": true, + //"RequestFilter": [ "/api/health/*", "/HttpReports*" ], + "WithRequest": true, + "WithResponse": true, + "WithCookie": false, + "WithHeader": true + } + +} diff --git a/IRaCIS.Core.API/appsettings.Production.json b/IRaCIS.Core.API/appsettings.Production.json new file mode 100644 index 00000000..cbc7530f --- /dev/null +++ b/IRaCIS.Core.API/appsettings.Production.json @@ -0,0 +1,30 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + //"RootUrl": "http://123.56.181.144:8060/", + "ConnectionStrings": { + "RemoteNew": "Server=123.56.181.144,14333\\MSSQLExpress14;Database=IRaCIS_New;User ID=sa;Password=dev123456DEV;", + "Remote8099": "Server=123.56.181.144,14333\\MSSQLExpress14;Database=IRaCIS_Test;User ID=sa;Password=dev123456DEV;" + }, + "HttpReports": { + "Transport": { + "CollectorAddress": "http://123.56.181.144:6099/", + "DeferSecond": 10, + "DeferThreshold": 100 + }, + "Server": "http://123.56.181.144:8060/", + "Service": "IRaCIS_8060", + "Switch": true, + //"RequestFilter": [ "/api/health/*", "/HttpReports*" ], + "WithRequest": true, + "WithResponse": true, + "WithCookie": false, + "WithHeader": true + } + +} diff --git a/IRaCIS.Core.API/appsettings.Staging.json b/IRaCIS.Core.API/appsettings.Staging.json new file mode 100644 index 00000000..fe3b4052 --- /dev/null +++ b/IRaCIS.Core.API/appsettings.Staging.json @@ -0,0 +1,27 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ConnectionStrings": { + "RemoteNew": "Server=123.56.181.144,14333\\MSSQLExpress14;Database=IRaCIS_New;User ID=sa;Password=dev123456DEV;" + }, + "HttpReports": { + "Transport": { + "CollectorAddress": "http://123.56.181.144:6099/", + "DeferSecond": 10, + "DeferThreshold": 100 + }, + "Server": "http://192.168.1.2:8000/", + "Service": "IRaCIS_1.2_8000", + "Switch": true, + //"RequestFilter": [ "/api/health/*", "/HttpReports*" ], + "WithRequest": true, + "WithResponse": true, + "WithCookie": false, + "WithHeader": true + } +} diff --git a/IRaCIS.Core.API/appsettings.json b/IRaCIS.Core.API/appsettings.json new file mode 100644 index 00000000..c7e15524 --- /dev/null +++ b/IRaCIS.Core.API/appsettings.json @@ -0,0 +1,91 @@ +{ + "ConnectionStrings": { + "hang": "Server=ZHOU;Database=IRaCIS;User ID=sa;Password=sa123456;", + "Hangfire": "Server=123.56.181.144,14333\\MSSQLExpress14;Database=Hangfire_IRaCIS;User ID=sa;Password=dev123456DEV;" + }, + "JwtSetting": { + "SecurityKey": "3e6dbfa227234a03977a2f421bdb7f4f", // Կ + "Issuer": "IRaCIS", // ䷢ + "Audience": "ZhiZhun", // + "TokenExpireDays": "7" // ʱ䣨7day + }, + "IpRateLimiting": { + "EnableEndpointRateLimiting": true, //False: globally executed, true: executed for each + "StackBlockedRequests": false, // set to false, rejected calls are not added to the throttle counter + "RealIpHeader": "X-Real-IP", + "ClientIdHeader": "X-ClientId", + "QuotaExceededResponse": { + "Content": "{{\"ErrorMessage\":\"The program performs flow limiting,Your requests are too frequent, please try again later, or contact the administrator to modify the IP flow restriction rules\",\"IsSuccess\":false}}", + "ContentType": "application/json", + "StatusCode": 429 + }, + "HttpStatusCode": 429, + //"IpWhitelist": [ "127.0.0.1", "::1/10", "192.168.0.0/24" ], + "EndpointWhitelist": [ "post:/study/archivestudy/*" ], + //"EndpointWhitelist": [ ], + //"EndpointWhitelist": ["post:/trial/getTrialList"], //demo + "IpWhitelist": [], + //"ClientWhitelist": [ "dev-id-1", "dev-id-2" ], + "GeneralRules": [ + { + "Endpoint": "*", + "Period": "1s", + "Limit": 3 + }, + { + "Endpoint": "*", + "Period": "15m", + "Limit": 100 + }, + { + "Endpoint": "*", + "Period": "12h", + "Limit": 1000 + }, + { + "Endpoint": "*", + "Period": "7d", + "Limit": 10000 + } + ] + }, + + "easycaching": { + "inmemory": { + "MaxRdSecond": 120, + "EnableLogging": false, + "LockMs": 5000, + "SleepMs": 300, + "DBConfig": { + "SizeLimit": 10000, + "ExpirationScanFrequency": 60, + "EnableReadDeepClone": true, + "EnableWriteDeepClone": false + } + } + }, + "imageShare": { + "ExpireDays": 7 + }, + + + //վַΪ˷ļ dicom ϴĵ... ʵֲⷢýƴӷأԶϵǰip˶໷ȡļ + //"RootUrl": "http://localhost:8060", + + "GrpcAddress": "http://123.56.181.144:7200", + "HostName": "123.56.181.144", + "PortNumber": "8022", + "UserName": "LongjunHe", + "Password": "zhizhun2018", + "SshHostKeyFingerprint": "", + "GiveUpSecurityAndAcceptAnySshHostKey": true, + "CodePrefix": "RE", + "UserCodePrefix": "U", + "Share": false, // + "FileSizeLimit": 1073741824, + "LoginExpiredTimeSpan": "15", // Minute + "OpenLog": true, + "AddClinicalInfo": true + + +} \ No newline at end of file diff --git a/IRaCIS.Core.API/web.config b/IRaCIS.Core.API/web.config new file mode 100644 index 00000000..cd48e317 --- /dev/null +++ b/IRaCIS.Core.API/web.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/IRaCIS.Core.API/wwwroot/swagger/ui/Index.html b/IRaCIS.Core.API/wwwroot/swagger/ui/Index.html new file mode 100644 index 00000000..3101e036 --- /dev/null +++ b/IRaCIS.Core.API/wwwroot/swagger/ui/Index.html @@ -0,0 +1,128 @@ + + + + + + %(DocumentTitle) + + + + + + %(HeadContent) + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + \ No newline at end of file diff --git a/IRaCIS.Core.API/wwwroot/swagger/ui/abp.js b/IRaCIS.Core.API/wwwroot/swagger/ui/abp.js new file mode 100644 index 00000000..9957fdf7 --- /dev/null +++ b/IRaCIS.Core.API/wwwroot/swagger/ui/abp.js @@ -0,0 +1,117 @@ +var abp = abp || {}; +(function () { + + /* Application paths *****************************************/ + + // Current application root path (including virtual directory if exists). + abp.appPath = abp.appPath || '/'; + + /* AUTHORIZATION **********************************************/ + // Implements Authorization API that simplifies usage of authorization scripts generated by Abp. + + abp.auth = abp.auth || {}; + + abp.auth.tokenCookieName = 'Abp.AuthToken'; + abp.auth.tokenHeaderName = 'Authorization'; + + abp.auth.setToken = function (authToken, expireDate) { + abp.utils.setCookieValue(abp.auth.tokenCookieName, authToken, expireDate, abp.appPath); + }; + + abp.auth.getToken = function () { + return abp.utils.getCookieValue(abp.auth.tokenCookieName); + } + + abp.auth.clearToken = function () { + abp.auth.setToken(); + } + + /* UTILS ***************************************************/ + + abp.utils = abp.utils || {}; + + /** + * Sets a cookie value for given key. + * This is a simple implementation created to be used by ABP. + * Please use a complete cookie library if you need. + * @param {string} key + * @param {string} value + * @param {Date} expireDate (optional). If not specified the cookie will expire at the end of session. + * @param {string} path (optional) + */ + abp.utils.setCookieValue = function (key, value, expireDate, path) { + var cookieValue = encodeURIComponent(key) + '='; + + if (value) { + cookieValue = cookieValue + encodeURIComponent(value); + } + + if (expireDate) { + cookieValue = cookieValue + "; expires=" + expireDate.toUTCString(); + } + + if (path) { + cookieValue = cookieValue + "; path=" + path; + } + + document.cookie = cookieValue; + }; + + /** + * Gets a cookie with given key. + * This is a simple implementation created to be used by ABP. + * Please use a complete cookie library if you need. + * @param {string} key + * @returns {string} Cookie value or null + */ + abp.utils.getCookieValue = function (key) { + var equalities = document.cookie.split('; '); + for (var i = 0; i < equalities.length; i++) { + if (!equalities[i]) { + continue; + } + + var splitted = equalities[i].split('='); + if (splitted.length != 2) { + continue; + } + + if (decodeURIComponent(splitted[0]) === key) { + return decodeURIComponent(splitted[1] || ''); + } + } + + return null; + }; + + /** + * Deletes cookie for given key. + * This is a simple implementation created to be used by ABP. + * Please use a complete cookie library if you need. + * @param {string} key + * @param {string} path (optional) + */ + abp.utils.deleteCookie = function (key, path) { + var cookieValue = encodeURIComponent(key) + '='; + + cookieValue = cookieValue + "; expires=" + (new Date(new Date().getTime() - 86400000)).toUTCString(); + + if (path) { + cookieValue = cookieValue + "; path=" + path; + } + + document.cookie = cookieValue; + } + + /* SECURITY ***************************************/ + abp.security = abp.security || {}; + abp.security.antiForgery = abp.security.antiForgery || {}; + + abp.security.antiForgery.tokenCookieName = 'XSRF-TOKEN'; + abp.security.antiForgery.tokenHeaderName = 'X-XSRF-TOKEN'; + + abp.security.antiForgery.getToken = function () { + return abp.utils.getCookieValue(abp.security.antiForgery.tokenCookieName); + }; + +})(); diff --git a/IRaCIS.Core.API/wwwroot/swagger/ui/abp.swagger.js b/IRaCIS.Core.API/wwwroot/swagger/ui/abp.swagger.js new file mode 100644 index 00000000..eb708cbb --- /dev/null +++ b/IRaCIS.Core.API/wwwroot/swagger/ui/abp.swagger.js @@ -0,0 +1,470 @@ +var abp = abp || {}; +(function () { + + /* md5*/ + + /* + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002. + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + + /* + * Configurable variables. You may need to tweak these to be compatible with + * the server-side, but the defaults work in most cases. + */ + var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ + var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ + var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ + + /* + * These are the functions you'll usually want to call + * They take string arguments and return either hex or base-64 encoded strings + */ + function hex_md5(s) { return binl2hex(core_md5(str2binl(s), s.length * chrsz)); } + function b64_md5(s) { return binl2b64(core_md5(str2binl(s), s.length * chrsz)); } + function str_md5(s) { return binl2str(core_md5(str2binl(s), s.length * chrsz)); } + function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); } + function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); } + function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); } + + /* + * Perform a simple self-test to see if the VM is working + */ + function md5_vm_test() { + return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72"; + } + + /* + * Calculate the MD5 of an array of little-endian words, and a bit length + */ + function core_md5(x, len) { + /* append padding */ + x[len >> 5] |= 0x80 << ((len) % 32); + x[(((len + 64) >>> 9) << 4) + 14] = len; + + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + + for (var i = 0; i < x.length; i += 16) { + var olda = a; + var oldb = b; + var oldc = c; + var oldd = d; + + a = md5_ff(a, b, c, d, x[i + 0], 7, -680876936); + d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586); + c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819); + b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330); + a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897); + d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426); + c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341); + b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983); + a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416); + d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417); + c = md5_ff(c, d, a, b, x[i + 10], 17, -42063); + b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162); + a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682); + d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101); + c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290); + b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329); + + a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510); + d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632); + c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713); + b = md5_gg(b, c, d, a, x[i + 0], 20, -373897302); + a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691); + d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083); + c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335); + b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848); + a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438); + d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690); + c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961); + b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501); + a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467); + d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784); + c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473); + b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734); + + a = md5_hh(a, b, c, d, x[i + 5], 4, -378558); + d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463); + c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562); + b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556); + a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060); + d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353); + c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632); + b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640); + a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174); + d = md5_hh(d, a, b, c, x[i + 0], 11, -358537222); + c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979); + b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189); + a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487); + d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835); + c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520); + b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651); + + a = md5_ii(a, b, c, d, x[i + 0], 6, -198630844); + d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415); + c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905); + b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055); + a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571); + d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606); + c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523); + b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799); + a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359); + d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744); + c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380); + b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649); + a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070); + d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379); + c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259); + b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551); + + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + } + return Array(a, b, c, d); + + } + + /* + * These functions implement the four basic operations the algorithm uses. + */ + function md5_cmn(q, a, b, x, s, t) { + return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b); + } + function md5_ff(a, b, c, d, x, s, t) { + return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); + } + function md5_gg(a, b, c, d, x, s, t) { + return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); + } + function md5_hh(a, b, c, d, x, s, t) { + return md5_cmn(b ^ c ^ d, a, b, x, s, t); + } + function md5_ii(a, b, c, d, x, s, t) { + return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); + } + + /* + * Calculate the HMAC-MD5, of a key and some data + */ + function core_hmac_md5(key, data) { + var bkey = str2binl(key); + if (bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz); + + var ipad = Array(16), opad = Array(16); + for (var i = 0; i < 16; i++) { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + + var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz); + return core_md5(opad.concat(hash), 512 + 128); + } + + /* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ + function safe_add(x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); + } + + /* + * Bitwise rotate a 32-bit number to the left. + */ + function bit_rol(num, cnt) { + return (num << cnt) | (num >>> (32 - cnt)); + } + + /* + * Convert a string to an array of little-endian words + * If chrsz is ASCII, characters >255 have their hi-byte silently ignored. + */ + function str2binl(str) { + var bin = Array(); + var mask = (1 << chrsz) - 1; + for (var i = 0; i < str.length * chrsz; i += chrsz) + bin[i >> 5] |= (str.charCodeAt(i / chrsz) & mask) << (i % 32); + return bin; + } + + /* + * Convert an array of little-endian words to a string + */ + function binl2str(bin) { + var str = ""; + var mask = (1 << chrsz) - 1; + for (var i = 0; i < bin.length * 32; i += chrsz) + str += String.fromCharCode((bin[i >> 5] >>> (i % 32)) & mask); + return str; + } + + /* + * Convert an array of little-endian words to a hex string. + */ + function binl2hex(binarray) { + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var str = ""; + for (var i = 0; i < binarray.length * 4; i++) { + str += hex_tab.charAt((binarray[i >> 2] >> ((i % 4) * 8 + 4)) & 0xF) + + hex_tab.charAt((binarray[i >> 2] >> ((i % 4) * 8)) & 0xF); + } + return str; + } + + /* + * Convert an array of little-endian words to a base-64 string + */ + function binl2b64(binarray) { + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var str = ""; + for (var i = 0; i < binarray.length * 4; i += 3) { + var triplet = (((binarray[i >> 2] >> 8 * (i % 4)) & 0xFF) << 16) + | (((binarray[i + 1 >> 2] >> 8 * ((i + 1) % 4)) & 0xFF) << 8) + | ((binarray[i + 2 >> 2] >> 8 * ((i + 2) % 4)) & 0xFF); + for (var j = 0; j < 4; j++) { + if (i * 8 + j * 6 > binarray.length * 32) str += b64pad; + else str += tab.charAt((triplet >> 6 * (3 - j)) & 0x3F); + } + } + return str; + } + + + /* Swagger */ + + abp.swagger = abp.swagger || {}; + + abp.swagger.addAuthToken = function () { + var authToken = abp.auth.getToken(); + if (!authToken) { + return false; + } + + var cookieAuth = new SwaggerClient.ApiKeyAuthorization(abp.auth.tokenHeaderName, 'Bearer ' + authToken, 'header'); + swaggerUi.api.clientAuthorizations.add('bearerAuth', cookieAuth); + return true; + } + + abp.swagger.addCsrfToken = function () { + var csrfToken = abp.security.antiForgery.getToken(); + if (!csrfToken) { + return false; + } + var csrfCookieAuth = new SwaggerClient.ApiKeyAuthorization(abp.security.antiForgery.tokenHeaderName, csrfToken, 'header'); + swaggerUi.api.clientAuthorizations.add(abp.security.antiForgery.tokenHeaderName, csrfCookieAuth); + return true; + } + + function loginUserInternal(tenantId, callback) { + + var usernameOrEmailAddress = document.getElementById('userName').value; + if (!usernameOrEmailAddress) { + alert('UserName Can Not Be Null'); + return false; + } + + var password = document.getElementById('password').value; + if (!password) { + alert('PassWord Can Not Be Null'); + return false; + } + + var xhr = new XMLHttpRequest(); + + xhr.onreadystatechange = function () { + + if (xhr.readyState === XMLHttpRequest.DONE) { + debugger; + if (xhr.status === 200) { + debugger; + + var resultdata = JSON.parse(xhr.responseText); + if (resultdata.ErrorMessage != '') { + alert(resultdata.ErrorMessage); + return false; + } + if (resultdata.code == 300) { + alert(resultdata.message); + } + else { + var responseJSON = JSON.parse(xhr.responseText); + var result = responseJSON; + var expireDate = new Date(Date.now() + (60*60*24 * 1000)); + abp.auth.setToken(result.Result.JWTStr, expireDate); + callback(); + } + + } else { + alert('Login failed !'); + } + } + }; + + xhr.open('POST', '/user/login', true); + xhr.setRequestHeader('Abp.TenantId', tenantId); + xhr.setRequestHeader('Content-type', 'application/json'); + var parm = { + + + UserName: usernameOrEmailAddress, + Password: hex_md5(password), + } + xhr.send(JSON.stringify(parm)); + //xhr.send("{" + "userName:'" + usernameOrEmailAddress + "'," + "passWord:'" + password + "'}"); + + }; + + abp.swagger.login = function (callback) { + //Get TenantId first + var tenancyName = document.getElementById('tenancyName').value; + + if (tenancyName) { + var xhrTenancyName = new XMLHttpRequest(); + xhrTenancyName.onreadystatechange = function () { + if (xhrTenancyName.readyState === XMLHttpRequest.DONE && xhrTenancyName.status === 200) { + var responseJSON = JSON.parse(xhrTenancyName.responseText); + var result = responseJSON.result; + if (result.state === 1) { // Tenant exists and active. + loginUserInternal(result.tenantId, callback); // Login for tenant + } else { + alert('There is no such tenant or tenant is not active !'); + } + } + }; + + xhrTenancyName.open('POST', '/api/services/app/Account/IsTenantAvailable', true); + xhrTenancyName.setRequestHeader('Content-type', 'application/json'); + xhrTenancyName.send("{" + "tenancyName:'" + tenancyName + "'}"); + } else { + loginUserInternal(null, callback); // Login for host + } + }; + + abp.swagger.logout = function () { + abp.auth.clearToken(); + } + + abp.swagger.closeAuthDialog = function () { + if (document.getElementById('abp-auth-dialog')) { + document.getElementsByClassName("swagger-ui")[1].removeChild(document.getElementById('abp-auth-dialog')); + } + } + + abp.swagger.openAuthDialog = function (loginCallback) { + abp.swagger.closeAuthDialog(); + + var abpAuthDialog = document.createElement('div'); + abpAuthDialog.className = 'dialog-ux'; + abpAuthDialog.id = 'abp-auth-dialog'; + + document.getElementsByClassName("swagger-ui")[1].appendChild(abpAuthDialog); + + // -- backdrop-ux + var backdropUx = document.createElement('div'); + backdropUx.className = 'backdrop-ux'; + abpAuthDialog.appendChild(backdropUx); + + // -- modal-ux + var modalUx = document.createElement('div'); + modalUx.className = 'modal-ux'; + abpAuthDialog.appendChild(modalUx); + + // -- -- modal-dialog-ux + var modalDialogUx = document.createElement('div'); + modalDialogUx.className = 'modal-dialog-ux'; + modalUx.appendChild(modalDialogUx); + + // -- -- -- modal-ux-inner + var modalUxInner = document.createElement('div'); + modalUxInner.className = 'modal-ux-inner'; + modalDialogUx.appendChild(modalUxInner); + + // -- -- -- -- modal-ux-header + var modalUxHeader = document.createElement('div'); + modalUxHeader.className = 'modal-ux-header'; + modalUxInner.appendChild(modalUxHeader); + + var modalHeader = document.createElement('h3'); + modalHeader.innerText = 'Authorize'; + modalUxHeader.appendChild(modalHeader); + + // -- -- -- -- modal-ux-content + var modalUxContent = document.createElement('div'); + modalUxContent.className = 'modal-ux-content'; + modalUxInner.appendChild(modalUxContent); + + modalUxContent.onkeydown = function (e) { + if (e.keyCode === 13) { + //try to login when user presses enter on authorize modal + abp.swagger.login(loginCallback); + } + }; + + //Inputs + createInput(modalUxContent, 'tenancyName', 'Tenancy Name (Leave empty for Host)'); + createInput(modalUxContent, 'userName', 'Username or email address','text','admin'); + createInput(modalUxContent, 'password', 'Password','password'); + + //Buttons + var authBtnWrapper = document.createElement('div'); + authBtnWrapper.className = 'auth-btn-wrapper'; + modalUxContent.appendChild(authBtnWrapper); + + //Close button + var closeButton = document.createElement('button'); + closeButton.className = 'btn modal-btn auth btn-done button'; + closeButton.innerText = 'Close'; + closeButton.style.marginRight = '5px'; + closeButton.onclick = abp.swagger.closeAuthDialog; + authBtnWrapper.appendChild(closeButton); + + //Authorize button + var authorizeButton = document.createElement('button'); + authorizeButton.className = 'btn modal-btn auth authorize button'; + authorizeButton.innerText = 'Login'; + authorizeButton.onclick = function () { + abp.swagger.login(loginCallback); + }; + authBtnWrapper.appendChild(authorizeButton); + } + + function createInput(container, id, title, type, value="") { + var wrapper = document.createElement('div'); + wrapper.className = 'wrapper'; + if (id == "tenancyName") { + wrapper.style.display = 'none'; + } + container.appendChild(wrapper); + + var label = document.createElement('label'); + label.innerText = title; + wrapper.appendChild(label); + + var section = document.createElement('section'); + section.className = 'block-tablet col-10-tablet block-desktop col-10-desktop'; + wrapper.appendChild(section); + + var input = document.createElement('input'); + input.id = id; + input.type = type ? type : 'text'; + input.style.width = '100%'; + input.value = value; + input.autocomplete = "off"; + + + section.appendChild(input); + } + +})(); \ No newline at end of file diff --git a/IRaCIS.Core.Application/AOP/AsyncInterceptor.cs b/IRaCIS.Core.Application/AOP/AsyncInterceptor.cs new file mode 100644 index 00000000..d08f1649 --- /dev/null +++ b/IRaCIS.Core.Application/AOP/AsyncInterceptor.cs @@ -0,0 +1,97 @@ +using Castle.DynamicProxy; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace IRaCIS.Core.Application.AOP +{ + public abstract class AsyncInterceptorBase : IInterceptor + { + public AsyncInterceptorBase() + { + } + + public void Intercept(IInvocation invocation) + { + BeforeProceed(invocation); + invocation.Proceed(); + if (IsAsyncMethod(invocation.MethodInvocationTarget)) + { + invocation.ReturnValue = InterceptAsync((dynamic)invocation.ReturnValue, invocation); + } + else + { + AfterProceedSync(invocation); + } + } + + private bool CheckMethodReturnTypeIsTaskType(MethodInfo method) + { + var methodReturnType = method.ReturnType; + if (methodReturnType.IsGenericType) + { + if (methodReturnType.GetGenericTypeDefinition() == typeof(Task<>) || + methodReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) + return true; + } + else + { + if (methodReturnType == typeof(Task) || + methodReturnType == typeof(ValueTask)) + return true; + } + return false; + } + + private bool IsAsyncMethod(MethodInfo method) + { + bool isDefAsync = Attribute.IsDefined(method, typeof(AsyncStateMachineAttribute), false); + bool isTaskType = CheckMethodReturnTypeIsTaskType(method); + bool isAsync = isDefAsync && isTaskType; + + return isAsync; + } + + protected object ProceedAsyncResult { get; set; } + + + private async Task InterceptAsync(Task task, IInvocation invocation) + { + await task.ConfigureAwait(false); + await AfterProceedAsync(invocation, false); + } + + private async Task InterceptAsync(Task task, IInvocation invocation) + { + ProceedAsyncResult = await task.ConfigureAwait(false); + await AfterProceedAsync(invocation, true); + return (TResult)ProceedAsyncResult; + } + + private async ValueTask InterceptAsync(ValueTask task, IInvocation invocation) + { + await task.ConfigureAwait(false); + await AfterProceedAsync(invocation, false); + } + + private async ValueTask InterceptAsync(ValueTask task, IInvocation invocation) + { + ProceedAsyncResult = await task.ConfigureAwait(false); + await AfterProceedAsync(invocation, true); + return (TResult)ProceedAsyncResult; + } + + protected virtual void BeforeProceed(IInvocation invocation) { } + + protected virtual void AfterProceedSync(IInvocation invocation) { } + + protected virtual Task AfterProceedAsync(IInvocation invocation, bool hasAsynResult) + { + return Task.CompletedTask; + } + } +} diff --git a/IRaCIS.Core.Application/AOP/QANoticeAOP.cs b/IRaCIS.Core.Application/AOP/QANoticeAOP.cs new file mode 100644 index 00000000..0e80ae3a --- /dev/null +++ b/IRaCIS.Core.Application/AOP/QANoticeAOP.cs @@ -0,0 +1,499 @@ +//using System; +//using Castle.DynamicProxy; +//using IRaCIS.Core.Application.Contracts.Dicom.DTO; +//using IRaCIS.Core.Infra.EFCore; + +//using System.Linq; +//using IRaCIS.Core.Domain.Models; +//using IRaCIS.Core.Domain.Share; + +//namespace IRaCIS.Core.API.Utility.AOP +//{ +//#pragma warning disable +// public class QANoticeAOP : IInterceptor +// { +// private readonly IRepository _qaNoticeRepository; + +// private readonly IRepository _studyRepository; +// private readonly IRepository _userTrialRepository; +// private readonly IRepository _userTrialSiteRepository; +// private readonly IUserInfo _userInfo; + +// public QANoticeAOP(IRepository qaNoticeRepository, +// IUserInfo userInfo, IRepository studyRepository, IRepository userTrialRepository, IRepository userTrialSiteRepository) +// { +// _qaNoticeRepository = qaNoticeRepository; + +// _studyRepository = studyRepository; +// _userTrialRepository = userTrialRepository; +// _userTrialSiteRepository = userTrialSiteRepository; +// _userInfo = userInfo; +// } + +// public void Intercept(IInvocation invocation) +// { +// //处理拦截的方法 +// invocation.Proceed(); + +// if (invocation.Method.Name == "UpdateStudyStatus") +// { +// var studyStatus = invocation.Arguments[0] as StudyStatusDetailCommand; + +// var study = _studyRepository.FirstOrDefault(t=>t.Id==studyStatus.StudyId); + + +// if (study.Status == (int)StudyStatus.Uploaded) +// { +// _qaNoticeRepository.Add(new QANotice() +// { +// TrialId = study.TrialId, +// SubjectVisitId = study.Id, +// FromUser = _userInfo.RealName, +// FromUserId = _userInfo.Id, +// FromUserType = _userInfo.UserTypeShortName, + +// NoticeTypeEnum = NoticeType.NotNeedNotice, +// NeedDeal = false, +// StudyStatusStr = "Uploaded", +// Message = $"CRC : {_userInfo.RealName} has uploaded {study.StudyCode} ", +// SendTime = DateTime.Now, +// }); +// } + +// #region 处理QA通知模块 + +// //查询项目的参与者 和 负责site下CRC用户 +// var trialUserList = _userTrialRepository.Where(t => t.TrialId == study.TrialId).ToList(); + +// // 找到该study 关联Site 下的CRC + +// var crcList = _userTrialSiteRepository.Where(t => +// t.SiteId == study.SiteId && t.User.UserTypeEnum == UserTypeEnum.ClinicalResearchCoordinator && t.TrialId == study.TrialId).ToList(); + + +// var qaList = trialUserList.Where(t => t.User.UserTypeEnum == UserTypeEnum.IQC).ToList(); + +// var pm = trialUserList.FirstOrDefault(t => t.User.UserTypeEnum == UserTypeEnum.ProjectManager); + + + + +// // CRC =>QA +// if (studyStatus.Status == (int)StudyStatus.QARequested) +// { +// //找出当前操作的CRC +// //PM 或者admin可以代替CRC角色 不能从CRC列表中查询用户 +// //var currentCRC = trialUserList.First(t => t.UserId == _userInfo.Id); + +// var notice = new QANotice() +// { +// TrialId = study.TrialId, +// SubjectVisitId = study.Id, +// FromUser = _userInfo.RealName, +// FromUserId = _userInfo.Id, +// FromUserType = _userInfo.UserTypeShortName, + +// //FromUser = currentCRC.UserRealName, +// //FromUserId = _userInfo.Id, +// //FromUserType = currentCRC.UserType, +// NoticeTypeEnum = NoticeType.CRC_RequestToQA_NoticeQA, +// NeedDeal = true, +// StudyStatusStr = "QA Requested", +// Message = +// $"CRC -> QA : {_userInfo.RealName} request QA {study.StudyCode} , Inquiry can be performed! ", +// SendTime = DateTime.Now, +// }; + +// qaList.ForEach(t => notice.QANoticeUserList.Add(new QANoticeUser() +// { +// QANoticeId = notice.Id, +// SubjectVisitId = study.Id, +// ToUser = t.User.LastName + " / " + t.User.FirstName, +// ToUserId = t.UserId, +// ToUserType = t.User.UserTypeRole.UserTypeShortName +// })); + +// _qaNoticeRepository.Add(notice); + +// //DealRequestToQA(study.Id); + +// var needDealNoticeList = _qaNoticeRepository.AsQueryable() +// .Where(t => t.SubjectVisitId == study.Id && t.NeedDeal && t.NoticeTypeEnum == NoticeType.CRC_RequestToQA_NoticeQA).ToList(); + +// needDealNoticeList.ForEach(t => +// { +// t.NeedDeal = false; +// t.DealTime = DateTime.Now; +// _qaNoticeRepository.Update(t); +// }); +// } + +// // QA =>CRC 向CRC推送消息影像有问题 同时作为 requestToQA 的边界 +// else if (studyStatus.Status == (int)StudyStatus.QAing) +// { +// //找出当前操作的QA 如果是pm 或者admin 代替操作 此时会有问题 所以 谁代替,就以谁的名义执行 +// //var currentQA = qaList.First(t => t.UserId == _userInfo.Id); +// //var currentQA = trialUserList.First(t => t.UserId == _userInfo.Id); + +// //在项目CRC列表中筛选出 负责该study关联 site的CRC +// var siteCRCList = _userTrialSiteRepository.Where(t => +// t.SiteId == study.SiteId && t.User.UserTypeEnum == UserTypeEnum.ClinicalResearchCoordinator && t.TrialId == study.TrialId).ToList(); + +// //查询项目的参与者 和 负责site下CRC用户 + + + +// var notice = new QANotice() +// { +// TrialId = study.TrialId, +// SubjectVisitId = study.Id, +// //FromUser = currentQA.UserRealName, +// //FromUserId = _userInfo.Id, +// //FromUserType = currentQA.UserType, +// FromUser = _userInfo.RealName, +// FromUserId = _userInfo.Id, +// FromUserType = _userInfo.UserTypeShortName, +// NoticeTypeEnum = NoticeType.QA_InQA_NoticeCRC, +// NeedDeal = true, +// StudyStatusStr = "In QA", +// Message = $"QA -> CRC : {_userInfo.RealName} inquiry {study.StudyCode} ", +// SendTime = DateTime.Now, +// }; + +// siteCRCList.ForEach(t => notice.QANoticeUserList.Add(new QANoticeUser() +// { +// QANoticeId = notice.Id, +// SubjectVisitId = study.Id, +// ToUser = t.User.LastName + " / " + t.User.FirstName, +// ToUserId = t.UserId, +// ToUserType = t.UserTypeRole.UserTypeShortName +// })); + +// //添加 发送给CRC的消息 消息和CRC是 一对多 +// _qaNoticeRepository.Add(notice); + + +// //处理 消息 标记已处理 +// var needDealNoticeList = _qaNoticeRepository.AsQueryable() +// .Where(t => t.SubjectVisitId == study.Id && t.NeedDeal && +// (t.NoticeTypeEnum == NoticeType.CRC_RequestToQA_NoticeQA || +// t.NoticeTypeEnum == NoticeType.CRC_ReUpload_NoticeQA || +// t.NoticeTypeEnum == NoticeType.CRC_QARecordDialogPost_NoticeQA)).ToList(); + +// needDealNoticeList.ForEach(t => +// { +// t.NeedDeal = false; +// t.DealTime = DateTime.Now; +// _qaNoticeRepository.Update(t); +// }); + + +// } +// // QA =>QA 给自己的消息 通知需要匿名化 同时作为 requestToQA 的边界 +// else if (studyStatus.Status == (int)StudyStatus.QAFinish) +// { + +// //找出当前操作的QA 如果是pm 或者admin 代替操作 此时会有问题 所以 谁代替,就以谁的名义执行 +// //var currentQA = qaList.First(t => t.UserId == _userInfo.Id); +// //var currentQA = trialUserList.First(t => t.UserId == _userInfo.Id); + +// //发送给当前项目QA列表 + +// var notice = new QANotice() +// { +// TrialId = study.TrialId, +// SubjectVisitId = study.Id, +// //FromUser = currentQA.UserRealName, +// //FromUserId = _userInfo.Id, +// //FromUserType = currentQA.UserType, +// FromUser = _userInfo.RealName, +// FromUserId = _userInfo.Id, +// FromUserType = _userInfo.UserTypeShortName, +// NoticeTypeEnum = NoticeType.QA_QAPass_NoticeQA, +// NeedDeal = true, +// StudyStatusStr = "QA-Passed", +// Message = +// $"QA -> QA : {_userInfo.RealName} inquiry {study.StudyCode} finished,Anonymization can be performed!", +// SendTime = DateTime.Now, +// }; + +// qaList.ForEach(t => notice.QANoticeUserList.Add(new QANoticeUser() +// { +// QANoticeId = notice.Id, +// SubjectVisitId = study.Id, +// ToUser = t.User.LastName+" / "+t.User.FirstName, +// ToUserId = t.UserId, +// ToUserType = t.User.UserTypeRole.UserTypeShortName +// })); + +// _qaNoticeRepository.Add(notice); + +// //处理 消息 标记已处理 存在意外情况 qa发给CRC的 但是qa里面设置了 通过或者不通过 此时qa发送的消息也设置为已处理 +// var needDealNoticeList = _qaNoticeRepository.AsQueryable() +// .Where(t => t.SubjectVisitId == study.Id && t.NeedDeal && +// (t.NoticeTypeEnum == NoticeType.CRC_RequestToQA_NoticeQA || +// t.NoticeTypeEnum == NoticeType.CRC_ReUpload_NoticeQA || +// t.NoticeTypeEnum == NoticeType.CRC_QARecordDialogPost_NoticeQA || + +// t.NoticeTypeEnum == NoticeType.QA_QARecordDialogPost_NoticeCRC || +// t.NoticeTypeEnum == NoticeType.QA_InQA_NoticeCRC || +// t.NoticeTypeEnum == NoticeType.QA_AddQARecord_NoticeCRC)).ToList(); + +// needDealNoticeList.ForEach(t => +// { +// t.NeedDeal = false; +// t.DealTime = DateTime.Now; +// _qaNoticeRepository.Update(t); +// }); + + +// } +// // QA =>CRC 暂时不用发送消息给CRC 因为CRC 暂时没有入口回复 同时作为 requestToQA 的边界 +// else if (studyStatus.Status == (int)StudyStatus.QAFInishNotPass) +// { + +// _qaNoticeRepository.Add(new QANotice() +// { +// TrialId = study.TrialId, +// SubjectVisitId = study.Id, +// FromUser = _userInfo.RealName, +// FromUserId = _userInfo.Id, +// FromUserType = _userInfo.UserTypeShortName, +// NoticeTypeEnum = NoticeType.NotNeedNotice, +// NeedDeal = false, +// StudyStatusStr = "QA-Failed", +// Message = $"QA : {_userInfo.RealName} set {study.StudyCode} QA-Failed !", +// SendTime = DateTime.Now, +// }); + +// //处理 消息 标记已处理 +// var needDealNoticeList = _qaNoticeRepository.AsQueryable() +// .Where(t => t.SubjectVisitId == study.Id && t.NeedDeal && +// (t.NoticeTypeEnum == NoticeType.CRC_RequestToQA_NoticeQA || +// t.NoticeTypeEnum == NoticeType.CRC_ReUpload_NoticeQA || +// t.NoticeTypeEnum == NoticeType.CRC_QARecordDialogPost_NoticeQA || + + +// t.NoticeTypeEnum == NoticeType.QA_QARecordDialogPost_NoticeCRC || +// t.NoticeTypeEnum == NoticeType.QA_InQA_NoticeCRC || +// t.NoticeTypeEnum == NoticeType.QA_AddQARecord_NoticeCRC)).ToList(); + +// needDealNoticeList.ForEach(t => +// { +// t.NeedDeal = false; +// t.DealTime = DateTime.Now; +// _qaNoticeRepository.Update(t); +// }); +// } + +// #endregion + + +// } + +// else if (invocation.Method.Name == "ReUploadSameStudy") +// { +// var studyId = Guid.Parse(invocation.Arguments[0].ToString()); + +// var study = _studyRepository.FirstOrDefault(t => t.Id == studyId); +// var status = study.Status; + +// //处理CRC 重传时 QA消息 + +// if (status == (int)StudyStatus.QAing) +// { +// //查询项目的参与者 和 负责site下CRC用户 +// var trialUserList = _userTrialRepository.Where(t => t.TrialId == study.TrialId).ToList(); + +// // 找到该study 关联Site 下的CRC + +// var crcList = _userTrialSiteRepository.Where(t => +// t.SiteId == study.SiteId && t.User.UserTypeEnum == UserTypeEnum.ClinicalResearchCoordinator && t.TrialId == study.TrialId).ToList(); + +// var qaList = trialUserList.Where(t => t.User.UserTypeEnum == UserTypeEnum.IQC).ToList(); + +// //CRC =>QA CRC的职能被PM 或者admin代替 +// //if (_userInfo.UserTypeEnumInt == (int)UserType.ClinicalResearchCoordinator) +// { +// //PM 或者admin可以代替CRC角色 不能从CRC列表中查询用户 +// //var currentCRC = trialUserList.First(t => t.UserId == _userInfo.Id); + +// var notice = new QANotice() +// { +// TrialId = study.TrialId, +// SubjectVisitId = study.Id, +// //FromUser = currentCRC.UserRealName, +// //FromUserId = _userInfo.Id, +// //FromUserType = currentCRC.UserType, +// FromUser = _userInfo.RealName, +// FromUserId = _userInfo.Id, +// FromUserType = _userInfo.UserTypeShortName, +// NoticeTypeEnum = NoticeType.CRC_ReUpload_NoticeQA, +// NeedDeal = true, +// Message = $"CRC -> QA :{_userInfo.RealName} has reuploaded {study.StudyCode} , Need to be inquiry again", +// SendTime = DateTime.Now +// }; + +// qaList.ForEach(t => notice.QANoticeUserList.Add(new QANoticeUser() +// { +// QANoticeId = notice.Id, +// SubjectVisitId = study.Id, +// ToUser = t.User.LastName+" / "+t.User.FirstName, +// ToUserId = t.UserId, +// ToUserType = t.User.UserTypeRole.UserTypeShortName +// })); + +// _qaNoticeRepository.Add(notice); + +// //这里作为 QA 设置 Inqa 状态的回复 或者QA和CRC对话的 +// var needDealNoticeList = _qaNoticeRepository.Where(t => t.SubjectVisitId == study.Id && t.NeedDeal +// && (t.NoticeTypeEnum == NoticeType.QA_InQA_NoticeCRC || t.NoticeTypeEnum == NoticeType.QA_QARecordDialogPost_NoticeCRC)) +// .ToList(); + +// needDealNoticeList.ForEach(t => +// { +// t.NeedDeal = false; +// t.DealTime = DateTime.Now; +// _qaNoticeRepository.Update(t); +// }); + +// } +// } +// else +// { +// //不是QAing 的重传 不发送qa消息 +// return; +// } + + + +// } + +// else if (invocation.Method.Name == "DicomAnonymize") +// { +// var studyId = Guid.Parse(invocation.Arguments[0].ToString()); + +// var study = _studyRepository.FirstOrDefault(t => t.Id == studyId); + +// #region 处理QA通知 匿名化完毕 通知PM + +// //查询项目的参与者 和 负责site下CRC用户 +// var trialUserList = _userTrialRepository.Where(t => t.TrialId == study.TrialId).ToList(); + +// // 找到该study 关联Site 下的CRC + +// var crcList = _userTrialSiteRepository.Where(t => +// t.SiteId == study.SiteId && t.User.UserTypeEnum == UserTypeEnum.ClinicalResearchCoordinator && t.TrialId == study.TrialId).ToList(); + +// var qaList = trialUserList.Where(t => t.User.UserTypeEnum == UserTypeEnum.IQC).ToList(); + +// // +// var pm = trialUserList.FirstOrDefault(t => t.User.UserTypeEnum == UserTypeEnum.ProjectManager); + + +// //找出当前操作的QA 如果是pm 或者admin 代替操作 此时会有问题 所以 谁代替,就以谁的名义执行 +// //var currentQA = trialUserList.First(t => +// // t.UserTypeEnum == UserType.IQC && t.UserId == _userInfo.Id); +// //var currentQA = trialUserList.First(t => t.UserId == _userInfo.Id); + +// var notice = new QANotice() +// { +// TrialId = study.TrialId, +// SubjectVisitId = study.Id, +// //FromUser = currentQA.UserRealName, +// //FromUserId = _userInfo.Id, +// //FromUserType = currentQA.UserType, +// FromUser = _userInfo.RealName, +// FromUserId = _userInfo.Id, +// FromUserType = _userInfo.UserTypeShortName, +// NoticeTypeEnum = NoticeType.QA_Anonymized_NoticeQA, +// NeedDeal = true, +// StudyStatusStr = "Anonymized", +// //Message = $"QA -> PM :{_userInfo.RealName} has anonymized {study.StudyCode} ,Forward can be performed!!", +// Message = $"QA -> QA :{_userInfo.RealName} has anonymized {study.StudyCode} ,Forward can be performed!!", + +// SendTime = DateTime.Now, + +// }; + +// //notice.QANoticeUserList.Add(new QANoticeUser() +// //{ +// // QANoticeId = notice.Id, +// // StudyId = study.Id, +// // ToUser = pm.UserRealName, +// // ToUserId = pm.UserId, +// // ToUserType = pm.UserType +// //}); + +// qaList.ForEach(t => notice.QANoticeUserList.Add(new QANoticeUser() +// { +// QANoticeId = notice.Id, +// SubjectVisitId = study.Id, +// ToUser = t.User.LastName+" / "+t.User.FirstName, +// ToUserId = t.UserId, +// ToUserType = t.User.UserTypeRole.UserTypeShortName +// })); + +// _qaNoticeRepository.Add(notice); + +// var needDealNoticeList = _qaNoticeRepository.AsQueryable() +// .Where(t => t.SubjectVisitId == study.Id && t.NeedDeal && (t.NoticeTypeEnum == NoticeType.QA_QAPass_NoticeQA)).ToList(); + +// needDealNoticeList.ForEach(t => +// { +// t.NeedDeal = false; +// t.DealTime = DateTime.Now; +// _qaNoticeRepository.Update(t); +// }); + + +// #endregion +// } + +// else if (invocation.Method.Name == "ForwardStudy") +// { +// var studyId = Guid.Parse(invocation.Arguments[0].ToString()); + +// var study = _studyRepository.FirstOrDefault(t => t.Id == studyId); + +// //匿名化操作产生的消息 设置为已经处理 +// _qaNoticeRepository.Add(new QANotice() +// { +// TrialId = study.TrialId, +// SubjectVisitId = study.Id, +// //FromUser = currentQA.UserRealName, +// //FromUserId = _userInfo.Id, +// //FromUserType = currentQA.UserType, +// FromUser = _userInfo.RealName, +// FromUserId = _userInfo.Id, +// FromUserType = _userInfo.UserTypeShortName, +// NoticeTypeEnum = NoticeType.NotNeedNotice, +// NeedDeal = false, +// StudyStatusStr = "Forwarded", +// //Message = $"PM :{_userInfo.RealName} has forwarded {study.StudyCode} !", +// Message = $"QA :{_userInfo.RealName} has forwarded {study.StudyCode} !", +// SendTime = DateTime.Now, +// }); + +// var needDealList = _qaNoticeRepository.Where(t => +// t.SubjectVisitId == study.Id && t.NeedDeal && t.NoticeTypeEnum == NoticeType.QA_Anonymized_NoticeQA).ToList(); + +// needDealList.ForEach(t => +// { +// t.NeedDeal = false; +// t.DealTime = DateTime.Now; +// _qaNoticeRepository.Update(t); +// }); +// } + +// var success = _qaNoticeRepository.SaveChanges(); + +// if (!success) +// { +// throw new Exception("Send QA message failed"); +// } +// } + + +// } +//} \ No newline at end of file diff --git a/IRaCIS.Core.Application/AOP/TrialStatusAutofacAOP.cs b/IRaCIS.Core.Application/AOP/TrialStatusAutofacAOP.cs new file mode 100644 index 00000000..0e5ebf13 --- /dev/null +++ b/IRaCIS.Core.Application/AOP/TrialStatusAutofacAOP.cs @@ -0,0 +1,89 @@ +using Castle.DynamicProxy; +using EasyCaching.Core; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Domain.Share; + +namespace IRaCIS.Core.Application.AOP +{ + public class TrialStatusAutofacAOP : IAsyncInterceptor + { + private readonly IEasyCachingProvider _provider; + + public TrialStatusAutofacAOP(IEasyCachingProvider provider) + { + _provider = provider; + } + + + + public void InterceptAsynchronous(IInvocation invocation) + { + invocation.Proceed(); + } + + //这里AOP 处理两个方法 分别是 项目的添加和更新、项目状态的变更 + + public void InterceptAsynchronous(IInvocation invocation) + { + + + //处理拦截的方法 + invocation.Proceed(); + + + + dynamic result = invocation.ReturnValue; + + //接口成功了,才修改缓存 + if (!result.IsSuccess) + { + return; + } + + #region 处理项目列表的查询 在前端界面已经在某个界面,但是服务器重置了,此时没有缓存项目信息,接口不能正确返回,因故采用,启动时查询,每天查询一次,缓存一天,然后项目添加、更改状态时,及时更新 + + //if (invocation.Method.Name == "GetTrialList") + //{ + // //在此 将当前查询的项目Id 和对应的项目状态进行缓存 + // dynamic result = invocation.ReturnValue; + // foreach (var item in result.CurrentPageData) + // { + // _provider.Remove(item.Id.ToString()); + // _provider.Set(item.Id.ToString(), item.TrialStatusStr.ToString(), TimeSpan.FromDays(1)); + // } + //} + + #endregion + + if (invocation.Method.Name == "AddOrUpdateTrial") + { + //如果是添加 那么将对应的初始状态加进去 更新状态是单独操作的 + + var trialModel = (invocation.Arguments[0] as TrialCommand).IfNullThrowConvertException(); + if (trialModel.Id == null || trialModel.Id == Guid.Empty) + { + _provider.Set(result.Data.Id.ToString(), StaticData.TrialOngoing, TimeSpan.FromDays(1)); + } + + + } + // 更新缓存 + else if (invocation.Method.Name == "UpdateTrialStatus") + { + //项目状态更新,也需要及时更新 + _provider.Set(invocation.Arguments[0].ToString(), invocation.Arguments[1].ToString(), TimeSpan.FromDays(1)); + + ////Test参数是否符合要求 + //var tt = invocation.Arguments[0].ToString(); + //var cc = _provider.Get(invocation.Arguments[0].ToString()); + + } + + } + + public void InterceptSynchronous(IInvocation invocation) + { + invocation.Proceed(); + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/AOP/UserAddAOP.cs b/IRaCIS.Core.Application/AOP/UserAddAOP.cs new file mode 100644 index 00000000..e5580fe0 --- /dev/null +++ b/IRaCIS.Core.Application/AOP/UserAddAOP.cs @@ -0,0 +1,50 @@ +using Castle.DynamicProxy; +using IRaCIS.Application.Services; +using IRaCIS.Application.Contracts; +using Microsoft.Extensions.Logging; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Core.API.Utility.AOP +{ + /// + ///服务动态生成api AOP 此时会失效 + /// + public class UserAddAOP : IInterceptor + { + private readonly IMailVerificationService _mailVerificationService; + private readonly ILogger _logger; + + public UserAddAOP(IMailVerificationService mailVerificationService, ILogger logger) + { + _mailVerificationService = mailVerificationService; + _logger = logger; + } + public void Intercept(IInvocation invocation) + { + + var userInfo = (invocation.Arguments[0] as UserCommand).IfNullThrowConvertException(); + + + //处理拦截的方法 + invocation.Proceed(); + + //在此 发送邮件 + dynamic result = invocation.ReturnValue; + + if (result.IsSuccess) + { + var userId = result.Data.Id; + var verificationCode = result.Data.VerificationCode; + + + _logger.LogInformation($"Sent to {userInfo.UserName} email {userInfo.EMail} init password {verificationCode}"); + + _mailVerificationService.SendMail(userId, userInfo.UserName, userInfo.EMail, verificationCode).GetAwaiter().GetResult(); + + } + + + + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Auth/IRaCISClaims.cs b/IRaCIS.Core.Application/Auth/IRaCISClaims.cs new file mode 100644 index 00000000..b25b4b05 --- /dev/null +++ b/IRaCIS.Core.Application/Auth/IRaCISClaims.cs @@ -0,0 +1,50 @@ +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Domain.Share; + +namespace IRaCIS.Core.Application.Auth +{ + public class IRaCISClaims + { + public Guid Id { get; set; } + public string FullName { get; set; } = String.Empty; + public string Code { get; set; } = String.Empty; + public string RealName { get; set; } = String.Empty; + + public string UserTypeShortName { get; set; } = String.Empty; + + public UserTypeEnum UserTypeEnum { get; set; } + + public string PermissionStr { get; set; } = String.Empty; + + public Guid UserTypeId { get; set; } + + public int IsAdmin { get; } + + public string Phone { get; set; } = String.Empty; + + public static IRaCISClaims Create(UserBasicInfo user) + { + return new IRaCISClaims + { + Id = user.Id, + FullName = user.UserName, + RealName = user.RealName, + UserTypeEnum=user.UserTypeEnum, + UserTypeId=user.UserTypeId, + Code = user.Code, + PermissionStr = user.PermissionStr, + + UserTypeShortName = user.UserTypeShortName + }; + } + public static IRaCISClaims Create(DoctorAccountDTO doctor) + { + return new IRaCISClaims + { + Id = doctor.Id, + FullName = doctor.FirstName + doctor.LastName, + Phone = doctor.Phone, + }; + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Auth/JwtSetting.cs b/IRaCIS.Core.Application/Auth/JwtSetting.cs new file mode 100644 index 00000000..1987c91b --- /dev/null +++ b/IRaCIS.Core.Application/Auth/JwtSetting.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace IRaCIS.Core.Application.Auth +{ + public class JwtSetting + { + /// + /// 颁发者 + /// + public string Issuer { get; set; } = String.Empty; + + /// + /// 接收者 + /// + public string Audience { get; set; } = String.Empty; + + /// + /// 令牌密码 + /// + public string SecurityKey { get; set; } = String.Empty; + + /// + /// 过期时间 + /// + public int TokenExpireDays { get; set; } + + //public Dictionary Claims { get; set; } + + /// + /// 签名 + /// + public SigningCredentials Credentials + { + get + { + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecurityKey)); + return new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + } + } + } +} diff --git a/IRaCIS.Core.Application/Auth/TokenService.cs b/IRaCIS.Core.Application/Auth/TokenService.cs new file mode 100644 index 00000000..a31a0994 --- /dev/null +++ b/IRaCIS.Core.Application/Auth/TokenService.cs @@ -0,0 +1,59 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infra.EFCore.AuthUser; +using Microsoft.Extensions.Options; + +namespace IRaCIS.Core.Application.Auth +{ + + public interface ITokenService + { + string GetToken(IRaCISClaims user); + } + + + public class TokenService : ITokenService + { + private readonly JwtSetting _jwtSetting; + + public TokenService(IOptions option) + { + _jwtSetting = option.Value; + } + + public string GetToken(IRaCISClaims user) + { + //创建用户身份标识,可按需要添加更多信息 + var claims = new Claim[] + { + new Claim(Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(JwtIRaCISClaimType.Id, user.Id.ToString()), + new Claim(JwtIRaCISClaimType.Name, user.FullName), + new Claim(JwtIRaCISClaimType.RealName, user.RealName), + new Claim(JwtIRaCISClaimType.Code,user.Code), + new Claim(JwtIRaCISClaimType.UserTypeId,user.UserTypeId.ToString()), + new Claim(JwtIRaCISClaimType.UserTypeEnum,user.UserTypeEnum.ToString()), + new Claim(JwtIRaCISClaimType.UserTypeEnumInt,((int)user.UserTypeEnum).ToString()), + new Claim(JwtIRaCISClaimType.UserTypeShortName,user.UserTypeShortName), + new Claim(JwtIRaCISClaimType.PermissionStr,user.PermissionStr) + }; + + ////创建令牌 + var token = new JwtSecurityToken( + issuer: _jwtSetting.Issuer, + audience: _jwtSetting.Audience, + signingCredentials: _jwtSetting.Credentials, + claims: claims, + notBefore: DateTime.Now, + expires: DateTime.Now.AddDays(_jwtSetting.TokenExpireDays) + ); + + string jwtToken = new JwtSecurityTokenHandler().WriteToken(token); + return jwtToken; + + } + } + + +} diff --git a/IRaCIS.Core.Application/BackGroundJob/CacheTrialStatusHangfireJob.cs b/IRaCIS.Core.Application/BackGroundJob/CacheTrialStatusHangfireJob.cs new file mode 100644 index 00000000..a7d82b5b --- /dev/null +++ b/IRaCIS.Core.Application/BackGroundJob/CacheTrialStatusHangfireJob.cs @@ -0,0 +1,46 @@ +using EasyCaching.Core; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.Extensions.Logging; + +namespace IRaCIS.Application.Services.BackGroundJob +{ + + public interface ICacheTrialStatusJob + { + Task MemoryCacheTrialStatus(); + } + public class CacheTrialStatusHangfireJob: ICacheTrialStatusJob + { + private readonly IRepository _trialRepository; + private readonly IEasyCachingProvider _provider; + private readonly ILogger _logger; + + public CacheTrialStatusHangfireJob(IRepository trialRepository, IEasyCachingProvider provider,ILogger logger) + { + _trialRepository = trialRepository; + _provider = provider; + _logger = logger; + } + public Task MemoryCacheTrialStatus() + { + _logger.LogInformation("hangfire 定时任务开始~"); + try + { + var list = _trialRepository.Select(t => new { TrialId = t.Id, TrialStatusStr = t.TrialStatusStr }) + .ToList(); + //_provider.GetCount(""); + + list.ForEach(t => _provider.Set(t.TrialId.ToString(), t.TrialStatusStr, TimeSpan.FromDays(7))); + } + catch (Exception e) + { + _logger.LogError("hangfire 定时任务执行失败"+e.Message); + + } + + _logger.LogInformation("hangfire 定时任务执行结束"); + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/BackGroundJob/CacheTrialStatusQuartZJob.cs b/IRaCIS.Core.Application/BackGroundJob/CacheTrialStatusQuartZJob.cs new file mode 100644 index 00000000..ab4c0ac7 --- /dev/null +++ b/IRaCIS.Core.Application/BackGroundJob/CacheTrialStatusQuartZJob.cs @@ -0,0 +1,53 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using EasyCaching.Core; +using IRaCIS.Core.Domain; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Models; +using Microsoft.Extensions.Logging; +using Quartz; + +namespace IRaCIS.Application.Services.BackGroundJob +{ + + public class CacheTrialStatusQuartZJob: IJob + { + + private readonly IRepository _trialRepository; + private readonly IEasyCachingProvider _provider; + private readonly ILogger _logger; + + public CacheTrialStatusQuartZJob(IRepository trialRepository, IEasyCachingProvider provider,ILogger logger) + { + _trialRepository = trialRepository; + _provider = provider; + _logger = logger; + } + + public Task Execute(IJobExecutionContext context) + + { + _logger.LogInformation($"开始执行QuartZ定时任务作业"); + try + { + var list = _trialRepository.Select(t => new { TrialId = t.Id, TrialStatusStr = t.TrialStatusStr }) + .ToList(); + //_provider.GetCount(""); + + list.ForEach(t => _provider.Set(t.TrialId.ToString(), t.TrialStatusStr, TimeSpan.FromDays(1))); + + } + catch (Exception e) + { + _logger.LogError($" 查询和缓存过程出现异常"+e.Message); + } + _logger.LogInformation("QuartZ定时任务作业结束"); + return Task.CompletedTask; + } + + + + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/BackGroundJob/ObtainTaskAutoCancelJob.cs b/IRaCIS.Core.Application/BackGroundJob/ObtainTaskAutoCancelJob.cs new file mode 100644 index 00000000..ebd09161 --- /dev/null +++ b/IRaCIS.Core.Application/BackGroundJob/ObtainTaskAutoCancelJob.cs @@ -0,0 +1,43 @@ +using IRaCIS.Core.Infra.EFCore; +using Microsoft.Extensions.Logging; + +namespace IRaCIS.Core.Application.BackGroundJob +{ + public interface IObtainTaskAutoCancelJob + { + Task CancelQCObtaion(Guid subjectVisitId,DateTime startTime); + } + + public class ObtainTaskAutoCancelJob : IObtainTaskAutoCancelJob + { + private readonly IRepository _subjectVisitRepository; + private readonly ILogger _logger; + + public ObtainTaskAutoCancelJob(IRepository subjectVisitRepository, ILogger logger) + { + _subjectVisitRepository = subjectVisitRepository; + _logger = logger; + } + public async Task CancelQCObtaion(Guid subjectVisitId, DateTime startTime) + { + try + { + var dbSubjectVisit = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == subjectVisitId).IfNullThrowException(); + + dbSubjectVisit.IsTake = false; + dbSubjectVisit.CurrentActionUserId = null; + dbSubjectVisit.CurrentActionUserExpireTime = null; + + var success = await _subjectVisitRepository.SaveChangesAsync(); + + _logger.LogWarning($"任务建立时间:{startTime} 取消时间:{DateTime.Now} 取消 受试者访视:{ subjectVisitId }success:{success}"); + } + catch (Exception e) + { + _logger.LogError("hangfire 定时任务执行失败" + e.Message); + + } + + } + } +} diff --git a/IRaCIS.Core.Application/BaseService.cs b/IRaCIS.Core.Application/BaseService.cs new file mode 100644 index 00000000..f33beae8 --- /dev/null +++ b/IRaCIS.Core.Application/BaseService.cs @@ -0,0 +1,109 @@ +using AutoMapper; +using IRaCIS.Application.Services.BusinessFilter; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Localization; +using Panda.DynamicWebApi; +using Panda.DynamicWebApi.Attributes; +using System.Diagnostics.CodeAnalysis; + +namespace IRaCIS.Core.Application +{ + +#pragma warning disable CS8618 + + + #region 非泛型版本 + + [Authorize, DynamicWebApi, UnifiedApiResultFilter] + public class BaseService : IBaseService, IDynamicWebApi + { + public IMapper _mapper { get; set; } + + public IUserInfo _userInfo { get; set; } + + public IRepository _repository { get; set; } + + public IStringLocalizer _localizer { get; set; } + + + + + public static IResponseOutput Null404NotFound(TEntity? businessObject) where TEntity : class + { + return new ResponseOutput() + .NotOk($"The query object {typeof(TEntity).Name} does not exist , or was deleted by someone else, or an incorrect parameter query caused", code: ApiResponseCodeEnum.DataNotExist); + } + } + + + public interface IBaseService + { + [MemberNotNull(nameof(_mapper))] + public IMapper _mapper { get; set; } + + [MemberNotNull(nameof(_userInfo))] + public IUserInfo _userInfo { get; set; } + + [MemberNotNull(nameof(_repository))] + public IRepository _repository { get; set; } + + [MemberNotNull(nameof(_localizer))] + public IStringLocalizer _localizer { get; set; } + + } + #endregion + + + #region 泛型版本测试 + + + public interface IBaseServiceTest where T : Entity + { + [MemberNotNull(nameof(_mapper))] + public IMapper _mapper { get; set; } + + [MemberNotNull(nameof(_userInfo))] + public IUserInfo _userInfo { get; set; } + + [MemberNotNull(nameof(_repository))] + public IRepository _repository { get; set; } + + [MemberNotNull(nameof(_localizer))] + public IStringLocalizer _localizer { get; set; } + + + + } + + + [Authorize, DynamicWebApi, UnifiedApiResultFilter] + public class BaseServiceTest : IBaseServiceTest, IDynamicWebApi where T : Entity + { + public IMapper _mapper { get; set; } + + public IUserInfo _userInfo { get; set; } + + public IRepository _repository { get; set; } + + public IStringLocalizer _localizer { get; set; } + + public static IResponseOutput Null404NotFound(TEntity? businessObject) where TEntity : class + { + return new ResponseOutput() + .NotOk($"The query object {typeof(TEntity).Name} does not exist , or was deleted by someone else, or an incorrect parameter query caused", code: ApiResponseCodeEnum.DataNotExist); + } + + + } + + + #endregion + + + + + + + +} diff --git a/IRaCIS.Core.Application/BusinessFilter/LogActionFilter.cs b/IRaCIS.Core.Application/BusinessFilter/LogActionFilter.cs new file mode 100644 index 00000000..64f11585 --- /dev/null +++ b/IRaCIS.Core.Application/BusinessFilter/LogActionFilter.cs @@ -0,0 +1,81 @@ +//using System.Diagnostics; +//using IRaCIS.Application.Interfaces; +//using IRaCIS.Application.Contracts; +//using IRaCIS.Core.Infra.EFCore; +//using IRaCIS.Core.Infrastructure.Extention; +//using Microsoft.AspNetCore.Mvc; +//using Microsoft.AspNetCore.Mvc.Filters; +//using Microsoft.Extensions.Logging; +//using Newtonsoft.Json; + +//namespace IRaCIS.Core.Application.Filter +//{ + +// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] +// public class LogFilter : Attribute +// { +// } +// public class LogActionFilter : IAsyncActionFilter +// { +// private readonly ILogService _logService; +// private readonly IUserInfo _userInfo; +// private readonly ILogger _logger; + +// public LogActionFilter(ILogService logService, IUserInfo userInfo , ILogger logger) +// { +// _logService = logService; +// _userInfo = userInfo; +// _logger = logger; +// } + +// public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) +// { + +// if (context.ActionDescriptor.EndpointMetadata!=null&& context.ActionDescriptor.EndpointMetadata.Any(m => m.GetType() == typeof(LogFilter))) +// { +// return LogAsync(context, next); +// } +// return next(); +// } + +// public async Task LogAsync(ActionExecutingContext context, ActionExecutionDelegate next) +// { +// var sw = new Stopwatch(); +// sw.Start(); + +// dynamic actionResult = (await next()).Result; +// sw.Stop(); +// var args = JsonConvert.SerializeObject(context.ActionArguments); +// var result = JsonConvert.SerializeObject(actionResult?.Value); + +// var attr = (ApiExplorerSettingsAttribute)context.ActionDescriptor.EndpointMetadata.FirstOrDefault(m => m.GetType() == typeof(ApiExplorerSettingsAttribute)); +// var groupName = attr?.GroupName; +// var res = actionResult?.Value as IResponseOutput; +// var input = new SystemLogDTO +// { +// ClientIP = string.Empty, +// OptUserId = _userInfo.Id, +// OptUserName = _userInfo.UserName, +// ApiPath = context.ActionDescriptor.AttributeRouteInfo.Template.ToLower(), +// Params = args, +// Result = result, +// RequestTime = DateTime.Now, +// ElapsedMilliseconds = sw.ElapsedMilliseconds, +// Status =res?.IsSuccess?? false, +// Message = res?.ErrorMessage, +// LogCategory = groupName +// }; + +// try +// { +// _logService.SaveLog2Db(input); +// } +// catch (Exception ex) +// { + +// _logger.LogError(ex.Message); +// } + +// } +// } +//} diff --git a/IRaCIS.Core.Application/BusinessFilter/ModelActionFilter .cs b/IRaCIS.Core.Application/BusinessFilter/ModelActionFilter .cs new file mode 100644 index 00000000..90f48d0e --- /dev/null +++ b/IRaCIS.Core.Application/BusinessFilter/ModelActionFilter .cs @@ -0,0 +1,27 @@ +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Newtonsoft.Json; + + +namespace IRaCIS.Core.Application.Filter +{ + public class ModelActionFilter : ActionFilterAttribute, IActionFilter + { + public override void OnActionExecuting(ActionExecutingContext context) + { + if (!context.ModelState.IsValid) + { + + var validationErrors = context.ModelState + .Keys + .SelectMany(k => context.ModelState[k]!.Errors) + .Select(e => e.ErrorMessage) + .ToArray(); + + context.Result = new JsonResult(ResponseOutput.NotOk("The inputs supplied to the API are invalid. " +JsonConvert.SerializeObject( validationErrors))); + } + } + } + +} diff --git a/IRaCIS.Core.Application/BusinessFilter/ModelBinding.cs b/IRaCIS.Core.Application/BusinessFilter/ModelBinding.cs new file mode 100644 index 00000000..9a8d8bc9 --- /dev/null +++ b/IRaCIS.Core.Application/BusinessFilter/ModelBinding.cs @@ -0,0 +1,28 @@ +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace IRaCIS.Core.Application.Filter +{ + #region snippet_DisableFormValueModelBindingAttribute + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter + { + public void OnResourceExecuting(ResourceExecutingContext context) + { + + + var factories = context.ValueProviderFactories; + //factories.RemoveType(); + factories.RemoveType(); + //factories.RemoveType(); + context.HttpContext.Request.EnableBuffering(); + } + + public void OnResourceExecuted(ResourceExecutedContext context) + { + } + } + #endregion +} diff --git a/IRaCIS.Core.Application/BusinessFilter/NotDefault.cs b/IRaCIS.Core.Application/BusinessFilter/NotDefault.cs new file mode 100644 index 00000000..6c644b31 --- /dev/null +++ b/IRaCIS.Core.Application/BusinessFilter/NotDefault.cs @@ -0,0 +1,53 @@ +namespace System.ComponentModel.DataAnnotations +{ + [AttributeUsage( + AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, + AllowMultiple = false)] + public class GuidNotEmptyAttribute : ValidationAttribute + { + public const string DefaultErrorMessage = "The {0} field must not be empty"; + public GuidNotEmptyAttribute() : base(DefaultErrorMessage) { } + + public override bool IsValid(object? value) + { + //NotEmpty doesn't necessarily mean required + if (value is null) + { + return true; + } + + switch (value) + { + case Guid guid: + return guid != Guid.Empty; + default: + return true; + } + } + } + + public class NotDefaultAttribute : ValidationAttribute + { + public const string DefaultErrorMessage = "The {0} field is is not passed or not set a valid value"; + public NotDefaultAttribute() : base(DefaultErrorMessage) { } + + public override bool IsValid(object? value) + { + //NotDefault doesn't necessarily mean required + if (value is null) + { + return true; + } + + var type = value.GetType(); + if (type.IsValueType) + { + var defaultValue = Activator.CreateInstance(type); + return !value.Equals(defaultValue); + } + + // non-null ref type + return true; + } + } +} diff --git a/IRaCIS.Core.Application/BusinessFilter/ProjectExceptionFilter.cs b/IRaCIS.Core.Application/BusinessFilter/ProjectExceptionFilter.cs new file mode 100644 index 00000000..807c1b40 --- /dev/null +++ b/IRaCIS.Core.Application/BusinessFilter/ProjectExceptionFilter.cs @@ -0,0 +1,53 @@ +using IRaCIS.Core.Infrastructure; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; + +namespace IRaCIS.Core.Application.Filter +{ + public class ProjectExceptionFilter : Attribute, IExceptionFilter + { + private readonly ILogger _logger; + + public ProjectExceptionFilter(ILogger logger) + { + _logger = logger; + } + public void OnException(ExceptionContext context) + { + //context.ExceptionHandled;//记录当前这个异常是否已经被处理过了 + + if (!context.ExceptionHandled) + { + if (context.Exception.GetType().Name == "DbUpdateConcurrencyException") + { + context.Result = new JsonResult(ResponseOutput.NotOk("并发更新,当前不允许该操作" + context.Exception.Message)); + } + + if (context.Exception.GetType() == typeof(BusinessValidationFailedException)) + { + context.Result = new JsonResult(ResponseOutput.NotOk("Verify error: " + context.Exception.Message)); + } + else if(context.Exception.GetType() == typeof(QueryBusinessObjectNotExistException)) + { + context.Result = new JsonResult(ResponseOutput.NotOk( context.Exception.Message)); + } + else + { + context.Result = new JsonResult(ResponseOutput.NotOk(" Program exception, please contact the developer! " + (context.Exception.InnerException is null? context.Exception.Message:context.Exception.InnerException?.Message) )); + } + + + _logger.LogError(context.Exception.InnerException is null ? (context.Exception.Message +context.Exception.StackTrace): (context.Exception.InnerException?.Message+ context.Exception.InnerException?.StackTrace)); + + + } + else + { + //继续 + } + context.ExceptionHandled = true;//标记当前异常已经被处理过了 + } + } +} diff --git a/IRaCIS.Core.Application/BusinessFilter/TrialAuditFilter.cs b/IRaCIS.Core.Application/BusinessFilter/TrialAuditFilter.cs new file mode 100644 index 00000000..a614a1e6 --- /dev/null +++ b/IRaCIS.Core.Application/BusinessFilter/TrialAuditFilter.cs @@ -0,0 +1,1575 @@ +//using IRaCIS.Application.Contracts; +//using IRaCIS.Core.Application.Contracts.Dicom.DTO; +//using IRaCIS.Core.Application.Contracts.DTO; +//using IRaCIS.Core.Infra.EFCore; +//using IRaCIS.Core.Domain.Models; +//using IRaCIS.Core.Domain.Share; +//using IRaCIS.Core.Infrastructure.Extention; +//using Microsoft.AspNetCore.Mvc.Filters; +//using Microsoft.EntityFrameworkCore; +//using Microsoft.Extensions.Logging; +//using IRaCIS.Core.Infrastructure; +//using IRaCIS.Core.Application.Contracts; + +//namespace IRaCIS.Core.Application.Filter +//{ +//#pragma warning disable CS8618 + +//#pragma warning disable CS8062 +// //by zhouhang 2021.08 + + +//审计类型大类 +using Microsoft.AspNetCore.Mvc.Filters; + +public enum AuditType +{ + TrialAudit = 0, + SubjectAudit = 1, + StudyAudit = 2 +} + +//具体审计操作 +public enum AuditOptType +{ + //DeleteTrial = 2, + AddOrUpdateTrial = 0, + + AddTrialSiteSurvey = 1, + + //参与人员 + AddTrialStaff = 3, + DeleteTrailStaff = 4, + + //Site + AddTrialSite = 5, + DeleteTrialSite = 6, + + //Site CRC + AddTrialSiteCRC = 7, + DeleteTrialSiteCRC = 8, + + //访视计划 + AddOrUpdateTrialVisitPlanItem = 9, + DeleteTrialVisitPlanItem = 10, + ConfirmTrialVisitPlan = 11, + + //项目模板 + AddOrUpdateTrialTemplate = 12, + DeleteTrialTemplate = 13, + + //subject 访视计划 + AddOrUpdateSubjectOutPlanVisit = 14, + DeleteSubjectOutPlanVisit = 15, + SetSVExecuted = 16, + + AddOrUpdateSubject = 17, + DeleteSubject = 18, + + //影像上传 + UploadImage = 19, + + //变更QA状态 比如设置为QA中 QA结束通过、不通过 + ChangeStudyStatus = 20, + + //匿名化 + Anonymized = 22, + + //转发 + Forwarded = 24 +} + +[AttributeUsage(AttributeTargets.Method)] +public class TrialAuditAttribute : Attribute,/*IActionFilter, */IAsyncActionFilter +{ + private readonly AuditType _auditType; + private readonly AuditOptType _auditOptType; + public TrialAuditAttribute(AuditType auditType, AuditOptType auditOptType) + { + _auditType = auditType; + _auditOptType = auditOptType; + } + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + await next(); + + } +} + +// [AttributeUsage(AttributeTargets.Method)] +// public class TrialAuditAttribute : Attribute,/*IActionFilter, */IAsyncActionFilter +// { +// private readonly AuditType _auditType; +// private readonly AuditOptType _auditOptType; +// public IDictionary _actionArguments; + +// #region 根据不同接口构造的属性 + +// private TrialAuditAttribute _attr; + +// private string _userIdStr; + +// private string _userRealName; + +// private IRepository _auditRepository; + +// private TrialUser _userTrial; + +// private TrialSiteUser _userTrialSite; + +// private VisitStage _visitPlan; + +// //private QATrialTemplate _qATrailTemplate; + +// private Subject _subject; + +// private SubjectVisit _subjectVisit; + +// private TrialSite _trialSite; + +// private DicomStudy _study; + +// private IRepository _studyRepository; + +// //private IRaCISDBContext _dbContext; + +// //private IDbContextTransaction transaction; + + +// #endregion + +// public TrialAuditAttribute(AuditType auditType, AuditOptType auditOptType) +// { +// _auditType = auditType; +// _auditOptType = auditOptType; +// } + + +// #region 同步版本 废弃 + +// //public void OnActionExecuted(ActionExecutedContext executedcontext) +// //{ + +// // //上传影像接口返回值类型特殊 需要单独处理 +// // if (_attr._auditOptType == AuditOptType.UploadImage) +// // { +// // //接口参数 +// // var archiveStudyInfo = (ArchiveStudyCommand)_actionArguments.Values.ToList()[0]; + +// // //接口返回结果 因为上传成功和不成功 泛型类型不一样 +// // dynamic archive = executedcontext.Result; +// // if (archive == null) +// // { +// // var logger = (ILogger)executedcontext.HttpContext.RequestServices.GetService( +// // typeof(ILogger)); + +// // logger.LogError("影像上传,OnActionExecuted Reuslt 为null"); +// // return; +// // } +// // var archiveResult = (IResponseOutput)archive.Value; + + + +// // if (archiveResult.IsSuccess) +// // { +// // _studyRepository = (IRepository)executedcontext.HttpContext.RequestServices.GetService( +// // typeof(IRepository)); + +// // var successResult = (IResponseOutput)archiveResult; + +// // _study = _studyRepository.GetAll().First(t => +// // t.Id == successResult.Data.ArchivedDicomStudies.ToList()[0].Id); + +// // _auditRepository.Add(new TrialAudit() +// // { +// // TrialId = _study.TrialId, +// // SubjectId = _study.SubjectId, +// // StudyId = _study.Id, +// // AuditType = (int)AuditType.StudyAudit, +// // Note = $"{ _userRealName} 动作变更了 { _study.StudyCode} 检查状态为 {DealStudyStatus(_study.Status)} ", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); + +// // _auditRepository.SaveChanges(); +// // } + +// // return; +// // } + +// // //接口返回结果 +// // dynamic actionResult = executedcontext.Result; +// // var result = (IResponseOutput)actionResult.Value;//此处审计只涉及到添加更新和删除, 统一了相关接口的返回结果,才能在此强制转换 + + + +// // switch (_attr._auditType) +// // { +// // //项目审计 +// // case AuditType.TrialAudit: + +// // switch (_attr._auditOptType) +// // { +// // //项目的添加和更新接口 +// // case AuditOptType.AddOrUpdateTrial: + +// // //接口参数 +// // var trialInfo = (TrialCommand)_actionArguments.Values.ToList()[0]; + +// // if (result.IsSuccess) +// // { +// // _auditRepository.Add(new TrialAudit() +// // { +// // //判断是添加还是更新 +// // TrialId = trialInfo.Id == null ? Guid.Parse(result.Data) : trialInfo.Id.Value, +// // SubjectId = Guid.Empty, +// // AuditType = (int)AuditType.TrialAudit, +// // Note = trialInfo.Id == null ? $"{ _userRealName} 添加了项目 {trialInfo.Code} " : $"{ _userRealName} 更新了项目 {trialInfo.Code} 基本信息 ", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); +// // } + +// // break; +// // //添加项目运维人员 +// // case AuditOptType.AddTrialStaff: + +// // //接口参数 +// // var trialUsers = (List)_actionArguments.Values.ToList()[0]; + +// // if (result.IsSuccess) +// // { + +// // _auditRepository.Add(new TrialAudit() +// // { +// // TrialId = trialUsers[0].TrialId, +// // SubjectId = Guid.Empty, +// // AuditType = (int)AuditType.TrialAudit, +// // Note = $"{ _userRealName} 添加了项目运维人员 {string.Join(',', trialUsers.Select(t => t.UserRealName).ToList())} ", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); +// // } + +// // break; + +// // //添加研究中心 +// // case AuditOptType.AddTrialSite: +// // //接口参数 +// // var trialSites = (List)_actionArguments.Values.ToList()[0]; + +// // if (result.IsSuccess) +// // { +// // //查询site信息 +// // var siteIds = trialSites.Select(t => t.SiteId).ToList(); +// // var _siteRepository = (IRepository)executedcontext.HttpContext.RequestServices.GetService(typeof(IRepository)); +// // var siteNames = _siteRepository.Where(t => siteIds.Contains(t.Id)).Select(u => u.SiteName); + +// // _auditRepository.Add(new TrialAudit() +// // { +// // //判断是添加还是更新 +// // TrialId = trialSites[0].TrialId, +// // SubjectId = Guid.Empty, +// // AuditType = (int)AuditType.TrialAudit, +// // Note = $"{ _userRealName} 添加了项目研究中心 {string.Join(',', siteNames)} ", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); +// // } + +// // break; + +// // //研究中心添加运维人员 +// // case AuditOptType.AddTrialSiteCRC: +// // //接口参数 +// // var trialSiteCRCs = (List)_actionArguments.Values.ToList()[0]; + +// // if (result.IsSuccess) +// // { +// // //查询site信息 +// // var _siteRepository = (IRepository)executedcontext.HttpContext.RequestServices.GetService(typeof(IRepository)); +// // var siteName = _siteRepository.GetAll().First(t => t.Id == trialSiteCRCs[0].SiteId).SiteName; + +// // _auditRepository.Add(new TrialAudit() +// // { +// // TrialId = trialSiteCRCs[0].TrialId, +// // SubjectId = Guid.Empty, +// // AuditType = (int)AuditType.TrialAudit, +// // Note = $"{ _userRealName} 为{ siteName } 添加了运维人员 {string.Join(',', trialSiteCRCs.Select(t => t.UserRealName).ToList())}", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); +// // } + +// // break; + +// // //删除项目运维人员 +// // case AuditOptType.DeleteTrailStaff: + +// // if (result.IsSuccess) +// // { +// // _auditRepository.Add(new TrialAudit() +// // { +// // TrialId = _userTrial.TrialId, +// // SubjectId = Guid.Empty, +// // AuditType = (int)AuditType.TrialAudit, +// // Note = $"{ _userRealName} 移除了项目参与人员 {_userTrial.UserRealName} ", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); +// // } + +// // break; +// // //删除项目site +// // case AuditOptType.DeleteTrialSite: + +// // if (result.IsSuccess) +// // { +// // //查询site信息 +// // var _siteRepository = (IRepository)executedcontext.HttpContext.RequestServices.GetService(typeof(IRepository)); +// // var siteName = _siteRepository.GetAll().First(t => t.Id == _trialSite.SiteId).SiteName; + +// // _auditRepository.Add(new TrialAudit() +// // { +// // TrialId = _trialSite.TrialId, +// // SubjectId = Guid.Empty, +// // AuditType = (int)AuditType.TrialAudit, +// // Note = $"{ _userRealName} 移除了研究中心 {siteName} ", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); +// // } + +// // break; +// // //删除项目Site CRC +// // case AuditOptType.DeleteTrialSiteCRC: + +// // if (result.IsSuccess) +// // { +// // //查询site信息 +// // var _siteRepository = (IRepository)executedcontext.HttpContext.RequestServices.GetService(typeof(IRepository)); +// // var siteName = _siteRepository.GetAll().First(t => t.Id == _userTrial.SiteId).SiteName; + +// // _auditRepository.Add(new TrialAudit() +// // { +// // TrialId = _userTrial.TrialId, +// // SubjectId = Guid.Empty, +// // AuditType = (int)AuditType.TrialAudit, +// // Note = $"{ _userRealName} 移除了 {siteName} 的运维人员 {_userTrial.UserRealName} ", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); +// // } +// // break; + +// // //添加访视计划Item +// // case AuditOptType.AddOrUpdateTrialVisitPlanItem: +// // //接口参数 +// // var visitPlanItem = (VisitPlanCommand)_actionArguments.Values.ToList()[0]; + +// // if (result.IsSuccess) +// // { +// // _auditRepository.Add(new TrialAudit() +// // { +// // TrialId = visitPlanItem.TrialId, +// // SubjectId = Guid.Empty, +// // AuditType = (int)AuditType.TrialAudit, +// // Note = visitPlanItem.Id == null ? $"{ _userRealName} 添加了访视计划项 {visitPlanItem.VisitName} " : $"{ _userRealName} 更新了访视计划项为 {visitPlanItem.VisitName} ", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); +// // } + +// // break; +// // //删除访视计划Item +// // case AuditOptType.DeleteTrialVisitPlanItem: + +// // if (result.IsSuccess) +// // { +// // _auditRepository.Add(new TrialAudit() +// // { +// // TrialId = _visitPlan.TrialId, +// // SubjectId = Guid.Empty, +// // AuditType = (int)AuditType.TrialAudit, +// // Note = $"{ _userRealName} 移除了访视计划项 {_visitPlan.VisitName} ", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); +// // } +// // break; + +// // //确认了访视计划 +// // case AuditOptType.ConfirmTrialVisitPlan: +// // var trialId = (Guid)_actionArguments.Values.ToList()[0]; + +// // if (result.IsSuccess) +// // { +// // _auditRepository.Add(new TrialAudit() +// // { +// // TrialId = trialId, +// // SubjectId = Guid.Empty, +// // AuditType = (int)AuditType.TrialAudit, +// // Note = $"{ _userRealName} 确认了访视计划,不允许修改 ", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); +// // } + +// // break; +// // //添加了QA模板 +// // case AuditOptType.AddOrUpdateTrialTemplate: +// // //接口参数 +// // var trialQATemplate = (TrialQATemplateAddCommand)_actionArguments.Values.ToList()[0]; + +// // if (result.IsSuccess) +// // { +// // _auditRepository.Add(new TrialAudit() +// // { + +// // TrialId = trialQATemplate.TrialId, +// // SubjectId = Guid.Empty, +// // AuditType = (int)AuditType.TrialAudit, +// // Note = trialQATemplate.Id == null ? $"{ _userRealName} 添加了QA模板 {trialQATemplate.Name} " : $"{ _userRealName} 更新了QA模板 {trialQATemplate.Name} ", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); +// // } + + +// // break; +// // //删除项目模板 +// // case AuditOptType.DeleteTrialTemplate: + +// // if (result.IsSuccess) +// // { +// // _auditRepository.Add(new TrialAudit() +// // { + +// // TrialId = _qATrailTemplate.TrialId, +// // SubjectId = Guid.Empty, +// // AuditType = (int)AuditType.TrialAudit, +// // Note = $"{ _userRealName} 移除了QA模板 {_qATrailTemplate.Name} ", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); +// // } +// // break; + +// // } + +// // break; +// // //受试者审计 +// // case AuditType.SubjectAudit: + + +// // switch (_attr._auditOptType) +// // { +// // // 添加或者更新受试者 +// // case AuditOptType.AddOrUpdateSubject: + +// // //接口参数 +// // var subject = (SubjectCommand)_actionArguments.Values.ToList()[0]; + +// // if (result.IsSuccess) +// // { +// // _auditRepository.Add(new TrialAudit() +// // { +// // TrialId = subject.TrialId, +// // SubjectId = subject.Id == null ? Guid.Parse(result.Data) : subject.Id.Value, +// // AuditType = (int)AuditType.SubjectAudit, +// // Note = subject.Id == null ? $"{ _userRealName} 添加了受试者 {subject.LastName + subject.FirstName} 并初始化了访视计划" : $"{ _userRealName} 对受试者 { subject.LastName + subject.FirstName} 信息进行了更新 ", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); +// // } + +// // break; +// // // 删除受试者 +// // case AuditOptType.DeleteSubject: +// // if (result.IsSuccess) +// // { +// // _auditRepository.Add(new TrialAudit() +// // { +// // TrialId = _subject.TrialId, +// // SubjectId = _subject.Id, +// // AuditType = (int)AuditType.SubjectAudit, +// // Note = $"{ _userRealName} 移除了受试者 {_subject.LastName + _subject.FirstName} ", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); +// // } + +// // break; +// // // 受试者计划外访视 +// // case AuditOptType.AddOrUpdateSubjectOutPlanVisit: + +// // //接口参数 +// // var subjectVisit = (SubjectVisitCommand)_actionArguments.Values.ToList()[0]; + +// // if (result.IsSuccess) +// // { +// // var _subjectRepository = +// // (IRepository)executedcontext.HttpContext.RequestServices.GetService( +// // typeof(IRepository)); + +// // _subject = _subjectRepository.FirstOrDefault(t => t.Id == subjectVisit.SubjectId); + +// // _auditRepository.Add(new TrialAudit() +// // { +// // TrialId = _subject.TrialId, +// // SubjectId = _subject.Id, +// // AuditType = (int)AuditType.SubjectAudit, +// // Note = subjectVisit.Id == null ? $"{ _userRealName} 为受试者 {_subject.LastName + _subject.FirstName} 添加了计划外访视 {subjectVisit.VisitName}" : $"{ _userRealName} 更新受试者 {_subject.LastName + _subject.FirstName} 计划外访视{subjectVisit.VisitName}", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); +// // } + +// // break; +// // // 删除计划外访视 +// // case AuditOptType.DeleteSubjectOutPlanVisit: + +// // if (result.IsSuccess) +// // { +// // _auditRepository.Add(new TrialAudit() +// // { +// // TrialId = _subjectVisit.TrialId, +// // SubjectId = _subject.Id, +// // AuditType = (int)AuditType.SubjectAudit, +// // Note = $"{ _userRealName} 移除了受试者{_subject.LastName + _subject.FirstName} 计划外访视 {_subjectVisit.VisitName} ", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); +// // } + +// // break; +// // // 人工设置已执行 +// // case AuditOptType.SetSVExecuted: + +// // if (result.IsSuccess) +// // { +// // _auditRepository.Add(new TrialAudit() +// // { +// // TrialId = _subjectVisit.TrialId, +// // SubjectId = _subject.Id, +// // AuditType = (int)AuditType.SubjectAudit, +// // Note = $"{ _userRealName} 将受试者 {_subject.LastName + _subject.FirstName} 访视 {_subjectVisit.VisitName} 设置为已执行 ", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); +// // } + + +// // break; +// // } + +// // break; + +// // //检查审计 +// // case AuditType.StudyAudit: +// // _studyRepository = +// // (IRepository)executedcontext.HttpContext.RequestServices.GetService( +// // typeof(IRepository)); + +// // switch (_attr._auditOptType) +// // { + +// // case AuditOptType.ChangeStudyStatus: + +// // //接口参数 +// // var studyStatusInfo = (StudyStatusDetailCommand)_actionArguments.Values.ToList()[0]; + +// // _study = _studyRepository.FirstOrDefault(t => t.Id == studyStatusInfo.StudyId); + +// // if (result.IsSuccess) +// // { +// // _auditRepository.Add(new TrialAudit() +// // { +// // TrialId = _study.TrialId, +// // SubjectId = _study.SubjectId, +// // StudyId = _study.Id, +// // AuditType = (int)AuditType.StudyAudit, +// // Note = $"{ _userRealName} 动作变更了 { _study.StudyCode} 检查状态为 {DealStudyStatus(_study.Status)} ", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); +// // } + +// // break; + +// // case AuditOptType.Anonymized: + +// // case AuditOptType.Forwarded: + +// // //接口参数 +// // var studyId = (Guid)_actionArguments.Values.ToList()[0]; + +// // _study = _studyRepository.FirstOrDefault(t => t.Id == studyId); + +// // if (result.IsSuccess) +// // { +// // _auditRepository.Add(new TrialAudit() +// // { +// // TrialId = _study.TrialId, +// // SubjectId = _study.SubjectId, +// // StudyId = _study.Id, +// // AuditType = (int)AuditType.StudyAudit, +// // Note = $"{ _userRealName} 动作变更了 { _study.StudyCode} 检查状态为 {DealStudyStatus(_study.Status)} ", +// // Detail = string.Empty, +// // OptUserId = Guid.Parse(_userIdStr), +// // OptUser = _userRealName, +// // OptTime = DateTime.Now +// // }); +// // } + + +// // break; +// // } + +// // break; + +// // } + +// // _auditRepository.SaveChanges(); + +// //} + + +// //public void OnActionExecuting(ActionExecutingContext context) +// //{ +// // //_dbContext = (IRaCISDBContext)context.HttpContext.RequestServices.GetService(typeof(IRaCISDBContext)); + + +// // _actionArguments = context.ActionArguments; + +// // _attr = (TrialAuditAttribute)context.ActionDescriptor.EndpointMetadata.First(m => m.GetType() == typeof(TrialAuditAttribute)); + +// // _userIdStr = context.HttpContext.User.Claims.First(t => t.Type == "id")?.Value ?? Guid.Empty.ToString(); +// // _userRealName = context.HttpContext.User.Claims.First(t => t.Type == "realName")?.Value ?? string.Empty; + +// // //获取接口仓储 +// // _auditRepository = (IRepository)context.HttpContext.RequestServices.GetService(typeof(IRepository)); + + + +// // switch (_attr._auditType) +// // { +// // //项目审计 +// // case AuditType.TrialAudit: + +// // switch (_attr._auditOptType) +// // { +// // //删除项目运维人员 +// // case AuditOptType.DeleteTrailStaff: + +// // // 删除项目Site CRC +// // case AuditOptType.DeleteTrialSiteCRC: + +// // //接口参数 +// // var userTrialId = (Guid)_actionArguments.Values.ToList()[0]; + +// // var _userTrialRepository = +// // (IRepository)context.HttpContext.RequestServices.GetService( +// // typeof(IRepository)); + +// // _userTrial = _userTrialRepository.GetAll().First(t => t.Id == userTrialId); +// // break; + + +// // //删除项目site +// // case AuditOptType.DeleteTrialSite: + +// // //接口参数 +// // var trialSiteId = (Guid)_actionArguments.Values.ToList()[0]; + +// // var _TrialsiteRepository = +// // (IRepository)context.HttpContext.RequestServices.GetService( +// // typeof(IRepository)); + +// // _trialSite = _TrialsiteRepository.GetAll().First(t => t.Id == trialSiteId); +// // break; + +// // case AuditOptType.DeleteTrialVisitPlanItem: + +// // //接口参数 +// // var visitPlanId = (Guid)_actionArguments.Values.ToList()[0]; + +// // var _visitPlanRepository = +// // (IRepository)context.HttpContext.RequestServices.GetService( +// // typeof(IRepository)); + +// // _visitPlan = _visitPlanRepository.GetAll().First(t => t.Id == visitPlanId); + +// // break; +// // case AuditOptType.DeleteTrialTemplate: + +// // //接口参数 +// // var qATrailTemplateId = (Guid)_actionArguments.Values.ToList()[0]; +// // var _qATrailTemplateRepository = +// // (IRepository)context.HttpContext.RequestServices.GetService( +// // typeof(IRepository)); + +// // _qATrailTemplate = _qATrailTemplateRepository.GetAll() +// // .FirstOrDefault(t => t.Id == qATrailTemplateId); + +// // break; + +// // } + +// // break; + +// // //受试者审计 +// // case AuditType.SubjectAudit: + +// // switch (_auditOptType) +// // { +// // case AuditOptType.DeleteSubject: +// // //接口参数 +// // var subjectId = (Guid)_actionArguments.Values.ToList()[0]; + +// // var _subjectRepository = +// // (IRepository)context.HttpContext.RequestServices.GetService( +// // typeof(IRepository)); + +// // _subject = _subjectRepository.FirstOrDefault(t => t.Id == subjectId); + +// // break; + +// // case AuditOptType.DeleteSubjectOutPlanVisit: +// // case AuditOptType.SetSVExecuted: + +// // //接口参数 +// // var subjectVisitId = (Guid)_actionArguments.Values.ToList()[0]; + +// // var _subjectVisitRepository = +// // (IRepository)context.HttpContext.RequestServices.GetService( +// // typeof(IRepository)); + +// // _subjectVisit = _subjectVisitRepository.FirstOrDefault(t => t.Id == subjectVisitId); + +// // _subjectRepository = +// // (IRepository)context.HttpContext.RequestServices.GetService( +// // typeof(IRepository)); + +// // _subject = _subjectRepository.FirstOrDefault(t => t.Id == _subjectVisit.SubjectId); + +// // break; +// // } +// // break; + + +// // //检查审计 +// // case AuditType.StudyAudit: + +// // switch (_attr._auditOptType) +// // { +// // case AuditOptType.UploadImage: + +// // break; +// // } +// // break; + +// // } + +// //} +// #endregion + +// public static string DealStudyStatus(int status) +// { + +// var message = string.Empty; +// switch (status) +// { +// case (int)StudyStatus.Uploaded: +// message = "Uploaded"; +// break; + +// case (int)StudyStatus.QARequested: + +// message = "QA Requested"; +// break; +// case (int)StudyStatus.QAing: + +// message = "In QA"; +// break; + +// //case (int)StudyStatus.Abandon: +// // message = "Study has abandon"; +// // break; + +// case (int)StudyStatus.QAFinish: +// message = "QA Completed Passed"; +// break; + +// case (int)StudyStatus.QAFInishNotPass: +// message = "QA Completed Failed"; +// break; + +// case (int)StudyStatus.Anonymizing: +// message = "Anonymizing"; +// break; + +// case (int)StudyStatus.Anonymized: +// message = "Anonymized"; +// break; + +// case (int)StudyStatus.AnonymizeFailed: +// message = "Anonymize Failed"; +// break; + +// case (int)StudyStatus.Forwarding: +// message = "Forwarding"; +// break; + +// case (int)StudyStatus.Forwarded: +// message = "Forwarded "; +// break; + +// case (int)StudyStatus.ForwardFailed: +// message = "Forward Failed "; +// break; +// } + +// return message; +// } + +// public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) +// { +// // 开启AOP事务(审计消息记录和 添加更新记录 在一个事务里面,不再是分开的事务) +// //var _dbContext = (IRaCISDBContext)context.HttpContext.RequestServices.GetService(typeof(IRaCISDBContext)); + +// //var strategy = _dbContext.Database.CreateExecutionStrategy(); + +// var _dbContext = (IRaCISDBContext)context.HttpContext.RequestServices.GetService(typeof(IRaCISDBContext)); + +// var strategy = _dbContext.Database.CreateExecutionStrategy(); + +// await strategy.Execute(async () => +// { +// var currentTransaction = _dbContext.Database.CurrentTransaction; + +// var transaction = currentTransaction ?? _dbContext.Database.BeginTransaction(System.Data.IsolationLevel.ReadUncommitted); + +// #region 执行方法前 + +// _actionArguments = context.ActionArguments; + +// _attr = (TrialAuditAttribute)context.ActionDescriptor.EndpointMetadata.First(m => m.GetType() == typeof(TrialAuditAttribute)); + +// _userIdStr = context.HttpContext.User.Claims.First(t => t.Type == "id")?.Value ?? Guid.Empty.ToString(); +// _userRealName = context.HttpContext.User.Claims.First(t => t.Type == "realName")?.Value ?? string.Empty; + +// //获取接口仓储 +// _auditRepository = (IRepository)context.HttpContext.RequestServices.GetService(typeof(IRepository)); + +// switch (_attr._auditType) +// { +// //项目审计 +// case AuditType.TrialAudit: + +// switch (_attr._auditOptType) +// { +// //删除项目运维人员 +// case AuditOptType.DeleteTrailStaff: + +// //接口参数 +// var userTrial = (Guid)_actionArguments.Values.ToList()[0]; + +// var _userTrialRepository = +// (IRepository)context.HttpContext.RequestServices.GetService( +// typeof(IRepository)); + +// _userTrial = _userTrialRepository.AsQueryable().Include(t => t.User).FirstOrDefault(t => t.Id == userTrial); +// break; + +// // 删除项目Site CRC +// case AuditOptType.DeleteTrialSiteCRC: + +// //接口参数 +// var userTrialSiteId = (Guid)_actionArguments.Values.ToList()[0]; + +// var _userTrialSiteRepository = +// (IRepository)context.HttpContext.RequestServices.GetService( +// typeof(IRepository)); + +// _userTrialSite = _userTrialSiteRepository.AsQueryable().Include(t => t.User).FirstOrDefault(t => t.Id == userTrialSiteId); +// break; + + +// //删除项目site +// case AuditOptType.DeleteTrialSite: + +// //接口参数 +// var trialSiteId = (Guid)_actionArguments.Values.ToList()[0]; + +// var _TrialsiteRepository = +// (IRepository)context.HttpContext.RequestServices.GetService( +// typeof(IRepository)); + +// _trialSite = _TrialsiteRepository.AsQueryable().Include(t => t.Site).FirstOrDefault(t => t.Id == trialSiteId); +// break; + +// case AuditOptType.DeleteTrialVisitPlanItem: + +// //接口参数 +// var visitPlanId = (Guid)_actionArguments.Values.ToList()[0]; + +// var _visitPlanRepository = +// (IRepository)context.HttpContext.RequestServices.GetService( +// typeof(IRepository)); + +// _visitPlan = _visitPlanRepository.FirstOrDefault(t => t.Id == visitPlanId).IfNullThrowException(); + +// break; +// case AuditOptType.DeleteTrialTemplate: + + + +// break; + +// } + +// break; + +// //受试者审计 +// case AuditType.SubjectAudit: + +// switch (_auditOptType) +// { +// case AuditOptType.DeleteSubject: +// //接口参数 +// var subjectId = (Guid)_actionArguments.Values.ToList()[0]; + +// var _subjectRepository = +// (IRepository)context.HttpContext.RequestServices.GetService( +// typeof(IRepository)); + +// _subject = _subjectRepository.FirstOrDefault(t => t.Id == subjectId).IfNullThrowException(); + +// break; + +// case AuditOptType.DeleteSubjectOutPlanVisit: +// case AuditOptType.SetSVExecuted: + +// //接口参数 +// var subjectVisitId = (Guid)_actionArguments.Values.ToList()[0]; + +// var _subjectVisitRepository = +// (IRepository)context.HttpContext.RequestServices.GetService( +// typeof(IRepository)); + +// _subjectVisit = _subjectVisitRepository.FirstOrDefault(t => t.Id == subjectVisitId).IfNullThrowException(); + +// _subjectRepository = +// (IRepository)context.HttpContext.RequestServices.GetService( +// typeof(IRepository)); + +// _subject = _subjectRepository.FirstOrDefault(t => t.Id == _subjectVisit.SubjectId).IfNullThrowException(); + +// break; +// } +// break; + + +// //检查审计 +// case AuditType.StudyAudit: + +// switch (_attr._auditOptType) +// { +// case AuditOptType.UploadImage: + +// break; +// } +// break; + +// } + +// #endregion + +// //获取实际执行方法next() 的结果 +// ActionExecutedContext executedcontext = await next(); + +// #region 方法执行后 + +// //上传影像接口返回值类型特殊 需要单独处理 +// if (_attr._auditOptType == AuditOptType.UploadImage) +// { + +// //接口参数 +// var archiveStudyInfo = (ArchiveStudyCommand)_actionArguments.Values.ToList()[0]; + +// if (executedcontext.Exception != null) +// { +// var logger = (ILogger)executedcontext.HttpContext.RequestServices.GetService( +// typeof(ILogger)); + +// logger!.LogError("影像上传 中间发生异常", executedcontext.Exception.StackTrace); + +// return; +// } + +// //接口返回结果 因为上传成功和不成功 泛型类型不一样 +// dynamic archive = executedcontext.Result; + + +// var archiveResult = (IResponseOutput)archive.Value; + +// if (archiveResult.IsSuccess) +// { + + +// _studyRepository = (IRepository)executedcontext.HttpContext.RequestServices.GetService(typeof(IRepository)); + +// _auditRepository = (IRepository)context.HttpContext.RequestServices.GetService(typeof(IRepository)); + +// var successResult = (IResponseOutput)archiveResult; + +// var studyList = _studyRepository.Where(t => t.SubjectVisitId == successResult.Data.ArchivedDicomStudies.ToList()[0].SubjectVisitId).ToList(); + +// studyList.ForEach(study => +// { +// var mes = study.Status == (int)StudyStatus.Uploaded ? "上传了" : "重传了"; +// _auditRepository.Add(new TrialAudit() +// { +// TrialId = study.TrialId, +// SubjectId = study.SubjectId, +// StudyId = study.Id, +// AuditType = (int)AuditType.StudyAudit, +// //Note = $"{ _userRealName} 动作变更了 { _study.StudyCode} 检查状态为 {DealStudyStatus(_study.Status)} ", +// Note = $"{ _userRealName} {mes} { study.StudyCode} ", +// Detail = string.Empty, +// OptUserId = Guid.Parse(_userIdStr), +// OptUser = _userRealName, +// OptTime = DateTime.Now +// }); +// }); + + + +// _auditRepository.SaveChanges(); + +// transaction.Commit(); + +// return; + +// } + + +// } + +// //接口返回结果 +// if (executedcontext.Result is null) +// { +// throw new BusinessValidationFailedException(executedcontext.Exception.Message); +// } +// dynamic actionResult = executedcontext.Result; + +// //此处审计只涉及到添加更新和删除, 统一了相关接口的返回结果,才能在此强制转换 +// //var result = (IResponseOutput)actionResult.Value; + +// var result = (IResponseOutput)actionResult.Value; + + +// switch (_attr._auditType) +// { +// //项目审计 +// case AuditType.TrialAudit: + +// switch (_attr._auditOptType) +// { +// //项目的添加和更新接口 +// case AuditOptType.AddOrUpdateTrial: + +// //接口参数 +// var trialInfo = (TrialCommand)_actionArguments.Values.ToList()[0]; + +// if (result.IsSuccess) +// { +// _auditRepository.Add(new TrialAudit() +// { +// //判断是添加还是更新 +// TrialId = trialInfo.Id == null ? Guid.Parse(result.Data) : trialInfo.Id.Value, +// SubjectId = Guid.Empty, +// AuditType = (int)AuditType.TrialAudit, +// Note = trialInfo.Id == null ? $"{ _userRealName} 添加了项目 {trialInfo.TrialCode} " : $"{ _userRealName} 更新了项目 {trialInfo.TrialCode} 基本信息 ", +// Detail = string.Empty, +// OptUserId = Guid.Parse(_userIdStr), +// OptUser = _userRealName, +// OptTime = DateTime.Now +// }); +// } + +// break; +// //添加项目运维人员 +// case AuditOptType.AddTrialStaff: + +// //接口参数 +// var trialUsers = (TrialUserAddCommand[])_actionArguments.Values.ToList()[0]; + +// if (result.IsSuccess) +// { + +// _auditRepository.Add(new TrialAudit() +// { +// TrialId = trialUsers[0].TrialId, +// SubjectId = Guid.Empty, +// AuditType = (int)AuditType.TrialAudit, +// //Note = $"{ _userRealName} 添加了项目运维人员 {string.Join(',', trialUsers.Select(t => t.UserRealName).ToList())} ", +// Detail = string.Empty, +// OptUserId = Guid.Parse(_userIdStr), +// OptUser = _userRealName, +// OptTime = DateTime.Now +// }); +// } + +// break; + +// //添加研究中心 +// case AuditOptType.AddTrialSite: +// //接口参数 +// var trialSites = (List)_actionArguments.Values.ToList()[0]; + +// if (result.IsSuccess) +// { +// //查询site信息 +// var siteIds = trialSites.Select(t => t.SiteId).ToList(); +// var _siteRepository = (IRepository)executedcontext.HttpContext.RequestServices.GetService(typeof(IRepository)); +// var siteNames = _siteRepository.Where(t => siteIds.Contains(t.Id)).Select(u => u.SiteName); + +// _auditRepository.Add(new TrialAudit() +// { +// //判断是添加还是更新 +// TrialId = trialSites[0].TrialId, +// SubjectId = Guid.Empty, +// AuditType = (int)AuditType.TrialAudit, +// Note = $"{ _userRealName} 添加了项目研究中心 {string.Join(',', siteNames)} ", +// Detail = string.Empty, +// OptUserId = Guid.Parse(_userIdStr), +// OptUser = _userRealName, +// OptTime = DateTime.Now +// }); +// } + +// break; + +// //研究中心添加运维人员 +// case AuditOptType.AddTrialSiteCRC: +// //接口参数 +// var trialSiteCRCs = (List)_actionArguments.Values.ToList()[0]; + +// if (result.IsSuccess) +// { +// //查询site信息 +// var _siteRepository = (IRepository)executedcontext.HttpContext.RequestServices.GetService(typeof(IRepository)); +// var siteName = _siteRepository.FirstOrDefault(t => t.Id == trialSiteCRCs[0].SiteId).IfNullThrowException().SiteName; + +// _auditRepository.Add(new TrialAudit() +// { +// TrialId = trialSiteCRCs[0].TrialId, +// SubjectId = Guid.Empty, +// AuditType = (int)AuditType.TrialAudit, +// Note = $"{ _userRealName} 为{ siteName } 添加了运维人员 {string.Join(',', trialSiteCRCs.Select(t => t.UserRealName).ToList())}", +// Detail = string.Empty, +// OptUserId = Guid.Parse(_userIdStr), +// OptUser = _userRealName, +// OptTime = DateTime.Now +// }); +// } + +// break; + +// //删除项目运维人员 +// case AuditOptType.DeleteTrailStaff: + +// if (result.IsSuccess) +// { +// _auditRepository.Add(new TrialAudit() +// { +// TrialId = _userTrial.TrialId, +// SubjectId = Guid.Empty, +// AuditType = (int)AuditType.TrialAudit, +// Note = $"{ _userRealName} 移除了项目参与人员 {_userTrial.User.LastName + " / " + _userTrial.User.FirstName} ", +// Detail = string.Empty, +// OptUserId = Guid.Parse(_userIdStr), +// OptUser = _userRealName, +// OptTime = DateTime.Now +// }); +// } + +// break; +// //删除项目site +// case AuditOptType.DeleteTrialSite: + +// if (result.IsSuccess) +// { +// //查询site信息 +// var _siteRepository = (IRepository)executedcontext.HttpContext.RequestServices.GetService(typeof(IRepository)); +// var siteName = _siteRepository.FirstOrDefault(t => t.Id == _trialSite.SiteId).IfNullThrowException().SiteName; + +// _auditRepository.Add(new TrialAudit() +// { +// TrialId = _trialSite.TrialId, +// SubjectId = Guid.Empty, +// AuditType = (int)AuditType.TrialAudit, +// Note = $"{ _userRealName} 移除了研究中心 {siteName} ", +// Detail = string.Empty, +// OptUserId = Guid.Parse(_userIdStr), +// OptUser = _userRealName, +// OptTime = DateTime.Now +// }); +// } + +// break; +// //删除项目Site CRC +// case AuditOptType.DeleteTrialSiteCRC: + +// if (result.IsSuccess) +// { +// //查询site信息 +// var _siteRepository = (IRepository)executedcontext.HttpContext.RequestServices.GetService(typeof(IRepository)); +// var siteName = _siteRepository.FirstOrDefault(t => t.Id == _userTrialSite.SiteId).IfNullThrowException().SiteName; + +// _auditRepository.Add(new TrialAudit() +// { +// TrialId = _userTrialSite.TrialId, +// SubjectId = Guid.Empty, +// AuditType = (int)AuditType.TrialAudit, +// Note = $"{ _userRealName} 移除了 {siteName} 的运维人员 {_userTrialSite.User.UserName} ", +// Detail = string.Empty, +// OptUserId = Guid.Parse(_userIdStr), +// OptUser = _userRealName, +// OptTime = DateTime.Now +// }); +// } +// break; + +// //添加访视计划Item +// case AuditOptType.AddOrUpdateTrialVisitPlanItem: +// //接口参数 +// var visitPlanItem = (VisitPlanCommand)_actionArguments.Values.ToList()[0]; + +// if (result.IsSuccess) +// { +// _auditRepository.Add(new TrialAudit() +// { +// TrialId = visitPlanItem.TrialId, +// SubjectId = Guid.Empty, +// AuditType = (int)AuditType.TrialAudit, +// Note = visitPlanItem.Id == null ? $"{ _userRealName} 添加了访视计划项 {visitPlanItem.VisitName} " : $"{ _userRealName} 更新了访视计划项为 {visitPlanItem.VisitName} ", +// Detail = string.Empty, +// OptUserId = Guid.Parse(_userIdStr), +// OptUser = _userRealName, +// OptTime = DateTime.Now +// }); +// } + +// break; +// //删除访视计划Item +// case AuditOptType.DeleteTrialVisitPlanItem: + +// if (result.IsSuccess) +// { +// _auditRepository.Add(new TrialAudit() +// { +// TrialId = _visitPlan.TrialId, +// SubjectId = Guid.Empty, +// AuditType = (int)AuditType.TrialAudit, +// Note = $"{ _userRealName} 移除了访视计划项 {_visitPlan.VisitName} ", +// Detail = string.Empty, +// OptUserId = Guid.Parse(_userIdStr), +// OptUser = _userRealName, +// OptTime = DateTime.Now +// }); +// } +// break; + +// //确认了访视计划 +// case AuditOptType.ConfirmTrialVisitPlan: +// var trialId = (Guid)_actionArguments.Values.ToList()[0]; + +// if (result.IsSuccess) +// { +// _auditRepository.Add(new TrialAudit() +// { +// TrialId = trialId, +// SubjectId = Guid.Empty, +// AuditType = (int)AuditType.TrialAudit, +// Note = $"{ _userRealName} 确认了访视计划,不允许修改 ", +// Detail = string.Empty, +// OptUserId = Guid.Parse(_userIdStr), +// OptUser = _userRealName, +// OptTime = DateTime.Now +// }); +// } + +// break; +// //添加了QA模板 +// case AuditOptType.AddOrUpdateTrialTemplate: +// //接口参数 + +// if (result.IsSuccess) +// { + +// } + + +// break; +// //删除项目模板 +// case AuditOptType.DeleteTrialTemplate: + +// if (result.IsSuccess) +// { + +// } +// break; + +// } + +// break; +// //受试者审计 +// case AuditType.SubjectAudit: + + +// switch (_attr._auditOptType) +// { +// // 添加或者更新受试者 +// case AuditOptType.AddOrUpdateSubject: + +// //接口参数 +// var subject = (SubjectCommand)_actionArguments.Values.ToList()[0]; + +// if (result.IsSuccess) +// { +// _auditRepository.Add(new TrialAudit() +// { +// TrialId = subject.TrialId, +// SubjectId = subject.Id == null ? Guid.Parse(result.Data) : subject.Id.Value, +// AuditType = (int)AuditType.SubjectAudit, +// Note = subject.Id == null ? $"{ _userRealName} 添加了受试者 {subject.ShortName } 并初始化了访视计划" : $"{ _userRealName} 对受试者 { subject.ShortName } 信息进行了更新 ", +// Detail = string.Empty, +// OptUserId = Guid.Parse(_userIdStr), +// OptUser = _userRealName, +// OptTime = DateTime.Now +// }); +// } + +// break; +// // 删除受试者 +// case AuditOptType.DeleteSubject: +// if (result.IsSuccess) +// { +// _auditRepository.Add(new TrialAudit() +// { +// TrialId = _subject.TrialId, +// SubjectId = _subject.Id, +// AuditType = (int)AuditType.SubjectAudit, +// Note = $"{ _userRealName} 移除了受试者 {_subject.LastName + _subject.FirstName} ", +// Detail = string.Empty, +// OptUserId = Guid.Parse(_userIdStr), +// OptUser = _userRealName, +// OptTime = DateTime.Now +// }); +// } + +// break; +// // 受试者计划外访视 +// case AuditOptType.AddOrUpdateSubjectOutPlanVisit: + +// //接口参数 +// var subjectVisit = (SubjectVisitCommand)_actionArguments.Values.ToList()[0]; + +// if (result.IsSuccess) +// { +// var _subjectRepository = +// (IRepository)executedcontext.HttpContext.RequestServices.GetService( +// typeof(IRepository)); + +// _subject = _subjectRepository.FirstOrDefault(t => t.Id == subjectVisit.SubjectId).IfNullThrowException(); + +// _auditRepository.Add(new TrialAudit() +// { +// TrialId = _subject.TrialId, +// SubjectId = _subject.Id, +// AuditType = (int)AuditType.SubjectAudit, +// Note = subjectVisit.Id == null ? $"{ _userRealName} 为受试者 {_subject.LastName + _subject.FirstName} 添加了计划外访视 {subjectVisit.VisitName}" : $"{ _userRealName} 更新受试者 {_subject.LastName + _subject.FirstName} 计划外访视{subjectVisit.VisitName}", +// Detail = string.Empty, +// OptUserId = Guid.Parse(_userIdStr), +// OptUser = _userRealName, +// OptTime = DateTime.Now +// }); +// } + +// break; +// // 删除计划外访视 +// case AuditOptType.DeleteSubjectOutPlanVisit: + +// if (result.IsSuccess) +// { +// _auditRepository.Add(new TrialAudit() +// { +// TrialId = _subjectVisit.TrialId, +// SubjectId = _subject.Id, +// AuditType = (int)AuditType.SubjectAudit, +// Note = $"{ _userRealName} 移除了受试者{_subject.LastName + _subject.FirstName} 计划外访视 {_subjectVisit.VisitName} ", +// Detail = string.Empty, +// OptUserId = Guid.Parse(_userIdStr), +// OptUser = _userRealName, +// OptTime = DateTime.Now +// }); +// } + +// break; +// // 人工设置已执行 +// case AuditOptType.SetSVExecuted: + +// if (result.IsSuccess) +// { +// _auditRepository.Add(new TrialAudit() +// { +// TrialId = _subjectVisit.TrialId, +// SubjectId = _subject.Id, +// AuditType = (int)AuditType.SubjectAudit, +// Note = $"{ _userRealName} 将受试者 {_subject.LastName + _subject.FirstName} 访视 {_subjectVisit.VisitName} 设置为已执行 ", +// Detail = string.Empty, +// OptUserId = Guid.Parse(_userIdStr), +// OptUser = _userRealName, +// OptTime = DateTime.Now +// }); +// } + + +// break; +// } + +// break; + +// //检查审计 +// case AuditType.StudyAudit: +// _studyRepository = +// (IRepository)executedcontext.HttpContext.RequestServices.GetService( +// typeof(IRepository)); + +// switch (_attr._auditOptType) +// { + +// case AuditOptType.ChangeStudyStatus: + +// //接口参数 +// var studyStatusInfo = (StudyStatusDetailCommand)_actionArguments.Values.ToList()[0]; + +// _study = _studyRepository.FirstOrDefault(t => t.Id == studyStatusInfo.StudyId).IfNullThrowException(); + +// if (result.IsSuccess) +// { +// _auditRepository.Add(new TrialAudit() +// { +// TrialId = _study.TrialId, +// SubjectId = _study.SubjectId, +// StudyId = _study.Id, +// AuditType = (int)AuditType.StudyAudit, +// Note = $"{ _userRealName} 动作变更了 { _study.StudyCode} 检查状态为 {DealStudyStatus(_study.Status)} ", +// Detail = string.Empty, +// OptUserId = Guid.Parse(_userIdStr), +// OptUser = _userRealName, +// OptTime = DateTime.Now +// }); +// } + +// break; + +// case AuditOptType.Anonymized: + +// case AuditOptType.Forwarded: + +// //接口参数 +// var studyId = (Guid)_actionArguments.Values.ToList()[0]; + +// _study = _studyRepository.FirstOrDefault(t => t.Id == studyId).IfNullThrowException(); + +// if (result.IsSuccess) +// { +// _auditRepository.Add(new TrialAudit() +// { +// TrialId = _study.TrialId, +// SubjectId = _study.SubjectId, +// StudyId = _study.Id, +// AuditType = (int)AuditType.StudyAudit, +// Note = $"{ _userRealName} 动作变更了 { _study.StudyCode} 检查状态为 {DealStudyStatus(_study.Status)} ", +// Detail = string.Empty, +// OptUserId = Guid.Parse(_userIdStr), +// OptUser = _userRealName, +// OptTime = DateTime.Now +// }); +// } + + +// break; +// } + +// break; + +// } + +// _auditRepository.SaveChanges(); + +// #endregion + + +// //提交事务 Commit transaction if all commands succeed, transaction will auto-rollback when disposed if either commands fails +// transaction.Commit(); +// } +// ); + + + + + + + + + +// } +// } +//} + + + + diff --git a/IRaCIS.Core.Application/BusinessFilter/TrialResourceFilter.cs b/IRaCIS.Core.Application/BusinessFilter/TrialResourceFilter.cs new file mode 100644 index 00000000..997a443c --- /dev/null +++ b/IRaCIS.Core.Application/BusinessFilter/TrialResourceFilter.cs @@ -0,0 +1,152 @@ +using EasyCaching.Core; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace IRaCIS.Core.Application.Filter +{ + /// + /// 主要为了 处理项目结束 锁库,不允许操作 + /// + public class TrialResourceFilter : Attribute, IAsyncResourceFilter /* , IResourceFilter*/ + { + private readonly IEasyCachingProvider _provider; + private readonly IUserInfo _userInfo; + + + public TrialResourceFilter(IEasyCachingProvider provider, IUserInfo userInfo) + { + _provider = provider; + _userInfo = userInfo; + } + + + + //优先选择异步的方法 + public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) + { + #region 处理新的用户类型,不能操作项目相关接口 + + if( _userInfo.UserTypeEnumInt == (int)UserTypeEnum.CRA) + { + context.Result = new JsonResult(ResponseOutput.NotOk("Sorry,Your UserType does not allow this operation")); + + return ; + } + + #endregion + + //bool isEditTrialStatus = context.ActionDescriptor.DisplayName==null? false:context.ActionDescriptor.DisplayName.Contains("UpdateTrialStatus"); + + //bool isTrialAdd = context.ActionDescriptor.DisplayName == null ? false: context.ActionDescriptor.DisplayName.Contains("AddOrUpdateTrial"); + + //TrialId 传递的途径多种,可能在path 可能在body 可能在数组中,也可能在对象中,可能就在url + var trialIdStr = string.Empty; + + //先尝试从path中取TrialId + if (context.RouteData.Values.Keys.Any(t => t.Contains("trialId"))) + { + var index = context.RouteData.Values.Keys.ToList().IndexOf("trialId"); + trialIdStr = context.RouteData.Values.Values.ToList()[index] as string; + } + else + { + #region body 中取数据 + + //设置可以多次读 + context.HttpContext.Request.EnableBuffering(); + var reader = new StreamReader(context.HttpContext.Request.Body); + var contentFromBody = await reader.ReadToEndAsync(); + //读取后,流的位置还原 + context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin); + //context.HttpContext.Request.Body.Position = 0; + + //找到参数位置在字符串中的索引 + var trialIdIndex = contentFromBody.IndexOf("\"TrialId\"", StringComparison.OrdinalIgnoreCase); + + if (trialIdIndex > -1) + { + trialIdStr = contentFromBody.Substring(trialIdIndex + "TrialId".Length + 4, 36); + } + //else if(isTrialAdd) + //{ + // //项目的添加和编辑时例外 trailId 在Id字段中 同时,添加和更新时有区别的 + // trialIdIndex = contentFromBody.IndexOf("\"Id\""); + + // //添加时为-1 不进行操作 + // if (trialIdIndex != -1) + // { + + // trialIdStr = contentFromBody.Substring(trialIdIndex + "Id".Length + 4, 36); + + // trialIdStr = Guid.TryParse(trialIdStr, out var trialId) ? trialId.ToString() : String.Empty ; + // } + + //} + + #endregion + } + + + //通过path 或者body 找到trialId 了 + if (trialIdStr != string.Empty) + { + + //如果没缓存数据,是不允许的 意外情况,IIS回收了,导致定时任务没执行或者缓存丢失 + if (_provider.GetCount() == 0) + { + + var _trialRepository = context.HttpContext.RequestServices.GetService(typeof(IRepository)) as IRepository; + + var list = _trialRepository.IfNullThrowException().Select(t => new { TrialId = t.Id, TrialStatusStr = t.TrialStatusStr }) + .ToList(); + + list.ForEach(t => _provider.Set(t.TrialId.ToString(), t.TrialStatusStr, TimeSpan.FromDays(7))); + } + + var cacheResultDic = _provider.GetAll(new[] { trialIdStr }); + + var trialStatusStr = cacheResultDic[trialIdStr]; + + + //项目完成和停止,都不能操作 + if (trialStatusStr.Value == StaticData.TrialCompleted || trialStatusStr.Value == StaticData.TrialStopped) + { + context.Result = new JsonResult(ResponseOutput.NotOk("Only trial in ongoing state can the operation be performed")); + } + + //仅仅管理员 在项目暂停 并且是编辑项目接口时 才放开操作 遗漏了正常情况 TrialOngoing + if (/*(trialStatusStr.Value == StaticData.TrialPaused && _userInfo.UserTypeEnumInt == (int)UserTypeEnum.SuperAdmin && isEditTrialStatus)||*/ trialStatusStr.Value == StaticData.TrialOngoing) + { + + await next.Invoke(); + + } + //项目暂停的基础上,是其他人,或者不是编辑项目状态,那么要禁止操作 + else + { + context.Result = new JsonResult(ResponseOutput.NotOk("Only trial in ongoing state can the operation be performed")); + + } + + } + ////没有找到trialId 判断是否是项目添加 + //else if (isTrialAdd) + //{ + // await next.Invoke(); + //} + else + { + //如果项目相关接口没有传递trialId 会来到这里,提醒,以便修改 + + context.Result = new JsonResult(ResponseOutput.NotOk("该接口参数中,没有传递trialId,请核查")); + } + + + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/BusinessFilter/UnifiedApiResultFilter.cs b/IRaCIS.Core.Application/BusinessFilter/UnifiedApiResultFilter.cs new file mode 100644 index 00000000..c9b9e63d --- /dev/null +++ b/IRaCIS.Core.Application/BusinessFilter/UnifiedApiResultFilter.cs @@ -0,0 +1,120 @@ +using System; +using Microsoft.AspNetCore.Mvc.Filters; +using System.Threading.Tasks; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Application.Services.BusinessFilter +{ + /// + /// 统一返回前端数据包装,之前在控制器包装,现在修改为动态Api 在ResultFilter这里包装,减少重复冗余代码 + /// by zhouhang 2021.09.12 周末 + /// + public class UnifiedApiResultFilter : Attribute, IAsyncResultFilter + { + /// + /// 异步版本 + /// + /// + /// + /// + public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) + { + + if (context.Result is ObjectResult objectResult) + { + var statusCode = objectResult.StatusCode ?? context.HttpContext.Response.StatusCode; + + //是200 并且没有包装 那么包装结果 + if (statusCode == 200 && !(objectResult.Value is IResponseOutput)) + { + //if (objectResult.Value == null) + //{ + // var apiResponse = ResponseOutput.DBNotExist(); + + // objectResult.Value = apiResponse; + // objectResult.DeclaredType = apiResponse.GetType(); + //} + //else + //{ + + var type = objectResult.Value?.GetType(); + + + if ( type!=null&& type.IsGenericType&&(type.GetGenericTypeDefinition()==typeof(ValueTuple<,>)|| type.GetGenericTypeDefinition()==typeof(Tuple<,>))) + { + + //报错 + //var tuple = (object, object))objectResult.Value; + + //var (val1, val2) = ((dynamic, dynamic))objectResult.Value; + //var apiResponse = ResponseOutput.Ok(val1, val2); + + //OK + var tuple = (dynamic)objectResult.Value; + var apiResponse = ResponseOutput.Ok(tuple.Item1, tuple.Item2); + + + objectResult.Value = apiResponse; + objectResult.DeclaredType = apiResponse.GetType(); + } + else + { + var apiResponse = ResponseOutput.Ok(objectResult.Value); + + objectResult.Value = apiResponse; + objectResult.DeclaredType = apiResponse.GetType(); + } + + + //} + + } + //如果不是200 是IResponseOutput 不处理 + else if (statusCode != 200 && (objectResult.Value is IResponseOutput)) + { + } + + else if(statusCode != 200&&!(objectResult.Value is IResponseOutput)) + { + var apiResponse = ResponseOutput.NotOk("Program error, contact the developer!"); + + objectResult.Value = apiResponse; + objectResult.DeclaredType = apiResponse.GetType(); + } + + } + + await next.Invoke(); + + } + + public static bool IsTupleType(Type type, bool checkBaseTypes = false) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + if (type == typeof(Tuple)) + return true; + + while (type != null) + { + if (type.IsGenericType) + { + var genType = type.GetGenericTypeDefinition(); + if (genType == typeof(Tuple<>) + || genType == typeof(Tuple<,>) + || genType == typeof(Tuple<,>)) + return true; + } + + if (!checkBaseTypes) + break; + + type = type.BaseType; + } + + return false; + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/BusinessFilter/UserTypeAuthorization.cs b/IRaCIS.Core.Application/BusinessFilter/UserTypeAuthorization.cs new file mode 100644 index 00000000..a46b853c --- /dev/null +++ b/IRaCIS.Core.Application/BusinessFilter/UserTypeAuthorization.cs @@ -0,0 +1,46 @@ +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.Filters; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace IRaCIS.Core.Application.BusinessFilter +{ + + //public class UserTypeRequirement : IAuthorizationRequirement + //{ + //} + + //public class UserTypeHandler : AuthorizationHandler + //{ + + // private IUserInfo _userInfo; + + // public UserTypeHandler(IUserInfo userInfo) + // { + // _userInfo = userInfo; + // } + + + // protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserTypeRequirement requirement) + // { + + // //if (context.User.Claims.Count() == 0) + // //{ + // // return Task.CompletedTask; + // //} + + // //string userId = context.User.Claims.First(c => c.Type == "Userid").Value; + // //string qq = context.User.Claims.First(c => c.Type == "QQ").Value; + + // //if (_UserService.Validata(userId, qq)) + // //{ + // // context.Succeed(requirement); //验证通过了 + // //} + // ////在这里就可以做验证 + + // return Task.CompletedTask; + // } + //} +} diff --git a/IRaCIS.Core.Application/IRaCIS.Core.Application.csproj b/IRaCIS.Core.Application/IRaCIS.Core.Application.csproj new file mode 100644 index 00000000..1373e9fb --- /dev/null +++ b/IRaCIS.Core.Application/IRaCIS.Core.Application.csproj @@ -0,0 +1,98 @@ + + + + net6.0 + default + enable + enable + True + + + + ..\bin\ + .\IRaCIS.Core.Application.xml + 1701;1702;1591 + + + + 1701;1702;1591;1587 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + true + PreserveNewest + + + Always + true + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IRaCIS.Core.Application/IRaCIS.Core.Application.xml b/IRaCIS.Core.Application/IRaCIS.Core.Application.xml new file mode 100644 index 00000000..f84b34e1 --- /dev/null +++ b/IRaCIS.Core.Application/IRaCIS.Core.Application.xml @@ -0,0 +1,2255 @@ + + + + IRaCIS.Core.Application + + + + + 颁发者 + + + + + 接收者 + + + + + 令牌密码 + + + + + 过期时间 + + + + + 签名 + + + + + 主要为了 处理项目结束 锁库,不允许操作 + + + + EmailNoticeConfigView 列表视图模型 + + + EmailNoticeConfigQuery 列表查询参数模型 + + + EmailNoticeConfigAddOrEdit 列表查询参数模型 + + + SystemBasicDataView 列表视图模型 + + + SystemBasicDataQuery 列表查询参数模型 + + + Name + + + Code + + + SystemBasicDataAddOrEdit 列表查询参数模型 + + + + EmailNoticeConfigService + + + + + ISystemBasicDataService + + + + SystemDocumentView 列表视图模型 + + + SystemDocumentQuery 列表查询参数模型 + + + SystemDocumentAddOrEdit 列表查询参数模型 + + + TrialDocumentUserConfirmView 列表视图模型 + + + TrialDocumentView 列表视图模型 + + + TrialDocumentQuery 列表查询参数模型 + + + TrialDocumentAddOrEdit 列表查询参数模型 + + + + ISystemDocumentService + + + + UserTypeRoleView 列表视图模型 + + + UserTypeRoleQuery 列表查询参数模型 + + + + UserTypeRoleService + + + + + 通过传递场景枚举 返回对应的下拉框数据 1:是外部 2:是内部 3:是Site调研 + + + + + + + 项目文档 配置 哪里用户类型下拉 + + + + + + 受试者临床信息 + + + + + 上传临床数据 + + + + + + + + + 获取访视+受试者级别的数据 + + + + + + NoneDicomStudyFileView 列表视图模型 + + + NoneDicomStudyFileQuery 列表查询参数模型 + + + Path + + + FileName + + + NoneDicomStudyFileAddOrEdit 列表查询参数模型 + + + NoneDicomStudyView 列表视图模型 + + + NoneDicomStudyQuery 列表查询参数模型 + + + BodyPart + + + Modality + + + Description + + + NoneDicomStudyAddOrEdit 列表查询参数模型 + + + PreviousHistoryView 列表视图模型 + + + PreviousHistoryQuery 列表查询参数模型 + + + PreviousOtherView 列表视图模型 + + + PreviousOtherQuery 列表查询参数模型 + + + PreviousSurgeryView 列表视图模型 + + + PreviousSurgeryQuery 列表查询参数模型 + + + PreviousPDFAddOrEdit 列表查询参数模型 + + + QCQuestionConfigureView 列表视图模型 + + + QCQuestionQuery 列表查询参数模型 + + + QuestionName + + + TypeValue + + + QCQuestionAddOrEdit 列表查询参数模型 + + + TrialQCQuestionConfigureView 列表视图模型 + + + TrialQCQuestionQuery 列表查询参数模型 + + + QuestionName + + + TypeValue + + + TrialQCQuestionAddOrEdit 列表查询参数模型 + + + + NoneDicomStudyService + + + + + 非Dicom检查 文件列表 + + + + + + + 上传非Dicom 文件 支持压缩包 + + + + + + + + + + 系统QC 问题管理 + + + + + 项目QC 问题 管理 + + + + + 父问题 下拉框选项 需要排除自己 、把自己设置为父亲 (互为父亲) 、是自己孙辈的(明明是自己子孙,却设置为自己父亲) + + + + + + + 批量添加 QC 问题 + + + + + + + + 检查项目 + TUMIDENT (Tumor Identification ) + TUSPLIT (Tumor Split ) + TUMERGE (Tumor Merged) + + + + 添加基线期病灶标识及测量信息 + + + + 获取访视病灶信息 + + + + TrialSiteEquipmentSurveyView 列表视图模型 + + + TrialSiteEquipmentSurveyQuery 列表查询参数模型 + + + TrialSiteEquipmentSurveyAddOrEdit 列表查询参数模型 + + + TrialSiteSurveyView 列表视图模型 + + + TrialSiteSurveyQuery 列表查询参数模型 + + + TrialSiteSurveyAddOrEdit 列表查询参数模型 + + + TrialSiteUserSurveyView 列表视图模型 + + + TrialSiteUserSurveyQuery 列表查询参数模型 + + + TrialSiteUserSurveyAddOrEdit 列表查询参数模型 + + + + 实测 标注在服务方法上 没用 + + + + + TrialSiteEquipmentSurveyService + + + + + TrialSiteSurveyService + + + + + 发送验证码 + + + + + + + + 验证后 如果数据库该项目不存在该邮箱 那么就插入记录 存在 + + + + + + + + + 直接查询相关所有数据 + + + + + + 实际这里只会是更新 添加在login的时候做了 + + + + + + + 删除调研表 + + + + + + + 获取 项目 某个site的调研记录 + + + + + + 初始登陆界面 项目基本信息+下拉框数据 + + + + + + + 驳回 + + + + + + + + 提交 后台自动识别是谁提交 + + + + + + + TrialSiteUserSurveyService + + + + + 受试者编号具体规则 + + + + + 是否 验证拍片日期 + + + + + 是否 提醒受试者编号规则 + + + + + 是否 有基准时间(首次给药时间) + + + + + 是否有 受试者年龄 + + + + + 出组后计划外访视名称 + + + + + 临床信息传输 1:系统录入2:系统录入+PDF 0:无 + + + + + 跨项目复制 + + + + + QC流程 0 不审,1 单审,2双审 + + + + + 影像一致性核查 + + + + + 1 Mint、2 PACS + + + + + 是否有 入组评估确认 + + + + + QC流程 0 不审,1 单审,2双审 + + + + + 影像一致性核查 + + + + + 1 Mint、2 PACS + + + + + 是否有 入组评估确认 + + + + + SystemBasicDataService + + + + + 模板列表 + + + + + + + 模板关联的场景 + + + + + + + 传递父亲Code 数组 返回多个下拉框数据 + + + + + + + SystemDocumentService + + + + + 管理端列表 + + + + + + + TrialDocumentService + + + + + Setting 界面的 项目所有文档列表 + + + + + + + 具体用户看到的 系统文件列表 + 项目类型文档 + + + + + + + 获取用户是否有文档未签署 + + + + + + + 获取确认列表情况 项目文档+系统文档+具体的人 + + + + + + + 已签名的文档 不允许删除 + + + + + + + + 浏览文档说明时调用,记录第一次看的时间 + + + + + + + + 用户 签名某个文档 + + + + + + 用户 废除某个文档 + + + + + + + + 从项目下参与者的维度 先看人员列表(展示统计数字) 点击数字 再看人员具体签署的 系统文档+项目文档(共用上面与人相关的具体文档列表) + + + + + + + 从 文档的维度 先看到文档列表(系统文档+项目文档 以及需要确认的人数 和已经确认人数) 点击数字查看某文档下面人确认情况 + + + + + + + Financial---项目收入价格验证 + + + + 指定资源Id,获取Dicom序列所属的实例信息列表 + Dicom序列的Id + + + 指定资源Id,获取Dicom序列所属的实例Id列表 + Dicom序列的Id + + + + + 指定资源Id,获取Dicom检查所属序列信息列表 + Dicom检查的Id + + + + 指定资源Id,渲染Dicom序列的Jpeg预览图像 + Dicom序列的Id + + + Pannel 进去 SiteTab + + + [new] setting页面Site列表,和getSiteCRCList对比 没有统计数据,增加了一些site信息 + + + 获取某一Site下面的负责的CRC列表 + + + [new] Setting页面 Site勾选列表( + + + Setting页面 Site批量添加 + + + + 编辑项目中Site 基本信息 + + + + + + + + + 删除 项目 下某一site + + + 批量添加Site下 CRC的负责人 + + + 删除CRC人员 + + + + 获取项目下的 site 下拉框数据 CRC只看到他负责的 + + + + + + + 设置受试者访视已执行 也就是将studyUploaded状态置为true 为了那些没有影像 人工设置准备 + + + + + + + 获取访视下的Dicom 检查信息 分所有的, 阅片的 不阅片 isReading : 0 查询所有 1 查询仅仅阅片的 + + + + + + + + 指定资源Id,渲染Dicom检查的Jpeg预览图像 + Dicom检查的Id + + + + 获取某个检查的关联检查列表(该受试者在这个想项目下的所有检查) + 点击检查检查列表中的一个检查获取对应的序列列表(调用之前的接口:/series/list/,根据StudyId,获取访视的序列列表) + + + + + 指定资源Id,获取Dicom检查信息 + Dicom检查的Id + + + + 批量验证 检查是否可以上传 并告知原因 + + + + + SystemAnonymizationService + + + + + 项目外部人员 录入流程相关 + + + + + 验证 在系统中是否存在该类型的账户 返回true 表示 不存在 可以添加和更新|存在但是信息一致,false 需要提示不一致项(前端 可以直接用我返回的错误信息,或者根据返回的用户信息实体,自己设置格式显示) + + + + + + + 添加和更新接口 已验证邮箱和账户类型不允许添加重复项 + + + + + + + 勾选用户 批量发送邮件 + + + + + + 不带Token 访问 用户选择 参与 不参与 Id: TrialExternalUserId + + + + + + + 不带Token 访问 Site调研用户 加入项目 Id: TrialSiteSurveyUserId + + + + + + + 不带Token 访问 页面获取项目基本信息 和参与情况 (已经确认了 就不允许再次确认) Id: TrialExternalUserId/TrialSiteSurveyUserId + + + + + + + 加入项目 + + + + + + + + TrialUserPreparation Service + + + + + 项目下 人员邀请 加入列表 + + + + + + + 不带Token访问 加入项目 记录 同意与否 + + + + + + + 用户加入项目 + + + + + + SystemAnonymizationView 列表视图模型 + + + SystemAnonymizationQuery 列表查询参数模型 + + + SystemAnonymizationAddOrEdit 列表查询参数模型 + + + TrialExternalUserView 列表视图模型 + + + TrialExternalUserQuery 列表查询参数模型 + + + TrialExternalUserAddOrEdit 列表查询参数模型 + + + TrialUserPreparation View 列表视图模型 + + + TrialUserPreparation Query 列表查询参数模型 + + + TrialUserPreparation AddOrEdit 列表查询参数模型 + + + + ISystemAnonymizationService + + + + + ITrialExternalUserService + + + + + ITrialUserPreparation Service + + + + + CRC 访视上传列表 + + + + + + + CRC 质疑列表 + + + + + + + QC 质疑列表 分页 + + + + + + + QC 访视列表 + + + + + + + 获取一致性核查列表 CRC/PM 公用 + + + + + + + 一致性核查 聊天记录列表 + + + + + + + + 转发列表 查询一致性核查通过的,针对不一致性核查的 要维护CV状态 + + + + + + + 获取某次访视 QA界面所有信息 单独每一项都有接口(往下看),这里是一个大接口,方便第一次获取完整的所有的数据 + + + 项目配置的针对访视检查是那种审核,0 不审,1 单审,2双审 + 当前 QC进入的是那种审核 1 单审,2复审 + + + + 获取某次访视 QC 问题核对答案 列表 初始化进去的时候是模板项,QC填写了就是对应的内容 + + + + 项目配置的针对访视检查是那种审核,0 不审,1 单审,2双审 + 当前 QC进入的是那种审核 1 单审,2复审 + + + + 获次QC 历史质疑列表 不分页 + + + 项目配置的针对访视检查是那种审核,0 不审,1 单审,2双审 + 当前 QC进入的是那种审核 1 单审,2复审 + + + + 获取访视下的受试者访视、受试者、site信息 + + + + + + + 访视下的Study 和Series列表 + + + + + + + 访视下的检查列表 + + + + + + + 展开 某一QC质疑 下得 聊天记录 + + + + + + + CRC/PM 看到某次访视下的所有质疑和聊天内容 包括初审和复审的 。 + + + + + + + + 质询发起人 + + + + + + + QC 质疑页面 处理用户 下拉框 + + + + + + + 添加计划外访视 下拉框 选择上一次访视 + + + + + + + 上传界面 受试者 访视、site 基本信息 + + + + + + + 添加和更新质疑 + + + + + + + + + + 关闭质疑,什么情况下允许? + + + + + + + + + + 访视级别统计 质疑最新的状态 + + + + + + 删除QC质疑记录 + + + + + + 针对 某条QC质疑 添加回复 + + + + + + + 一致性核查 质疑的添加/回复 + + + + + + + 关闭 一致性核查质疑 + + + + + + 手动设置一致性核查通过 + + + + + + + + CRC 请求回退 + + + + + + + 一致性核查 回退 对话记录不清除 只允许PM回退 + + + + + + 一致性核查 excel上传 支持三种格式 + + + + + + + + + + 添加或者更新 QC核对问题列表 两个人不能同时操作,就算意外进去了,提交数据,也不会覆盖前一个人数据, 后台已经做好判断 + + + + + 1、设置为不读片,2 设置为读片(取消 先前设置为不读片) 4 设置为删除(数据库记录软删除) 5 恢复为未删除 + + + + + + + + + + type 1 :study 2: series 3:非dicom QC修改检查部位和 拍片类型 + + + + + + + + + + 验证是否质疑都关闭了 可以审核通过和不通过 + + + + + + + 删除检查列表 + + + + + + + + + 手动领取 或者取消 QC任务 + + + + true 获取 false是取消领取 + + + + + CRC RequestToQC 批量提交 + + + + + + 设置QC 通过或者不通过 7:QC failed 8:QC passed + + + + + + + + + + 设置、取消 访视紧急 + + + + + + + + + QA设置 需要重传 + + + + + + + + + CRC 设置已经重传完成 + + + + + + 上传界面 更新受试者首次给药日期 是否入组确认,以及访视 是否PD进展 + + + + + + + 验证QC是否可以操作 数据库查询判断当前QC执行人和登陆的用户是否一致 + + + + + + + 映射配置 + + + + + 获取签名文本 + + + + + + + 签名认证 + + + + + + + 业务接口操作成功后, 让签署数据生效 + + + + + + + 签名确认 包括项目的三组配置 + QC问题确认 后修改状态 (适用于不会回退的,项目废除、状态修改, 存在回退 不在这里弄,提供单独接口修改状态) + + + + + + 更新项目状态 + + + + + + + + + + + 项目状态 变更历史 + + + + + + + 废除项目 + + + + + + + + + 获取 配置的所有信息 没有分多个接口 + + + + + + + 配置 基础逻辑信息 + + + + + + + 配置流程 + + + + + + + 配置加急信息 + + + + + + + 构造函数注入 + + + + + 构造函数注入 + + + + + 构造函数注入 + + + + + 服务动态生成api AOP 此时会失效 + + + + + 统一返回前端数据包装,之前在控制器包装,现在修改为动态Api 在ResultFilter这里包装,减少重复冗余代码 + by zhouhang 2021.09.12 周末 + + + + + 异步版本 + + + + + + + + 数据字典-基础数据维护 + + + + + New 查询条件 IsConfig 代表是字典类型配置项 否就是我们普通的项 和普通项的子项 + + + + + + + New + + + + + + + New + + + + + + + 传递父亲 code 字符串 数组 返回多个下拉框数据 + + + + + + + 获取项目多选字典 + + Title、Department、Rank、Position、ReadingType、Subspeciality Sponsor CROCompany ReadingStandard ReviewMode ReviewType ProjectState + + + + 根据Key,获取单个字典数组 + + + 根据Type、Key 获取字典 树结构 + + + 删除字典数据 + + + 获取所有字典数据 + + + + 打包医生官方简历 + + + + + + + + 打包医生的所有附件 + + + + + + + 医生文档关联关系维护 + + + + + 删除附件 + + + + + + + 根据医生Id 和 附件类型,获取记录 + + 医生Id + 附件类型 + + + + + 获取单个医生的多种证书附件 + + 医生Id + 类型数组 + + + + + 根据医生Id获取医生附件 + + 医生Id + + + + + 保存多个附件 + + + + + + + 将简历设置为官方简历 + + + + + + + + + 设置简历的语言类型 + + + + 0-未设置,1-中文,2-英文 + + + + + Reviewer列表分页查询 + + + + + 获取可筛选筛选及已经筛选的医生列表 + + + + + 获取提交CRO或者CRO审核的Reviewer列表 + + + 根据状态获取医生列表,入组 相关接口 (提交CRO-1) CRO确认-4 + + + + + 获取项目下医生入组状态列表[Confirmation] + + + + + 添加/更新 医生基本信息 BasicInfo + + + + + 详情、编辑-获取 医生基本信息 BasicInfo + + ReviewerID + + + + + 详情、编辑-获取医生工作信息 Employment + + + + + 获取医生入组信息 正在提交的数量 已同意入组项目个数 正在读的 + + + + + Get Statement of Work list.[New] + + + + + Get Ack Statement of Work[New] + + + + + 根据医生Id获取医生教育经历和继续学习经历列表 + + + + + 新增医生教育经历 + + + + + + 添加/更新医生继续学习经历 + + + + 删除医生继续学习经历 + + 医生Id + + + + + 查询-医生科学研究信息 + + 医生Id + + + + + 根据医生Id,获取临床试验经历 界面所有数据 + + + + 添加或更新医生临床经验列表项 + + + + 删除临床经验 + + + + + 更新-GCP和其他临床经验 + + + + + + + 更新其他技能经验 + + + + + 添加休假时间段 + + Status不传 + + + + + 删除休假时间段 + + 记录Id + + + + + 获取休假时间段列表 + + + + + + 获取某个月下的某些医生最终确认的工作量,用于计算月度费用 + + + + + 计算月度费用,并调用AddOrUpdateMonthlyPayment和AddOrUpdateMonthlyPaymentDetail方法, + 将费用计算的月度数据及详情保存 + + + + + 保存费用计算的月度数据 + + + + + 保存费用计算的月度详情 + + + + + 获取待计算费用的Reviewer对应的月份列表 + + + + + 查询Reviewer某个月的费用是否被锁定 + + + + + 根据记录Id,删除汇率记录 + + 汇率记录Id + + + + Financials /MonthlyPayment 列表查询接口 + + + + + Financials /MonthlyPaymentDetail 详情查询接口 + + + + + NEW 导出Excel压缩包 数据获取 + + + + + Financials / Payment History 列表查询接口(已经支付锁定的数据,包含调整的)[New] + + + + + Financials / Payment History 详情接口[New] + + + + + Revenues列表接口,收入统计[New] 0是Detail 1是按照项目 2是按照人 3按照月份 + + + + + 收入支出分析接口,按照项目维度分析统计 + + + + + 收入支出分析接口,按照医生维度分析统计 + + + + + 获取劳务费用列表 + + + + + 锁定医生费用,锁定后,无法变更该医生对应月份的费用和工作量[New] + + + + + 添加或更新费用调整[AUTH] + + + + + 删除费用调整记录 + + + + + 获取费用调整列表 + + + + + 获取职称单价列表 + + + + + 获取医生支付信息列表 + + + + + 根据医生Id获取支付信息 + + 医生Id + + + + + 根据rankId 获取ReviewerId,用于当Rank的单价信息改变时,触发费用计算 + + + + + + + 添加或更新项目支付价格信息 + + + + + 获取项目支付价格信息列表 + + + + + 获取项目收入费用信息列表[New] + + + + + 批量添加或更新奖励费用单价 + + + + + 获取所有奖励单价列表-用于计算时,一次性获取所有 + + + + + 分页获取奖励单价列表 + + + + + 获取保存到Dicom文件中的信息 + + + + + + 分页获取CRO列表 + + + 根据CRO 名称查询所有CRO 列表 + + + 添加CRO信息 + + + 删除CRO信息 + + + 获取所有医院列表 + + + 添加医院 + + + 删除医院信息 + + + 分页获取医院列表 + + + 分页获取研究中心列表 + + + 添加研究中心 + + + 删除研究中心 + + + 分页获取申办方列表 + + + 分页获取申办方列表 + + + 添加申办方 + + + 删除申办方 + + + + 添加菜单 New + + + + + + + 删除菜单 已验证 父节点不允许删除 New + + + + + + + 所有的菜单树 包括禁用的 New + + + + + + 菜单列表 参数都是可传的 根据需要传递 New + + + + + + + 获取某用户类型(角色) 菜单树 勾选情况 New + + + + + + 发送验证码 邮箱或者手机号 New + + + + 发送验证码 邮箱或者手机号 + + + + + + + + + 验证设置新密码 + + + + + + + 获取用户列表 + + + + + + + 根据用户Id获取用户详细信息[New] + + + + + + + 添加用户 + + + + + + + 更新用户 + + + + + + + 删除用户 + + + + + + + 禁用或者启用账户 + + + + + + + + 重置密码为 默认密码 + + + + + + + 修改密码,当前支持旧密码修改密码,手机及邮箱验证码后续支持[New] + + + + + + + 用户登陆 + + + + + + + + Dashboard统计、全局工作量统计、入组两个维度统计(按照项目、按照人) + + + + 根据项目和医生,分页获取工作量统计[New] + + + 项目入组 医生维度统计[New] + + + 用户参与项目 统计[New] + + + 用户参与项目 列表[New] + + + 读片数分类统计[New] + + + 获取最近几个月份的数据[New] + + + 读片数量排行前几的数据[New] + + + 按Rank统计Reviewer 数量[New] + + + 最近几个季度入组人次[New] type==0 按照月份 + + + 参与项目数排行 [New] + + + 最新工作量 (已确定的)[New] + + + + Setting页面 获取项目参与人员列表 + + + + + + Setting页面 为 site 勾选CRC用户列表 + + + Setting页面 项目参与人员勾选列表 + + + + Setting页面 批量添加项目参与人员 + + + + + + 项目参与人员退出 其中IQC退出 回去释放工作量 + + + + 分页获取临床项目列表 默认后台加急状态为3 查所有的 + + + + + + + 获取项目基本信息 + + + + + + + 添加项目 + + + + + + + 手动更新项目状态 + + 项目Id + 状态值 + + + + 删除临床项目 + 临床试验项目Id + + + + 根据项目Id 获取医生Id,用于出发计算费用 + + + + 分页获取医生参与的临床实验项目列表(查询条件) + + + + + 医生确认入组或拒绝入组 + + 项目Id + 9-拒绝入组,10-确认入组 + + + + + 添加或更新受试者信息[New] + + state:1-访视中,2-出组。0-全部 + + + + 分页获取受试者列表[New] + /// state:1-访视中,2-出组。0-全部 + + + + 计划外访视 获取受试者选择下拉框列表 + + + + 暂时不用 + 获取项目访视计划 + + + 根据项目Id,获取项目访视计划(不分页)[New] + + + + 获取访视计划下拉框列表 + + + + + + 添加或更新访视计划某项[New] + + + 删除项目计划某一项[New] + + + + 保存协议- ack Sow [AUTH] + + + + + 删除协议 + + + + + 0代表裁判和Tp 都可以 1、代表Tp 2 代表裁判 + + + + + + 获取某个项目入组的医生工作量统计列表 + + + + + 获取入组某个项目的医生的最近几个月的工作量详情(带有填充数据) + + + + + 获取来自Reviewer自己的数据,某个月添加的多条 + + + + + 工作量是否存在,用于判断只能添加一条的工作量记录 + + + 查询某个医生是否在某天有某个项目的工作量数据,处理添加来自医生自己的工作量数据 + + + + + 添加或更新工作量 + + + + + 删除工作量 + + + + + 获取工作量详情(用于判断工作量锁定时,调用) + + + + + 添加或更新项目医生项目价格 + + + + + 获取医生项目列表 + + + + + + 为项目筛选医生 提交 【select】 + 项目Id + 医生Id数组 + + + + + 入组流程-向CRO提交医生[Submit] + + + + + 入组流程-CRO确定医生名单 [ Approve] + + + + + 入组流程-后台确认医生入组[Confirm] + + + + + optType 0表示回退,回退之后,列表没这条数据了, 1表示出组,需要填写出组时间 + + + + + + + + + + Reviewer 列表查询参数 + + + + + 入组 Selection 列表查询参数 + + + + + 添加用户是的返回模型 + + + + + 读片数量分类统计 + + + + + 后台 工作量审核视图模型 + + + + + 工作量审核数据库查询模型 + + + + + 工作量分页列表模型 医生端查询模型 + + + + + 后台查询模型 + + + + + 医生多条件查询 + + + + + 筛选医生列表 + + + + + + + //入组 相关接口 (提交CRO-1) CRO确认-4 + + + + + 基本信息详情展示、编辑使用 + + + + + + + 添加医生基本信息 + + + + + + + 获取医生 工作信息 + + + + + + + 更新医生 工作信息 + + + + + + + 获取医生技能信息 + + + + + 更新医生技能信息 + + + + + 获取医生 审核状态 + + + + + 审核简历 和合作关系 + + + + 医生详情 入组信息 + + + 获取医生参与项目的Sow协议 + + + 获取医生入组的 ack Sow + + + 判断当前时间是否在休假 + + + + 上传入组后的Ack-SOW + + + + 获取医院列表 + + + 按类型统计读片数量 + + + 按月份统计读片数量 + + + 读片数量排行 + + + 按Position统计 Reviewers 数量 + + + 每月入组人次 + + + 参与项目数排行 + + + 最新工作量 (已确定的) + + + 入组流程-筛选医生 [select] + + + 入组流程-向CRO提交医生[Submit] + + + 入组流程-CRO确定医生名单 [ Approve] + + + 入组流程-向CRO提交医生[Submit] + + + diff --git a/IRaCIS.Core.Application/Resources/en-US.json b/IRaCIS.Core.Application/Resources/en-US.json new file mode 100644 index 00000000..6ce94d0b --- /dev/null +++ b/IRaCIS.Core.Application/Resources/en-US.json @@ -0,0 +1,42 @@ +{ + "test{0}": "英文本地化{0}", + "RequiredAttribute": "{0} is required", + + // SiteSurvey 服务-------------------------------------------------------------------------------------------------------------------------- + + // TrialSiteEquipmentSurveyService + "CPMNotOperation": "CPM/APM Disallow operation", //CPM/APM 不允许操作 + "IsLockNotOperation": "The operation cannot be performed if it is locked", //已锁定 不允许操作 + + // TrialSiteSurveyService + "ValidationEmail": "Please input a legal email", // 验证邮箱 + "ValidationPhone": "Please input a legal phone", // 验证手机号 + "SiteNotExistUpdateDisable": "The project Site does not have the survey record of the handover person, so it is not allowed to choose to update", // site不存在禁止更新 + "RecordLockUpdateDisable": "Your record is not locked, you are not allowed to choose to update, if submitted, can be rejected after operation", //记录锁定禁止更新 + "SiteLockUpdateDisableOther": "At the current Site, your survey record has been locked, and it is not allowed to update other people's email survey record", //Site锁定禁止更新其他人 + "SiteLockUpdateDisableSelf": "At the current Site, your survey record has been locked, and there are other unlocked records, so you are not allowed to update your own survey record", //Site锁定禁止更新自己 + "SiteLockUpdateDisableEmail": "当前Site 存在未锁定的调研记录,不允许更新已锁定邮箱的调研记录", //Site锁定禁止更新邮箱调研记录 + "EmailNotLatestDisableEmail{0}": "该邮箱{0}对应的调查表不是最新锁定的记录,不允许更新!", //邮箱调研表不是最新 不允许更新 + "SiteExistOtherUpdateDisable": "该Site下已经有其他用户已填写的调研表,您不被允许继续填写", //存在其他用户调研表不允许填写 + "IsLockUpdateDisable": "已锁定,不允许操作", //已锁定,不允许操作 + "OnlyAbolishNotFiled": "只允许废除未提交的记录", //只允许废除未提交的记录 + "AdminOperateDisable": "不允许Admin操作", // 不允许Admin操作 + "UserWrongNotSubmit": "人员信息有不正确项,不允许提交", // 人员信息有不正确项,不允许提交 + "FillInTheType": "请填写生成账号的类型。人员姓名{0}", // 请填写生成账号的类型。人员姓名 + + // TrialSiteUserSurveyService + "InfoInconformity": "该用户在系统中账户名为:{0} ,与填写信息存在不一致项, 现将界面信息修改为与系统一致,可进行保存", // 信息不一致 修改一致进行保存 + + + // TrialSiteUser 服务------------------------------------------------------------------------------------------------------------ + + // TrialConfigService + "NoDataDound": "未在系统中找到该签名场景的数据", //未在系统中找到该签名场景的数据 + "PasswordError": "password error", //密码错误 + "UserBeDisabled": "The user has been disabled!", + "ProjSetDisable": "项目不在Initializing/Ongoing,不允许确认配置", //不允许确认配置 + "QCRepate": "QC问题显示序号不允许重复", // QC问题显示序号不允许重复 + "ParentNumToLow": "父问题的序号要比子问题序号小,请确认", // 父问题的序号要比子问题序号小,请确认 + "ExistUnconfirmedItems": "项目、基础配置、流程配置、加急配置、访视计划,有未确认项", // 存在未确认项 + "NotAllCanOperate": "only in Initializing or Ongoing State can operate" //只有“初始化中”和“正在进行中”才能进行操作 +} diff --git a/IRaCIS.Core.Application/Resources/zh-CN.json b/IRaCIS.Core.Application/Resources/zh-CN.json new file mode 100644 index 00000000..2dc09e95 --- /dev/null +++ b/IRaCIS.Core.Application/Resources/zh-CN.json @@ -0,0 +1,45 @@ +{ + "test{0}": "中文本地化{0}", + "RequiredAttribute": "{0} 字段是必须的", + // SiteSurvey 服务-------------------------------------------------------------------------------------------------------------------------- + + // TrialSiteEquipmentSurveyService + "CPMNotOperation": "CPM/APM 不允许操作", //CPM/APM 不允许操作 + "IsLockNotOperation": "已锁定 不允许操作", //已锁定 不允许操作 + + // TrialSiteSurveyService + "ValidationEmail": "请输入正确的邮箱", // 验证邮箱 + "ValidationPhone": "请输入正确的手机号", // 验证手机号 + "SiteNotExistUpdateDisable": "该项目Site不存在该交接人的调研记录,不允许选择更新", // site不存在禁止更新 + "RecordLockUpdateDisable": "您的记录未锁定,不允许选择更新,若已经提交,可被驳回后进行操作", //记录锁定禁止更新 + "SiteLockUpdateDisableOther": "当前Site 您的调研记录已锁定,不允许更新其他人邮箱调研记录", //Site锁定禁止更新其他人 + "SiteLockUpdateDisableSelf": "当前Site 您的调研记录已锁定,也存在其他未锁定的记录,不允许更新自己的调研记录", //Site锁定禁止更新自己 + "SiteLockUpdateDisableEmail": "当前Site 存在未锁定的调研记录,不允许更新已锁定邮箱的调研记录", //Site锁定禁止更新邮箱调研记录 + "EmailNotLatestDisableEmail{0}": "该邮箱{0}对应的调查表不是最新锁定的记录,不允许更新!", //邮箱调研表不是最新 不允许更新 + "SiteExistOtherUpdateDisable": "该Site下已经有其他用户已填写的调研表,您不被允许继续填写", //存在其他用户调研表不允许填写 + "IsLockUpdateDisable": "已锁定,不允许操作", //已锁定,不允许操作 + "OnlyAbolishNotFiled": "只允许废除未提交的记录", //只允许废除未提交的记录 + "AdminOperateDisable": "不允许Admin操作", // 不允许Admin操作 + "UserWrongNotSubmit": "人员信息有不正确项,不允许提交", // 人员信息有不正确项,不允许提交 + "FillInTheType{0}": "请填写生成账号的类型。人员姓名{0}", // 请填写生成账号的类型。人员姓名 + + // TrialSiteUserSurveyService + "InfoInconformity{0}": "该用户在系统中账户名为:{0} ,与填写信息存在不一致项, 现将界面信息修改为与系统一致,可进行保存", // 信息不一致 修改一致进行保存 + + + // TrialSiteUser 服务------------------------------------------------------------------------------------------------------------ + + // TrialConfigService + "NoDataDound": "未在系统中找到该签名场景的数据", //未在系统中找到该签名场景的数据 + "PasswordError": "密码错误", //密码错误 + "UserBeDisabled": "用户被禁用", // 用户被禁用 + "ProjSetDisable": "项目不在Initializing/Ongoing,不允许确认配置", //不允许确认配置 + "QCRepate": "QC问题显示序号不允许重复", // QC问题显示序号不允许重复 + "ParentNumToLow": "父问题的序号要比子问题序号小,请确认", // 父问题的序号要比子问题序号小,请确认 + "ExistUnconfirmedItems": "项目、基础配置、流程配置、加急配置、访视计划,有未确认项", // 存在未确认项 + "NotAllCanOperate": "只有“初始化中”和“正在进行中”才能进行操作", //只有“初始化中”和“正在进行中”才能进行操作 + + // TrialExternalUserService + "InfoDoNotAgree{0}{1}": "该用户在系统中账户名为:{0} 电话:{1},与填写信息存在不一致项, 现将界面信息修改为与系统一致,可进行保存" // 用户信息不一致 + "UserExist" +} diff --git a/IRaCIS.Core.Application/Service/Common/DTO/DictionaryModel.cs b/IRaCIS.Core.Application/Service/Common/DTO/DictionaryModel.cs new file mode 100644 index 00000000..ef0d168d --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/DTO/DictionaryModel.cs @@ -0,0 +1,155 @@ +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Contracts +{ + + + public class BasicDicView: AddOrEditBasicDic + { + + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid UpdateUserId { get; set; } + + public string ConfigType { get; set; } + + public string ConfigTypeDes { get; set; } + } + + + public class AddOrEditBasicDic + { + public Guid? Id { get; set; } + + public string Code { get; set; } = String.Empty; + + public string KeyName { get; set; } = String.Empty; + + public string Description { get; set; } = String.Empty; + + + + public string Value { get; set; } = String.Empty; + + public string ValueCN { get; set; } = String.Empty; + + + + public int ShowOrder { get; set; } + + + //有父亲 就有值 + public Guid? ParentId { get; set; } + + public bool IsEnable { get; set; } + + + //默认不是字典项 类型配置 + public bool IsConfig { get; set; } + + //是配置的话,就有值 + public Guid? ConfigTypeId { get; set; } + + } + + public class BasicDicSelect + { + public Guid Id { get; set; } + public string KeyName { get; set; } = string.Empty; + public string ValueCN { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public string Code { get; set; } = string.Empty; + + public int ShowOrder { get; set; } + + public Guid? ParentId { get; set; } + public string ParentCode { get; set; } = string.Empty; + } + + public class BasicDicQuery:PageInput + { + public string? Code { get; set; } + + public string? KeyName { get; set; } + + public bool? IsConfig { get; set; } + + public Guid? ConfigTypeId { get; set; } + + } + + + + + + + + + + + + + + + + + + + + + + + + + public class DicViewModelDTO : AddOrUpdateDicDTO + { + + } + + + public class AddOrUpdateDicDTO + { + public Guid? Id { get; set; } + public string KeyName { get; set; } = String.Empty; + public string Value { get; set; } = String.Empty; + public string Description { get; set; } = String.Empty; + public string ValueCN { get; set; } = String.Empty; + public int ShowOrder { get; set; } + public string Type { get; set; } = String.Empty; + } + + public class DicQueryDTO : PageInput + { + public string KeyName { get; set; } = String.Empty; + } + + public class KeyNameType + { + public Guid KeyId { get; set; } + public string KeyName { get; set; } = String.Empty; + public string Type { get; set; } = String.Empty; + } + + public class DicResultDTO + { + public Dictionary> DicList = new Dictionary>(); + } + + + public class TrialDictionaryView + { + public Guid? Id { get; set; } + public string KeyName { get; set; } = String.Empty; + public string Value { get; set; } = String.Empty; + public int ShowOrder { get; set; } + } + + public class TrialDicSelect + { + public TrialDictionaryView[] Phase { get; set; } = new TrialDictionaryView[0]; + public TrialDictionaryView[] IndicationType { get; set; } = new TrialDictionaryView[0]; + public TrialDictionaryView[] DeclarationType { get; set; } = new TrialDictionaryView[0]; + } +} diff --git a/IRaCIS.Core.Application/Service/Common/DTO/EmailNoticeConfigViewModel.cs b/IRaCIS.Core.Application/Service/Common/DTO/EmailNoticeConfigViewModel.cs new file mode 100644 index 00000000..8d27ef23 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/DTO/EmailNoticeConfigViewModel.cs @@ -0,0 +1,64 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-02-15 11:55:57 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using Newtonsoft.Json; +namespace IRaCIS.Core.Application.Contracts +{ + /// EmailNoticeConfigView 列表视图模型 + public class EmailNoticeConfigView : EmailNoticeConfigAddOrEdit + { + + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public Guid UpdateUserId { get; set; } + + public DateTime UpdateTime { get; set; } + + [JsonIgnore] + public SystemBasicDataSelect Scenario { get; set; } + + + //public Guid? ScenarioParentId => Scenario.ParentId; + + public string ScenarioName => Scenario.Value; + public string ScenarioNameCN => Scenario.ValueCN; + + } + + ///EmailNoticeConfigQuery 列表查询参数模型 + public class EmailNoticeConfigQuery:PageInput + { + public Guid? ScenarioId { get; set; } + + public bool? IsReturnRequired { get; set; } + public bool? IsUrgent { get; set; } + public bool? IsEnable { get; set; } + + } + + /// EmailNoticeConfigAddOrEdit 列表查询参数模型 + public class EmailNoticeConfigAddOrEdit + { + public Guid Id { get; set; } + public string Code { get; set; } = String.Empty; + + public string AuthorizationCode { get; set; } = String.Empty; + + public Guid ScenarioId { get; set; } + public string Title { get; set; } = String.Empty; + public string Body { get; set; } = String.Empty; + public string FromEmail { get; set; } = String.Empty; + public string ReceiveEmail { get; set; } = String.Empty; + public string CopyEmail { get; set; } = String.Empty; + public bool IsReturnRequired { get; set; } + public bool IsUrgent { get; set; } + public bool IsEnable { get; set; } + public bool IsAutoSend { get; set; } + } + + +} + + diff --git a/IRaCIS.Core.Application/Service/Common/DTO/FileModel.cs b/IRaCIS.Core.Application/Service/Common/DTO/FileModel.cs new file mode 100644 index 00000000..e23569a2 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/DTO/FileModel.cs @@ -0,0 +1,18 @@ + +namespace IRaCIS.Application.Contracts +{ + + + public class UploadFileInfoDTO + { + public Guid Id { get; set; } + public string FilePath { get; set; } = string.Empty; + + //[JsonIgnore] + //public string FullFilePathNoToken => FilePath; + public string FullFilePath { get; set; } = string.Empty; + + } + + +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Common/DTO/SysMessageModel.cs b/IRaCIS.Core.Application/Service/Common/DTO/SysMessageModel.cs new file mode 100644 index 00000000..24f7f560 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/DTO/SysMessageModel.cs @@ -0,0 +1,16 @@ +using System; + +namespace IRaCIS.Application.Contracts +{ + public class SysMessageDTO + { + public int Id { get; set; } + public int ToDoctorId { get; set; } + public int FromUserId { get; set; } + public string Title { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + public string MessageTime { get; set; } = string.Empty; + public bool HasRead { get; set; } + public string Memo { get; set; } = string.Empty; + } +} diff --git a/IRaCIS.Core.Application/Service/Common/DTO/SystemBasicDataViewModel.cs b/IRaCIS.Core.Application/Service/Common/DTO/SystemBasicDataViewModel.cs new file mode 100644 index 00000000..76faf650 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/DTO/SystemBasicDataViewModel.cs @@ -0,0 +1,67 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-02-15 15:46:00 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using IRaCIS.Core.Domain.Share; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +namespace IRaCIS.Core.Application.Contracts +{ + /// SystemBasicDataView 列表视图模型 + public class SystemBasicDataView: SystemBasicDataAddOrEdit + { + + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid UpdateUserId { get; set; } + + } + + + public class SystemBasicDataSelect + { + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; + public string ValueCN { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public string Code { get; set; } = string.Empty; + + public Guid? ParentId { get; set; } + public string ParentCode { get; set; } = string.Empty; + } + + + ///SystemBasicDataQuery 列表查询参数模型 + public class SystemBasicDataQuery:PageInput + { + /// Name + public string? Name { get; set; } + + /// Code + public string? Code { get; set; } + + } + + /// SystemBasicDataAddOrEdit 列表查询参数模型 + public class SystemBasicDataAddOrEdit + { + public Guid? Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public int ShowOrder { get; set; } + public string Code { get; set; } = string.Empty; + public Guid? ParentId { get; set; } + + public string ValueCN { get; set; } = string.Empty; + + public bool IsEnable { get; set; }=true; + } + + +} + + diff --git a/IRaCIS.Core.Application/Service/Common/DTO/SystemLogModel.cs b/IRaCIS.Core.Application/Service/Common/DTO/SystemLogModel.cs new file mode 100644 index 00000000..32819a63 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/DTO/SystemLogModel.cs @@ -0,0 +1,99 @@ +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Contracts +{ + public class SystemLogDTO + { + public Guid Id { get; set; } + public string ApiPath { get; set; } = string.Empty; + public string Params { get; set; } = string.Empty; + public string Result { get; set; } = string.Empty; + public DateTime RequestTime { get; set; } = DateTime.Now; + public long ElapsedMilliseconds { get; set; } = 0; + public Guid OptUserId { get; set; } = Guid.Empty; + public string OptUserName { get; set; } = string.Empty; + public string ClientIP { get; set; } = string.Empty; + public bool Status { get; set; } = true; + public string Message { get; set; } = string.Empty; + public string LogCategory { get; set; } = string.Empty; + } + + public class QueryLogQueryDTO : PageInput + { + public string Keyword { get; set; } = string.Empty; + public string LogCategory { get; set; } = string.Empty; + public DateTime? BeginTime { get; set; } + public DateTime? EndTime { get; set; } + } + + + public class AuditQueryDTO : PageInput + { + public Guid TrialId { get; set; } + + public Guid? StudyId { get; set; } + + public Guid? SubjectId { get; set; } + + public int? AuditType { get; set; } + + public string SubjectInfo { get; set; } = string.Empty; + + public Guid? OptUserId { get; set; } + + + public DateTime? StartDate { get; set; } + + public DateTime? EndDate { get; set; } + } + + + public class AuditDTO + { + public Guid Id { get; set; } + + public int AuditType { get; set; } + + public Guid TrialId { get; set; } + + public Guid StudyId { get; set; } + + public Guid? SubjectId { get; set; } + + public string SubjectName { get; set; } = string.Empty; + + public string SubjectCode { get; set; } = string.Empty; + + public Guid OptUserId { get; set; } + + public string OptUser { get; set; } = string.Empty; + + public DateTime OptTime { get; set; } = DateTime.Now; + + public string Note { get; set; } = string.Empty; + + public string Detail { get; set; } = string.Empty; + + public string TrialCode { get; set; } = string.Empty; + + public string TrialIndication { get; set; } = string.Empty; + } + + + public class OptUserDto + { + public Guid OptUserId { get; set; } + public string OptUser { get; set; } = string.Empty; + } + + public class AuditSubjectSelectDto + { + public Guid? SubjectId { get; set; } + + public string SubjectCode { get; set; } = string.Empty; + + public string SubjectName { get; set; } = string.Empty; + + } + +} diff --git a/IRaCIS.Core.Application/Service/Common/DictionaryService.cs b/IRaCIS.Core.Application/Service/Common/DictionaryService.cs new file mode 100644 index 00000000..26f1e9ed --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/DictionaryService.cs @@ -0,0 +1,311 @@ +using IRaCIS.Core.Infrastructure.ExpressionExtend; +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Mvc; +using Panda.DynamicWebApi.Attributes; + +namespace IRaCIS.Application.Services +{ + /// + /// 数据字典-基础数据维护 + /// + + [ApiExplorerSettings(GroupName = "Common")] + public class DictionaryService : BaseService, IDictionaryService + { + private readonly IRepository _dicRepository; + private readonly IRepository _doctorDictionaryRepository; + private readonly IRepository _trialDictionaryRepository; + private readonly IRepository _doctorRepository; + private readonly IRepository _trialRepository; + + public DictionaryService(IRepository sysDicRepository, IRepository doctorDictionaryRepository, IRepository trialDictionaryRepository, + IRepository doctorRepository, IRepository trialRepository) + { + _dicRepository = sysDicRepository; + _doctorDictionaryRepository = doctorDictionaryRepository; + _trialDictionaryRepository = trialDictionaryRepository; + _doctorRepository = doctorRepository; + _trialRepository = trialRepository; + } + + + /// + /// New 查询条件 IsConfig 代表是字典类型配置项 否就是我们普通的项 和普通项的子项 + /// + /// + /// + [HttpPost] + public async Task> GetBasicDicList(BasicDicQuery basicDicQuery) + { + + var systemBasicDataQueryable = _repository.GetQueryable().Where(t => t.ParentId == null) + .WhereIf(!string.IsNullOrEmpty(basicDicQuery.Code), t => t.Code.Contains(basicDicQuery.Code!)) + .WhereIf(!string.IsNullOrEmpty(basicDicQuery.KeyName), t => t.KeyName.Contains(basicDicQuery.KeyName!)) + .WhereIf(basicDicQuery.ConfigTypeId != null, t => t.ConfigTypeId == basicDicQuery.ConfigTypeId!) + .WhereIf(basicDicQuery.IsConfig != null, t => t.IsConfig == basicDicQuery.IsConfig) + + .ProjectTo(_mapper.ConfigurationProvider); + + return await systemBasicDataQueryable.ToPagedListAsync(basicDicQuery.PageIndex, basicDicQuery.PageSize, String.IsNullOrEmpty(basicDicQuery.SortField) ? "Code" : basicDicQuery.SortField, basicDicQuery.Asc); + } + + + /// + /// New + /// + /// + /// + [HttpPost] + public async Task AddOrUpdateBasicDic(AddOrEditBasicDic addOrEditBasic) + { + + var entity = await _repository.InsertOrUpdateAsync(addOrEditBasic, true); + + return ResponseOutput.Ok(entity.Id.ToString()); + } + + + /// + /// New + /// + /// + /// + [HttpGet("{parentId:guid}")] + public async Task> GetChildList(Guid parentId) + { + return await _repository.GetQueryable().Where(t => t.ParentId == parentId) + .OrderBy(t => t.ShowOrder).ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + } + + + /// + /// 传递父亲 code 字符串 数组 返回多个下拉框数据 + /// + /// + /// + [HttpPost] + public async Task>> GetBasicDataSelect(string[] searchArray) + { + + var searchList = await _repository.GetQueryable().Where(t => searchArray.Contains(t.Parent.Code) && t.ParentId != null && t.IsEnable).ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + + return searchList.GroupBy(t => t.ParentCode).ToDictionary(g => g.Key, g => g.ToList()); + + } + + public async Task> GetBasicDataSelect(string searchKey) + { + var searchList = await _repository.GetQueryable().Where(t => t.Parent.Code== searchKey && t.ParentId != null && t.IsEnable).ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + + return searchList; + } + + + + #region old 待废弃 + + /// + /// 获取项目多选字典 + /// + /// Title、Department、Rank、Position、ReadingType、Subspeciality Sponsor CROCompany ReadingStandard ReviewMode ReviewType ProjectState + /// + [HttpPost] + public DicResultDTO GetDictionary(string[] searchArray) + { + var doctorViewList = _dicRepository.ProjectTo(_mapper.ConfigurationProvider).OrderBy(t => t.KeyName) + .ThenBy(t => t.ShowOrder).ToList(); + + var projectDicResult = new DicResultDTO(); + foreach (var searchItem in searchArray) + { + var item = searchItem.Trim(); + var tempDic = new Dictionary(); + doctorViewList.Where(o => o.KeyName == item).ToList().ForEach(o => tempDic.Add(o.Id!.Value, o.Value)); + projectDicResult.DicList.Add(item, tempDic); + } + return projectDicResult; + } + + + public DicResultDTO GetAllDictionary() + { + var list = _dicRepository.ProjectTo(_mapper.ConfigurationProvider).OrderBy(t => t.KeyName) + .ThenBy(t => t.ShowOrder).ToList(); + + var types = list.Select(u => u.KeyName).Distinct(); + + var projectDicResult = new DicResultDTO(); + foreach (var type in types) + { + var tempDic = new Dictionary(); + + //list.Where(o => o.KeyName == type).ToList().ForEach(o => tempDic.Add(o.Id, string.IsNullOrEmpty(o.ValueCN)?o.Value: o.Value + " / " + o.ValueCN)); + list.Where(o => o.KeyName == type).ToList().ForEach(o => tempDic.Add(o.Id!.Value, o.Value)); + + projectDicResult.DicList.Add(type, tempDic); + } + + // //用户类型从字典表 移到另外的表了,现在为了前端不变,在这里获取,给出数据 + + //var userTypes= _userTypeRoleRepository.GetAll().OrderBy(t => t.Order).Select(t => new {t.Id, t.UserType}).ToList(); + //var userTypeDic = new Dictionary(); + //userTypes.ForEach(o => userTypeDic.Add(o.Id, o.UserType)); + + // projectDicResult.DicList.Add("UserType", userTypeDic); + + + return projectDicResult; + } + + /// 根据Key,获取单个字典数组 + [HttpPost] + public PageOutput getDictionarySelectList(DicQueryDTO dicSearchModel) + { + + + var dicQueryable = _dicRepository + .WhereIf(!string.IsNullOrEmpty(dicSearchModel.KeyName), t => t.KeyName == dicSearchModel.KeyName && t.Value != "") + .ProjectTo(_mapper.ConfigurationProvider); + + var pageList = dicQueryable.ToPagedList(dicSearchModel.PageIndex, dicSearchModel.PageSize, dicSearchModel.SortField, dicSearchModel.Asc); + + return pageList; + + } + + /// 根据Type、Key 获取字典 树结构 + public List GetDicTree() + { + var keyNameTypeDistinctList = _dicRepository.Where(t => t.ParentId != null) + .ProjectTo(_mapper.ConfigurationProvider).Distinct().ToList(); + + var treeNodeList = new List(); + var group = keyNameTypeDistinctList.GroupBy(t => t.Type); + foreach (var groupItem in group) + { + var node = new DictionaryTreeNode() + { + Id = Guid.NewGuid(), + KeyName = groupItem.Key, + Type = groupItem.Key, + Children = keyNameTypeDistinctList.Where(t => t.Type == groupItem.Key).Select(t => + new DictionaryTreeNode() + { + Id = Guid.NewGuid(), + KeyName = t.KeyName, + Type = t.Type, + Children = new List() + }).ToList() + }; + treeNodeList.Add(node); + } + return treeNodeList; + } + + + /// 添加或更新字典数据 + //[HttpPost] + //public IResponseOutput AddOrUpdateDictionary(AddOrUpdateDicDTO viewModel) + //{ + // #region 封装前 + // //if (viewModel.Id == null) + // //{ + // // var existItem = _dicRepository.FirstOrDefault(dic => dic.KeyName.Equals(viewModel.KeyName) && dic.Value.Equals(viewModel.Value)); + // // if (existItem != null) + // // { + // // return ResponseOutput.NotOk("The added item has the same name as a sub-item of current categpry. Please modify the name."); + // // } + // // var result = _dicRepository.Add(_mapper.Map(viewModel)); + // // var success = _dicRepository.SaveChanges(); + // // return ResponseOutput.Result(success); + // //} + // //else + // //{ + // // var existItem = _dicRepository.FirstOrDefault(dic => dic.KeyName.Equals(viewModel.KeyName) && dic.Value.Equals(viewModel.Value)); + // // if (existItem != null && existItem.Id != viewModel.Id) + // // { + // // return ResponseOutput.NotOk("The updated item has the same name as a sub-item of current categpry. Please modify the name."); + // // } + // // var updateItem = _dicRepository.FirstOrDefault(t => t.Id == viewModel.Id); + // // _mapper.Map(viewModel, updateItem); + // // var success = _dicRepository.SaveChanges(); + // // return ResponseOutput.Result(success); + // //} + // #endregion + + // var exp = new EntityVerifyExp() + // { + // VerifyExp = dic => dic.KeyName.Equals(viewModel.KeyName) && dic.Value.Equals(viewModel.Value), + // VerifyMsg = "The item has the same name as a sub-item of current categpry" + // }; + + // //var entity = _dicRepository.UseMapper(_mapper).InsertOrUpdate(viewModel, true, exp); + + // return ResponseOutput.Ok(entity.Id); + + //} + + + /// 删除字典数据 + [HttpDelete("{id:guid}")] + public async Task DeleteDictionary(Guid id) + { + if ((await _doctorDictionaryRepository.AnyAsync(t => t.DictionaryId == id)) || + (await _doctorRepository.AnyAsync(t => t.SpecialityId == id|| t.PositionId == id|| t.DepartmentId == id|| t.RankId == id)) + + ) + { + return ResponseOutput.NotOk("This item is referenced by content of the reviewer's resume."); + } + + if (await _trialDictionaryRepository.AnyAsync(t => t.DictionaryId == id) || + await _trialRepository.AnyAsync(t => t.ReviewModeId == id)) + { + return ResponseOutput.NotOk("This item is referenced by content of the trial infomation."); + } + + var success = await _dicRepository.DeleteFromQueryAsync(t => t.Id == id); + return ResponseOutput.Result(success); + } + + /// 获取所有字典数据 + public async Task> getDictionarySelect() + { + return await _dicRepository.Select(t => t.KeyName).Distinct().ToListAsync(); + } + + //[Obsolete] + [NonDynamicMethod] + public DicViewModelDTO GetDetailById(Guid id) + { + var result = _dicRepository.ProjectTo(_mapper.ConfigurationProvider).FirstOrDefault(u => u.Id == id).IfNullThrowException(); + return result; + } + + + + public TrialDicSelect GetGenerateTrialCodeDic() + { + var list = _dicRepository.Where(t => t.KeyName == "Phase" || t.KeyName == "IndicationType" || t.KeyName == "DeclarationType").ProjectTo(_mapper.ConfigurationProvider).ToList(); + + return new TrialDicSelect() + { + Phase = list.Where(t => t.KeyName == "Phase").OrderBy(t => t.ShowOrder).ToArray(), + IndicationType = list.Where(t => t.KeyName == "IndicationType").OrderBy(t => t.ShowOrder).ToArray(), + DeclarationType = list.Where(t => t.KeyName == "DeclarationType").OrderBy(t => t.ShowOrder).ToArray() + }; + + } + + + #endregion + + + + + + + } +} diff --git a/IRaCIS.Core.Application/Service/Common/EmailNoticeConfigService.cs b/IRaCIS.Core.Application/Service/Common/EmailNoticeConfigService.cs new file mode 100644 index 00000000..a1f40598 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/EmailNoticeConfigService.cs @@ -0,0 +1,65 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-02-15 13:11:20 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Core.Application.Contracts +{ + /// + /// EmailNoticeConfigService + /// + [ApiExplorerSettings(GroupName = "Common")] + public class EmailNoticeConfigService : BaseService, IEmailNoticeConfigService + { + private readonly IRepository repository; + + public EmailNoticeConfigService(IRepository repository) + { + this.repository = repository; + } + + [HttpPost] + public async Task> GetEmailNoticeConfigList(EmailNoticeConfigQuery queryEmailNoticeConfig) + { + var emailNoticeConfigQueryable = _repository + .WhereIf(queryEmailNoticeConfig.ScenarioId != null, t => t.ScenarioId == queryEmailNoticeConfig.ScenarioId) + .WhereIf(queryEmailNoticeConfig.IsReturnRequired != null, t => t.IsReturnRequired == queryEmailNoticeConfig.IsReturnRequired) + .WhereIf(queryEmailNoticeConfig.IsUrgent != null, t => t.IsUrgent == queryEmailNoticeConfig.IsUrgent) + .WhereIf(queryEmailNoticeConfig.IsEnable != null, t => t.IsEnable == queryEmailNoticeConfig.IsEnable) + .ProjectTo(_mapper.ConfigurationProvider); + + return await emailNoticeConfigQueryable.ToPagedListAsync(queryEmailNoticeConfig.PageIndex, queryEmailNoticeConfig.PageSize, queryEmailNoticeConfig.SortField, queryEmailNoticeConfig.Asc); + } + + + public async Task AddOrUpdateEmailNoticeConfig(EmailNoticeConfigAddOrEdit addOrEditEmailNoticeConfig) + { + + var entity = await _repository.InsertOrUpdateAsync(addOrEditEmailNoticeConfig, true); + + return ResponseOutput.Ok(entity.Id.ToString()); + + } + + + [HttpDelete("{emailNoticeConfigId:guid}")] + public async Task DeleteEmailNoticeConfig(Guid emailNoticeConfigId) + { + var success = await repository.DeleteFromQueryAsync(t => t.Id == emailNoticeConfigId); + + return ResponseOutput.Result(success); + } + + + + public async Task> GetEmailScenarioEnumSelect() + { + return await Task.FromResult(EnumToSelectExtension.ToSelect()); + } + } +} diff --git a/IRaCIS.Core.Application/Service/Common/FileService.cs b/IRaCIS.Core.Application/Service/Common/FileService.cs new file mode 100644 index 00000000..4dee7235 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/FileService.cs @@ -0,0 +1,209 @@ +using IRaCIS.Application.Interfaces; +using System.Text; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using IRaCIS.Core.Infrastructure; + +namespace IRaCIS.Application.Services +{ + public class FileService : IFileService + { + + private readonly IDoctorService _doctorService; + private readonly IAttachmentService _attachmentService; + private readonly IHostEnvironment _hostEnvironment; + private string defaultUploadFilePath = string.Empty; + private readonly ILogger _logger; + public FileService(IDoctorService doctorService, IAttachmentService attachmentService, + IHostEnvironment hostEnvironment, ILogger logger) + { + _doctorService = doctorService; + _attachmentService = attachmentService; + _hostEnvironment = hostEnvironment; + + defaultUploadFilePath = Directory.GetParent(_hostEnvironment.ContentRootPath.TrimEnd('\\')).IfNullThrowException().FullName; + _logger = logger; + } + + /// + /// 打包医生官方简历 + /// + /// + /// + /// + public async Task CreateOfficialResumeZip(int language, Guid[] doctorIds) + { + + //准备下载文件的临时路径 + var guidStr = Guid.NewGuid().ToString(); + + //string uploadFolderPath = HostingEnvironment.MapPath("/UploadFile/"); + string uploadFolderPath = Path.Combine(defaultUploadFilePath, "UploadFile"); + + var tempSavePath = Path.Combine(uploadFolderPath, "temp", guidStr); //待压缩的文件夹,将需要下载的文件拷贝到此文件夹 + if (!Directory.Exists(tempSavePath)) + { + Directory.CreateDirectory(tempSavePath); + } + + //找到服务器简历路径 循环拷贝简历到临时路径 + foreach (var doctorId in doctorIds) + { + var doctor = await _doctorService.GetBasicInfo(doctorId); + var doctorName = doctor.FirstName + "_" + doctor.LastName; + + //找官方简历存在服务器的相对路径 + var sourceCvPath = await _attachmentService.GetDoctorOfficialCV(language, doctorId); + if (!string.IsNullOrWhiteSpace(sourceCvPath)) + { + //服务器简历文件实际路径 + //var sourceCvFullPath = HostingEnvironment.MapPath(sourceCvPath); + var sourceCvPathTemp = sourceCvPath.Substring(1, sourceCvPath.Length - 1);//.Replace('/','\\'); + string sourceCvFullPath = Path.Combine(defaultUploadFilePath, sourceCvPathTemp); + + var arr = sourceCvPath.Split('.'); + string extensionName = arr[arr.Length - 1]; //得到扩展名 + + //需要拷贝到的路径 + var doctorPath = Path.Combine(tempSavePath, doctor.ReviewerCode.ToString() + "_" + doctorName + "." + extensionName); + + if (File.Exists(sourceCvFullPath)) + { + File.Copy(sourceCvFullPath, doctorPath, true); + } + } + } + + //创建ZIP + DateTime now = DateTime.Now; + StringBuilder sb = new StringBuilder(); + sb.Append(now.Year).Append(now.Month.ToString().PadLeft(2, '0')).Append(now.Day.ToString().PadLeft(2, '0')) + .Append(now.Hour.ToString().PadLeft(2, '0')).Append(now.Minute.ToString().PadLeft(2, '0')) + .Append(now.Second.ToString().PadLeft(2, '0')).Append(now.Millisecond.ToString().PadLeft(3, '0')); + + string targetZipPath = Path.Combine(uploadFolderPath, "CV_" + sb.ToString() + ".zip"); + ZipHelper.CreateZip(tempSavePath, targetZipPath); + + //返回Zip路径 + return Path.Combine("/UploadFile/", "CV_" + sb.ToString() + ".zip"); + } + + /// + /// 打包医生的所有附件 + /// + /// + /// + public async Task CreateDoctorsAllAttachmentZip(Guid[] doctorIds) + { + //准备下载文件的临时路径 + var guidStr = Guid.NewGuid().ToString(); + //string uploadFolderPath = HostingEnvironment.MapPath("/UploadFile/"); + string uploadFolderPath = Path.Combine(defaultUploadFilePath, "UploadFile"); + var tempSavePath = Path.Combine(uploadFolderPath, "temp", guidStr); //待压缩的文件夹,将需要下载的文件拷贝到此文件夹 + if (!Directory.Exists(tempSavePath)) + { + Directory.CreateDirectory(tempSavePath); + } + + + foreach (var doctorId in doctorIds) + { + //获取医生基本信息 + var doctor = await _doctorService.GetBasicInfo(doctorId); + var doctorName = doctor.FirstName + "_" + doctor.LastName; + var doctorCode = doctor.ReviewerCode; + + var doctorDestPath = Path.Combine(tempSavePath, doctorCode + "_" + doctorName); + if (!Directory.Exists(doctorDestPath)) + { + Directory.CreateDirectory(doctorDestPath); + } + + //服务器上传后的源路径 + string doctorFileSourcePath = Path.Combine(uploadFolderPath, doctorId.ToString()); + + if (Directory.Exists(doctorFileSourcePath)) + { + CopyDirectory(doctorFileSourcePath, doctorDestPath); + } + } + string target = Guid.NewGuid().ToString(); + string targetPath = Path.Combine(uploadFolderPath, target + ".zip"); + ZipHelper.CreateZip(tempSavePath, targetPath); + return Path.Combine("/UploadFile/", target + ".zip"); + } + + + public async Task CreateZipPackageByAttachment(Guid doctorId, Guid[] attachmentIds) + { + var doctor = await _doctorService.GetBasicInfo(doctorId); + var doctorName = doctor.FirstName + "_" + doctor.LastName; + + Guid temp = Guid.NewGuid(); + //string root = HostingEnvironment.MapPath("/UploadFile/"); //文件根目录 + string root = Path.Combine(defaultUploadFilePath, "UploadFile"); + + var tempPath = Path.Combine(root, "temp", temp.ToString(), doctor.ReviewerCode + doctorName); //待压缩的文件夹,将需要下载的文件拷贝到此文件夹 + var packagePath = Path.Combine(root, "temp", temp.ToString()); //打包目录 + if (!Directory.Exists(tempPath)) + { + Directory.CreateDirectory(tempPath); + } + var attachemnts = (await _attachmentService.GetAttachments(doctorId)).Where(a => attachmentIds.Contains(a.Id)); + + foreach (var item in attachemnts) + { + var arr = item.Path.Trim().Split('/'); + var myPath = string.Empty; + var myFile = string.Empty; + //需要改进 + if (arr.Length > 0) + { + myFile = arr[arr.Length - 1]; + foreach (var arrItem in arr) + { + if (arrItem != string.Empty && !"UploadFile".Equals(arrItem)) + { + myPath += (arrItem + "/"); + } + } + myPath = myPath.TrimEnd('/'); + } + var sourcePath = Path.Combine(root, myPath); + + if (!string.IsNullOrWhiteSpace(sourcePath) && File.Exists(sourcePath)) + { + File.Copy(sourcePath, Path.Combine(tempPath, myFile), true); + } + } + string target = Guid.NewGuid().ToString(); + string targetPath = Path.Combine(root, target + ".zip"); + ZipHelper.CreateZip(packagePath, targetPath); + + return Path.Combine("/UploadFile/", target + ".zip"); + } + + private static void CopyDirectory(string srcPath, string destPath) + { + + DirectoryInfo dir = new DirectoryInfo(srcPath); + FileSystemInfo[] fileInfoArray = dir.GetFileSystemInfos(); //获取目录下(不包含子目录)的文件和子目录 + foreach (FileSystemInfo fileInfo in fileInfoArray) + { + if (fileInfo is DirectoryInfo) //判断是否文件夹 + { + if (!Directory.Exists(destPath + "\\" + fileInfo.Name)) + { + Directory.CreateDirectory(destPath + "\\" + fileInfo.Name); //目标目录下不存在此文件夹即创建子文件夹 + } + CopyDirectory(fileInfo.FullName, destPath + "\\" + fileInfo.Name); //递归调用复制子文件夹 + } + else + { + File.Copy(fileInfo.FullName, destPath + "\\" + fileInfo.Name, true); //不是文件夹即复制文件,true表示可以覆盖同名文件 + } + } + + } + } +} diff --git a/IRaCIS.Core.Application/Service/Common/Interface/IDictionaryService.cs b/IRaCIS.Core.Application/Service/Common/Interface/IDictionaryService.cs new file mode 100644 index 00000000..0c628954 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/Interface/IDictionaryService.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using EasyCaching.Core.Interceptor; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface IDictionaryService + { + + Task> getDictionarySelect(); + PageOutput getDictionarySelectList(DicQueryDTO dicSearchModel); + + Task DeleteDictionary(Guid id); + DicResultDTO GetDictionary(string[] searchArray); + DicResultDTO GetAllDictionary(); + //IResponseOutput AddOrUpdateDictionary(AddOrUpdateDicDTO viewModel); + + [EasyCachingAble(Expiration = 10)] + List GetDicTree(); + DicViewModelDTO GetDetailById(Guid Id); + + TrialDicSelect GetGenerateTrialCodeDic(); + } +} diff --git a/IRaCIS.Core.Application/Service/Common/Interface/IEmailNoticeConfigService.cs b/IRaCIS.Core.Application/Service/Common/Interface/IEmailNoticeConfigService.cs new file mode 100644 index 00000000..eca5e5fe --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/Interface/IEmailNoticeConfigService.cs @@ -0,0 +1,15 @@ +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-02-15 13:11:20 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + + +namespace IRaCIS.Core.Application.Contracts +{ + public interface IEmailNoticeConfigService + { + Task AddOrUpdateEmailNoticeConfig(EmailNoticeConfigAddOrEdit addOrEditEmailNoticeConfig); + Task DeleteEmailNoticeConfig(Guid emailNoticeConfigId); + Task> GetEmailNoticeConfigList(EmailNoticeConfigQuery queryEmailNoticeConfig); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Common/Interface/IFileService.cs b/IRaCIS.Core.Application/Service/Common/Interface/IFileService.cs new file mode 100644 index 00000000..10c6c072 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/Interface/IFileService.cs @@ -0,0 +1,17 @@ +using System; + +namespace IRaCIS.Application.Interfaces +{ + public interface IFileService + { + + //IResponseOutput DownloadOfficialResume(Guid[] doctorIds); + + + Task CreateOfficialResumeZip(int language, Guid[] doctorIds); + + Task CreateDoctorsAllAttachmentZip(Guid[] doctorIds); + + Task CreateZipPackageByAttachment(Guid doctorId, Guid[] attachmentIds); + } +} diff --git a/IRaCIS.Core.Application/Service/Common/Interface/ILogService.cs b/IRaCIS.Core.Application/Service/Common/Interface/ILogService.cs new file mode 100644 index 00000000..706b3a0f --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/Interface/ILogService.cs @@ -0,0 +1,21 @@ +using System; +using IRaCIS.Application.Contracts; +using System.Collections.Generic; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface ILogService + { + IResponseOutput SaveLog2Db(SystemLogDTO viewModel); + PageOutput GetLogList(QueryLogQueryDTO param); + + PageOutput GetAuditList(AuditQueryDTO param); + + List GetOptUserList(Guid trialId); + + List GetSubjectList(Guid trialId); + + + } +} diff --git a/IRaCIS.Core.Application/Service/Common/Interface/IMessageService.cs b/IRaCIS.Core.Application/Service/Common/Interface/IMessageService.cs new file mode 100644 index 00000000..ddaf0c27 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/Interface/IMessageService.cs @@ -0,0 +1,15 @@ +using System; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface IMessageService + { + int GetUnReadMessageCount(Guid doctorId); + IResponseOutput DeleteSysMessage(Guid messageId); + IResponseOutput MarkedAsRead(Guid messageId); + + PageOutput GetMessageList(Guid doctorId, int pageSize, int pageIndex); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Common/Interface/ISystemBasicDataService.cs b/IRaCIS.Core.Application/Service/Common/Interface/ISystemBasicDataService.cs new file mode 100644 index 00000000..c0b88444 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/Interface/ISystemBasicDataService.cs @@ -0,0 +1,25 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-02-15 15:47:41 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + + +namespace IRaCIS.Core.Application.Contracts +{ + /// + /// ISystemBasicDataService + /// + public interface ISystemBasicDataService + { + + + Task> GetSystemBasicDataList(SystemBasicDataQuery querySystemBasicData); + + Task AddOrUpdateSystemBasicData(SystemBasicDataAddOrEdit addOrEditSystemBasicData); + + Task DeleteSystemBasicData(Guid systemBasicDataId); + + + } +} diff --git a/IRaCIS.Core.Application/Service/Common/LogService.cs b/IRaCIS.Core.Application/Service/Common/LogService.cs new file mode 100644 index 00000000..8a483110 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/LogService.cs @@ -0,0 +1,114 @@ +//using IRaCIS.Application.Interfaces; +//using IRaCIS.Application.Contracts; +//using IRaCIS.Core.Infra.EFCore; +//using IRaCIS.Core.Infrastructure; + +//using Microsoft.AspNetCore.Http; +//using Microsoft.AspNetCore.Mvc; +//using Panda.DynamicWebApi.Attributes; + +//namespace IRaCIS.Application.Services +//{ +// /// +// /// 日志、项目审计日志 +// /// +// [ApiExplorerSettings(GroupName = "Common")] +// public class LogService : BaseService, ILogService +// { +// private readonly IRepository _systemLogRepository; +// private readonly IHttpContextAccessor _context; +// private readonly IRepository _trialAuditRepository; +// private readonly IRepository _subjectRepository; +// private readonly IRepository _trialRepository; + +// public LogService(IRepository systemLogRepository, IHttpContextAccessor context, IRepository trialAuditRepository, +// IRepository subjectRepository, IRepository trialRepository) +// { +// _systemLogRepository = systemLogRepository; +// _context = context; +// _trialAuditRepository = trialAuditRepository; +// _subjectRepository = subjectRepository; +// _trialRepository = trialRepository; +// } + +// [HttpPost] +// public PageOutput GetAuditList(AuditQueryDTO param) +// { +// var subjectInfo = param.SubjectInfo == null ? string.Empty : param.SubjectInfo.Trim(); + +// var query = _trialAuditRepository.Where(x => x.TrialId == param.TrialId) +// .WhereIf(param.AuditType != null, t => t.AuditType == param.AuditType) +// .WhereIf(param.OptUserId != null, t => t.OptUserId == param.OptUserId) +// .WhereIf(param.SubjectId != null, t => t.SubjectId == param.SubjectId) +// .WhereIf(!string.IsNullOrEmpty(subjectInfo), t => t.Subject.Code.Contains(subjectInfo) || (t.Subject.LastName + " / " + t.Subject.FirstName).Contains(subjectInfo)) +// .WhereIf(param.StudyId != null, t => t.StudyId == param.StudyId) +// .WhereIf(param.StartDate != null, t => t.OptTime >= param.StartDate) +// .WhereIf(param.EndDate != null, t => t.OptTime <= param.EndDate) +// .ProjectTo(_mapper.ConfigurationProvider); + + +// return query.ToPagedList(param.PageIndex, param.PageSize, string.IsNullOrWhiteSpace(param.SortField) ? "OptTime" : param.SortField, param.Asc); + +// } + +// /// 查询系统日志信息 +// [HttpPost] +// public PageOutput GetLogList(QueryLogQueryDTO param) +// { + +// var LogCategory = param.LogCategory == null ? string.Empty : param.LogCategory.Trim(); +// var keyword = param.Keyword == null ? string.Empty : param.Keyword.Trim(); +// var logQueryable = _systemLogRepository +// .WhereIf(param.BeginTime!=null,t=>t.RequestTime>= param.BeginTime) +// .WhereIf(param.EndTime != null, t => t.RequestTime <= param.EndTime) +// .WhereIf(!string.IsNullOrEmpty(LogCategory), t => t.LogCategory == param.LogCategory) +// .WhereIf(!string.IsNullOrEmpty(keyword), t => t.Params.Contains(keyword) || t.Result.Contains(keyword)) +// .ProjectTo(_mapper.ConfigurationProvider); + +// return logQueryable.ToPagedList(param.PageIndex, param.PageSize, string.IsNullOrWhiteSpace(param.SortField) ? "RequestTime" : param.SortField, param.Asc); + +// } + +// [HttpGet("{trialId:guid}")] +// public List GetOptUserList(Guid trialId) +// { +// var list = _trialAuditRepository.Where(t => t.TrialId == trialId).Select(u => new OptUserDto() +// { +// OptUserId = u.OptUserId, +// OptUser = u.OptUser +// }).Distinct().ToList(); + +// return list; +// } + +// /// +// /// 审计列表 受试者下拉框 从受试者那里进去看的时候,这里需要固定,如果不采用下拉框,请传递指定格式的受试者信息查询才行 +// /// +// /// +// /// +// [HttpGet("{trialId:guid}")] +// public List GetSubjectList(Guid trialId) +// { +// var query = from trialAudit in _trialAuditRepository.Where(t => t.TrialId == trialId) +// join subject in _subjectRepository.AsQueryable() on trialAudit.SubjectId equals subject.Id +// select new AuditSubjectSelectDto() +// { +// SubjectCode = subject.Code, +// SubjectId = trialAudit.SubjectId, +// SubjectName = subject.LastName + " / " + subject.FirstName +// }; + +// return query.Distinct().ToList(); +// } + +// [NonDynamicMethod] +// public IResponseOutput SaveLog2Db(SystemLogDTO input) +// { +// input.ClientIP = IPHelper.GetIP(_context?.HttpContext?.Request); + +// _systemLogRepository.Add(_mapper.Map(input)); +// var success = _systemLogRepository.SaveChanges(); +// return ResponseOutput.Result(success); +// } +// } +//} diff --git a/IRaCIS.Core.Application/Service/Common/MailService.cs b/IRaCIS.Core.Application/Service/Common/MailService.cs new file mode 100644 index 00000000..15983e92 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/MailService.cs @@ -0,0 +1,231 @@ +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Models; +using MailKit.Security; +using MimeKit; + +namespace IRaCIS.Application.Services +{ + public interface IMailVerificationService + { + Task SendMail(Guid userId, string userName, string emailAddress, int verificationCode); + + Task AnolymousSendEmail(string emailAddress, int verificationCode); + + Task SendMailEditEmail(Guid userId, string userName, string emailAddress, int verificationCode); + } + + public class MailVerificationService : IMailVerificationService + { + private readonly IRepository _verificationCodeRepository; + + private readonly IRepository _systemBasicDatarepository; + + + public MailVerificationService(IRepository verificationCodeRepository, IRepository systemBasicDatarepository) + { + _verificationCodeRepository = verificationCodeRepository; + _systemBasicDatarepository = systemBasicDatarepository; + + } + + + public async Task SendMailEditEmail(Guid userId, string userName, string emailAddress, int verificationCode) + { + + + var messageToSend = new MimeMessage(); + //发件地址 + messageToSend.From.Add(new MailboxAddress("GRR", "iracis_grr@163.com")); + //收件地址 + messageToSend.To.Add(new MailboxAddress(userName, emailAddress)); + //主题 + messageToSend.Subject = "Reset PassWord (Verification Code)"; + + messageToSend.Body = new TextPart("plain") + { + Text = $@"Hey {userName},you are modify your email . The verification code is: {verificationCode}, which is valid within 3 minutes. If it is not your own operation, please ignore it! + + -- GRR" + }; + using (var smtp = new MailKit.Net.Smtp.SmtpClient()) + { + smtp.MessageSent += (sender, args) => + { + // args.Response + var code = verificationCode.ToString(); + _ = _verificationCodeRepository.AddAsync(new VerificationCode() + { + CodeType = 0, + HasSend = true, + Code = code, + UserId = userId, + ExpirationTime = DateTime.Now.AddMinutes(3) + }).Result; + _ = _verificationCodeRepository.SaveChangesAsync().Result; + + }; + + smtp.ServerCertificateValidationCallback = (s, c, h, e) => true; + + await smtp.ConnectAsync("smtp.163.com", 25, SecureSocketOptions.StartTls); + + await smtp.AuthenticateAsync("iracis_grr@163.com", "XLWVQKZAEKLDWOAH"); + + await smtp.SendAsync(messageToSend); + + await smtp.DisconnectAsync(true); + + } + } + + + + public async Task SendMail(Guid userId, string userName, string emailAddress, int verificationCode) + { + + + var messageToSend = new MimeMessage(); + //发件地址 + messageToSend.From.Add(new MailboxAddress("GRR", "iracis_grr@163.com")); + //收件地址 + messageToSend.To.Add(new MailboxAddress(userName, emailAddress)); + //主题 + messageToSend.Subject = "Reset PassWord (Verification Code)"; + + messageToSend.Body = new TextPart("plain") + { + Text = $@"Hey {userName},you are resetting your password via email. The verification code is: {verificationCode}, which is valid within 3 minutes. If it is not your own operation, please ignore it! + + -- GRR" + }; + using (var smtp = new MailKit.Net.Smtp.SmtpClient()) + { + smtp.MessageSent += (sender, args) => + { + // args.Response + var code = verificationCode.ToString(); + _= _verificationCodeRepository.AddAsync(new VerificationCode() + { + CodeType = 0, + HasSend = true, + Code = code, + UserId = userId, + ExpirationTime = DateTime.Now.AddMinutes(3) + }).Result; + _= _verificationCodeRepository.SaveChangesAsync().Result; + + }; + + smtp.ServerCertificateValidationCallback = (s, c, h, e) => true; + + await smtp.ConnectAsync("smtp.163.com", 25, SecureSocketOptions.StartTls); + + await smtp.AuthenticateAsync("iracis_grr@163.com", "XLWVQKZAEKLDWOAH"); + + await smtp.SendAsync(messageToSend); + + await smtp.DisconnectAsync(true); + + } + } + + + public async Task AnolymousSendEmail(string emailAddress, int verificationCode) + { + + + + var messageToSend = new MimeMessage(); + //发件地址 + messageToSend.From.Add(new MailboxAddress("GRR", "iracis_grr@163.com")); + //收件地址 + messageToSend.To.Add(new MailboxAddress(String.Empty, emailAddress)); + //主题 + messageToSend.Subject = "GRR Site survey (Verification Code)"; + + messageToSend.Body = new TextPart("plain") + { + Text = $@"Hey ,you are login for site survey via email. The verification code is: {verificationCode}, which is valid within 3 minutes. If it is not your own operation, please ignore it! + + -- GRR" + }; + using (var smtp = new MailKit.Net.Smtp.SmtpClient()) + { + smtp.MessageSent += (sender, args) => + { + // args.Response + var code = verificationCode.ToString(); + _ = _verificationCodeRepository.AddAsync(new VerificationCode() + { + CodeType = 0, + HasSend = true, + Code = code, + UserId = Guid.Empty,//此时不知道用户 + EmailOrPhone = emailAddress, + ExpirationTime = DateTime.Now.AddMinutes(3) + }).Result; + _ = _verificationCodeRepository.SaveChangesAsync().Result; + + }; + + smtp.ServerCertificateValidationCallback = (s, c, h, e) => true; + + await smtp.ConnectAsync("smtp.163.com", 25, SecureSocketOptions.StartTls); + + await smtp.AuthenticateAsync("iracis_grr@163.com", "XLWVQKZAEKLDWOAH"); + + await smtp.SendAsync(messageToSend); + + await smtp.DisconnectAsync(true); + } + } + + public async Task SendEmailForExternalUser(string emailAddress, string verificationCode) + { + var messageToSend = new MimeMessage(); + //发件地址 + messageToSend.From.Add(new MailboxAddress("GRR", "iracis_grr@163.com")); + //收件地址 + messageToSend.To.Add(new MailboxAddress(String.Empty, emailAddress)); + //主题 + messageToSend.Subject = "GRR External User survey (Verification Code)"; + + messageToSend.Body = new TextPart("plain") + { + Text = $@"Hey ,you are login for site survey via email. The verification code is: {verificationCode}, If it is not your own operation, please ignore it! + + -- GRR" + }; + using (var smtp = new MailKit.Net.Smtp.SmtpClient()) + { + smtp.MessageSent += (sender, args) => + { + // args.Response + var code = verificationCode.ToString(); + _ = _verificationCodeRepository.AddAsync(new VerificationCode() + { + CodeType = 0, + HasSend = true, + Code = code, + UserId = Guid.Empty,//此时不知道用户 + EmailOrPhone = emailAddress, + ExpirationTime = DateTime.Now.AddMinutes(3) + }).Result; + _ = _verificationCodeRepository.SaveChangesAsync().Result; + + }; + + smtp.ServerCertificateValidationCallback = (s, c, h, e) => true; + + await smtp.ConnectAsync("smtp.163.com", 25, SecureSocketOptions.StartTls); + + await smtp.AuthenticateAsync("iracis_grr@163.com", "XLWVQKZAEKLDWOAH"); + + await smtp.SendAsync(messageToSend); + + await smtp.DisconnectAsync(true); + } + } + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Common/SystemBasicDataService.cs b/IRaCIS.Core.Application/Service/Common/SystemBasicDataService.cs new file mode 100644 index 00000000..ea49519b --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/SystemBasicDataService.cs @@ -0,0 +1,87 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-02-15 15:57:21 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using IRaCIS.Core.Application.Contracts; +using Microsoft.AspNetCore.Mvc; +namespace IRaCIS.Core.Application.Services +{ + /// + /// SystemBasicDataService + /// + [ApiExplorerSettings(GroupName = "Common")] + public class SystemBasicDataService : BaseService, ISystemBasicDataService + { + + + /// + /// 模板列表 + /// + /// + /// + [HttpPost] + public async Task> GetSystemBasicDataList(SystemBasicDataQuery querySystemBasicData) + { + var systemBasicDataQueryable = _repository.GetQueryable().Where(t => t.ParentId == null) + .ProjectTo(_mapper.ConfigurationProvider); + + return await systemBasicDataQueryable.ToPagedListAsync(querySystemBasicData.PageIndex, querySystemBasicData.PageSize, String.IsNullOrEmpty(querySystemBasicData.SortField) ? "Code" : querySystemBasicData.SortField, querySystemBasicData.Asc); + } + + + [HttpGet("{code}")] + public async Task GetSystemBasicData(string code) + { + return await _repository.Where(t => t.Code == code).ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync(); + } + + + /// + /// 模板关联的场景 + /// + /// + /// + [HttpGet("{parentId:guid}")] + public async Task> GetChildList(Guid parentId) + { + return await _repository.GetQueryable().Where(t => t.ParentId == parentId&&t.IsEnable).OrderBy(t => t.Code).ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + } + + + public async Task AddOrUpdateSystemBasicData(SystemBasicDataAddOrEdit addOrEditSystemBasicData) + { + + var entity = await _repository.InsertOrUpdateAsync(addOrEditSystemBasicData, true); + + return ResponseOutput.Ok(entity.Id.ToString()); + } + + + + + [HttpDelete("{systemBasicDataId:guid}")] + public async Task DeleteSystemBasicData(Guid systemBasicDataId) + { + var success = await _repository.DeleteFromQueryAsync(t => t.Id == systemBasicDataId); + return ResponseOutput.Result(success); + } + + + /// + /// 传递父亲Code 数组 返回多个下拉框数据 + /// + /// + /// + [HttpPost] + public async Task>> GetBasicDataSelect(string[] searchArray) + { + + var searchList = await _repository.GetQueryable().Where(t => searchArray.Contains(t.Parent.Code) && t.ParentId != null).ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + + return searchList.GroupBy(t => t.ParentCode).ToDictionary(g => g.Key, g => g.ToList()); + + } + } +} diff --git a/IRaCIS.Core.Application/Service/Common/_MapConfig.cs b/IRaCIS.Core.Application/Service/Common/_MapConfig.cs new file mode 100644 index 00000000..60ce7566 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/_MapConfig.cs @@ -0,0 +1,47 @@ +using AutoMapper; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Domain.Models; + +namespace IRaCIS.Core.Application.Service +{ + public class CommonConfig : Profile + { + public CommonConfig() + { + CreateMap() + .ForMember(o => o.MessageTime, t => t.MapFrom(u => u.MessageTime.ToString())); + + CreateMap(); + + CreateMap(); + + + + CreateMap().ReverseMap(); + CreateMap(); + + + CreateMap(); + + CreateMap() + .ForMember(o => o.ParentCode, t => t.MapFrom(u => u.Parent.Code)); + + CreateMap().ReverseMap(); + + + + CreateMap() + .ForMember(o => o.ConfigType, t => t.MapFrom(u => u.ConfigDictionary.Code)) + .ForMember(o => o.ConfigTypeDes, t => t.MapFrom(u => u.ConfigDictionary.Description)); + + CreateMap().ReverseMap(); + + + CreateMap() + .ForMember(o => o.ParentCode, t => t.MapFrom(u => u.Parent.Code)); + + } + } + +} diff --git a/IRaCIS.Core.Application/Service/Doctor/AttachmentService.cs b/IRaCIS.Core.Application/Service/Doctor/AttachmentService.cs new file mode 100644 index 00000000..94071da3 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/AttachmentService.cs @@ -0,0 +1,254 @@ +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Mvc; +using Panda.DynamicWebApi.Attributes; + +namespace IRaCIS.Application.Services +{ + /// + /// 医生文档关联关系维护 + /// + [ApiExplorerSettings(GroupName = "Reviewer")] + public class AttachmentService : BaseService, IAttachmentService + { + private readonly IRepository attachmentrepository; + + public AttachmentService(IRepository attachmentrepository) + { + this.attachmentrepository = attachmentrepository; + } + + /// + /// 删除附件 + /// + /// + /// + + public async Task DeleteAttachment([FromBody]AttachementCommand param) + { + //var attachment = _doctorAttachmentApp.GetDetailById(id); + //string file = HostingEnvironment.MapPath(attachment.Path); + + //if (File.Exists(file)) + //{ + // File.Delete(file); + //} + //var temp = HostingEnvironment.MapPath(param.Path); + //if (File.Exists(temp)) + //{ + // File.Delete(temp); + //} + + var success =await attachmentrepository.DeleteFromQueryAsync(a => a.Id == param.Id); + return ResponseOutput.Result(success); + } + + + /// + /// 根据医生Id 和 附件类型,获取记录 + /// + /// 医生Id + /// 附件类型 + /// + [HttpGet("{doctorId:guid}/{type}")] + public async Task> GetAttachmentByType(Guid doctorId, string type) + { + var attachmentList = await attachmentrepository.Where(a => a.DoctorId == doctorId && a.Type.Equals(type)).ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + attachmentList.ForEach(t => t.FullPath = t.Path + "?access_token=" + _userInfo.UserToken); + + return attachmentList; + } + + /// + /// 获取单个医生的多种证书附件 + /// + /// 医生Id + /// 类型数组 + /// + [HttpPost("{doctorId:guid}")] + public async Task> GetAttachmentByTypes(Guid doctorId, string[] types) + { + + var attachmentList =await attachmentrepository.Where(a => a.DoctorId == doctorId && types.Contains(a.Type)).OrderBy(s => s.Type).ThenBy(m => m.CreateTime).ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + attachmentList.ForEach(t => t.FullPath = t.Path + "?access_token=" + _userInfo.UserToken); + + return attachmentList; + } + + /// + /// 根据医生Id获取医生附件 + /// + /// 医生Id + /// + [HttpGet("{doctorId:guid}")] + public async Task> GetAttachments(Guid doctorId) + { + var attachmentList =await attachmentrepository.Where(a => a.DoctorId == doctorId).OrderBy(s => s.Type).ThenBy(m => m.CreateTime).ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + attachmentList.ForEach(t => t.FullPath = t.Path + "?access_token=" + _userInfo.UserToken); + + return attachmentList; + } + + [NonDynamicMethod] + public async Task GetDetailById(Guid attachmentId) + { + var attachment = await attachmentrepository.FirstOrDefaultAsync(a => a.Id == attachmentId).IfNullThrowException(); + var temp= _mapper.Map(attachment); + temp.FullPath = temp.Path + "?access_token=" + _userInfo.UserToken; + return temp; + } + + /// + /// 保存多个附件 + /// + /// + /// + public async Task> SaveAttachments(IEnumerable attachmentList) + { + var attachments = _mapper.Map>(attachmentList).ToList(); + + //1 是中文 2是英文 中英文第一份简历默认设置为官方 + + var zhCount = attachments.Count(t => t.Language == 1); + var usCount = attachments.Count(t => t.Language == 2); + + if (zhCount == 1) + { + var k = attachments.First(t => t.Language == 1); + k.IsOfficial = true; + } + if (usCount == 1) + { + var k = attachments.First(t => t.Language == 2); + k.IsOfficial = true; + } + + + //处理重传 + var reUpload = attachmentList.FirstOrDefault(t => t.ReUpload == true); + if (reUpload != null) + { + //因为界面现实的列表用了 接口返回的列表,所以要把返回的模型对应的字段也要更改 + var attach = attachments.First(t => t.Id == reUpload.Id); + attach.CreateTime = DateTime.Now; + + + //重传的时候,发现 相同语言的官方简历数量为2 那么将重传的简历设置为非官方 + if (attachments.Count(t => t.Language == reUpload.Language && t.IsOfficial) == 2) + { + await attachmentrepository.UpdateFromQueryAsync(t => t.Id == reUpload.Id, u => new Attachment() + { + Path = reUpload.Path, + CreateTime = DateTime.Now, + Language = reUpload.Language, + IsOfficial = false + }); + attach.IsOfficial = false; + } + else //相同语言的重传 + { + await attachmentrepository.UpdateFromQueryAsync(t => t.Id == reUpload.Id, u => new Attachment() + { + Path = reUpload.Path, + CreateTime = DateTime.Now, + Language = reUpload.Language + }); + + } + + } + + + var newAttachment = attachments.Where(t => t.Id == Guid.Empty); + + await _repository.AddRangeAsync(newAttachment); + await _repository.SaveChangesAsync(); + + //_doctorAttachmentRepository.AddRange(newAttachment); + //_doctorAttachmentRepository.SaveChanges(); + + var list = _mapper.Map>(attachments).ToList(); + + list.ForEach(t => t.FullPath = t.Path + "?access_token=" + _userInfo.UserToken); + + return list; + } + + public async Task> AddAttachment(AttachmentDTO attachment) + { + + var newAttachment = _mapper.Map(attachment); + //如果这个医生不存在 这个语言的官方简历 就设置为官方简历 + if (! await attachmentrepository.AnyAsync(t => t.Type == "Resume" && t.DoctorId == attachment.DoctorId && t.Language == attachment.Language && t.IsOfficial)) + { + newAttachment.IsOfficial = true; + + attachment.IsOfficial = true; + } + + await _repository.AddAsync(newAttachment); + var success = await _repository.SaveChangesAsync(); + return ResponseOutput.Result(success, attachment); + } + + + [NonDynamicMethod] + public async Task GetDoctorOfficialCV(int language, Guid doctorId) + { + var result = await attachmentrepository.FirstOrDefaultAsync(a => a.DoctorId == doctorId && + a.IsOfficial && a.Type.Equals("Resume") && a.Language == language); + if (result != null) + { + return result.Path; + } + return string.Empty; + } + + + /// + /// 将简历设置为官方简历 + /// + /// + /// + /// + /// + + [HttpPost("{doctorId:guid}/{attachmentId:guid}/{language}")] + public async Task SetOfficial(Guid doctorId, Guid attachmentId, int language) + { + var resumeList = await _repository.GetQueryable().Where(t => t.DoctorId == doctorId && t.Type == "Resume" && t.Language == language).ToListAsync(); + foreach (var item in resumeList) + { + if (item.Id == attachmentId) item.IsOfficial = true; + else item.IsOfficial = false; + await _repository.UpdateAsync(item); + } + + return ResponseOutput.Result(await _repository.SaveChangesAsync()); + } + + /// + /// 设置简历的语言类型 + /// + /// + /// + /// 0-未设置,1-中文,2-英文 + /// + + [HttpPost("{doctorId:guid}/{attachmentId:guid}/{language}")] + public async Task SetLanguage(Guid doctorId, Guid attachmentId, int language) + { + bool result =await attachmentrepository.UpdateFromQueryAsync(t => t.Id == attachmentId, a => new Attachment + { + Language = language, + IsOfficial = false + }); + return ResponseOutput.Result(result); + } + + + } +} diff --git a/IRaCIS.Core.Application/Service/Doctor/DTO/AttachmentModel.cs b/IRaCIS.Core.Application/Service/Doctor/DTO/AttachmentModel.cs new file mode 100644 index 00000000..7cce4330 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/DTO/AttachmentModel.cs @@ -0,0 +1,61 @@ +namespace IRaCIS.Application.Contracts +{ + public class AttachmentDTO + { + public Guid Id { get; set; } + public Guid DoctorId { get; set; } + public bool IsOfficial { get; set; } + public string Type { get; set; } = string.Empty; + public string Path { get; set; } = string.Empty; + public string FullPath { get; set; } = string.Empty; + public string FileName { get; set; } = string.Empty; + public DateTime? CreateTime { get; set; } + public int Language { get; set; } + + public bool ReUpload { get; set; } = false; + } + + public class ReviewerAckDTO + { + public Guid Id { get; set; } + public Guid DoctorId { get; set; } + public string Type { get; set; } = string.Empty; + public string Path { get; set; } = string.Empty; + public string FullPath => Path; + public string FileName { get; set; } = string.Empty; + } + + + public class TrialSOWPathDTO + { + public Guid TrialId { get; set; } + + public string SowName { get; set; } = string.Empty; + public string SowPath { get; set; } = string.Empty; + } + + public class DeleteSowPathDTO + { + public Guid TrialId { get; set; } + public string Path { get; set; } = string.Empty; + + } + + public class UploadAgreementAttachmentDTO + { + public Guid Id { get; set; } + + public Guid DoctorId { get; set; } + public string Type { get; set; } = string.Empty; + public string Path { get; set; } = string.Empty; + public string FullPath => Path; + + public string FileName { get; set; } = string.Empty; + } + + public class AttachementCommand + { + public Guid Id { get; set; } + public string Path { get; set; } = string.Empty; + } +} diff --git a/IRaCIS.Core.Application/Service/Doctor/DTO/DoctorAccountRegisterModel.cs b/IRaCIS.Core.Application/Service/Doctor/DTO/DoctorAccountRegisterModel.cs new file mode 100644 index 00000000..456a61b7 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/DTO/DoctorAccountRegisterModel.cs @@ -0,0 +1,12 @@ +using System; + +namespace IRaCIS.Application.Contracts +{ + public class DoctorAccountRegisterModel : DoctorAccountLoginDTO + { + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public string EMail { get; set; } = string.Empty; + public DateTime RegisterTime { get; set; } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Doctor/DTO/DoctorModel.cs b/IRaCIS.Core.Application/Service/Doctor/DTO/DoctorModel.cs new file mode 100644 index 00000000..9f9e771f --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/DTO/DoctorModel.cs @@ -0,0 +1,665 @@ +using System; +using System.Collections.Generic; + +using IRaCIS.Core.Infrastructure.Extention; +using IRaCIS.Core.Domain.Share; +using Newtonsoft.Json; +using System.Linq; + +namespace IRaCIS.Application.Contracts +{ + + #region 医生管理列表 + + public class DoctorDTO + { + + [JsonIgnore] + public List DictionaryList { get; set; } = new List(); + + + + //临床实践中使用的模式 + public List ReadingTypeList => DictionaryList.Where(t => t.ParentCode == StaticData.ReadingType).OrderBy(t => t.ShowOrder).Select(t => t.Value).ToList(); + public List ReadingTypeCNList => DictionaryList.Where(t => t.ParentCode == StaticData.ReadingType).OrderBy(t => t.ShowOrder).Select(t => t.ValueCN).ToList(); + + public List ReadingTypeIds => DictionaryList.Where(t => t.ParentCode == StaticData.ReadingType).OrderBy(t => t.ShowOrder).Select(t => t.Id).ToList(); + + + //第二专业 + public List SubspecialityList => DictionaryList.Where(t => t.ParentCode == StaticData.Subspeciality).OrderBy(t => t.ShowOrder).Select(t => t.Value).ToList(); + + public List SubspecialityCNList => DictionaryList.Where(t => t.ParentCode == StaticData.Subspeciality).OrderBy(t => t.ShowOrder).Select(t => t.ValueCN).ToList(); + + public List SubspecialityIds => DictionaryList.Where(t => t.ParentCode == StaticData.Subspeciality).OrderBy(t => t.ShowOrder).Select(t => t.Id).ToList(); + + + + public string ReadingTypeOther { get; set; } = String.Empty; + public string ReadingTypeOtherCN { get; set; } = String.Empty; + + public Guid Id { get; set; } + public DateTime CreateTime { get; set; } + public string ReviewerCode { get; set; } = String.Empty;//GUID + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public string ChineseName { get; set; } = string.Empty; + public List TitleIdList { get; set; } = new List(); + public List TitleList { get; set; } = new List(); + public List TitleCNList { get; set; } = new List(); + public string Phone { get; set; } = string.Empty; + public string Introduction { get; set; } = string.Empty; + public string EMail { get; set; } = string.Empty; + public string WeChat { get; set; } = string.Empty; + + //部门 + public string Department { get; set; } = string.Empty; + public string DepartmentCN { get; set; } = string.Empty; + public Guid? DepartmentId { get; set; } + public string DepartmentOther { get; set; } = String.Empty; + public string DepartmentOtherCN { get; set; } = String.Empty; + + //增加的 + public Guid? SpecialityId { get; set; } = Guid.Empty; + public string Speciality { get; set; } = string.Empty; + public string SpecialityCN { get; set; } = string.Empty; + public string SpecialityOther { get; set; } = string.Empty; + public string SpecialityOtherCN { get; set; } = string.Empty; + + //职称 + public string Rank { get; set; } = string.Empty; + public string RankCN { get; set; } = string.Empty; + public Guid? RankId { get; set; } + public string RankOther { get; set; } = String.Empty; + public string RankOtherCN { get; set; } = String.Empty; + + //职位 + public string Position { get; set; } = string.Empty; + public string PositionCN { get; set; } = string.Empty; + public Guid? PositionId { get; set; } + public string PositionOther { get; set; } = String.Empty; + public string PositionOtherCN { get; set; } = String.Empty; + + + + + public string SubspecialityOther { get; set; } = String.Empty; + public string SubspecialityOtherCN { get; set; } = String.Empty; + + public int GCP { get; set; } + public Guid? GCPId { get; set; } + public string ResumePath { get; set; } = string.Empty; + public bool HasResume + { + get; set; + } + public bool Reconfirmed { get; set; } + public int CooperateStatus { get; set; } + public int ResumeStatus { get; set; } + public bool AcceptingNewTrial { get; set; } = false; + public bool ActivelyReading { get; set; } = false; + + //医院 + + public Guid? HospitalId { get; set; } + public string HospitalOther { get; set; } = String.Empty; + public string HospitalName { get; set; } = string.Empty; + public string City { get; set; } = String.Empty; + public string Country { get; set; } = String.Empty; + + public string HospitalNameCN { get; set; } = string.Empty; + public string CityCN { get; set; } = String.Empty; + public string CountryCN { get; set; } = String.Empty; + + + public int? Reading { get; set; } + public int? Approved { get; set; } + public int? Submitted { get; set; } + + public int? Finished { get; set; } + } + + /// + /// Reviewer 列表查询参数 + /// + + public class DoctorSearchDTO : PageInput + { + public string Name { get; set; } = string.Empty; + public List ReadingTypeIdList { get; set; } = new List(); + public List SubspecialityIdList { get; set; } = new List(); + + public List EvaluationCriteriaIdList { get; set; } = new List(); + + public List TitleIdList { get; set; } = new List(); + public Guid? DepartmentId { get; set; } + public Guid? SpecialityId { get; set; } + public Guid? PositionId { get; set; } + public Guid? RankId { get; set; } + public Guid? HospitalId { get; set; } + + //合作状态 + public ContractorStatusEnum? ContractorStatus { get; set; } + + // 简历审核状态 + public ResumeStatusEnum? InformationConfirmed { get; set; } + public int? EnrollStatus { get; set; } //入组状态 + + public bool? AcceptingNewTrial { get; set; }//是否接受新的项目 + public bool? ActivelyReading { get; set; }// 是否接受新的读片任务 + public int? Nation { get; set; }// 0-中国医生,2-美国医生,3-全部 + } + + /// + /// 入组 Selection 列表查询参数 + /// + public class ReviewerSelectionQueryDTO : DoctorSearchDTO + { + public Guid TrialId { get; set; } + } + + public class ReviewerSubmissionQueryDTO : PageInput + { + public Guid TrialId { get; set; } = Guid.Empty; + public int IntoGroupSearchState { get; set; } + } + + public class ReviewerConfirmationQueryDTO : PageInput + { + public Guid TrialId { get; set; } = Guid.Empty; + } + + public class SelectionReviewerDTO : DoctorDTO + { + public int DoctorTrialState { get; set; } + public string OptUserName { get; set; } = string.Empty; + public DateTime? OptTime { get; set; } + public string? OptTimeStr => OptTime?.ToString("yyyy-MM-dd HH:mm:ss"); + } + + public class DoctorOptDTO + { + public Guid Id { get; set; } + public string Code { get; set; } = String.Empty;//GUID + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public string ChineseName { get; set; } = string.Empty; + + public string City { get; set; } = string.Empty; + public string HospitalName { get; set; } = string.Empty; + public string Country { get; set; } = string.Empty; + } + + + public class ConfirmationReviewerDTO : DoctorOptDTO + { + public int DoctorTrialState { get; set; } + public string OptUserName { get; set; } = string.Empty; + public DateTime? OptTime { get; set; } + public string? OptTimeStr => OptTime?.ToString("yyyy-MM-dd HH:mm:ss"); + } + public class DoctorStateModelDTO + { + public Guid DoctorId { get; set; } + public int IntoGroupState { get; set; } + public string OptUserName { get; set; } = String.Empty; + public DateTime? OptTime { get; set; } + + } + + #endregion + + public class DoctorDetailDTO + { + public DoctorBasicInfoDTO BasicInfoView { get; set; } + public EmploymentDTO EmploymentView { get; set; } + public SpecialtyDTO SpecialtyView { get; set; } + public IEnumerable EducationList { get; set; } + public IEnumerable PostgraduateList { get; set; } + public ResearchPublicationDTO ResearchPublicationView { get; set; } + public TrialExperienceModel TrialExperienceView { get; set; } + public ResumeConfirmDTO AuditView { get; set; } + public IEnumerable AttachmentList { get; set; } + + public List SowList { get; set; } + public List AckSowList { get; set; } + + public DoctorEnrollInfoDTO IntoGroupInfo { get; set; } + + public bool InHoliday { get; set; } + public DoctorDetailDTO() + { + BasicInfoView = new DoctorBasicInfoDTO(); + EmploymentView = new EmploymentDTO(); + SpecialtyView = new SpecialtyDTO(); + EducationList = new List(); + PostgraduateList = new List(); + ResearchPublicationView = new ResearchPublicationDTO(); + TrialExperienceView = new TrialExperienceModel(); + AuditView = new ResumeConfirmDTO(); + AttachmentList = new List(); + IntoGroupInfo = new DoctorEnrollInfoDTO(); + SowList = new List(); + AckSowList = new List(); + } + } + + public class DoctorEnrollInfoDTO + { + public Guid? DoctorId { get; set; } + public int? Submitted { get; set; } + public int? Approved { get; set; } + public int? Reading { get; set; } + } + + + + #region 基本信息模型 + + public class DoctorBasicInfo + { + public Guid? Id { get; set; } + public string ReviewerCode { get; set; } = string.Empty; + public string FirstName { get; set; } = String.Empty; + public string LastName { get; set; } = String.Empty; + public string ChineseName { get; set; } = String.Empty; + public int Sex { get; set; } + public string Phone { get; set; } = String.Empty; + public string Introduction { get; set; } = String.Empty; + public string EMail { get; set; } = String.Empty; + public string WeChat { get; set; } = String.Empty; + public int Nation { get; set; } + } + + public class DoctorBasicInfoCommand : DoctorBasicInfo + { + + //职称 + public List TitleIds { get; set; } = new List(); + } + + public class TempObj + { + public int ShowOrder { get; set; } + public Guid TitleId { get; set; } + public string TitleCN { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + } + public class DicView + { + public int ShowOrder { get; set; } + public Guid Id { get; set; } + public string ValueCN { get; set; } = string.Empty; + public string ParentCode { get; set; } = string.Empty; + + + public string Value { get; set; } = string.Empty; + } + + + public class DoctorBasicInfoDTO : DoctorBasicInfo + { + public List DoctorDicViewDtos = new List(); + + //职称 + public List TitleIds => DoctorDicViewDtos.Select(t => t.Id).ToList(); + + public List TitleList=> DoctorDicViewDtos.Select(t => t.Value).ToList(); + + public List TitleCNList=> DoctorDicViewDtos.Select(t => t.ValueCN).ToList(); + + #region ef select + //[JsonIgnore] + //public List TempObjList { get; set; } + ////职称 + //public List TitleIds + //{ + // get + // { + // if (TempObjList.Count > 0) + // { + // return TempObjList.Select(t => t.TitleId).ToList(); + // } + // else + // { + // return new List(); + // } + // } + + //} + //public List TitleList + //{ + // get + // { + // if (TempObjList.Count > 0) + // { + // return TempObjList.Select(t => t.Title).ToList(); + // } + // else + // { + // return new List(); + // } + // } + //} + //public List TitleCNList + //{ + // get + // { + // if (TempObjList.Count > 0) + // { + // return TempObjList.Select(t => t.TitleCN).ToList(); + // } + // else + // { + // return new List(); + // } + // } + //} + + #endregion + } + + public class SowDTO + { + public string FileName { get; set; } = string.Empty; + public string TrialCode { get; set; } = string.Empty; + public string FilePath { get; set; } = string.Empty; + public string FullPath { get { return FilePath; } } + public DateTime CreateTime { get; set; } + } + + + #endregion + + + #region 工作信息模型 + + + //public class DoctorHospitalView + //{ + // public string HospitalName { get; set; } + // public string UniversityAffiliated { get; set; } + // public string Country { get; set; } + // public string Province { get; set; } + // public string City { get; set; } + + // public string HospitalNameCN { get; set; } + + // public string UniversityAffiliatedCN { get; set; } + // public string CountryCN { get; set; } + + // public string ProvinceCN { get; set; } + + // public string CityCN { get; set; } + //} + + public class EmploymentDTO : EmploymentInfo + { + //public DoctorHospitalView Hospital { get; set; } + + public string Department { get; set; } = String.Empty; + + public string Rank { get; set; } = String.Empty; + + public string Position { get; set; } = String.Empty; + + #region 医院信息 + public string HospitalName { get; set; } = String.Empty; + + public string UniversityAffiliated { get; set; } = String.Empty; + public string Country { get; set; } = String.Empty; + + public string Province { get; set; } = String.Empty; + + public string City { get; set; } = String.Empty; + + #endregion + + public string DepartmentCN { get; set; } = String.Empty; + + public string RankCN { get; set; } = String.Empty; + + public string PositionCN { get; set; } = String.Empty; + + + public string HospitalNameCN { get; set; } = String.Empty; + + public string UniversityAffiliatedCN { get; set; } = String.Empty; + public string CountryCN { get; set; } = String.Empty; + + public string ProvinceCN { get; set; } = String.Empty; + + public string CityCN { get; set; } = String.Empty; + } + + public class EmploymentCommand : EmploymentInfo + { + + } + + public class EmploymentInfo + { + public Guid Id { get; set; } + //部门 + public Guid? DepartmentId { get; set; } = Guid.Empty; + public string DepartmentOther { get; set; } = string.Empty; + public string DepartmentOtherCN { get; set; } = string.Empty; + //职称 + public Guid? RankId { get; set; } = Guid.Empty; + public string RankOther { get; set; } = string.Empty; + public string RankOtherCN { get; set; } = string.Empty; + //职位 主席 副主席 + public Guid? PositionId { get; set; } = Guid.Empty; + public string PositionOther { get; set; } = string.Empty; + public string PositionOtherCN { get; set; } = string.Empty; + + public Guid? HospitalId { get; set; } = Guid.Empty; + + } + + #endregion + + #region Specialty模型 + public class SpecialtyDTO : SpecialtyCommand + { + [JsonIgnore] + public List DictionaryList { get; set; } = new List(); + + public string Speciality { get; set; } = string.Empty; + + + //临床实践中使用的模式 + public new List ReadingTypeIds + { + get + { + if (DictionaryList.Count > 0) + { + return DictionaryList.Where(t => t.ParentCode == StaticData.ReadingType).Select(t => t.Id).ToList(); + } + else + { + return new List(); + } + } + + } + + public new List SubspecialityIds + { + get + { + if (DictionaryList.Count > 0) + { + return DictionaryList.Where(t => t.ParentCode == StaticData.Subspeciality).Select(t => t.Id).ToList(); + } + else + { + return new List(); + } + } + + } + + + public List ReadingTypeList + { + get + { + if (DictionaryList.Count > 0) + { + return DictionaryList.Where(t => t.ParentCode == StaticData.ReadingType).Select(t => t.Value).ToList(); + } + else + { + return new List(); + } + } + + } + public List SubspecialityList + { + get + { + if (DictionaryList.Count > 0) + { + return DictionaryList.Where(t => t.ParentCode == StaticData.Subspeciality).Select(t => t.Value).ToList(); + } + else + { + return new List(); + } + } + + } + + public List ReadingTypeCNList + { + get + { + if (DictionaryList.Count > 0) + { + return DictionaryList.Where(t => t.ParentCode == StaticData.ReadingType).Select(t => t.ValueCN).ToList(); + } + else + { + return new List(); + } + } + + } + public List SubspecialityCNList + { + get + { + if (DictionaryList.Count > 0) + { + return DictionaryList.Where(t => t.ParentCode == StaticData.Subspeciality).Select(t => t.ValueCN).ToList(); + } + else + { + return new List(); + } + } + + } + } + + + + public class SpecialtyCommand + { + public List ReadingTypeIds { get; set; } = new List(); + + public List SubspecialityIds { get; set; } = new List(); + + public Guid Id { get; set; } + public string OtherSkills { get; set; } = string.Empty; + + public string ReadingTypeOther { get; set; } = string.Empty; + public string ReadingTypeOtherCN { get; set; } = string.Empty; + //亚专科 + + public string SubspecialityOther { get; set; } = string.Empty; + public string SubspecialityOtherCN { get; set; } = string.Empty; + + public Guid? SpecialityId { get; set; } = Guid.Empty; + public string SpecialityCN { get; set; } = string.Empty; + public string SpecialityOther { get; set; } = string.Empty; + public string SpecialityOtherCN { get; set; } = string.Empty; + + + } + + + #endregion + + #region 医生账户 + public class DoctorAccountLoginDTO + { + public string Phone { get; set; } = String.Empty; + public string Password { get; set; } = String.Empty; + } + + public class DoctorAccountDTO + { + public Guid Id { get; set; } + public string FirstName { get; set; } = String.Empty; + public string LastName { get; set; } = String.Empty; + public string Phone { get; set; } = String.Empty; + public string PhotoPath { get; set; } = String.Empty; + } + + public class DoctorAccountUpdatePasswordCommand + { + public string Phone { get; set; } = String.Empty; + public string OldPassword { get; set; } = String.Empty; + public string NewPassword { get; set; } = String.Empty; + } + #endregion + + #region 审核模型 + + public class ResumeConfirmCommand + { + //int userId, int doctorId, int status, string memo + //public Guid FromUserId { get; set; } + public Guid Id { get; set; } + public ResumeStatusEnum ResumeStatus { get; set; } + public int ReviewStatus { get; set; } + public bool AcceptingNewTrial { get; set; } = false; + public bool ActivelyReading { get; set; } = false; + public string AdminComment { get; set; } = String.Empty; + public string MessageContent { get; set; } = String.Empty; + public ContractorStatusEnum CooperateStatus { get; set; } + } + + public class ResumeConfirmDTO + { + public Guid Id { get; set; } + public int CooperateStatus { get; set; } + public int ResumeStatus { get; set; } + public int ReviewStatus { get; set; } //复审状态 + public bool AcceptingNewTrial { get; set; } + public bool ActivelyReading { get; set; } + public string AdminComment { get; set; } = String.Empty; + public bool InHoliday { get; set; } + } + + #endregion + + + public class TrialPaymentPriceQueryDTO : PageInput + { + public string KeyWord { get; set; } = String.Empty; + + public Guid? CroId { get; set; } + + } + + public class DoctorPaymentInfoQueryDTO : PageInput + { + public string SearchName { get; set; } = String.Empty; + public Guid? HospitalId { get; set; } + } +} diff --git a/IRaCIS.Core.Application/Service/Doctor/DTO/EducationModel.cs b/IRaCIS.Core.Application/Service/Doctor/DTO/EducationModel.cs new file mode 100644 index 00000000..f9fdbdee --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/DTO/EducationModel.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; + +namespace IRaCIS.Application.Contracts +{ + public class EducationCommand + { + public Guid? Id { get; set; } + public Guid DoctorId { get; set; } + public DateTime BeginDate { get; set; } + public DateTime EndDate { get; set; } + public string Degree { get; set; } = String.Empty; + public string Major { get; set; } = String.Empty; + public string Organization { get; set; } = String.Empty; + public string Country { get; set; } = String.Empty; + public string Province { get; set; } = String.Empty; + public string City { get; set; } = String.Empty; + + public string DegreeCN { get; set; } = String.Empty; + public string MajorCN { get; set; } = String.Empty; + public string OrganizationCN { get; set; } = String.Empty; + public string CountryCN { get; set; } = String.Empty; + public string ProvinceCN { get; set; } = String.Empty; + public string CityCN { get; set; } = String.Empty; + + } + + public class EducationInfoViewModel : EducationCommand + { + public DateTime? CreateTime { get; set; } + public string BeginDateStr => BeginDate.ToString("yyyy-MM"); + public string EndDateStr => EndDate.ToString("yyyy-MM"); + + } + public class PostgraduateCommand + { + public Guid? Id { get; set; } + public Guid DoctorId { get; set; } + + public DateTime BeginDate { get; set; } + + public DateTime EndDate { get; set; } + + public string Training { get; set; } = String.Empty; + + public string Major { get; set; } = String.Empty; + + public string Hospital { get; set; } = String.Empty; + + public string School { get; set; } = String.Empty; + + public string Country { get; set; } = String.Empty; + + public string Province { get; set; } = String.Empty; + + public string City { get; set; } = String.Empty; + + public string TrainingCN { get; set; } = String.Empty; + + public string MajorCN { get; set; } = String.Empty; + + public string HospitalCN { get; set; } = String.Empty; + + public string SchoolCN { get; set; } = String.Empty; + + public string CountryCN { get; set; } = String.Empty; + + public string ProvinceCN { get; set; } = String.Empty; + + public string CityCN { get; set; } = String.Empty; + + } + + + public class PostgraduateViewModel: PostgraduateCommand + { + public DateTime? CreateTime { get; set; } + public string BeginDateStr => BeginDate.ToString("yyyy-MM"); + + public string EndDateStr => EndDate.ToString("yyyy-MM"); + } + public class DoctorEducationExperienceDTO + { + public IEnumerable EducationList=new List(); + + public IEnumerable PostgraduateList = new List(); + } + +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Doctor/DTO/HolidayModel.cs b/IRaCIS.Core.Application/Service/Doctor/DTO/HolidayModel.cs new file mode 100644 index 00000000..8635d431 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/DTO/HolidayModel.cs @@ -0,0 +1,13 @@ +using System; + +namespace IRaCIS.Application.Contracts +{ + public class VacationCommand + { + public Guid? Id { get; set; } + public Guid DoctorId { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public int Status { get; set; } = 1; + } +} diff --git a/IRaCIS.Core.Application/Service/Doctor/DTO/ResearchPublicationModel.cs b/IRaCIS.Core.Application/Service/Doctor/DTO/ResearchPublicationModel.cs new file mode 100644 index 00000000..d5a61a58 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/DTO/ResearchPublicationModel.cs @@ -0,0 +1,21 @@ +using System; + +namespace IRaCIS.Application.Contracts +{ + public class ResearchPublicationDTO + { + public Guid? Id { get; set; } + public Guid DoctorId { get; set; } + public string Research { get; set; } = String.Empty; + public string Grants { get; set; } = String.Empty; + public string Publications { get; set; } = String.Empty; + public string AwardsHonors { get; set; } = String.Empty; + + public string ResearchCN { get; set; } = String.Empty; + public string GrantsCN { get; set; } = String.Empty; + public string PublicationsCN { get; set; } = String.Empty; + public string AwardsHonorsCN { get; set; } = String.Empty; + + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Doctor/DTO/TrialExperienceModel.cs b/IRaCIS.Core.Application/Service/Doctor/DTO/TrialExperienceModel.cs new file mode 100644 index 00000000..325cecc0 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/DTO/TrialExperienceModel.cs @@ -0,0 +1,81 @@ +namespace IRaCIS.Application.Contracts +{ + public class TrialExperienceCommand + { + public Guid? Id { get; set; } + + public Guid DoctorId { get; set; } + + public Guid? PhaseId { get; set; } + public string EvaluationContent { get; set; } = String.Empty; + + + //public string Term { get; set; } + + //public string EvaluationCriteria { get; set; } + + public List EvaluationCriteriaIdList { get; set; } = new List(); + + + } + + public class TrialExperienceListDTO: TrialExperienceCommand + { + public string Phase { get; set; } = String.Empty; + + public List EvaluationCriteriaList { get; set; } = new List(); + + } + + //public class EvaluationCriteriaDTO + //{ + // public Guid EvaluationCriteriaId { get; set; } + // public string EvaluationCriteria { get; set; } + //} + + + public class TrialExperienceModel : GcpAndOtherExperienceDTO + { + public List ClinicalTrialExperienceList = new List(); + + public string ExpiryDateStr { get; set; } = string.Empty; + public string GCPFullPath { get; set; } = String.Empty; + + } + + + public class GcpAndOtherExperienceDTO + { + public Guid Id { get; set; } + + public int GCP { get; set; } + + public Guid? GCPId { get; set; } + + public string OtherClinicalExperience { get; set; }=String.Empty; + public string OtherClinicalExperienceCN { get; set; } = String.Empty; + + public string Type { get; set; } = string.Empty; + + public string Path { get; set; } = string.Empty; + + public string FileName { get; set; } = string.Empty; + + } + + public class GCPExperienceCommand + { + public Guid Id { get; set; } + public int GCP { get; set; } + + public Guid? GCPId { get; set; } + } + + public class ClinicalExperienceCommand + { + public Guid DoctorId { get; set; } + + public string OtherClinicalExperience { get; set; } = String.Empty; + public string OtherClinicalExperienceCN { get; set; } = String.Empty; + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Doctor/DoctorListService.cs b/IRaCIS.Core.Application/Service/Doctor/DoctorListService.cs new file mode 100644 index 00000000..bec38a1a --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/DoctorListService.cs @@ -0,0 +1,211 @@ +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Domain.Share; +using System.Linq.Dynamic.Core; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Application.Services +{ + [ApiExplorerSettings(GroupName = "Reviewer")] + public class DoctorListService : BaseService, IDoctorListQueryService + { + private readonly IRepository _doctorRepository; + + public DoctorListService(IRepository doctorRepository) + { + _doctorRepository = doctorRepository; + } + + /// + /// Reviewer列表分页查询 + /// + [HttpPost] + public async Task> GetDoctorSearchList(DoctorSearchDTO doctorSearch) + { + + // 项目经验 多选 + var evaluationCriteriaCount = doctorSearch.EvaluationCriteriaIdList.Count(); + + // 搜索条件 ReadingType 、Subspeciality、Title 多选 + var count = doctorSearch.ReadingTypeIdList.Count + doctorSearch.TitleIdList.Count + doctorSearch.SubspecialityIdList.Count; + + var guidList = doctorSearch.ReadingTypeIdList.Concat(doctorSearch.SubspecialityIdList).Concat(doctorSearch.TitleIdList); + + + var query = _doctorRepository.AsQueryable() + .WhereIf(doctorSearch.DepartmentId != null, t => t.DepartmentId == doctorSearch.DepartmentId) + .WhereIf(doctorSearch.SpecialityId != null, t => t.SpecialityId == doctorSearch.SpecialityId) + .WhereIf(doctorSearch.HospitalId != null, t => t.HospitalId == doctorSearch.HospitalId) + .WhereIf(doctorSearch.PositionId != null, t => t.PositionId == doctorSearch.PositionId) + .WhereIf(doctorSearch.RankId != null, t => t.RankId == doctorSearch.RankId) + .WhereIf(doctorSearch.ContractorStatus != null, t => t.CooperateStatus == doctorSearch.ContractorStatus) + .WhereIf(doctorSearch.InformationConfirmed != null, t => t.ResumeStatus == doctorSearch.InformationConfirmed) + .WhereIf(doctorSearch.AcceptingNewTrial != null, t => t.AcceptingNewTrial == doctorSearch.AcceptingNewTrial) + .WhereIf(!string.IsNullOrWhiteSpace(doctorSearch.Name), t => t.ChineseName.Contains(doctorSearch.Name) || (t.LastName + t.FirstName).Contains(doctorSearch.Name)) + .WhereIf(doctorSearch.Nation != null, t => t.Nation == doctorSearch.Nation) + .WhereIf(evaluationCriteriaCount > 0, t => t.TrialExperienceCriteriaList.Count(t => doctorSearch.EvaluationCriteriaIdList.Contains(t.EvaluationCriteriaId)) == evaluationCriteriaCount) + //用户类型 看到简历的范围这里需要确认 + .WhereIf(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ReviewerCoordinator, t => t.UserList.Any(u => u.UserId == _userInfo.Id)) + .WhereIf(count > 0, t => t.DoctorDicRelationList.Count(u => guidList.Contains(u.DictionaryId)) == count) + .WhereIf(doctorSearch.EnrollStatus != null && doctorSearch.EnrollStatus == (int)ReviewerEnrollStatus.Yes, t => t.EnrollList.Any(u => u.EnrollStatus == (int)EnrollStatus.DoctorReading)) + + .ProjectTo(_mapper.ConfigurationProvider); + + return await query.ToPagedListAsync(doctorSearch.PageIndex, doctorSearch.PageSize, doctorSearch.SortField == string.Empty ? "CreateTime" : doctorSearch.SortField, doctorSearch.Asc); + + + } + + + #region 入组查询相关 + /// + /// 获取可筛选筛选及已经筛选的医生列表 + /// + [HttpPost] + public async Task> GetSelectionReviewerList( + ReviewerSelectionQueryDTO selectionQuery) + { + //项目配置需要的医生过滤 2表示混合 + var nation = await _repository.Where(s => s.Id == selectionQuery.TrialId).Select(t=>t.AttendedReviewerType).FirstOrDefaultAsync().IfNullThrowException(); + + // 临床项目经验 多选 + var evaluationCriteriaCount = selectionQuery.EvaluationCriteriaIdList.Count(); + + // 搜索条件 ReadingType 、Subspeciality、Title 多选 + var count = selectionQuery.ReadingTypeIdList.Count + selectionQuery.TitleIdList.Count + selectionQuery.SubspecialityIdList.Count; + + var guidList = selectionQuery.ReadingTypeIdList.Concat(selectionQuery.SubspecialityIdList).Concat(selectionQuery.TitleIdList); + + var query = _doctorRepository.WhereIf(nation != 2, t => t.Nation == nation) + .WhereIf(selectionQuery.DepartmentId != null, t => t.DepartmentId == selectionQuery.DepartmentId) + .WhereIf(selectionQuery.SpecialityId != null, t => t.SpecialityId == selectionQuery.SpecialityId) + .WhereIf(selectionQuery.HospitalId != null, t => t.HospitalId == selectionQuery.HospitalId) + .WhereIf(selectionQuery.PositionId != null, t => t.PositionId == selectionQuery.PositionId) + .WhereIf(selectionQuery.RankId != null, t => t.RankId == selectionQuery.RankId) + .WhereIf(selectionQuery.ContractorStatus != null, t => t.CooperateStatus == selectionQuery.ContractorStatus) + .WhereIf(selectionQuery.InformationConfirmed != null, t => t.ResumeStatus == selectionQuery.InformationConfirmed) + .WhereIf(selectionQuery.AcceptingNewTrial != null, t => t.AcceptingNewTrial == selectionQuery.AcceptingNewTrial) + .WhereIf(!string.IsNullOrWhiteSpace(selectionQuery.Name), t => t.ChineseName.Contains(selectionQuery.Name) || (t.LastName + t.FirstName).Contains(selectionQuery.Name)) + .WhereIf(evaluationCriteriaCount > 0, t => t.TrialExperienceCriteriaList.Count(t => selectionQuery.EvaluationCriteriaIdList.Contains(t.EvaluationCriteriaId)) == evaluationCriteriaCount) + //用户类型 看到简历的范围这里需要确认 + .WhereIf(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ReviewerCoordinator, t => t.UserList.Any(u => u.UserId == _userInfo.Id)) + .WhereIf(count > 0, t => t.DoctorDicRelationList.Count(u => guidList.Contains(u.DictionaryId)) == count) + .WhereIf(selectionQuery.EnrollStatus != null && selectionQuery.EnrollStatus == (int)ReviewerEnrollStatus.Yes, t => t.EnrollList.Any(u => u.EnrollStatus == (int)EnrollStatus.DoctorReading)) + + .ProjectTo(_mapper.ConfigurationProvider); + + var result = await query.ToPagedListAsync(selectionQuery.PageIndex, selectionQuery.PageSize, selectionQuery.SortField == string.Empty ? "ReviewerCode" : selectionQuery.SortField, selectionQuery.Asc); + + //是否已申请 申请时间 申请人 + var doctorStateList = await _repository.Where(x => x.TrialId == selectionQuery.TrialId && x.EnrollStatus == (int)EnrollStatus.HasApplyDownloadResume) + .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + + result.CurrentPageData.ToList().ForEach(doctor => + { + //简历申请列表 --处理已经申请的 + var doctorState = doctorStateList.FirstOrDefault(t => t.DoctorId == doctor.Id && t.IntoGroupState == (int)EnrollStatus.HasApplyDownloadResume); + if (doctorState != null) + { + doctor.DoctorTrialState = (int)EnrollStatus.HasApplyDownloadResume; + doctor.OptTime = doctorState.OptTime; + doctor.OptUserName = doctorState.OptUserName; + } + }); + + return result; + + + } + + /// + /// 获取提交CRO或者CRO审核的Reviewer列表 + /// + /// + /// 根据状态获取医生列表,入组 相关接口 (提交CRO-1) CRO确认-4 + /// + [HttpPost] + public async Task> GetSubmissionOrApprovalReviewerList( + ReviewerSubmissionQueryDTO param) + { + + var doctorQuery = _repository.Where(x => x.TrialId == param.TrialId) + //提交CRO 以及下载简历列表 + .WhereIf(param.IntoGroupSearchState == 1, t => t.EnrollStatus >= (int)EnrollStatus.HasApplyDownloadResume) + //CRO确认列表 状态为 已提交CRO + .WhereIf(param.IntoGroupSearchState == 4, t => t.EnrollStatus >= (int)EnrollStatus.HasCommittedToCRO) + .ProjectTo(_mapper.ConfigurationProvider); + + var doctorPageList = await doctorQuery.ToPagedListAsync(param.PageIndex, param.PageSize, param.SortField == "" ? "Code" : param.SortField, param.Asc); + + + var enrollStateList = await _repository.Where(x => x.TrialId == param.TrialId) + //提交CRO 以及下载简历列表 + .WhereIf(param.IntoGroupSearchState == 1, t => t.EnrollStatus == (int)EnrollStatus.HasCommittedToCRO) + //CRO确认列表 状态为 已提交CRO + .WhereIf(param.IntoGroupSearchState == 4, t => t.EnrollStatus == (int)EnrollStatus.InviteIntoGroup) + .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + + + doctorPageList.CurrentPageData.ToList().ForEach(u => + { + var opt = enrollStateList.FirstOrDefault(t => t.DoctorId == u.Id); + if (opt != null) + { + u.DoctorTrialState = param.IntoGroupSearchState == 1 ? (int)EnrollStatus.HasCommittedToCRO : (int)EnrollStatus.InviteIntoGroup; + u.OptTime = opt.OptTime; + u.OptUserName = opt.OptUserName; + } + }); + + return doctorPageList; + + + + } + + /// + /// 获取项目下医生入组状态列表[Confirmation] + /// + [HttpPost] + public async Task> GetConfirmationReviewerList( + ReviewerConfirmationQueryDTO param) + { + + + var doctorQuery = _repository.Where(x => x.TrialId == param.TrialId && x.EnrollStatus >= (int)EnrollStatus.InviteIntoGroup) + .ProjectTo(_mapper.ConfigurationProvider); + + var doctorPageList = await doctorQuery.ToPagedListAsync(param.PageIndex, param.PageSize, param.SortField == "" ? "Code" : param.SortField, param.Asc); + + var enrollStateList = await _repository.Where(x => x.TrialId == param.TrialId && x.EnrollStatus > (int)EnrollStatus.InviteIntoGroup) + .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + + + + doctorPageList.CurrentPageData.ToList().ForEach(u => + { + u.DoctorTrialState = (int)EnrollStatus.InviteIntoGroup; + var opt = enrollStateList.FirstOrDefault(t => t.DoctorId == u.Id); + if (opt != null) + { + u.DoctorTrialState = opt.IntoGroupState; + u.OptTime = opt.OptTime; + u.OptUserName = opt.OptUserName; + } + }); + + return doctorPageList; + + } + + + + #endregion + + + + + } +} + diff --git a/IRaCIS.Core.Application/Service/Doctor/DoctorService.cs b/IRaCIS.Core.Application/Service/Doctor/DoctorService.cs new file mode 100644 index 00000000..679aecd8 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/DoctorService.cs @@ -0,0 +1,558 @@ +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infrastructure; +using Microsoft.AspNetCore.Mvc; +using System.Linq.Expressions; + +namespace IRaCIS.Application.Services +{ + [ApiExplorerSettings(GroupName = "Reviewer")] + public class DoctorService : BaseService, IDoctorService + { + private readonly IRepository _doctorRepository; + private readonly IRepository _messageRepository; + private readonly IRepository _enrollRepository; + private readonly IRepository _doctorDictionaryRepository; + private readonly IRepository _attachmentRepository; + private readonly IRepository _userDoctorRepository; + private readonly IRepository _trialRepository; + private readonly IRepository _trialExtRepository; + private readonly IRepository _vacationRepository; + + + + public DoctorService(IRepository doctorInfoRepository, + IRepository dictionaryRepository, + IRepository sysMessageRepository, IRepository intoGroupRepository, + IRepository doctorDictionaryRepository, + IRepository attachmentRepository, + IRepository userDoctorRepository, + IRepository trialRepository, + IRepository trialExtRepository, IRepository vacationRepository) + { + _doctorRepository = doctorInfoRepository; + _messageRepository = sysMessageRepository; + _enrollRepository = intoGroupRepository; + _doctorDictionaryRepository = doctorDictionaryRepository; + _attachmentRepository = attachmentRepository; + _userDoctorRepository = userDoctorRepository; + _trialRepository = trialRepository; + _trialExtRepository = trialExtRepository; + _vacationRepository = vacationRepository; + } + + + + + #region 医生基本信息--查询、新增、更新 + + /// + /// 添加/更新 医生基本信息 BasicInfo + /// + + [HttpPost] + public async Task> AddOrUpdateDoctorBasicInfo(DoctorBasicInfoCommand basicInfoModel) + { + Expression> verifyExp = t => t.Phone == basicInfoModel.Phone || t.EMail == basicInfoModel.EMail; + + var verifyPair = new KeyValuePair>, string>(verifyExp, "current phone or email number already existed"); + + if (basicInfoModel.Id == Guid.Empty || basicInfoModel.Id == null) + { + + var doctor = _mapper.Map(basicInfoModel); + + //验证用户手机号 + if (await _doctorRepository.AnyAsync(t => t.Phone == doctor.Phone)) + { + return ResponseOutput.NotOk("The current phone number already existed!", new DoctorBasicInfoCommand()); + } + + if (await _doctorRepository.AnyAsync(t => t.EMail == doctor.EMail)) + { + return ResponseOutput.NotOk("The current email already existed!", new DoctorBasicInfoCommand()); + } + + doctor.Code = await _repository.GetQueryable().MaxAsync(t => t.Code) + 1; + + doctor.ReviewerCode = AppSettings.CodePrefix + doctor.Code.ToString("D4"); + + doctor.Password = MD5Helper.Md5(doctor.Phone); + + //插入中间表 + basicInfoModel.TitleIds.ForEach(titleId => doctor.DoctorDicRelationList.Add(new DoctorDictionary() { DoctorId = doctor.Id, KeyName = StaticData.Title, DictionaryId = titleId })); + + + await _doctorRepository.AddAsync(doctor); + //_doctorRepository.Add(doctor); + + await _repository.AddAsync(new UserDoctor() { DoctorId = doctor.Id, UserId = _userInfo.Id }); + //_userDoctorRepository.Add(new UserDoctor() { DoctorId = doctor.Id, UserId = _userInfo.Id }); + + var success = await _repository.SaveChangesAsync(); + return ResponseOutput.Result(success, _mapper.Map(doctor)); + + } + else + { + var updateModel = basicInfoModel; + + var phone = updateModel.Phone.Trim(); + if ((await _doctorRepository.FirstOrDefaultAsync(t => t.Phone == phone && t.Id != updateModel.Id) )!= null) + { + return ResponseOutput.NotOk("The current phone number already existed!", new DoctorBasicInfoCommand()); + } + var email = updateModel.EMail.Trim(); + if (await _doctorRepository.AnyAsync(t => t.EMail == email && t.Id != updateModel.Id)) + { + return ResponseOutput.NotOk("The current email already existed!", new DoctorBasicInfoCommand()); + } + + var doctor = await _doctorRepository.FirstOrDefaultAsync(t => t.Id == updateModel.Id).IfNullThrowException(); + + //删除中间表 Title对应的记录 + await _repository.DeleteFromQueryAsync(t => t.DoctorId == updateModel.Id && t.KeyName == StaticData.Title); + + + var adddata=new List(); + //重新插入新的 Title记录 + updateModel.TitleIds.ForEach(titleId => adddata.Add(new DoctorDictionary() { DoctorId = updateModel.Id.Value, KeyName = StaticData.Title, DictionaryId = titleId })); + + await _repository.AddRangeAsync(adddata); + + _mapper.Map(basicInfoModel, doctor); + + var success = await _repository.SaveChangesAsync(); + + return ResponseOutput.Result(success, basicInfoModel); + + } + + } + + + /// + ///详情、编辑-获取 医生基本信息 BasicInfo + /// + /// ReviewerID + /// + + [HttpGet("{doctorId:guid}")] + public async Task GetBasicInfo(Guid doctorId) + { + + #region 用导航属性直接查询 + + //SELECT[t].[Id], [t].[Code], [t].[ChineseName], [t].[EMail], [t].[FirstName], [t].[Introduction], [t].[LastName], [t].[Phone], [t].[Sex], [t].[WeChat], [t].[Nation], [t0].[Title], [t0].[TitleCN], [t0].[TitleId], [t0].[ShowOrder], [t0].[Id], [t0].[Id0] + //FROM( + // SELECT TOP(1)[d].[Id], [d].[Code], [d].[ChineseName], [d].[EMail], [d].[FirstName], [d].[Introduction], [d].[LastName], [d].[Phone], [d].[Sex], [d].[WeChat], [d].[Nation] + // FROM[Doctor] AS[d] WITH(NOLOCK) + // WHERE[d].[Id] = @__doctorId_0 + //) AS[t] + //LEFT JOIN( + // SELECT[d1].[Value] AS[Title], [d1].[ValueCN] AS[TitleCN], [d0].[DictionaryId] AS[TitleId], [d1].[ShowOrder], [d0].[Id], [d1].[Id] AS[Id0], [d0].[DoctorId] + // FROM [DoctorDictionary] AS [d0] WITH (NOLOCK) + // INNER JOIN[Dictionary] AS [d1] WITH (NOLOCK) ON [d0].[DictionaryId] = [d1].[Id] + // WHERE[d0].[KeyName] = N'Title' + //) AS[t0] ON[t].[Id] = [t0].[DoctorId] + //ORDER BY[t].[Id], [t0].[ShowOrder], [t0].[Id] + + //var doctorQueryable = _doctorRepository + // .Find(t => t.Id == doctorId) + // .Select(doctor => new DoctorBasicInfoDTO() + // { + // Id = doctor.Id, + // Code = doctor.Code, + // ChineseName = doctor.ChineseName, + // EMail = doctor.EMail, + // FirstName = doctor.FirstName, + // Introduction = doctor.Introduction, + // LastName = doctor.LastName, + // Phone = doctor.Phone, + // Sex = doctor.Sex, + // WeChat = doctor.WeChat, + // Nation = doctor.Nation, + + // //不要分三个属性查询,会做三次左连接,这样 只会一个左连接 + // TempObjList = doctor.DoctorDicList.Where(t => t.KeyName == StaticData.Title) + // .Select(t => new TempObj { Title = t.Dictionary.Value, TitleCN = t.Dictionary.ValueCN, TitleId = t.DictionaryId, ShowOrder = t.Dictionary.ShowOrder }).OrderBy(k => k.ShowOrder).ToList(), + // }); + + //var doctorBasicInfo = doctorQueryable.FirstOrDefault(); + + + + + #endregion + + var doctorBasicInfo = (await _doctorRepository.Where(t => t.Id == doctorId) + .ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync()).IfNullThrowException(); + + return doctorBasicInfo; + + + } + + #endregion + + #region Employment信息--查询和更新 + /// + /// 详情、编辑-获取医生工作信息 Employment + /// + [HttpGet("{doctorId:Guid}")] + public async Task GetEmploymentInfo(Guid doctorId) + { + + #region init EF select 废弃 + //var dic = GetDictionary(); + + //var employmentQueryable = from doctorItem in _doctorRepository + // .Where(t => t.Id == doctorId) + // join hospitalItem in _hospitalRepository.AsQueryable() on doctorItem.HospitalId equals hospitalItem.Id into g + // from hospital in g.DefaultIfEmpty() + // select new EmploymentDTO() + // { + // Id = doctorItem.Id, + // //部门 + // DepartmentId = doctorItem.DepartmentId, + // DepartmentOther = doctorItem.DepartmentOther, + // DepartmentOtherCN = doctorItem.DepartmentOtherCN, + + // //医院 + // HospitalId = doctorItem.HospitalId, + + // PositionId = doctorItem.PositionId, + // PositionOther = doctorItem.PositionOther, + // PositionOtherCN = doctorItem.PositionOtherCN, + + // RankId = doctorItem.RankId, + // RankOther = doctorItem.RankOther, + // RankOtherCN = doctorItem.RankOtherCN, + + // City = hospital.City, + // Country = hospital.Country, + // UniversityAffiliated = hospital.UniversityAffiliated, + // HospitalName = hospital.HospitalName, + // Province = hospital.Province, + + // CityCN = hospital.CityCN, + // CountryCN = hospital.CountryCN, + // UniversityAffiliatedCN = hospital.UniversityAffiliatedCN, + // HospitalNameCN = hospital.HospitalNameCN, + // ProvinceCN = hospital.ProvinceCN + // }; + + //var employmentInfo = employmentQueryable.FirstOrDefault(); + + //if (employmentInfo != null) + //{ + // //医院信息设置 + // if (employmentInfo.HospitalId == Guid.Empty) + // { + // employmentInfo.City = string.Empty; + // employmentInfo.Country = string.Empty; + // employmentInfo.UniversityAffiliated = string.Empty; + // employmentInfo.HospitalName = string.Empty; + // employmentInfo.Province = string.Empty; + // } + // employmentInfo.Department = employmentInfo.DepartmentId == Guid.Empty ? employmentInfo.DepartmentOther : dic.FirstOrDefault(o => o.Id == employmentInfo.DepartmentId)?.Value ?? ""; + + // employmentInfo.Rank = employmentInfo.RankId == Guid.Empty ? employmentInfo.RankOther : dic.FirstOrDefault(o => o.Id == employmentInfo.RankId)?.Value ?? ""; + + // employmentInfo.Position = employmentInfo.PositionId == Guid.Empty ? employmentInfo.PositionOther : dic.FirstOrDefault(o => o.Id == employmentInfo.PositionId)?.Value ?? ""; + + + // employmentInfo.DepartmentCN = employmentInfo.DepartmentId == Guid.Empty ? employmentInfo.DepartmentOther : dic.FirstOrDefault(o => o.Id == employmentInfo.DepartmentId)?.ValueCN ?? ""; + + // employmentInfo.RankCN = employmentInfo.RankId == Guid.Empty ? employmentInfo.RankOther : dic.FirstOrDefault(o => o.Id == employmentInfo.RankId)?.ValueCN ?? ""; + + // employmentInfo.PositionCN = employmentInfo.PositionId == Guid.Empty ? employmentInfo.PositionOther : dic.FirstOrDefault(o => o.Id == employmentInfo.PositionId)?.ValueCN ?? ""; + //} + #endregion + + var query = _doctorRepository.Where(t => t.Id == doctorId) + .ProjectTo(_mapper.ConfigurationProvider); + + var employmentInfo = (await query.FirstOrDefaultAsync()).IfNullThrowException(); + + return employmentInfo; + } + + [HttpPost] + + public async Task UpdateEmploymentInfo(EmploymentCommand doctorWorkInfoModel) + { + #region 废弃 + //var success = _doctorRepository.Update(d => d.Id == doctorWorkInfoModel.Id, u => new Doctor() + //{ + // DepartmentId = doctorWorkInfoModel.DepartmentId, + // DepartmentOther = doctorWorkInfoModel.DepartmentOther, + // DepartmentOtherCN = doctorWorkInfoModel.DepartmentOtherCN, + + // SpecialityId = doctorWorkInfoModel.DepartmentId, + // SpecialityOther = doctorWorkInfoModel.DepartmentOther, + // SpecialityOtherCN = doctorWorkInfoModel.DepartmentOtherCN, + + // RankId = doctorWorkInfoModel.RankId, + // RankOther = doctorWorkInfoModel.RankOther, + // RankOtherCN = doctorWorkInfoModel.RankOtherCN, + + // PositionId = doctorWorkInfoModel.PositionId, + // PositionOther = doctorWorkInfoModel.PositionOther, + // PositionOtherCN = doctorWorkInfoModel.PositionOtherCN, + + // HospitalId = doctorWorkInfoModel.HospitalId, + // UpdateTime = DateTime.Now + //}); + + //var doctor = _doctorRepository.FirstOrDefault(d => d.Id == doctorWorkInfoModel.Id); + //_mapper.Map(doctorWorkInfoModel, doctor); + //var success = _doctorRepository.SaveChanges(); + #endregion + + + var entity = await _repository.InsertOrUpdateAsync(doctorWorkInfoModel, true); + + //_doctorRepository.UseMapper(_mapper).InsertOrUpdate(doctorWorkInfoModel, autoSave: true); + + return ResponseOutput.Ok(); + } + + #endregion + + + #region 医生技能信息 查询和 更新 + [HttpGet, Route("{doctorId:Guid}")] + public async Task GetSpecialtyInfo(Guid doctorId) + { + + #region 利用导航属性直接查询出来 生成的sql ok 废弃 + + //var specialtyQueryable = _doctorRepository + // .Where(t => t.Id == doctorId).Include(u => u.DoctorDicRelationList) + // .Select(specialty => new SpecialtyDTO() + // { + // Id = specialty.Id, + // ReadingTypeOther = specialty.ReadingTypeOther, + // ReadingTypeOtherCN = specialty.ReadingTypeOtherCN, + + // SubspecialityOther = specialty.SubspecialityOther, + // SubspecialityOtherCN = specialty.SubspecialityOtherCN, + + + // DictionaryList = specialty.DoctorDicRelationList.Where(t => t.KeyName == StaticData.ReadingType || t.KeyName == StaticData.Subspeciality) + // .Select(t => new SpecialtyDTO.DoctorDictionaryView() { DictionaryId = t.DictionaryId, Value = t.Dictionary.Value, ValueCN = t.Dictionary.ValueCN, ShowOrder = t.Dictionary.ShowOrder, KeyName = t.Dictionary.KeyName }) + // .OrderBy(t => t.ShowOrder).ToList(), + + // SpecialityId = specialty.SpecialityId, + // Speciality = specialty.Speciality.Value, + // SpecialityCN = specialty.Speciality.ValueCN, + + // SpecialityOther = specialty.SpecialityOther, + // SpecialityOtherCN = specialty.SpecialityOtherCN + // }); + + //var specialtyInfo = specialtyQueryable.FirstOrDefault(); + + //return specialtyInfo; + + #endregion + + + var test = await (_doctorRepository.Where(t => t.Id == doctorId) + .ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync()).IfNullThrowException(); + + return test; + } + + + [HttpPost] + public async Task UpdateSpecialtyInfo(SpecialtyCommand specialtyUpdateModel) + { + var doctor = await _doctorRepository.FirstOrDefaultAsync(t => t.Id == specialtyUpdateModel.Id); + + if (doctor == null) return Null404NotFound(doctor); + + + + ////删除中间表 + //_doctorDictionaryRepository.Delete(t => + // t.DoctorId == specialtyUpdateModel.Id && t.KeyName == StaticData.Subspeciality); + //_doctorDictionaryRepository.Delete(t => + // t.DoctorId == specialtyUpdateModel.Id && t.KeyName == StaticData.ReadingType); + + await _repository.DeleteFromQueryAsync(t => + t.DoctorId == specialtyUpdateModel.Id && (t.KeyName == StaticData.Subspeciality || t.KeyName == StaticData.ReadingType)); + + + + + + //重新插入新的 + var adddata = new List(); + specialtyUpdateModel.ReadingTypeIds.ForEach(readingTypeId => adddata.Add( + new DoctorDictionary() + { + DoctorId = specialtyUpdateModel.Id, + KeyName = StaticData.ReadingType, + DictionaryId = readingTypeId + })); + specialtyUpdateModel.SubspecialityIds.ForEach(subspecialityId => adddata.Add( + new DoctorDictionary() + { + DoctorId = specialtyUpdateModel.Id, + KeyName = StaticData.Subspeciality, + DictionaryId = subspecialityId + })); + + await _repository.AddRangeAsync(adddata); + + _mapper.Map(specialtyUpdateModel, doctor); + + var success = await _repository.SaveChangesAsync(); + return ResponseOutput.Result(success); + + + } + #endregion + + #region 简历审核 + + [HttpPost] + public async Task UpdateAuditResume(ResumeConfirmCommand auditResumeParam) + { + var userId = _userInfo.Id; + //判断 合作协议、正式简历 是否有。如果没有,显示提示信息,并且不能保存 + var attachmentList = await _repository.GetQueryable().Where(u => u.DoctorId == auditResumeParam.Id) + .Select(u => u.Type).ToListAsync(); + if (auditResumeParam.ResumeStatus == ResumeStatusEnum.Pass && ((!attachmentList.Contains("Resume")) || (!attachmentList.Contains("Consultant Agreement")))) + { + return ResponseOutput.NotOk("Resume & Consultant Agreement must be upload "); + } + var success = await _doctorRepository.UpdateFromQueryAsync(o => o.Id == auditResumeParam.Id, u => new Doctor() + { + CooperateStatus = auditResumeParam.CooperateStatus, + ResumeStatus = auditResumeParam.ResumeStatus, + AdminComment = auditResumeParam.AdminComment, + ReviewStatus = auditResumeParam.ReviewStatus, + AcceptingNewTrial = auditResumeParam.AcceptingNewTrial, + ActivelyReading = auditResumeParam.ActivelyReading, + AuditTime = DateTime.Now, + AuditUserId = userId + }); + + if (success) + { + if (!string.IsNullOrWhiteSpace(auditResumeParam.MessageContent)) + { + var message = new Message + { + FromUserId = userId, + ToDoctorId = auditResumeParam.Id, + Title = "Resume review results", + Content = auditResumeParam.MessageContent, + HasRead = false, + MessageTime = DateTime.Now + }; + await _repository.AddAsync(message); + success = await _repository.SaveChangesAsync(); + } + } + + return ResponseOutput.Result(success); + } + + + [HttpGet("{doctorId:guid}")] + public async Task GetAuditState(Guid doctorId) + { + var doctor = (await _doctorRepository + .ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync(t => t.Id == doctorId)).IfNullThrowException(); + + doctor.InHoliday = (await _repository.CountAsync(x=>x.DoctorId==doctorId&&x.EndDate<=DateTime.Now&&x.StartDate<=DateTime.Now)) > 0; + + return doctor; + } + + + /// + /// 获取医生入组信息 正在提交的数量 已同意入组项目个数 正在读的 + /// + [HttpPost, Route("{doctorId:guid}")] + public DoctorEnrollInfoDTO GetDoctorIntoGroupInfo(Guid doctorId) + { + var doctorQueryable = + from doctor in _doctorRepository.Where(t => t.Id == doctorId) + join intoGroupItem in _enrollRepository.AsQueryable() on doctor.Id equals intoGroupItem.DoctorId + into t + from intoGroupItem in t.DefaultIfEmpty() + group intoGroupItem by intoGroupItem.DoctorId + into g + select new DoctorEnrollInfoDTO + { + DoctorId = g.Key, + + //Submitted = g.Sum(t => + // t.EnrollStatus == (int)EnrollStatus.HasCommittedToCRO ? 1 : 0), + //Approved = g.Sum(t => + // t.EnrollStatus == (int)EnrollStatus.InviteIntoGroup ? 1 : 0), + //Reading = g.Sum(t => + // t.EnrollStatus == (int)EnrollStatus.DoctorReading ? 1 : 0) + + Submitted = g.Count(t => + t.EnrollStatus == (int)EnrollStatus.HasCommittedToCRO), + Approved = g.Count(t => + t.EnrollStatus == (int)EnrollStatus.InviteIntoGroup), + Reading = g.Count(t => + t.EnrollStatus == (int)EnrollStatus.DoctorReading) + }; + + return doctorQueryable.FirstOrDefault().IfNullThrowException(); + } + + + /// + /// Get Statement of Work list.[New] + /// + [HttpGet("{doctorId:guid}")] + public List GetDoctorSowList(Guid doctorId) + { + var query = from enroll in _enrollRepository.Where(u => u.DoctorId == doctorId && u.EnrollStatus >= 10) + join trialExt in _trialExtRepository.AsQueryable() on enroll.TrialId equals trialExt.TrialId + join trial in _trialRepository.AsQueryable() on enroll.TrialId equals trial.Id + select new SowDTO + { + FileName = trialExt.SowName, + FilePath = trialExt.SowPath, + TrialCode = trial.TrialCode, + CreateTime = trialExt.CreateTime + }; + return query.ToList().Where(u => !string.IsNullOrWhiteSpace(u.FileName)).ToList(); + } + + /// + /// Get Ack Statement of Work[New] + /// + [HttpGet("{doctorId:guid}")] + public List GetDoctorAckSowList(Guid doctorId) + { + var query = from enroll in _enrollRepository.Where(u => u.DoctorId == doctorId) + join attachment in _attachmentRepository.Where(u => u.DoctorId == doctorId) + on enroll.AttachmentId equals attachment.Id + join trial in _trialRepository.AsQueryable() on enroll.TrialId equals trial.Id + select new SowDTO + { + FileName = attachment.FileName, + FilePath = attachment.Path, + TrialCode = trial.TrialCode, + CreateTime = attachment.CreateTime + }; + return query.ToList(); + } + + #endregion + + } +} diff --git a/IRaCIS.Core.Application/Service/Doctor/EducationService.cs b/IRaCIS.Core.Application/Service/Doctor/EducationService.cs new file mode 100644 index 00000000..a9306dff --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/EducationService.cs @@ -0,0 +1,140 @@ + +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Application.Services +{ + [ApiExplorerSettings(GroupName = "Reviewer")] + public class EducationService : BaseService, IEducationService + { + private readonly IRepository _postgraduateRepository; + private readonly IRepository _educationRepository; + + public EducationService(IRepository doctorNormalEducationRepository, + IRepository doctorContinueLearningRepository) + { + _educationRepository = doctorNormalEducationRepository; + _postgraduateRepository = doctorContinueLearningRepository; + } + + /// + /// 根据医生Id获取医生教育经历和继续学习经历列表 + /// + [HttpGet("{doctorId:Guid}")] + public async Task GetEducation(Guid doctorId) + { + var educationList = await _educationRepository.Where(o => o.DoctorId == doctorId) + .OrderBy(t => t.CreateTime).ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + + var postgraduateList = await _repository.GetQueryable().Where(o => o.DoctorId == doctorId) + .OrderBy(t => t.CreateTime).ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + + + return new DoctorEducationExperienceDTO() + { + EducationList = educationList, + PostgraduateList = postgraduateList + }; + + } + /// + /// 新增医生教育经历 + /// + /// + /// + [HttpPost] + public async Task AddOrUpdateEducationInfo(EducationCommand educationInfoViewModel) + { + if (educationInfoViewModel.Id == Guid.Empty || educationInfoViewModel.Id == null) + { + var doctorEducationInfo = _mapper.Map(educationInfoViewModel); + switch (educationInfoViewModel.Degree) + { + case StaticData.Bachelor: + doctorEducationInfo.ShowOrder = 1; + break; + case StaticData.Master: + doctorEducationInfo.ShowOrder = 2; + break; + case StaticData.Doctorate: + doctorEducationInfo.ShowOrder = 3; + break; + } + await _educationRepository.AddAsync(doctorEducationInfo); + var success = await _repository.SaveChangesAsync(); + + return ResponseOutput.Result(success, doctorEducationInfo.Id.ToString()); + } + + else + { + var needUpdate = await _educationRepository.FirstOrDefaultAsync(t => t.Id == educationInfoViewModel.Id); + + if (needUpdate == null) return Null404NotFound(needUpdate); + + _mapper.Map(educationInfoViewModel, needUpdate); + + var success = await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(success); + + } + //_educationRepository.Update(needUpdate); + + + } + + [HttpDelete, Route("{doctorId:guid}")] + public async Task DeleteEducationInfo(Guid id) + { + var success = await _educationRepository.DeleteFromQueryAsync(o => o.Id == id); + + return ResponseOutput.Result(success); + } + /// 添加/更新医生继续学习经历 + + [HttpPost] + public async Task AddOrUpdatePostgraduateInfo(PostgraduateCommand postgraduateViewModel) + { + #region 封装前 + + //if (postgraduateViewModel.Id == Guid.Empty || postgraduateViewModel.Id == null) + //{ + // var doctorContinueLearning = _mapper.Map(postgraduateViewModel); + // _postgraduateRepository.Add(doctorContinueLearning); + // var success = _postgraduateRepository.SaveChanges(); + + // return ResponseOutput.Result(success, doctorContinueLearning.Id.ToString()); + //} + //else + //{ + // _postgraduateRepository.Update(_mapper.Map(postgraduateViewModel)); + // var success = _postgraduateRepository.SaveChanges(); + + // return ResponseOutput.Result(success); + + //} + #endregion + + var entity = await _repository.InsertOrUpdateAsync(postgraduateViewModel, true); + return ResponseOutput.Ok(entity.Id); + } + /// + /// 删除医生继续学习经历 + /// + /// 医生Id + /// + + [HttpDelete("{doctorId:guid}")] + public async Task DeletePostgraduateInfo(Guid doctorId) + { + var success = await _repository.DeleteFromQueryAsync(o => o.Id == doctorId); + return ResponseOutput.Result(success); + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Doctor/Interface/IAttachmentService.cs b/IRaCIS.Core.Application/Service/Doctor/Interface/IAttachmentService.cs new file mode 100644 index 00000000..f93c4786 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/Interface/IAttachmentService.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +using IRaCIS.Application.Contracts; + +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface IAttachmentService + { + Task> SaveAttachments(IEnumerable attachmentList); + + + Task> AddAttachment(AttachmentDTO attachment); + Task DeleteAttachment(AttachementCommand param); + Task GetDetailById(Guid attachmentId); + Task> GetAttachmentByType(Guid doctorId, string type); + Task> GetAttachmentByTypes(Guid doctorId, string[] types); + Task> GetAttachments(Guid doctorId); + Task GetDoctorOfficialCV(int language, Guid doctorId); + + Task SetOfficial(Guid doctorId, Guid attachmentId, int language); + + Task SetLanguage(Guid doctorId, Guid attachmentId, int language); + } +} diff --git a/IRaCIS.Core.Application/Service/Doctor/Interface/IDoctorAccountService.cs b/IRaCIS.Core.Application/Service/Doctor/Interface/IDoctorAccountService.cs new file mode 100644 index 00000000..44d21496 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/Interface/IDoctorAccountService.cs @@ -0,0 +1,14 @@ +using System; + +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface IDoctorAccountService + { + IResponseOutput Register(DoctorAccountRegisterModel doctorAccount); + DoctorAccountDTO Login(DoctorAccountLoginDTO doctorAccount); + IResponseOutput UpdatePassword(DoctorAccountUpdatePasswordCommand doctorAccount); + } +} diff --git a/IRaCIS.Core.Application/Service/Doctor/Interface/IDoctorListQueryService.cs b/IRaCIS.Core.Application/Service/Doctor/Interface/IDoctorListQueryService.cs new file mode 100644 index 00000000..8c8faf99 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/Interface/IDoctorListQueryService.cs @@ -0,0 +1,32 @@ +using IRaCIS.Application.Contracts; + +namespace IRaCIS.Application.Interfaces +{ + public interface IDoctorListQueryService + { + /// + /// 医生多条件查询 + /// + Task> GetDoctorSearchList(DoctorSearchDTO param); + + /// + /// 筛选医生列表 + /// + /// + /// + // + Task> GetSelectionReviewerList( + ReviewerSelectionQueryDTO doctorSearchModel); + + /// + /// //入组 相关接口 (提交CRO-1) CRO确认-4 + /// + Task> GetSubmissionOrApprovalReviewerList( + ReviewerSubmissionQueryDTO doctorIntoGroupSearchModel); + + + //医生确认状态列表 + Task> GetConfirmationReviewerList( + ReviewerConfirmationQueryDTO trialIdPageModel); + } +} diff --git a/IRaCIS.Core.Application/Service/Doctor/Interface/IDoctorService.cs b/IRaCIS.Core.Application/Service/Doctor/Interface/IDoctorService.cs new file mode 100644 index 00000000..cffc2524 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/Interface/IDoctorService.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface IDoctorService + { + #region 医生 基本信息 + + /// + /// 基本信息详情展示、编辑使用 + /// + /// + /// + Task GetBasicInfo(Guid doctorId); + + /// + /// 添加医生基本信息 + /// + /// + /// + Task> AddOrUpdateDoctorBasicInfo(DoctorBasicInfoCommand addBasicInfoParam); + + #endregion + + #region 医生 工作信息 + + /// + /// 获取医生 工作信息 + /// + /// + /// + Task GetEmploymentInfo(Guid doctorId); + /// + /// 更新医生 工作信息 + /// + /// + /// + Task UpdateEmploymentInfo(EmploymentCommand updateDoctorWorkInfoViewModel); + + #endregion + + /// + /// 获取医生技能信息 + /// + Task GetSpecialtyInfo(Guid doctorId); + + /// + /// 更新医生技能信息 + /// + Task UpdateSpecialtyInfo(SpecialtyCommand specialtyUpdateModel); + + /// + /// 获取医生 审核状态 + /// + Task GetAuditState(Guid doctorId); + + /// + /// 审核简历 和合作关系 + /// + Task UpdateAuditResume(ResumeConfirmCommand auditResumeParam); + + /// 医生详情 入组信息 + DoctorEnrollInfoDTO GetDoctorIntoGroupInfo(Guid doctorId); + + /// 获取医生参与项目的Sow协议 + List GetDoctorSowList(Guid doctorId); + + /// 获取医生入组的 ack Sow + List GetDoctorAckSowList(Guid doctorId); + } +} diff --git a/IRaCIS.Core.Application/Service/Doctor/Interface/IEducationService.cs b/IRaCIS.Core.Application/Service/Doctor/Interface/IEducationService.cs new file mode 100644 index 00000000..958fdaed --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/Interface/IEducationService.cs @@ -0,0 +1,26 @@ +using System; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface IEducationService + { + + Task GetEducation(Guid doctorId); + + #region 教育经历 + Task AddOrUpdateEducationInfo(EducationCommand doctorEducationInfoViewModel); + + Task DeleteEducationInfo(Guid doctorId); + + #endregion + + #region 继续教育经历 + Task AddOrUpdatePostgraduateInfo(PostgraduateCommand doctorContinueLearningViewModel); + Task DeletePostgraduateInfo(Guid doctorId); + + #endregion + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Doctor/Interface/IResearchPublicationService.cs b/IRaCIS.Core.Application/Service/Doctor/Interface/IResearchPublicationService.cs new file mode 100644 index 00000000..ba983ded --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/Interface/IResearchPublicationService.cs @@ -0,0 +1,13 @@ +using System; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface IResearchPublicationService + { + Task GetResearchPublication(Guid doctorId); + + Task AddOrUpdateResearchPublication(ResearchPublicationDTO param); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Doctor/Interface/ITrialExperienceService.cs b/IRaCIS.Core.Application/Service/Doctor/Interface/ITrialExperienceService.cs new file mode 100644 index 00000000..bb803c37 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/Interface/ITrialExperienceService.cs @@ -0,0 +1,15 @@ +using System; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface ITrialExperienceService + { + Task GetTrialExperience(Guid doctorId); + Task AddOrUpdateTrialExperience(TrialExperienceCommand model); + Task DeleteTrialExperience(Guid id); + Task UpdateGcpExperience(GCPExperienceCommand model); + Task UpdateOtherExperience(ClinicalExperienceCommand updateOtherClinicalExperience); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Doctor/Interface/IVacationService.cs b/IRaCIS.Core.Application/Service/Doctor/Interface/IVacationService.cs new file mode 100644 index 00000000..834864a9 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/Interface/IVacationService.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +using IRaCIS.Application.Contracts; + +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface IVacationService + { + Task AddOrUpdateVacation(VacationCommand vacationViewModel); + Task DeleteVacation(Guid id); + Task> GetVacationList(Guid doctorId, int pageIndex, int pageSize); + + /// 判断当前时间是否在休假 + Task OnVacation(Guid reviewerId); + } +} diff --git a/IRaCIS.Core.Application/Service/Doctor/ResearchPublicationService.cs b/IRaCIS.Core.Application/Service/Doctor/ResearchPublicationService.cs new file mode 100644 index 00000000..9d2bb6df --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/ResearchPublicationService.cs @@ -0,0 +1,46 @@ +using AutoMapper.QueryableExtensions; +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Application.Services +{ + [ApiExplorerSettings(GroupName = "Reviewer")] + public class ResearchPublicationService : BaseService, IResearchPublicationService + { + private readonly IRepository researchPublicationRepository; + + public ResearchPublicationService(IRepository _researchPublicationRepository) + { + researchPublicationRepository = _researchPublicationRepository; + } + + /// + /// 查询-医生科学研究信息 + /// + /// 医生Id + /// + [HttpGet("{doctorId:guid}")] + public async Task GetResearchPublication(Guid doctorId) + { + var doctorScientificResearchInfo = await researchPublicationRepository.Where(o => o.DoctorId == doctorId) + .ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync(); + + return doctorScientificResearchInfo; + } + + + [HttpPost] + public async Task AddOrUpdateResearchPublication(ResearchPublicationDTO param) + { + + var entity = await _repository.InsertOrUpdateAsync(param, true); + + return ResponseOutput.Ok(entity.Id); + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Doctor/TrialExperienceService.cs b/IRaCIS.Core.Application/Service/Doctor/TrialExperienceService.cs new file mode 100644 index 00000000..704acfb1 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/TrialExperienceService.cs @@ -0,0 +1,189 @@ +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Application.Services +{ + [ApiExplorerSettings(GroupName = "Reviewer")] + public class TrialExperienceService : BaseService, ITrialExperienceService + { + //private readonly IRepository _trialExperienceRepository; + //private readonly IRepository _doctorRepository; + //private readonly IRepository _attachmentRepository; + //private readonly IRepository _trialExperienceCriteriaRepository; + + + + //public TrialExperienceService(IRepository trialExperienceRepository, IRepository doctorRepository, IRepository attachmentRepository, + // IRepository trialExperienceCriteriaRepository) + //{ + // _trialExperienceRepository = trialExperienceRepository; + // _doctorRepository = doctorRepository; + // _attachmentRepository = attachmentRepository; + // _trialExperienceCriteriaRepository = trialExperienceCriteriaRepository; + + //} + + private IQueryable _doctor => _repository.GetQueryable(); + private IQueryable _attachment => _repository.GetQueryable(); + private IQueryable _trialExperience => _repository.GetQueryable(); + private IQueryable _trialExperienceCriteria => _repository.GetQueryable(); + + /// + /// 根据医生Id,获取临床试验经历 界面所有数据 + /// + [HttpGet("{doctorId:guid}")] + public async Task GetTrialExperience(Guid doctorId) + { + var trialExperience = new TrialExperienceModel(); + + var doctor = await _doctor.Where(o => o.Id == doctorId) + .ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync(); + + trialExperience.ClinicalTrialExperienceList = await GetTrialExperienceList(doctorId); + + if (doctor != null) + { + trialExperience.GCP = doctor.GCP; + trialExperience.Id = doctor.Id; + trialExperience.OtherClinicalExperience = doctor.OtherClinicalExperience ?? ""; + trialExperience.OtherClinicalExperienceCN = doctor.OtherClinicalExperienceCN ?? ""; + var attachment = await _attachment.FirstOrDefaultAsync(t => t.Id == doctor.GCPId); + if (attachment != null) + { + trialExperience.ExpiryDateStr = attachment.ExpiryDate == null ? "" : attachment.ExpiryDate.Value.ToString("yyyy-MM-dd HH:mm"); + + trialExperience.Path = attachment.Path; + trialExperience.GCPFullPath = attachment.Path + "?access_token=" + _userInfo.UserToken; + trialExperience.Type = attachment.Type; + trialExperience.FileName = attachment.FileName; + trialExperience.GCPId = attachment.Id; + } + } + + return trialExperience; + } + + private async Task> GetTrialExperienceList(Guid doctorId) + { + var doctorClinicalTrialExperienceList = await _trialExperience.Where(o => o.DoctorId == doctorId).OrderBy(t => t.CreateTime) + .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + + return doctorClinicalTrialExperienceList; + + } + /// 添加或更新医生临床经验列表项 + + [HttpPost] + public async Task AddOrUpdateTrialExperience(TrialExperienceCommand trialExperienceViewModel) + { + if (trialExperienceViewModel.Id == Guid.Empty || trialExperienceViewModel.Id == null) + { + var trialExperience = + _mapper.Map(trialExperienceViewModel); + + trialExperience = await _repository.AddAsync(trialExperience); + + List criteriaList = new List(); + trialExperienceViewModel.EvaluationCriteriaIdList.ForEach(t => criteriaList.Add(new TrialExperienceCriteria() + { + DoctorId = trialExperienceViewModel.DoctorId, + //EvaluationCriteria = t.EvaluationCriteria, + EvaluationCriteriaId = t, + TrialExperienceId = trialExperience.Id + })); + + await _repository.AddRangeAsync(criteriaList); + + + var success = await _repository.SaveChangesAsync(); + return ResponseOutput.Result(success, trialExperience.Id); + } + else + { + var needUpdate = await _trialExperience.FirstOrDefaultAsync(t => t.Id == trialExperienceViewModel.Id); + + if (needUpdate == null) return Null404NotFound(needUpdate); + + _mapper.Map(trialExperienceViewModel, needUpdate); + await _repository.UpdateAsync(needUpdate); + + await _repository.DeleteFromQueryAsync(t => t.TrialExperienceId == needUpdate.Id); + + List criteriaList = new List(); + + trialExperienceViewModel.EvaluationCriteriaIdList.ForEach(t => criteriaList.Add(new TrialExperienceCriteria() + { + DoctorId = trialExperienceViewModel.DoctorId, + EvaluationCriteriaId = t, + TrialExperienceId = needUpdate.Id + })); + + await _repository.AddRangeAsync(criteriaList); + + + var success = await _repository.SaveChangesAsync(); + return ResponseOutput.Result(success, trialExperienceViewModel.Id); + } + } + + /// + /// 删除临床经验 + /// + + [HttpDelete, Route("{doctorId:guid}")] + public async Task DeleteTrialExperience(Guid doctorId) + { + var success = await _repository.DeleteFromQueryAsync(o => o.Id == doctorId); + return ResponseOutput.Result(success); + } + /// + /// 更新-GCP和其他临床经验 + /// + /// + /// + + [HttpPost] + public async Task UpdateGcpExperience(GCPExperienceCommand updateGCPExperienceParam) + { + //_attachmentRepository.Delete(t => t.DoctorId == updateGCPExperienceParam.Id && t.Type == StaticData.GCP); + + var successs = await _repository.UpdateFromQueryAsync(o => o.Id == updateGCPExperienceParam.Id, u => new Doctor() + { + GCP = updateGCPExperienceParam.GCP, + GCPId = updateGCPExperienceParam.GCP==0&&updateGCPExperienceParam.GCPId==null?Guid.Empty: updateGCPExperienceParam.GCPId!.Value + }); + + if (updateGCPExperienceParam.GCP == 0 && updateGCPExperienceParam.GCPId != null) + { + await _repository.DeleteFromQueryAsync(a => a.Id == updateGCPExperienceParam.GCPId); + } + + + + return ResponseOutput.Result(successs, updateGCPExperienceParam.GCPId.ToString()); + + } + + /// + /// 更新其他技能经验 + /// + + [HttpPost] + public async Task UpdateOtherExperience(ClinicalExperienceCommand updateOtherClinicalExperience) + { + var success = await _repository.UpdateFromQueryAsync(o => o.Id == updateOtherClinicalExperience.DoctorId, u => new Doctor() + { + OtherClinicalExperience = updateOtherClinicalExperience.OtherClinicalExperience ?? string.Empty, + OtherClinicalExperienceCN = updateOtherClinicalExperience.OtherClinicalExperienceCN ?? string.Empty + }); + + return ResponseOutput.Result(success); + } + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Doctor/VacationService.cs b/IRaCIS.Core.Application/Service/Doctor/VacationService.cs new file mode 100644 index 00000000..2b46ccff --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/VacationService.cs @@ -0,0 +1,88 @@ +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Mvc; +using Panda.DynamicWebApi.Attributes; + +namespace IRaCIS.Application.Services +{ + [ ApiExplorerSettings(GroupName = "Reviewer")] + public class VacationService : BaseService, IVacationService + { + private readonly IRepository _vacationRepository; + + public VacationService(IRepository vacationRepository) + { + _vacationRepository = vacationRepository; + } + /// + /// 添加休假时间段 + /// + /// Status不传 + /// + + [HttpPost] + public async Task AddOrUpdateVacation(VacationCommand param) + { + if (param.Id == Guid.Empty|| param.Id ==null) + { + var result = await _vacationRepository.AddAsync(_mapper.Map(param)); + + var success = await _repository.SaveChangesAsync(); + + return ResponseOutput.Result(success, result.Id); + + } + else + { + var success = await _vacationRepository.UpdateFromQueryAsync(u => u.Id == param.Id, + h => new Vacation + { + StartDate = param.StartDate, + EndDate = param.EndDate + }); + + return ResponseOutput.Result(success); + + } + + } + /// + /// 删除休假时间段 + /// + /// 记录Id + /// + + [HttpDelete("{holidayId:guid}")] + public async Task DeleteVacation(Guid holidayId) + { + var success = await _vacationRepository.DeleteFromQueryAsync(u => u.Id == holidayId); + + return ResponseOutput.Result(success); + + } + + /// + /// 获取休假时间段列表 + /// + /// + [HttpGet("{doctorId:guid}/{pageIndex:int}/{pageSize:int}")] + public async Task> GetVacationList(Guid doctorId, int pageIndex, int pageSize) + { + var query = _vacationRepository.Where(u => u.DoctorId == doctorId) + .ProjectTo(_mapper.ConfigurationProvider); + + return await query.ToPagedListAsync(pageIndex, pageSize, "StartDate"); + + } + + [NonDynamicMethod] + public async Task OnVacation(Guid doctorId) + { + var count = await _vacationRepository.CountAsync(u => u.DoctorId == doctorId && u.EndDate >= DateTime.Now && u.StartDate <= DateTime.Now); + + return ResponseOutput.Result(count > 0); + } + + } +} diff --git a/IRaCIS.Core.Application/Service/Doctor/_MapConfig.cs b/IRaCIS.Core.Application/Service/Doctor/_MapConfig.cs new file mode 100644 index 00000000..1f8c11ff --- /dev/null +++ b/IRaCIS.Core.Application/Service/Doctor/_MapConfig.cs @@ -0,0 +1,152 @@ +using AutoMapper; +using AutoMapper.EquivalencyExpression; +using IRaCIS.Application.Contracts; +using IRaCIS.Application.Contracts.Pay; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Domain.Share; + +namespace IRaCIS.Core.Application.Service +{ + public class DoctorConfig : Profile + { + public DoctorConfig() + { + #region reviewer + + //基本信息 工作信息 添加时转换使用 + CreateMap().EqualityComparison((odto, o) => odto.Id == o.Id); + + //学习经历 添加时转换使用 + CreateMap().EqualityComparison((odto, o) => odto.Id == o.Id); + CreateMap().EqualityComparison((odto, o) => odto.Id == o.Id); + CreateMap().EqualityComparison((odto, o) => odto.Id == o.Id); + CreateMap().EqualityComparison((odto, o) => odto.Id == o.Id); + + //医生账户 + CreateMap(); + CreateMap(); + + CreateMap().EqualityComparison((odto, o) => odto.Id == o.Id); + + + CreateMap().EqualityComparison((odto, o) => odto.Id == o.Id); + + CreateMap().EqualityComparison((odto, o) => odto.Id == o.Id); + + + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap(); + + #endregion + CreateMap(); + + CreateMap(); + + CreateMap().ReverseMap(); + + //医生列表、项目显示列表模型转换 + CreateMap(); + + CreateMap() + .ForMember(d => d.UserTypeShortName, u => u.MapFrom(t => t.UserTypeRole.UserTypeShortName)) + .ForMember(d => d.Code, u => u.MapFrom(t => t.UserCode)) + .ForMember(d => d.PermissionStr, u => u.MapFrom(t => t.UserTypeRole.PermissionStr)) + .ForMember(d => d.RealName, u => u.MapFrom(user => string.IsNullOrEmpty(user.FirstName) ? user.LastName : user.LastName + " / " + user.FirstName)); + + CreateMap() + .ForMember(d => d.Phase, u => u.MapFrom(t => t.Phase.Value)) + .ForMember(d => d.EvaluationCriteriaList, u => u.MapFrom(t => t.ExperienceCriteriaList.Select(t => t.EvaluationCriteria.Value))) + .ForMember(d => d.EvaluationCriteriaIdList, u => u.MapFrom(t => t.ExperienceCriteriaList.Select(t => t.EvaluationCriteriaId))); + + CreateMap() + .ForMember(d => d.Code, u => u.MapFrom(t => t.ReviewerCode)) + .ForMember(d => d.RealName, u => u.MapFrom(t => t.ChineseName)) + .ForMember(d => d.IsReviewer, u => u.MapFrom(t => true)) + .ForMember(d => d.UserName, u => u.MapFrom(doctor => doctor.LastName + " / " + doctor.FirstName)); + + #region 医生基本信息 + CreateMap(); + CreateMap().IncludeMembers(t => t.Hospital).Include() + .ForMember(d => d.Department, u => u.MapFrom(s => s.Department.Value)) + .ForMember(d => d.DepartmentCN, u => u.MapFrom(s => s.Department.ValueCN)) + .ForMember(d => d.Position, u => u.MapFrom(s => s.Position.Value)) + .ForMember(d => d.PositionCN, u => u.MapFrom(s => s.Position.ValueCN)) + .ForMember(d => d.Rank, u => u.MapFrom(s => s.Rank.Value)) + .ForMember(d => d.RankCN, u => u.MapFrom(s => s.Rank.ValueCN)) + .ForMember(d => d.Speciality, u => u.MapFrom(s => s.Speciality.Value)) + .ForMember(d => d.SpecialityCN, u => u.MapFrom(s => s.Speciality.ValueCN)) + .ForMember(d => d.HasResume, u => u.MapFrom(s => s.AttachmentList.Any(u => u.Type == "Resume" && u.IsOfficial))) + .ForMember(d => d.Submitted, u => u.MapFrom(s => s.EnrollList.Count(t => t.EnrollStatus == (int)EnrollStatus.HasCommittedToCRO))) + .ForMember(d => d.Approved, u => u.MapFrom(s => s.EnrollList.Count(t => t.EnrollStatus == (int)EnrollStatus.InviteIntoGroup))) + .ForMember(d => d.Reading, u => u.MapFrom(s => s.EnrollList.Count(t => t.EnrollStatus == (int)EnrollStatus.DoctorReading))) + .ForMember(d => d.Finished, u => u.MapFrom(s => s.EnrollList.Count(t => t.EnrollStatus == (int)EnrollStatus.Finished))) + .ForMember(d => d.Reconfirmed, u => u.MapFrom(s => s.ReviewStatus == 1)) + .ForMember(o => o.DictionaryList, t => t.MapFrom(u => u.DoctorDicRelationList.Where(t => t.KeyName == StaticData.ReadingType || t.KeyName == StaticData.Subspeciality).Select(t => t.Dictionary).OrderBy(t => t.ShowOrder))); + CreateMap(); + + CreateMap(); + + + //这样会左连接三次 + // CreateMap() + //.ForMember(d => d.TitleCNList, u => u.MapFrom(s => s.DoctorDicRelationList.Where(t => t.KeyName == StaticData.Title) + //.Select(t => new { TitleCN = t.Dictionary.ValueCN, ShowOrder = t.Dictionary.ShowOrder }).OrderBy(k => k.ShowOrder).Select(t => t.TitleCN))) + // .ForMember(d => d.TitleList, u => u.MapFrom(s => s.DoctorDicRelationList.Where(t => t.KeyName == StaticData.Title) + //.Select(t => new { Title = t.Dictionary.Value, ShowOrder = t.Dictionary.ShowOrder }).OrderBy(k => k.ShowOrder).Select(t => t.Title))) + // .ForMember(d => d.TitleIds, u => u.MapFrom(s => s.DoctorDicRelationList.Where(t => t.KeyName == StaticData.Title) + //.Select(t => new { TitleId = t.Dictionary.Id, ShowOrder = t.Dictionary.ShowOrder }).OrderBy(k => k.ShowOrder).Select(t => t.TitleId))); + + //这样只会查询一次 + CreateMap() + .ForMember(o => o.DoctorDicViewDtos, t => t.MapFrom(u => u.DoctorDicRelationList.Where(t => t.KeyName == StaticData.Title).Select(t => t.Dictionary).OrderBy(t => t.ShowOrder))); + CreateMap() + .ForMember(t=>t.ParentCode,u=>u.MapFrom(c=>c.Parent.Code)); + //CreateMap(); + + CreateMap() + .ForMember(o => o.Speciality, t => t.MapFrom(u => u.Speciality.Value)) + .ForMember(o => o.DictionaryList, t => t.MapFrom(u => u.DoctorDicRelationList.Where(t => t.KeyName == StaticData.ReadingType || t.KeyName == StaticData.Subspeciality).Select(t => t.Dictionary).OrderBy(t => t.ShowOrder))); + CreateMap(); + + //医生职业信息 + CreateMap().IncludeMembers(t => t.Hospital) + .ForMember(d => d.Department, u => u.MapFrom(s => s.Department.Value)) + .ForMember(d => d.DepartmentCN, u => u.MapFrom(s => s.Department.ValueCN)) + .ForMember(d => d.Position, u => u.MapFrom(s => s.Position.Value)) + .ForMember(d => d.PositionCN, u => u.MapFrom(s => s.Position.ValueCN)) + .ForMember(d => d.Rank, u => u.MapFrom(s => s.Rank.Value)) + .ForMember(d => d.RankCN, u => u.MapFrom(s => s.Rank.ValueCN)); + CreateMap(); + + CreateMap() + .ForMember(d => d.IntoGroupState, u => u.MapFrom(s => s.EnrollStatus)) + .ForMember(d => d.OptTime, u => u.MapFrom(s => s.CreateTime)) + .ForMember(d => d.OptUserName, u => u.MapFrom(s => s.CreateUser.UserName)); + + CreateMap().IncludeMembers(t => t.Doctor, t => t.Doctor.Hospital) + .ForMember(d => d.Id, u => u.MapFrom(s => s.Doctor.Id)); + CreateMap(); + CreateMap(); + + + #endregion + + + } + } + +} diff --git a/IRaCIS.Core.Application/Service/Document/DTO/SystemDocumentViewModel.cs b/IRaCIS.Core.Application/Service/Document/DTO/SystemDocumentViewModel.cs new file mode 100644 index 00000000..9d397184 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Document/DTO/SystemDocumentViewModel.cs @@ -0,0 +1,162 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-01-05 09:17:10 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Infrastructure.Extention; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace IRaCIS.Core.Application.Contracts +{ + /// SystemDocumentView 列表视图模型 + public class SystemDocumentView : SystemDocumentAddOrEdit + { + public string FullFilePath { get; set; } = string.Empty; + public DateTime CreateTime { get; set; } + public DateTime UpdateTime { get; set; } + + public List NeedConfirmedUserTypeList { get; set; }=new List(); + } + + public class UnionDocumentView : SystemDocumentAddOrEdit + { + public string FullFilePath { get; set; } = string.Empty; + public DateTime CreateTime { get; set; } + public DateTime UpdateTime { get; set; } + + public bool IsSystemDoc { get; set; } + + } + + + public class UnionDocumentWithConfirmInfoView: UnionDocumentView + { + public DateTime? ConfirmTime { get; set; } + + public Guid? ConfirmUserId { get; set; } + + public string UserName { get; set; } = string.Empty; + + public string RealName { get; set; } = string.Empty; + + public string UserTypeShortName { get; set; } = string.Empty; + } + + + public class TrialUserDto + { + public Guid UserId { get; set; } + public string UserName { get; set; } = string.Empty; + + public string RealName { get; set; } = string.Empty; + } + + public class DocumentUnionWithUserStatView: UnionDocumentView + { + public int? DocumentUserCount { get; set; } + public int? DocumentConfirmedUserCount { get; set; } + } + + public class TrialUserUnionDocumentView + { + public Guid UserId { get; set; } + + public string UserTypeShortName { get; set; } = string.Empty; + public string UserName { get; set; } = string.Empty; + public string RealName { get; set; } = string.Empty; + + + public int? SystemDocumentCount { get; set; } + public int? TrialDocumentCount { get; set; } + public int? TrialDocumentConfirmedCount { get; set; } + public int? SystemDocumentConfirmedCount { get; set; } + + //public List DocumentList { get; set; } + } + + + + + + + ///SystemDocumentQuery 列表查询参数模型 + public class SystemDocumentQuery : PageInput + { + + + public Guid? SystemDocumentId { get; set; } + + public string Type { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + + + } + + public class TrialUserDocUnionQuery: PageInput + { + [NotDefault] + public Guid TrialId { get; set; } + + public string Type { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + + } + + public class UserConfirmCommand + { + [NotDefault] + public Guid TrialId { get; set; } + + [NotDefault] + public Guid DocumentId { get; set; } + + + public bool isSystemDoc { get; set; } + + + public string UserName { get; set; } = String.Empty; + + public string PassWord { get; set; } = String.Empty; + + public string SignText { get; set; } = String.Empty; + + + } + + public class DocumentTrialUnionQuery : TrialUserDocUnionQuery + { + + public Guid? UserTypeId { get; set; } + public Guid? UserId { get; set; } + } + + /// SystemDocumentAddOrEdit 列表查询参数模型 + public class SystemDocumentAddOrEdit + { + public Guid? Id { get; set; } + public string Type { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Path { get; set; } = string.Empty; + + public bool IsAbandon { get; set; } + + public int SignViewMinimumMinutes { get; set; } + + } + + public class AddOrEditSystemDocument : SystemDocumentAddOrEdit + { + + public List NeedConfirmedUserTypeIdList { get; set; }=new List(); + } +} + + + + + diff --git a/IRaCIS.Core.Application/Service/Document/DTO/TrialDocumentUserConfirmViewModel.cs b/IRaCIS.Core.Application/Service/Document/DTO/TrialDocumentUserConfirmViewModel.cs new file mode 100644 index 00000000..8bd7c33a --- /dev/null +++ b/IRaCIS.Core.Application/Service/Document/DTO/TrialDocumentUserConfirmViewModel.cs @@ -0,0 +1,47 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-01-05 09:17:10 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using System; +using System.Collections.Generic; +namespace IRaCIS.Core.Application.Contracts +{ + /// TrialDocumentUserConfirmView 列表视图模型 + public class TrialDocumentUserConfirmView + { + public Guid TrialId { get; set; } + + public Guid? TrialDocumentId { get; set; } + + public DateTime? ConfirmTime { get; set; } + + public Guid? ConfirmUserId { get; set; } + + public string UserName { get; set; } = string.Empty; + + public string RealName { get; set; } = string.Empty; + + + + } + + + + + + + public class NeedConfirmedUserTypeView + { + public Guid NeedConfirmUserTypeId { get; set; } + + public string UserTypeShortName { get; set; } = string.Empty; + } + + + + + +} + + diff --git a/IRaCIS.Core.Application/Service/Document/DTO/TrialDocumentViewModel.cs b/IRaCIS.Core.Application/Service/Document/DTO/TrialDocumentViewModel.cs new file mode 100644 index 00000000..761a6f52 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Document/DTO/TrialDocumentViewModel.cs @@ -0,0 +1,72 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-01-05 09:17:10 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using IRaCIS.Core.Infrastructure.Extention; +using System.ComponentModel.DataAnnotations; + +namespace IRaCIS.Core.Application.Contracts +{ + /// TrialDocumentView 列表视图模型 + public class TrialDocumentView : TrialDocumentAddOrEdit + { + public string FullFilePath { get; set; } = String.Empty; + + public bool IsSomeUserSigned{get;set;} + + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid UpdateUserId { get; set; } + + public List NeedConfirmedUserTypeList { get; set; } = new List(); + + + } + + + + ///TrialDocumentQuery 列表查询参数模型 + public class TrialDocumentQuery : PageInput + { + + public string Type { get; set; } = String.Empty; + + public string Name { get; set; } = String.Empty; + + [NotDefault] + public Guid TrialId { get; set; } + + } + + /// TrialDocumentAddOrEdit 列表查询参数模型 + public class TrialDocumentAddOrEdit + { + public Guid? Id { get; set; } + public Guid TrialId { get; set; } + + public string Type { get; set; } = String.Empty; + public string Name { get; set; } = String.Empty; + public string Path { get; set; } = String.Empty; + + public string Description { get; set; } = String.Empty; + public bool IsAbandon { get; set; } + + public int SignViewMinimumMinutes { get; set; } + + } + + public class AddOrEditTrialDocument: TrialDocumentAddOrEdit + { + + public List NeedConfirmedUserTypeIdList { get; set; } = new List(); + } + + + + +} + + diff --git a/IRaCIS.Core.Application/Service/Document/Interface/ISystemDocumentService.cs b/IRaCIS.Core.Application/Service/Document/Interface/ISystemDocumentService.cs new file mode 100644 index 00000000..dfbf08ea --- /dev/null +++ b/IRaCIS.Core.Application/Service/Document/Interface/ISystemDocumentService.cs @@ -0,0 +1,31 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-01-05 09:17:00 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using IRaCIS.Core.Infrastructure.Extention; +namespace IRaCIS.Core.Application.Contracts +{ + /// + /// ISystemDocumentService + /// + public interface ISystemDocumentService + { + + //PageOutput GetSystemDocumentList(SystemDocumentQuery querySystemDocument); + + + //IResponseOutput AddOrUpdateSystemDocument(AddOrEditSystemDocument addOrEditSystemDocument); + + //IResponseOutput DeleteSystemDocument(Guid systemDocumentId); + + Task> GetSystemDocumentListAsync(SystemDocumentQuery querySystemDocument); + + Task AddOrUpdateSystemDocumentAsync(AddOrEditSystemDocument addOrEditSystemDocument); + + Task DeleteSystemDocumentAsync(Guid systemDocumentId); + + + + } +} diff --git a/IRaCIS.Core.Application/Service/Document/Interface/ITrialDocumentService.cs b/IRaCIS.Core.Application/Service/Document/Interface/ITrialDocumentService.cs new file mode 100644 index 00000000..19a0dd63 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Document/Interface/ITrialDocumentService.cs @@ -0,0 +1,30 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-01-05 09:17:03 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Core.Application.Contracts +{ + public interface ITrialDocumentService + { + Task AddOrUpdateTrialDocument(AddOrEditTrialDocument addOrEditTrialDocument); + Task DeleteTrialDocument(Guid trialDocumentId, Guid trialId); + Task> GetDocumentConfirmList(DocumentTrialUnionQuery querySystemDocument); + Task> GetTrialDocumentList(TrialDocumentQuery queryTrialDocument); + + Task> GetUserDocumentList(TrialUserDocUnionQuery querySystemDocument); + Task SetFirstViewDocumentTime(Guid documentId, bool isSystemDoc); + Task UserConfirm(UserConfirmCommand userConfirmCommand); + Task> GetTrialUserSelect(Guid trialId); + + + PageOutput GetTrialSystemDocumentList(DocumentTrialUnionQuery querySystemDocument); + List GetTrialUserDocumentList(Guid trialId); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Document/SystemDocumentService.cs b/IRaCIS.Core.Application/Service/Document/SystemDocumentService.cs new file mode 100644 index 00000000..1238d235 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Document/SystemDocumentService.cs @@ -0,0 +1,122 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-01-05 09:17:03 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using IRaCIS.Core.Domain.Models; +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; + +namespace IRaCIS.Core.Application.Services +{ + /// + /// SystemDocumentService + /// + [ApiExplorerSettings(GroupName = "Trial")] + public class SystemDocumentService : BaseService, ISystemDocumentService + { + + private readonly IWebHostEnvironment _hostEnvironment; + private readonly IRepository systemDocumentRepository; + + public SystemDocumentService(IWebHostEnvironment hostEnvironment, IRepository systemDocumentRepository) + { + _hostEnvironment = hostEnvironment; + this.systemDocumentRepository = systemDocumentRepository; + } + + /// + /// 管理端列表 + /// + /// + /// + [HttpPost] + public async Task> GetSystemDocumentListAsync(SystemDocumentQuery querySystemDocument) + { + var systemDocumentQueryable = systemDocumentRepository + .WhereIf(!string.IsNullOrEmpty(querySystemDocument.Name), t => t.Name.Contains(querySystemDocument.Name)) + .WhereIf(!string.IsNullOrEmpty(querySystemDocument.Type), t => t.Type.Contains(querySystemDocument.Type)) + .ProjectTo(_mapper.ConfigurationProvider, new { token = _userInfo.UserToken, userId = _userInfo.Id }); + + return await systemDocumentQueryable.ToPagedListAsync(querySystemDocument.PageIndex, querySystemDocument.PageSize, querySystemDocument.SortField, querySystemDocument.Asc); + } + + public async Task AddOrUpdateSystemDocumentAsync(AddOrEditSystemDocument addOrEditSystemDocument) + { + if (addOrEditSystemDocument.Id == null) + { + var entity = _mapper.Map(addOrEditSystemDocument); + + + if (await systemDocumentRepository.AnyAsync(t => t.Type == addOrEditSystemDocument.Type && t.Name == addOrEditSystemDocument.Name)) + { + return ResponseOutput.NotOk("同类型已存在该文件名"); + } + + await systemDocumentRepository.AddAsync(entity,true); + + return ResponseOutput.Ok(entity.Id.ToString()); + + } + else + { + var document = await systemDocumentRepository.Where(t => t.Id == addOrEditSystemDocument.Id, true).Include(t => t.NeedConfirmedUserTypeList).FirstOrDefaultAsync(); + + if (document == null) return Null404NotFound(document); + + if (await systemDocumentRepository.AnyAsync(t => t.Type == addOrEditSystemDocument.Type && t.Name == addOrEditSystemDocument.Name && t.Id != addOrEditSystemDocument.Id)) + { + return ResponseOutput.NotOk("同类型已存在该文件名"); + } + var dbDocumentType = document.Type; + + _mapper.Map(addOrEditSystemDocument, document); + + if (dbDocumentType != addOrEditSystemDocument.Type) + { + var rootPath = Directory.GetParent(_hostEnvironment.ContentRootPath.TrimEnd('\\')).IfNullThrowException().FullName; + var beforeFilePath = Path.Combine(rootPath, document.Path); + + document.Path = document.Path.Replace(dbDocumentType, addOrEditSystemDocument.Type); + + var nowPath = Path.Combine(rootPath, document.Path); + + if (File.Exists(beforeFilePath)) + { + File.Move(beforeFilePath, nowPath, true); + File.Delete(beforeFilePath); + } + + } + var success = _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(document.Id.ToString()); + } + + + + } + + + [HttpDelete("{systemDocumentId:guid}")] + public async Task DeleteSystemDocumentAsync(Guid systemDocumentId) + { + + if (await _repository.Where(t => t.Id == systemDocumentId).AnyAsync(u => u.SystemDocConfirmedUserList.Any())) + { + return ResponseOutput.NotOk("该文档下已有签名的用户"); + } + + var success = await _repository.DeleteFromQueryAsync(t => t.Id == systemDocumentId); + + return ResponseOutput.Result(success); + } + + + } +} diff --git a/IRaCIS.Core.Application/Service/Document/TrialDocumentService.cs b/IRaCIS.Core.Application/Service/Document/TrialDocumentService.cs new file mode 100644 index 00000000..f3a48dad --- /dev/null +++ b/IRaCIS.Core.Application/Service/Document/TrialDocumentService.cs @@ -0,0 +1,642 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-01-05 09:17:03 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Hosting; +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Domain.Share; +using Panda.DynamicWebApi.Attributes; + +namespace IRaCIS.Core.Application.Services +{ + /// + /// TrialDocumentService + /// + [ApiExplorerSettings(GroupName = "Trial")] + public class TrialDocumentService : BaseService, ITrialDocumentService + { + + + private readonly IWebHostEnvironment _hostEnvironment; + private readonly IRepository trialDocumentRepository; + + public TrialDocumentService(IWebHostEnvironment hostEnvironment, IRepository trialDocumentRepository) + { + _hostEnvironment = hostEnvironment; + this.trialDocumentRepository = trialDocumentRepository; + } + + /// + /// Setting 界面的 项目所有文档列表 + /// + /// + /// + [HttpPost] + public async Task> GetTrialDocumentList(TrialDocumentQuery queryTrialDocument) + { + + var trialDocumentQueryable = trialDocumentRepository.Where(t => t.TrialId == queryTrialDocument.TrialId) + .WhereIf(!string.IsNullOrEmpty(queryTrialDocument.Name), t => t.Name.Contains(queryTrialDocument.Name)) + .WhereIf(!string.IsNullOrEmpty(queryTrialDocument.Type), t => t.Type.Contains(queryTrialDocument.Type)) + .ProjectTo(_mapper.ConfigurationProvider, new { token = _userInfo.UserToken }); + + return await trialDocumentQueryable.ToPagedListAsync(queryTrialDocument.PageIndex, queryTrialDocument.PageSize, queryTrialDocument.SortField, queryTrialDocument.Asc); + } + + + + /// + /// 具体用户看到的 系统文件列表 + 项目类型文档 + /// + /// + /// + [HttpPost] + public async Task> GetUserDocumentList(TrialUserDocUnionQuery querySystemDocument) + { + #region https://github.com/dotnet/efcore/issues/16243 操作不行 + ////系统文档查询 + //var systemDocumentQueryable = _systemDocumentRepository + // .WhereIf(!_userInfo.IsAdmin, t => t.NeedConfirmedUserTypeList.Any(t => t.NeedConfirmUserTypeId == _userInfo.UserTypeId)) + // .WhereIf(!_userInfo.IsAdmin, t => t.IsAbandon == false || (t.IsAbandon == true && t.SystemDocConfirmedUserList.Any(t => t.ConfirmUserId == _userInfo.Id))) + //.ProjectTo(_mapper.ConfigurationProvider, new { userId = _userInfo.Id, token = _userInfo.UserToken }); + + ////项目文档查询 + //var trialDocQueryable = _trialDocumentRepository.Where(t => t.TrialId == querySystemDocument.TrialId) + // .WhereIf(!_userInfo.IsAdmin, t => t.NeedConfirmedUserTypeList.Any(t => t.NeedConfirmUserTypeId == _userInfo.UserTypeId)) + // .WhereIf(!_userInfo.IsAdmin, t => t.IsAbandon == false || (t.IsAbandon == true && t.TrialDocConfirmedUserList.Any(t => t.ConfirmUserId == _userInfo.Id))) + // .ProjectTo(_mapper.ConfigurationProvider, new { userId = _userInfo.Id, token = _userInfo.UserToken }); + + //var unionQuery = systemDocumentQueryable.Union(trialDocQueryable); + // .WhereIf(!string.IsNullOrEmpty(querySystemDocument.Name), t => t.Name.Contains(querySystemDocument.Name)) + // .WhereIf(!string.IsNullOrEmpty(querySystemDocument.Type), t => t.Type.Contains(querySystemDocument.Type)); + #endregion + + #region 仅仅文档信息 + + ////系统文档查询 + //var systemDocumentQueryable = _systemDocumentRepository + // .WhereIf(!_userInfo.IsAdmin, t => t.NeedConfirmedUserTypeList.Any(t => t.NeedConfirmUserTypeId == _userInfo.UserTypeId)) + // .WhereIf(!_userInfo.IsAdmin, t => t.IsAbandon == false || (t.IsAbandon == true && t.SystemDocConfirmedUserList.Any(t => t.ConfirmUserId == _userInfo.Id))) + // .Select(t => new UnionDocumentView() + // { + // Id = t.Id, + // IsSystemDoc = true, + // CreateTime = t.CreateTime, + // FullFilePath = t.Path + "?access_token=" + _userInfo.UserToken, + // IsAbandon = t.IsAbandon, + // Name = t.Name, + // Path = t.Path, + // Type = t.Type, + // UpdateTime = t.UpdateTime, + // SignViewMinimumMinutes = t.SignViewMinimumMinutes, + + // }); + + ////项目文档查询 + //var trialDocQueryable = _trialDocumentRepository.Where(t => t.TrialId == querySystemDocument.TrialId) + // .WhereIf(!_userInfo.IsAdmin, t => t.NeedConfirmedUserTypeList.Any(t => t.NeedConfirmUserTypeId == _userInfo.UserTypeId)) + // .WhereIf(!_userInfo.IsAdmin, t => t.IsAbandon == false || (t.IsAbandon == true && t.TrialDocConfirmedUserList.Any(t => t.ConfirmUserId == _userInfo.Id))) + // .Select(t => new UnionDocumentView() + // { + // Id = t.Id, + // IsSystemDoc = false, + // CreateTime = t.CreateTime, + // FullFilePath = t.Path + "?access_token=" + _userInfo.UserToken, + // IsAbandon = t.IsAbandon, + // Name = t.Name, + // Path = t.Path, + // Type = t.Type, + // UpdateTime = t.UpdateTime, + // SignViewMinimumMinutes = t.SignViewMinimumMinutes, + + // }); + + #endregion + + var trialFininshedTime = await _repository.Where(t => t.Id == querySystemDocument.TrialId).Select(t => t.TrialFinishedTime).FirstOrDefaultAsync(); + + //系统文档查询 + var systemDocumentQueryable = from needConfirmedUserType in _repository.Where(t => t.NeedConfirmUserTypeId == _userInfo.UserTypeId) + //.Where(u => u.UserTypeRole.UserList.SelectMany(cc => cc.UserTrials.Where(t => t.TrialId == querySystemDocument.TrialId)).Any(e => e.Trial.TrialFinishedTime < u.SystemDocument.CreateTime)) + .WhereIf(trialFininshedTime != null, u => u.SystemDocument.CreateTime < trialFininshedTime) + .WhereIf(!_userInfo.IsAdmin, t => t.SystemDocument.IsAbandon == false || (t.SystemDocument.IsAbandon == true && t.SystemDocument.SystemDocConfirmedUserList.Any(t => t.ConfirmUserId == _userInfo.Id))) + + join trialUser in _repository.Where(t => t.TrialId == querySystemDocument.TrialId && t.UserId == _userInfo.Id) + on needConfirmedUserType.NeedConfirmUserTypeId equals trialUser.User.UserTypeId + join confirm in _repository.GetQueryable() on new { ConfirmUserId = trialUser.UserId, SystemDocumentId = needConfirmedUserType.SystemDocumentId } equals new { confirm.ConfirmUserId, confirm.SystemDocumentId } into cc + from confirm in cc.DefaultIfEmpty() + select new UnionDocumentWithConfirmInfoView() + { + IsSystemDoc = true, + + Id = needConfirmedUserType.SystemDocument.Id, + CreateTime = needConfirmedUserType.SystemDocument.CreateTime, + IsAbandon = needConfirmedUserType.SystemDocument.IsAbandon, + SignViewMinimumMinutes = needConfirmedUserType.SystemDocument.SignViewMinimumMinutes, + Name = needConfirmedUserType.SystemDocument.Name, + Path = needConfirmedUserType.SystemDocument.Path, + Type = needConfirmedUserType.SystemDocument.Type, + UpdateTime = needConfirmedUserType.SystemDocument.UpdateTime, + + FullFilePath = needConfirmedUserType.SystemDocument.Path + "?access_token=" + _userInfo.UserToken, + + ConfirmUserId = confirm.ConfirmUserId, + ConfirmTime = confirm.ConfirmTime, + RealName = trialUser.User.LastName + " / " + trialUser.User.FirstName, + UserName = trialUser.User.UserName, + UserTypeShortName = trialUser.User.UserTypeRole.UserTypeShortName + }; + + //项目文档查询 + var trialDocQueryable = from trialDoc in trialDocumentRepository.Where(t => t.TrialId == querySystemDocument.TrialId) + .WhereIf(!_userInfo.IsAdmin, t => t.NeedConfirmedUserTypeList.Any(t => t.NeedConfirmUserTypeId == _userInfo.UserTypeId)) + .WhereIf(!_userInfo.IsAdmin, t => t.IsAbandon == false || (t.IsAbandon == true && t.TrialDocConfirmedUserList.Any(t => t.ConfirmUserId == _userInfo.Id))) + + join trialUser in _repository.Where(t => t.TrialId == querySystemDocument.TrialId && t.UserId == _userInfo.Id) on trialDoc.TrialId equals trialUser.TrialId + join confirm in _repository.Where(t => t.TrialDocument.TrialId == querySystemDocument.TrialId) on + new { trialUser.UserId, TrialDocumentId = trialDoc.Id } equals new { UserId = confirm.ConfirmUserId, confirm.TrialDocumentId } into cc + from confirm in cc.DefaultIfEmpty() + select new UnionDocumentWithConfirmInfoView() + { + Id = trialDoc.Id, + IsSystemDoc = false, + CreateTime = trialDoc.CreateTime, + FullFilePath = trialDoc.Path + "?access_token=" + _userInfo.UserToken, + IsAbandon = trialDoc.IsAbandon, + Name = trialDoc.Name, + Path = trialDoc.Path, + Type = trialDoc.Type, + UpdateTime = trialDoc.UpdateTime, + SignViewMinimumMinutes = trialDoc.SignViewMinimumMinutes, + + ConfirmUserId = confirm.ConfirmUserId, + ConfirmTime = confirm.ConfirmTime, + RealName = trialUser.User.LastName + " / " + trialUser.User.FirstName, + UserName = trialUser.User.UserName, + UserTypeShortName = trialUser.User.UserTypeRole.UserTypeShortName + + }; + + + + var unionQuery = systemDocumentQueryable.Union(trialDocQueryable) + .WhereIf(!string.IsNullOrEmpty(querySystemDocument.Name), t => t.Name.Contains(querySystemDocument.Name)) + .WhereIf(!string.IsNullOrEmpty(querySystemDocument.Type), t => t.Type.Contains(querySystemDocument.Type)); + + return await unionQuery.ToPagedListAsync(querySystemDocument.PageIndex, querySystemDocument.PageSize, querySystemDocument.SortField, querySystemDocument.Asc); + } + + + /// + /// 获取用户是否有文档未签署 + /// + /// + /// + [HttpGet("{trialId:guid}")] + public async Task GetUserIsHaveDocumentNeedSign(Guid trialId) + { + var trialFininshedTime = await _repository.Where(t => t.Id == trialId).Select(t => t.TrialFinishedTime).FirstOrDefaultAsync(); + + //系统文档查询 + var systemDocumentQueryable = from needConfirmedUserType in _repository.Where(t => t.NeedConfirmUserTypeId == _userInfo.UserTypeId) + //.Where(u => u.UserTypeRole.UserList.SelectMany(cc => cc.UserTrials.Where(t => t.TrialId == querySystemDocument.TrialId)).Any(e => e.Trial.TrialFinishedTime < u.SystemDocument.CreateTime)) + .WhereIf(trialFininshedTime != null, u => u.SystemDocument.CreateTime < trialFininshedTime) + .WhereIf(!_userInfo.IsAdmin, t => t.SystemDocument.IsAbandon == false || (t.SystemDocument.IsAbandon == true && t.SystemDocument.SystemDocConfirmedUserList.Any(t => t.ConfirmUserId == _userInfo.Id))) + + join trialUser in _repository.Where(t => t.TrialId == trialId && t.UserId == _userInfo.Id) + on needConfirmedUserType.NeedConfirmUserTypeId equals trialUser.User.UserTypeId + join confirm in _repository.GetQueryable() on new { ConfirmUserId = trialUser.UserId, SystemDocumentId = needConfirmedUserType.SystemDocumentId } equals new { confirm.ConfirmUserId, confirm.SystemDocumentId } into cc + from confirm in cc.DefaultIfEmpty() + select new + { + //ConfirmUserId = confirm.ConfirmUserId, + ConfirmTime = confirm.ConfirmTime, + }; + + //项目文档查询 + var trialDocQueryable = from trialDoc in trialDocumentRepository.Where(t => t.TrialId == trialId) + .WhereIf(!_userInfo.IsAdmin, t => t.NeedConfirmedUserTypeList.Any(t => t.NeedConfirmUserTypeId == _userInfo.UserTypeId)) + .WhereIf(!_userInfo.IsAdmin, t => t.IsAbandon == false || (t.IsAbandon == true && t.TrialDocConfirmedUserList.Any(t => t.ConfirmUserId == _userInfo.Id))) + + join trialUser in _repository.Where(t => t.TrialId == trialId && t.UserId == _userInfo.Id) on trialDoc.TrialId equals trialUser.TrialId + join confirm in _repository.Where(t => t.TrialDocument.TrialId == trialId) on + new { trialUser.UserId, TrialDocumentId = trialDoc.Id } equals new { UserId = confirm.ConfirmUserId, confirm.TrialDocumentId } into cc + from confirm in cc.DefaultIfEmpty() + select new + { + //ConfirmUserId = confirm.ConfirmUserId, + ConfirmTime = confirm.ConfirmTime, + }; + + var unionQuery = systemDocumentQueryable.Union(trialDocQueryable); + + return await unionQuery.AnyAsync(t => t.ConfirmTime == null); + } + + + + /// + /// 获取确认列表情况 项目文档+系统文档+具体的人 + /// + /// + /// + [HttpPost] + public async Task> GetDocumentConfirmList(DocumentTrialUnionQuery querySystemDocument) + { + + + #region linq join 方式 + //var trialDocQuery = from trialDocumentNeedConfirmedUserType in _trialDocumentNeedConfirmedUserTypeRepository.Where(t => t.TrialDocument.TrialId == querySystemDocument.TrialId) + // join trialUser in _trialUserRepository.Where(t => t.TrialId == querySystemDocument.TrialId) + // .WhereIf(querySystemDocument.UserId != null, t => t.UserId == querySystemDocument.UserId) + // on trialDocumentNeedConfirmedUserType.NeedConfirmUserTypeId equals trialUser.User.UserTypeId + + // join confirm in _trialDocuserConfrimedRepository.AsQueryable() on trialUser.UserId equals confirm.ConfirmUserId into cc + // from confirm in cc.DefaultIfEmpty() + // select new UnionDocumentConfirmListView() + // { + // Id = trialDocumentNeedConfirmedUserType.TrialDocument.Id, + // CreateTime = trialDocumentNeedConfirmedUserType.TrialDocument.CreateTime, + // IsAbandon = trialDocumentNeedConfirmedUserType.TrialDocument.IsAbandon, + // SignViewMinimumMinutes = trialDocumentNeedConfirmedUserType.TrialDocument.SignViewMinimumMinutes, + // Name = trialDocumentNeedConfirmedUserType.TrialDocument.Name, + // Path = trialDocumentNeedConfirmedUserType.TrialDocument.Path, + // Type = trialDocumentNeedConfirmedUserType.TrialDocument.Type, + // UpdateTime = trialDocumentNeedConfirmedUserType.TrialDocument.UpdateTime, + + // UserConfirmInfo = /*confirm == null ? null : */new UnionDocumentUserConfirmView() + // { + + // ConfirmUserId = confirm.ConfirmUserId, + // ConfirmTime = confirm.ConfirmTime, + // RealName = trialUser.User.LastName + " / " + trialUser.User.LastName, + // UserName = trialUser.User.UserName, + // }, + + // FullFilePath = trialDocumentNeedConfirmedUserType.TrialDocument.Path + "?access_token=" + _userInfo.UserToken + // }; + + #endregion + + var trialFininshedTime = await _repository.Where(t => t.Id == querySystemDocument.TrialId).Select(t => t.TrialFinishedTime).FirstOrDefaultAsync(); + + var trialDocQuery = from trialDocumentNeedConfirmedUserType in _repository.Where(t => t.TrialDocument.TrialId == querySystemDocument.TrialId) + //.Where(t => t.TrialDocument.Trial.TrialUserList.Any(cc => cc.User.UserTypeId == t.NeedConfirmUserTypeId)) + join trialUser in _repository.Where(t => t.TrialId == querySystemDocument.TrialId) + .WhereIf(querySystemDocument.UserId != null, t => t.UserId == querySystemDocument.UserId) + .WhereIf(querySystemDocument.UserTypeId != null, t => t.User.UserTypeId == querySystemDocument.UserTypeId) + on trialDocumentNeedConfirmedUserType.NeedConfirmUserTypeId equals trialUser.User.UserTypeId + + join confirm in _repository.Where(t => t.TrialDocument.TrialId == querySystemDocument.TrialId) on + new { trialUser.UserId, TrialDocumentId = trialDocumentNeedConfirmedUserType.TrialDocumentId } equals new { UserId = confirm.ConfirmUserId, confirm.TrialDocumentId } into cc + from confirm in cc.DefaultIfEmpty() + select new UnionDocumentWithConfirmInfoView() + { + IsSystemDoc = false, + + Id = trialDocumentNeedConfirmedUserType.TrialDocument.Id, + CreateTime = trialDocumentNeedConfirmedUserType.TrialDocument.CreateTime, + IsAbandon = trialDocumentNeedConfirmedUserType.TrialDocument.IsAbandon, + SignViewMinimumMinutes = trialDocumentNeedConfirmedUserType.TrialDocument.SignViewMinimumMinutes, + Name = trialDocumentNeedConfirmedUserType.TrialDocument.Name, + Path = trialDocumentNeedConfirmedUserType.TrialDocument.Path, + Type = trialDocumentNeedConfirmedUserType.TrialDocument.Type, + UpdateTime = trialDocumentNeedConfirmedUserType.TrialDocument.UpdateTime, + + + + ConfirmUserId = confirm.ConfirmUserId, + ConfirmTime = confirm.ConfirmTime, + RealName = trialUser.User.LastName + " / " + trialUser.User.FirstName, + UserName = trialUser.User.UserName, + UserTypeShortName = trialUser.User.UserTypeRole.UserTypeShortName, + + FullFilePath = trialDocumentNeedConfirmedUserType.TrialDocument.Path + "?access_token=" + _userInfo.UserToken + }; + + + + var systemDocQuery = from needConfirmEdUserType in _repository.WhereIf(trialFininshedTime != null, u => u.SystemDocument.CreateTime < trialFininshedTime) + + join trialUser in _repository.Where(t => t.TrialId == querySystemDocument.TrialId) + .WhereIf(querySystemDocument.UserId != null, t => t.UserId == querySystemDocument.UserId) + on needConfirmEdUserType.NeedConfirmUserTypeId equals trialUser.User.UserTypeId + join confirm in _repository.GetQueryable() on new { ConfirmUserId = trialUser.UserId, SystemDocumentId = needConfirmEdUserType.SystemDocumentId } equals new { confirm.ConfirmUserId, confirm.SystemDocumentId } into cc + from confirm in cc.DefaultIfEmpty() + select new UnionDocumentWithConfirmInfoView() + { + IsSystemDoc = true, + + Id = needConfirmEdUserType.SystemDocument.Id, + CreateTime = needConfirmEdUserType.SystemDocument.CreateTime, + IsAbandon = needConfirmEdUserType.SystemDocument.IsAbandon, + SignViewMinimumMinutes = needConfirmEdUserType.SystemDocument.SignViewMinimumMinutes, + Name = needConfirmEdUserType.SystemDocument.Name, + Path = needConfirmEdUserType.SystemDocument.Path, + Type = needConfirmEdUserType.SystemDocument.Type, + UpdateTime = needConfirmEdUserType.SystemDocument.UpdateTime, + + + ConfirmUserId = confirm.ConfirmUserId, + ConfirmTime = confirm.ConfirmTime, + RealName = trialUser.User.LastName + " / " + trialUser.User.FirstName, + UserName = trialUser.User.UserName, + UserTypeShortName = trialUser.User.UserTypeRole.UserTypeShortName, + + FullFilePath = needConfirmEdUserType.SystemDocument.Path + "?access_token=" + _userInfo.UserToken + }; + + var unionQuery = trialDocQuery.Union(systemDocQuery) + .WhereIf(!string.IsNullOrEmpty(querySystemDocument.Name), t => t.Name.Contains(querySystemDocument.Name)) + .WhereIf(!string.IsNullOrEmpty(querySystemDocument.Type), t => t.Type.Contains(querySystemDocument.Type)); + + return await unionQuery.ToPagedListAsync(querySystemDocument.PageIndex, querySystemDocument.PageSize, querySystemDocument.SortField, querySystemDocument.Asc); + } + + + + [HttpGet("{trialId:guid}")] + public async Task> GetTrialUserSelect(Guid trialId) + { + return await _repository.Where(t => t.TrialId == trialId) + .Select(t => new TrialUserDto() { UserId = t.UserId, RealName = t.User.LastName + " / " + t.User.FirstName, UserName = t.User.UserName }) + .ToListAsync(); + } + + + + [HttpGet("{trialId:guid}")] + public async Task> GetTrialDocAndSystemDocType(Guid trialId) + { + return await trialDocumentRepository.Where(t => t.TrialId == trialId).Select(t => t.Type).Union(_repository.GetQueryable().Select(t => t.Type)).Distinct() + + .ToListAsync(); + } + + public async Task AddOrUpdateTrialDocument(AddOrEditTrialDocument addOrEditTrialDocument) + { + if (addOrEditTrialDocument.Id == null) + { + var entity = _mapper.Map(addOrEditTrialDocument); + + + if (await trialDocumentRepository.AnyAsync(t => t.Type == addOrEditTrialDocument.Type && t.Name == addOrEditTrialDocument.Name && t.TrialId == addOrEditTrialDocument.TrialId)) + { + return ResponseOutput.NotOk("同类型已存在该文件名"); + } + + await _repository.AddAsync(entity, true); + return ResponseOutput.Ok(entity.Id.ToString()); + } + else + { + if (await trialDocumentRepository.AnyAsync(t => t.Type == addOrEditTrialDocument.Type && t.Name == addOrEditTrialDocument.Name && t.Id != addOrEditTrialDocument.Id && t.TrialId == addOrEditTrialDocument.TrialId)) + { + return ResponseOutput.NotOk("同类型已存在该文件名"); + } + + var document = trialDocumentRepository.Where(t => t.Id == addOrEditTrialDocument.Id, true).Include(t => t.NeedConfirmedUserTypeList).FirstOrDefault(); + + if (document == null) return Null404NotFound(document); + + var dbDocumentType = document.Type; + + _mapper.Map(addOrEditTrialDocument, document); + + if (dbDocumentType != addOrEditTrialDocument.Type) + { + + + var rootPath = Directory.GetParent(_hostEnvironment.ContentRootPath.TrimEnd('\\')).IfNullThrowException().FullName; + var beforeFilePath = Path.Combine(rootPath, document.Path); + + document.Path = document.Path.Replace(dbDocumentType, addOrEditTrialDocument.Type); + + var nowPath = Path.Combine(rootPath, document.Path); + + if (File.Exists(beforeFilePath)) + { + File.Move(beforeFilePath, nowPath, true); + File.Delete(beforeFilePath); + } + + } + + var success = await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(document.Id.ToString()); + } + } + + + /// + /// 已签名的文档 不允许删除 + /// + /// + /// + /// + [HttpDelete("{trialId:guid}/{trialDocumentId:guid}")] + public async Task DeleteTrialDocument(Guid trialDocumentId, Guid trialId) + { + if (await trialDocumentRepository.Where(t => t.Id == trialDocumentId).AnyAsync(t => t.TrialDocConfirmedUserList.Any())) + { + return ResponseOutput.NotOk("该文档,已有用户签名 不允许删除"); + } + + var success = await trialDocumentRepository.DeleteFromQueryAsync(t => t.Id == trialDocumentId); + + return ResponseOutput.Result(success); + } + + + /// + /// 浏览文档说明时调用,记录第一次看的时间 + /// + /// + /// + /// + [HttpPut("{trialId:guid}/{documentId:guid}/{isSystemDoc:bool}")] + [UnitOfWork] + public async Task SetFirstViewDocumentTime(Guid documentId, bool isSystemDoc) + { + + var success = false; + if (isSystemDoc) + { + await _repository.AddAsync(new SystemDocConfirmedUser() { SystemDocumentId = documentId, SignFirstViewTime = DateTime.Now }); + //success = await _repository.UpdateFromQueryAsync(t => t.Id == documentId, d => new SystemDocConfirmedUser() { SignFirstViewTime = DateTime.Now }); + } + else + { + + await _repository.AddAsync(new TrialDocUserTypeConfirmedUser() { TrialDocumentId = documentId, SignFirstViewTime = DateTime.Now }); + + //success = await _repository.UpdateFromQueryAsync(t => t.Id == documentId , d => new TrialDocUserTypeConfirmedUser() { SignFirstViewTime = DateTime.Now }); + } + + success= await _repository.SaveChangesAsync(); + + return ResponseOutput.Result(success); + } + + /// + /// 用户 签名某个文档 + /// + + /// + [NonDynamicMethod] + public async Task UserConfirm(UserConfirmCommand userConfirmCommand) + { + + var user = await _repository.FirstOrDefaultAsync(u => u.UserName == userConfirmCommand.UserName && u.Password == userConfirmCommand.PassWord); + if (user == null) + { + return ResponseOutput.NotOk("password error"); + } + else if (user.Status == UserStateEnum.Disable) + { + return ResponseOutput.NotOk("The user has been disabled!"); + } + + + if (userConfirmCommand.isSystemDoc) + { + if (await _repository.AnyAsync(t => t.SystemDocumentId == userConfirmCommand.DocumentId && t.ConfirmUserId == _userInfo.Id)) + { + return ResponseOutput.NotOk("该文档已经签名"); + } + if (!await _repository.AnyAsync(t => t.Id == userConfirmCommand.DocumentId) || await trialDocumentRepository.AnyAsync(t => t.Id == userConfirmCommand.DocumentId && t.IsAbandon)) + { + return ResponseOutput.NotOk("文件已删除或者废除,签署失败!"); + } + + await _repository.AddAsync(new SystemDocConfirmedUser() { ConfirmTime = DateTime.Now, ConfirmUserId = _userInfo.Id, SystemDocumentId = userConfirmCommand.DocumentId }); + } + else + { + if (await _repository.AnyAsync(t => t.TrialDocumentId == userConfirmCommand.DocumentId && t.ConfirmUserId == _userInfo.Id)) + { + return ResponseOutput.NotOk("该文档已经签名"); + } + + if (!await trialDocumentRepository.AnyAsync(t => t.Id == userConfirmCommand.DocumentId) || await _repository.AnyAsync(t => t.Id == userConfirmCommand.DocumentId && t.IsAbandon)) + { + return ResponseOutput.NotOk("文件已删除或者废除,签署失败!"); + } + + await _repository.AddAsync(new TrialDocUserTypeConfirmedUser() { ConfirmTime = DateTime.Now, ConfirmUserId = _userInfo.Id, TrialDocumentId = userConfirmCommand.DocumentId }); + } + + await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(); + } + + + /// + /// 用户 废除某个文档 + /// + /// + /// + /// + [HttpPut("{documentId:guid}/{isSystemDoc:bool}")] + public async Task UserAbandonDoc(Guid documentId, bool isSystemDoc) + { + if (isSystemDoc) + { + await _repository.UpdateFromQueryAsync(t => t.Id == documentId, u => new SystemDocument() { IsAbandon = true }); + } + else + { + await trialDocumentRepository.UpdateFromQueryAsync(t => t.Id == documentId, u => new TrialDocument() { IsAbandon = true }); + } + return ResponseOutput.Ok(); + } + + /// + /// 从项目下参与者的维度 先看人员列表(展示统计数字) 点击数字 再看人员具体签署的 系统文档+项目文档(共用上面与人相关的具体文档列表) + /// + /// + /// + [HttpGet("{trialId:guid}")] + public List GetTrialUserDocumentList(Guid trialId) + { + var query = _repository.Where(t => t.TrialId == trialId) + .Select(t => new TrialUserUnionDocumentView() + { + UserId = t.UserId, + UserName = t.User.UserName, + RealName = t.User.LastName + " / " + t.User.FirstName, + UserTypeShortName = t.User.UserTypeRole.UserTypeShortName, + TrialDocumentCount = t.Trial.TrialDocumentList.Count(u => u.NeedConfirmedUserTypeList.Any(k => k.NeedConfirmUserTypeId == t.User.UserTypeId)), + TrialDocumentConfirmedCount = t.Trial.TrialDocumentList.SelectMany(u => u.TrialDocConfirmedUserList).Count(k => k.ConfirmUserId == t.UserId), + SystemDocumentConfirmedCount = t.User.SystemDocConfirmedList.Count(), + //这样写不行 + //SystemDocumentCount = _systemDocumentRepository.Where(s => s.NeedConfirmedUserTypeList.Any(kk => kk.NeedConfirmUserTypeId == t.User.UserTypeId)) + // .WhereIf(!_userInfo.IsAdmin, s => s.IsAbandon == false || (s.IsAbandon == true && s.SystemDocConfirmedUserList.Any(uu => uu.ConfirmUserId == t.UserId))).Count() + SystemDocumentCount = t.User.UserTypeRole.SystemDocNeedConfirmedUserTypeList.Where(cc => cc.NeedConfirmUserTypeId == t.User.UserTypeId).Select(y => y.SystemDocument).Count() + }); + + return query.ToList(); + } + + + /// + /// 从 文档的维度 先看到文档列表(系统文档+项目文档 以及需要确认的人数 和已经确认人数) 点击数字查看某文档下面人确认情况 + /// + /// + /// + [HttpPost] + public PageOutput GetTrialSystemDocumentList(DocumentTrialUnionQuery querySystemDocument) + { + var systemDocumentQueryable = _repository + .WhereIf(!_userInfo.IsAdmin, t => t.IsAbandon == false) + .Select(t => new DocumentUnionWithUserStatView() + { + Id = t.Id, + IsSystemDoc = true, + CreateTime = t.CreateTime, + FullFilePath = t.Path + "?access_token=" + _userInfo.UserToken, + IsAbandon = t.IsAbandon, + Name = t.Name, + Path = t.Path, + Type = t.Type, + UpdateTime = t.UpdateTime, + SignViewMinimumMinutes = t.SignViewMinimumMinutes, + DocumentConfirmedUserCount = t.SystemDocConfirmedUserList.Count(), + + //DocumentUserCount= _trialUserRepository.Where(tu=>tu.TrialId== querySystemDocument.TrialId).Count(u=>t.NeedConfirmedUserTypeList.Any(cc=>cc.NeedConfirmUserTypeId== u.User.UserTypeId )) + DocumentUserCount = t.NeedConfirmedUserTypeList.SelectMany(u => u.UserTypeRole.UserList.SelectMany(b => b.UserTrials.Where(r => r.TrialId == querySystemDocument.TrialId))).Count() + }); + + var trialDocQueryable = trialDocumentRepository.Where(t => t.TrialId == querySystemDocument.TrialId).Select(t => new DocumentUnionWithUserStatView() + { + Id = t.Id, + IsSystemDoc = false, + CreateTime = t.CreateTime, + FullFilePath = t.Path + "?access_token=" + _userInfo.UserToken, + IsAbandon = t.IsAbandon, + Name = t.Name, + Path = t.Path, + Type = t.Type, + UpdateTime = t.UpdateTime, + SignViewMinimumMinutes = t.SignViewMinimumMinutes, + + DocumentConfirmedUserCount = t.TrialDocConfirmedUserList.Count(), + DocumentUserCount = t.Trial.TrialUserList.Count(cc => t.NeedConfirmedUserTypeList.Any(k => k.NeedConfirmUserTypeId == cc.User.UserTypeId)) + + }); + + var unionQuery = systemDocumentQueryable.Union(trialDocQueryable) + .WhereIf(!string.IsNullOrEmpty(querySystemDocument.Name), t => t.Name.Contains(querySystemDocument.Name)) + .WhereIf(!string.IsNullOrEmpty(querySystemDocument.Type), t => t.Type.Contains(querySystemDocument.Type)); + + return unionQuery.ToPagedList(querySystemDocument.PageIndex, querySystemDocument.PageSize, querySystemDocument.SortField, querySystemDocument.Asc); + } + + } +} diff --git a/IRaCIS.Core.Application/Service/Document/_MapConfig.cs b/IRaCIS.Core.Application/Service/Document/_MapConfig.cs new file mode 100644 index 00000000..066da7fc --- /dev/null +++ b/IRaCIS.Core.Application/Service/Document/_MapConfig.cs @@ -0,0 +1,71 @@ +using AutoMapper; +using AutoMapper.EquivalencyExpression; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Domain.Models; + +namespace IRaCIS.Core.Application.Service +{ + public class DocumentConfig : Profile + { + public DocumentConfig() + { + + var userId = Guid.Empty; + var token = string.Empty; + CreateMap() + //.ForMember(d => d.UserConfirmInfo, u => u.MapFrom(s => s.SystemDocConfirmedUserList.FirstOrDefault(t=>t.ConfirmUserId==userId))) + .ForMember(d => d.FullFilePath, u => u.MapFrom(s => s.Path + "?access_token=" + token)); + + CreateMap() + .ForMember(d => d.IsSomeUserSigned, u => u.MapFrom(s => s.TrialDocConfirmedUserList.Any())) + .ForMember(d => d.FullFilePath, u => u.MapFrom(s => s.Path + "?access_token=" + token)); + + + CreateMap() + .ForMember(d => d.IsSystemDoc, u => u.MapFrom(s => true)) + .ForMember(d => d.FullFilePath, u => u.MapFrom(s => s.Path + "?access_token=" + token)); + + CreateMap() + .ForMember(d => d.IsSystemDoc, u => u.MapFrom(s => false)) + .ForMember(d => d.FullFilePath, u => u.MapFrom(s => s.Path + "?access_token=" + token)); + + CreateMap().ForMember(d => d.UserTypeShortName, t => t.MapFrom(c => c.UserTypeRole.UserTypeShortName)); + CreateMap().ForMember(d => d.UserTypeShortName, t => t.MapFrom(c => c.UserTypeRole.UserTypeShortName)); + + + //CreateMap() + // .ForMember(t => t.UserConfirmInfo, c => c.MapFrom(t => t.TrialDocConfirmedUserList.Where(u => u.ConfirmUserId == userId).FirstOrDefault())) + // .ForMember(d => d.FullFilePath, u => u.MapFrom(s => s.Path + "?access_token=" + token)); ; + + CreateMap() + .ForMember(d => d.UserName, c => c.MapFrom(t => t.User.UserName)) + .ForMember(d => d.RealName, c => c.MapFrom(t => t.User.LastName + " / " + t.User.FirstName)); + + //CreateMap() + // .ForMember(d => d.UserName, c => c.MapFrom(t => t.User.UserName)) + // .ForMember(d => d.RealName, c => c.MapFrom(t => t.User.LastName + " / " + t.User.FirstName)); + + + CreateMap(); + + + + + CreateMap() + .ForMember(d => d.NeedConfirmedUserTypeList, c => c.MapFrom(t => t.NeedConfirmedUserTypeIdList)); + + CreateMap().EqualityComparison((odto, o) => odto == o.NeedConfirmUserTypeId) + .ForMember(d => d.NeedConfirmUserTypeId, c => c.MapFrom(t => t)) + .ForMember(d => d.TrialDocumentId, c => c.Ignore()); + + + + CreateMap().ForMember(d => d.NeedConfirmedUserTypeList, c => c.MapFrom(t => t.NeedConfirmedUserTypeIdList)); + CreateMap().EqualityComparison((odto, o) => odto == o.NeedConfirmUserTypeId) + .ForMember(d => d.NeedConfirmUserTypeId, c => c.MapFrom(t => t)) + .ForMember(d => d.SystemDocumentId, c => c.Ignore()); + } + } + +} diff --git a/IRaCIS.Core.Application/Service/Financial/CalculateService.cs b/IRaCIS.Core.Application/Service/Financial/CalculateService.cs new file mode 100644 index 00000000..fb40d46e --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/CalculateService.cs @@ -0,0 +1,714 @@ +using AutoMapper; +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Application.Contracts.Pay; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infrastructure.ExpressionExtend; +using System.Linq.Expressions; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Infrastructure.Extention; +using Panda.DynamicWebApi.Attributes; + +namespace IRaCIS.Application.Services +{ + public class CalculateService : ICalculateService + { + private readonly IRepository _paymentRepository; + private readonly IRepository _trialPaymentRepository; + private readonly IRepository _doctorPayInfoRepository; + private readonly IRepository _trialRepository; + private readonly IRepository _doctorRepository; + private readonly IRepository _doctorWorkloadRepository; + private readonly IRepository _rankPriceRepository; + private readonly IRepository _paymentDetailRepository; + private readonly IVolumeRewardService _volumeRewardPriceService; + private readonly IRepository _exchangeRateRepository; + private readonly IRepository _payAdjustmentRepository; + private readonly IRepository _enrollRepository; + private readonly IMapper _mapper; + + public CalculateService(IRepository paymentRepository, IRepository trialPaymentPriceRepository, + IRepository reviewerPayInfoRepository, + IRepository trialRepository, + IRepository doctorRepository, + IRepository workloadRepository, + IRepository rankPriceRepository, + IRepository paymentDetailRepository, + IVolumeRewardService volumeRewardService, + IRepository exchangeRateRepository, + IRepository EnrollRepository, + IRepository paymentAdjustmentRepository, IMapper mapper) + { + _paymentRepository = paymentRepository; + _trialPaymentRepository = trialPaymentPriceRepository; + _doctorPayInfoRepository = reviewerPayInfoRepository; + _trialRepository = trialRepository; + _doctorRepository = doctorRepository; + _doctorWorkloadRepository = workloadRepository; + _rankPriceRepository = rankPriceRepository; + _paymentDetailRepository = paymentDetailRepository; + _volumeRewardPriceService = volumeRewardService; + _exchangeRateRepository = exchangeRateRepository; + _payAdjustmentRepository = paymentAdjustmentRepository; + this._enrollRepository = EnrollRepository; + _mapper = mapper; + } + + /// + /// 获取某个月下的某些医生最终确认的工作量,用于计算月度费用 + /// + private async Task< List> GetFinalConfirmedWorkloadAndPayPriceList(CalculateDoctorAndMonthDTO calculateFeeParam) + { + Expression> workloadLambda = x => true; + + DateTime bTime = new DateTime(calculateFeeParam.CalculateMonth.Year, calculateFeeParam.CalculateMonth.Month, 1); + var eTime = bTime.AddMonths(1); + workloadLambda = workloadLambda.And(t => + t.WorkTime >= bTime && t.WorkTime < eTime); + + workloadLambda = workloadLambda.And(t => calculateFeeParam.NeedCalculateReviewers.Contains(t.DoctorId) && t.DataFrom == (int)WorkLoadFromStatus.FinalConfirm); + var workLoadQueryable = from doctor in _doctorRepository.AsQueryable() + join workLoad in _doctorWorkloadRepository.Where(workloadLambda) on + doctor.Id equals workLoad.DoctorId + join trial in _trialRepository.AsQueryable() on workLoad.TrialId equals trial.Id + join trialPay in _trialPaymentRepository.AsQueryable() on trial.Id equals trialPay.TrialId + into temp + from trialPay in temp.DefaultIfEmpty() + join doctorPayInfo in _doctorPayInfoRepository.AsQueryable() on doctor.Id equals doctorPayInfo.DoctorId + join rankPrice in _rankPriceRepository.AsQueryable() on doctorPayInfo.RankId equals rankPrice.Id + select new CalculatePaymentDTO() + { + Id = workLoad.Id, + DoctorId = workLoad.DoctorId, + WorkTime = workLoad.WorkTime, + DataFrom = workLoad.DataFrom, + TrialId = workLoad.TrialId, + TrialCode = trial.TrialCode, + + Timepoint = workLoad.Timepoint, + TimepointIn24H = workLoad.TimepointIn24H, + TimepointIn48H = workLoad.TimepointIn48H, + Global = workLoad.Global, + Adjudication = workLoad.Adjudication, + AdjudicationIn24H = workLoad.AdjudicationIn24H, + AdjudicationIn48H = workLoad.AdjudicationIn48H, + Training = workLoad.Training, + RefresherTraining = workLoad.RefresherTraining, + Downtime = workLoad.Downtime, + + TrialAdditional = trialPay.TrialAdditional, + PersonalAdditional = doctorPayInfo.Additional, + AdjustmentMultiple = trialPay.AdjustmentMultiple, + + TimepointPrice = rankPrice.Timepoint, + TimepointIn24HPrice = rankPrice.TimepointIn24H, + TimepointIn48HPrice = rankPrice.TimepointIn48H, + AdjudicationPrice = rankPrice.Adjudication, + AdjudicationIn24HPrice = rankPrice.AdjudicationIn24H, + AdjudicationIn48HPrice = rankPrice.AdjudicationIn48H, + DowntimePrice = rankPrice.Downtime, + GlobalPrice = rankPrice.Global, + TrainingPrice = rankPrice.Training, + RefresherTrainingPrice = rankPrice.RefresherTraining + }; + + return await workLoadQueryable.ToListAsync(); + } + + /// + /// 计算月度费用,并调用AddOrUpdateMonthlyPayment和AddOrUpdateMonthlyPaymentDetail方法, + /// 将费用计算的月度数据及详情保存 + /// + [NonDynamicMethod] + public async Task CalculateMonthlyPayment(CalculateDoctorAndMonthDTO param, string token) + { + var yearMonth = param.CalculateMonth.ToString("yyyy-MM"); + var rate = await _exchangeRateRepository.FirstOrDefaultAsync(u => u.YearMonth == yearMonth); + + decimal exchangeRate = rate?.Rate ?? 0; + + var workLoadAndPayPriceList = await GetFinalConfirmedWorkloadAndPayPriceList(param); + var volumeRewardPriceList = await _volumeRewardPriceService.GetVolumeRewardPriceList(); + + #region 奖励数据校验 + for (int i = 0; i < volumeRewardPriceList.Count; i++) + { + if (i == 0 && volumeRewardPriceList[i].Min != 0) + { + return ResponseOutput.NotOk("Volume reward data error."); + } + if (i > 0) + { + if (volumeRewardPriceList[i - 1].Max + 1 != volumeRewardPriceList[i].Min) + return ResponseOutput.NotOk("Volume reward data error."); + } + } + + + #endregion + + + List paymentList = new List(); + List reviewerPaymentUSDList = new List(); + + // 获取所有医生费用 一次从数据库里面全部取出来 + + var allDoctorList = workLoadAndPayPriceList.Where(x => param.NeedCalculateReviewers.Contains(x.DoctorId)).ToList(); + var allDoctorIds = allDoctorList.Select(x => x.DoctorId).Distinct().ToList(); + var listTrialId = allDoctorList.Select(x => x.TrialId).Distinct().ToList(); + + var trialDoctorlist= await (from enroll in _enrollRepository.Where(x=> listTrialId.Contains(x.TrialId)|| allDoctorIds.Contains(x.DoctorId)) + join price in _trialPaymentRepository.Where() on enroll.TrialId equals price.TrialId + select new DoctorPrice() + { + IsNewTrial = price.IsNewTrial, + AdjustmentMultiple = enroll.AdjustmentMultiple, + TrialId=enroll.TrialId, + DoctorId = enroll.DoctorId, + Training=enroll.Training, + Adjudication=enroll.Adjudication, + Adjudication24H=enroll.Adjudication24H, + Adjudication48H= enroll.Adjudication48H, + Downtime=enroll.Downtime, + Global=enroll.Global, + RefresherTraining=enroll.RefresherTraining, + Timepoint= enroll.Timepoint, + Timepoint24H=enroll.Timepoint24H, + Timepoint48H=enroll.Timepoint48H, + }).ToListAsync(); + + + foreach (var doctor in param.NeedCalculateReviewers) + { + if (await _paymentRepository.AnyAsync(u => u.DoctorId == doctor && u.YearMonth == yearMonth && u.IsLock)) + { + break; + } + List paymentDetailList = new List(); + decimal totalNormal = 0; + + //计算单个医生费用统,并且插入到统计表 + var doctorWorkloadAndPayPriceList = workLoadAndPayPriceList.Where(u => u.DoctorId == doctor).ToList(); + + //阅片数量 计算奖励费用 + int readCount = 0; + + int codeOrder = 0; + + //这里需要改 + + foreach (var item in doctorWorkloadAndPayPriceList) + { + var doctordata = trialDoctorlist.Where(x => x.IsNewTrial ?? false && x.Training == item.Training && x.DoctorId == item.DoctorId).FirstOrDefault(); + + if (doctordata != null) + { + item.Training = doctordata.Training??0; + item.Adjudication = doctordata.Adjudication??0; + item.AdjudicationIn24H = doctordata.Adjudication24H??0; + item.AdjudicationIn48H = doctordata.Adjudication48H??0; + item.Downtime = doctordata.Downtime??0; + item.Global = doctordata.Global??0; + item.RefresherTraining = doctordata.RefresherTraining??0; + item.Timepoint = doctordata.Timepoint??0; + item.TimepointIn24H = doctordata.Timepoint24H??0; + item.TimepointIn48H = doctordata.Timepoint48H??0; + item.PersonalAdditional = 0; + } + ++codeOrder; + readCount += (item.Timepoint + item.TimepointIn24H + item.TimepointIn48H + + item.Adjudication + item.AdjudicationIn24H + item.AdjudicationIn48H); + decimal trainingTotal = item.Training * item.TrainingPrice; + decimal refresherTrainingTotal = item.RefresherTraining * item.RefresherTrainingPrice; + decimal downtimeTotal = item.Downtime * item.DowntimePrice; + //规则定义 global 的价格是Tp和个人附加的一半 + decimal globalTotal = item.Global * (item.TimepointPrice / 2 + item.PersonalAdditional / 2); + + //项目如果没有添加附加数据 默认为0 + decimal timePointTotal = item.Timepoint * (item.TimepointPrice * item.AdjustmentMultiple + item.PersonalAdditional + (item.TrialAdditional == null ? 0 : (decimal)item.TrialAdditional)); + + decimal timePointIn24HTotal = item.TimepointIn24H * (item.TimepointIn24HPrice * item.AdjustmentMultiple + item.PersonalAdditional); + decimal timePointIn48HTotal = item.TimepointIn48H * (item.TimepointIn48HPrice * item.AdjustmentMultiple + item.PersonalAdditional); + decimal adjudicationTotal = item.Adjudication * (item.AdjudicationPrice * item.AdjustmentMultiple + item.PersonalAdditional + (item.TrialAdditional == null ? 0 : (decimal)item.TrialAdditional)); + decimal adjudicationIn24HTotal = item.AdjudicationIn24H * (item.AdjudicationIn24HPrice * item.AdjustmentMultiple + item.PersonalAdditional); + decimal adjudicationIn48HTotal = item.AdjudicationIn48H * (item.AdjudicationIn48HPrice * item.AdjustmentMultiple + item.PersonalAdditional); + + totalNormal += (trainingTotal + refresherTrainingTotal + downtimeTotal + globalTotal + timePointTotal + timePointIn24HTotal + + timePointIn48HTotal + adjudicationTotal + adjudicationIn24HTotal + adjudicationIn48HTotal); + + #region 统计明细信息 + + paymentDetailList.Add(new PaymentDetailCommand + { + TrialCode = item.TrialCode, + PaymentType = "Training", + Count = item.Training, + BasePrice = item.TrainingPrice, + PersonalAdditional = 0, + TrialAdditional = 0, + PaymentId = Guid.Empty, + DoctorId = item.DoctorId, + TrialId = item.TrialId, + ShowTypeOrder = 1, + ShowCodeOrder = codeOrder, + ExchangeRate = exchangeRate, + YearMonth = yearMonth, + PaymentUSD = item.Training * item.TrainingPrice, + PaymentCNY = item.Training * item.TrainingPrice * exchangeRate + }); + paymentDetailList.Add(new PaymentDetailCommand + { + TrialCode = item.TrialCode, + PaymentType = "Refresher Training", + Count = item.RefresherTraining, + BasePrice = item.RefresherTrainingPrice, + PersonalAdditional = 0, + TrialAdditional = 0, + PaymentId = Guid.Empty, + DoctorId = item.DoctorId, + TrialId = item.TrialId, + ShowTypeOrder = 2, + ShowCodeOrder = codeOrder, + ExchangeRate = exchangeRate, + YearMonth = yearMonth, + PaymentUSD = item.RefresherTraining * item.RefresherTrainingPrice, + PaymentCNY = item.RefresherTraining * item.RefresherTrainingPrice * exchangeRate + }); + paymentDetailList.Add(new PaymentDetailCommand + { + TrialCode = item.TrialCode, + PaymentType = "Downtime", + Count = item.Downtime, + BasePrice = item.DowntimePrice, + PersonalAdditional = 0, + TrialAdditional = 0, + PaymentId = Guid.Empty, + DoctorId = item.DoctorId, + TrialId = item.TrialId, + ShowTypeOrder = 3, + ShowCodeOrder = codeOrder, + ExchangeRate = exchangeRate, + YearMonth = yearMonth, + PaymentUSD = item.Downtime * item.DowntimePrice, + PaymentCNY = item.Downtime * item.DowntimePrice * exchangeRate + }); + + paymentDetailList.Add(new PaymentDetailCommand + { + TrialCode = item.TrialCode, + PaymentType = "Timepoint Regular", + Count = item.Timepoint, + BasePrice = item.TimepointPrice, + PersonalAdditional = doctordata!=null?0: item.PersonalAdditional, + TrialAdditional = doctordata != null ? 0 : item.TimepointPrice * (item.AdjustmentMultiple - 1) + (item.TrialAdditional == null ? 0 : (decimal)item.TrialAdditional), + PaymentId = Guid.Empty, + DoctorId = item.DoctorId, + TrialId = item.TrialId, + ShowTypeOrder = 4, + ShowCodeOrder = codeOrder, + ExchangeRate = exchangeRate, + YearMonth = yearMonth, + PaymentUSD = item.Timepoint * (item.TimepointPrice * item.AdjustmentMultiple + item.PersonalAdditional + (item.TrialAdditional == null ? 0 : (decimal)item.TrialAdditional)), + PaymentCNY = item.Timepoint * (item.TimepointPrice * item.AdjustmentMultiple + item.PersonalAdditional + (item.TrialAdditional == null ? 0 : (decimal)item.TrialAdditional)) * exchangeRate + }); + + paymentDetailList.Add(new PaymentDetailCommand + { + TrialCode = item.TrialCode, + PaymentType = "Timepoint 48-Hour", + Count = item.TimepointIn48H, + BasePrice = item.TimepointIn48HPrice, + PersonalAdditional = doctordata != null ? 0 : item.PersonalAdditional, + TrialAdditional = doctordata != null ? 0 : item.TimepointIn48HPrice * (item.AdjustmentMultiple - 1) + 0,//48小时不加项目附加 + PaymentId = Guid.Empty, + DoctorId = item.DoctorId, + TrialId = item.TrialId, + ShowTypeOrder = 5, + ShowCodeOrder = codeOrder, + ExchangeRate = exchangeRate, + YearMonth = yearMonth, + PaymentUSD = item.TimepointIn48H * (item.TimepointIn48HPrice * item.AdjustmentMultiple + 0 + item.PersonalAdditional), + PaymentCNY = item.TimepointIn48H * (item.TimepointIn48HPrice * item.AdjustmentMultiple + 0 + item.PersonalAdditional) * exchangeRate + }); + paymentDetailList.Add(new PaymentDetailCommand + { + TrialCode = item.TrialCode, + PaymentType = "Timepoint 24-Hour", + Count = item.TimepointIn24H, + BasePrice = item.TimepointIn24HPrice, + PersonalAdditional = doctordata != null ? 0 : item.PersonalAdditional, + TrialAdditional = doctordata != null ? 0 : item.TimepointIn24HPrice * (item.AdjustmentMultiple - 1) + 0, + PaymentId = Guid.Empty, + DoctorId = item.DoctorId, + TrialId = item.TrialId, + ShowTypeOrder = 6, + ShowCodeOrder = codeOrder, + ExchangeRate = exchangeRate, + YearMonth = yearMonth, + PaymentUSD = item.TimepointIn24H * (item.TimepointIn24HPrice * item.AdjustmentMultiple + 0 + item.PersonalAdditional), + PaymentCNY = item.TimepointIn24H * (item.TimepointIn24HPrice * item.AdjustmentMultiple + 0 + item.PersonalAdditional) * exchangeRate + }); + paymentDetailList.Add(new PaymentDetailCommand + { + TrialCode = item.TrialCode, + PaymentType = "Adjudication Regular", + Count = item.Adjudication, + BasePrice = item.AdjudicationPrice, + PersonalAdditional = doctordata != null ? 0 : item.PersonalAdditional, + TrialAdditional = doctordata != null ? 0 : item.AdjudicationPrice * (item.AdjustmentMultiple - 1) + (item.TrialAdditional == null ? 0 : (decimal)item.TrialAdditional), + PaymentId = Guid.Empty, + DoctorId = item.DoctorId, + TrialId = item.TrialId, + ShowTypeOrder = 7, + ShowCodeOrder = codeOrder, + ExchangeRate = exchangeRate, + YearMonth = yearMonth, + PaymentUSD = item.Adjudication * (item.AdjudicationPrice * item.AdjustmentMultiple + item.PersonalAdditional + (item.TrialAdditional == null ? 0 : (decimal)item.TrialAdditional)), + PaymentCNY = item.Adjudication * (item.AdjudicationPrice * item.AdjustmentMultiple + item.PersonalAdditional + (item.TrialAdditional == null ? 0 : (decimal)item.TrialAdditional)) * exchangeRate + }); + + paymentDetailList.Add(new PaymentDetailCommand + { + TrialCode = item.TrialCode, + PaymentType = "Adjudication 48-Hour", + Count = item.AdjudicationIn48H, + BasePrice = item.AdjudicationIn48HPrice, + PersonalAdditional = doctordata != null ? 0 : item.PersonalAdditional, + TrialAdditional = doctordata != null ? 0 : item.AdjudicationIn48HPrice * (item.AdjustmentMultiple - 1) + 0, + PaymentId = Guid.Empty, + DoctorId = item.DoctorId, + TrialId = item.TrialId, + ShowTypeOrder = 8, + ShowCodeOrder = codeOrder, + ExchangeRate = exchangeRate, + YearMonth = yearMonth, + PaymentUSD = item.AdjudicationIn48H * (item.AdjudicationIn48HPrice * item.AdjustmentMultiple + item.PersonalAdditional + 0), + PaymentCNY = item.AdjudicationIn48H * (item.AdjudicationIn48HPrice * item.AdjustmentMultiple + item.PersonalAdditional + 0) * exchangeRate + }); + paymentDetailList.Add(new PaymentDetailCommand + { + TrialCode = item.TrialCode, + PaymentType = "Adjudication 24-Hour", + Count = item.AdjudicationIn24H, + BasePrice = item.AdjudicationIn24HPrice, + PersonalAdditional = doctordata != null ? 0 : item.PersonalAdditional, + TrialAdditional = doctordata != null ? 0 : item.AdjudicationIn24HPrice * (item.AdjustmentMultiple - 1) + 0, + PaymentId = Guid.Empty, + DoctorId = item.DoctorId, + TrialId = item.TrialId, + ShowTypeOrder = 9, + ShowCodeOrder = codeOrder, + ExchangeRate = exchangeRate, + YearMonth = yearMonth, + PaymentUSD = item.AdjudicationIn24H * (item.AdjudicationIn24HPrice * item.AdjustmentMultiple + 0 + item.PersonalAdditional), + PaymentCNY = item.AdjudicationIn24H * (item.AdjudicationIn24HPrice * item.AdjustmentMultiple + 0 + item.PersonalAdditional) * exchangeRate + }); + paymentDetailList.Add(new PaymentDetailCommand + { + TrialCode = item.TrialCode, + PaymentType = "Global", + Count = item.Global, + BasePrice = item.TimepointPrice / 2,//item.GlobalPrice, + PersonalAdditional = doctordata != null ? 0 : item.PersonalAdditional / 2, + TrialAdditional = 0, + PaymentId = Guid.Empty, + DoctorId = item.DoctorId, + TrialId = item.TrialId, + ShowTypeOrder = 10, + ShowCodeOrder = codeOrder, + ExchangeRate = exchangeRate, + YearMonth = yearMonth, + PaymentUSD = item.Global * (item.TimepointPrice / 2 + item.PersonalAdditional / 2), + PaymentCNY = item.Global * (item.TimepointPrice / 2 + item.PersonalAdditional / 2) * exchangeRate + }); + #endregion + } + + int typeOrder = 0; + if (readCount > 0) + { + paymentDetailList.Add(new PaymentDetailCommand + { + TrialCode = "Volume Reward", + PaymentType = "Total TP & AD", + Count = readCount, + BasePrice = 0, + PersonalAdditional = 0, + TrialAdditional = 0, + PaymentId = Guid.Empty, + DoctorId = doctor, + TrialId = Guid.Empty, + ShowTypeOrder = typeOrder, + ShowCodeOrder = (++codeOrder), + ExchangeRate = exchangeRate, + YearMonth = yearMonth, + PaymentUSD = 0, + PaymentCNY = 0 + }); + + foreach (var awardItem in volumeRewardPriceList) + { + ++typeOrder; + if ((readCount - awardItem.Min + 1) < 0) + { + break; + } + if (awardItem.Min == 0) + { + paymentDetailList.Add(new PaymentDetailCommand + { + TrialCode = "Volume Reward", + PaymentType = awardItem.Min + "-" + awardItem.Max, + Count = readCount >= awardItem.Max ? + (awardItem.Max - awardItem.Min) : (readCount - awardItem.Min), + BasePrice = awardItem.Price, + PersonalAdditional = 0, + TrialAdditional = 0, + PaymentId = Guid.Empty,//result.Data, + DoctorId = doctor, + TrialId = Guid.Empty, + ShowTypeOrder = typeOrder, + ShowCodeOrder = (++codeOrder), + ExchangeRate = exchangeRate, + YearMonth = yearMonth, + PaymentUSD = (readCount >= awardItem.Max ? + (awardItem.Max - awardItem.Min) : (readCount - awardItem.Min)) * awardItem.Price, + PaymentCNY = (readCount >= awardItem.Max ? + (awardItem.Max - awardItem.Min) : (readCount - awardItem.Min)) * awardItem.Price * exchangeRate + }); + } + else + { + paymentDetailList.Add(new PaymentDetailCommand + { + TrialCode = "Volume Reward", + PaymentType = awardItem.Min + "-" + awardItem.Max, + Count = readCount >= awardItem.Max ? + (awardItem.Max - awardItem.Min + 1) : (readCount - awardItem.Min + 1), + BasePrice = awardItem.Price, + PersonalAdditional = 0, + TrialAdditional = 0, + PaymentId = Guid.Empty, + DoctorId = doctor, + TrialId = Guid.Empty, + ShowTypeOrder = typeOrder, + ShowCodeOrder = (++codeOrder), + ExchangeRate = exchangeRate, + YearMonth = yearMonth, + PaymentUSD = (readCount >= awardItem.Max ? + (awardItem.Max - awardItem.Min + 1) : (readCount - awardItem.Min + 1)) * awardItem.Price, + PaymentCNY = (readCount >= awardItem.Max ? + (awardItem.Max - awardItem.Min + 1) : (readCount - awardItem.Min + 1)) * awardItem.Price * exchangeRate + }); + } + } + + } + decimal award = 0; + volumeRewardPriceList = volumeRewardPriceList.OrderBy(u => u.Min).ToList(); + + var levelTemp = -1; //用来计算属于哪一个挡位 + foreach (var awarPriceitem in volumeRewardPriceList) + { + if (awarPriceitem.Min == 0) + { + if (readCount > awarPriceitem.Max) + { + ++levelTemp; + award += (awarPriceitem.Max - awarPriceitem.Min) * awarPriceitem.Price; + } + if (awarPriceitem.Min < readCount && readCount < awarPriceitem.Max) + { + ++levelTemp; + award += (readCount - awarPriceitem.Min) * awarPriceitem.Price; + break; ; + } + } + else + { + if (readCount > awarPriceitem.Max) + { + ++levelTemp; + award += (awarPriceitem.Max - awarPriceitem.Min + 1) * awarPriceitem.Price; + } + if (awarPriceitem.Min < readCount && readCount < awarPriceitem.Max) + { + ++levelTemp; + award += (readCount - awarPriceitem.Min + 1) * awarPriceitem.Price; + break; ; + } + } + } + decimal totalUSD = award + totalNormal;//总费用 + + var result = await AddOrUpdateMonthlyPayment(new PaymentCommand + { + DoctorId = doctor, + Year = param.CalculateMonth.Year, + Month = param.CalculateMonth.Month, + PaymentUSD = totalUSD, + CalculateUser = token, + CalculateTime = DateTime.Now, + ExchangeRate = exchangeRate, + PaymentCNY = exchangeRate * totalUSD, + }); + + + reviewerPaymentUSDList.Add(new ReviewerPaymentUSD { DoctorId = doctor, PaymentUSD = totalUSD, RecordId = result.Data }); + foreach (var detail in paymentDetailList) + { + //var data = trialDoctorlist.FirstOrDefault(x => x.DoctorId == detail.DoctorId && x.TrialId == detail.TrialId && x.IsNewTrial == true && (x.AdjustmentMultiple??0) != 0); + //if (data != null) + //{ + // detail.BasePrice = data.AdjustmentMultiple??0; + // detail.PersonalAdditional = 0; + // detail.TrialAdditional = 0; + //} + + detail.PaymentId = result.Data; + } + await AddOrUpdateMonthlyPaymentDetail(paymentDetailList, result.Data); + + await UpdatePaymentAdjustment(doctor, yearMonth); + } + return ResponseOutput.Ok(reviewerPaymentUSDList); + } + + + + + // 重新计算调整费用 + + private async Task UpdatePaymentAdjustment(Guid reviewerId, string yearMonth) + { + var adjustList = await _payAdjustmentRepository.Where(u => u.YearMonth == yearMonth && + !u.IsLock && u.ReviewerId == reviewerId).ToListAsync(); + + + var needUpdatePayment = adjustList.GroupBy(t => t.ReviewerId).Select(g => new + { + ReviewerId = g.Key, + AdjustCNY = g.Sum(t => t.AdjustmentCNY), + AdjustUSD = g.Sum(t => t.AdjustmentUSD) + }); + + foreach (var reviewer in needUpdatePayment) + { + await _paymentRepository.UpdateFromQueryAsync(u => u.YearMonth == yearMonth && + !u.IsLock && u.DoctorId == reviewer.ReviewerId, t => new Payment() + { + AdjustmentUSD = reviewer.AdjustUSD, + AdjustmentCNY = reviewer.AdjustCNY + + }); + } + } + + /// + /// 保存费用计算的月度数据 + /// + private async Task> AddOrUpdateMonthlyPayment(PaymentCommand addOrUpdateModel) + { + var success = false; + var paymentModel = await _paymentRepository.FirstOrDefaultAsync(t => + t.DoctorId == addOrUpdateModel.DoctorId && t.YearMonth == addOrUpdateModel.YearMonth); + + + + //var taxCNY = GetTax(addOrUpdateModel.PaymentCNY); + //var actuallyPaidCNY = addOrUpdateModel.PaymentCNY - taxCNY; + //var bankTransferCNY = addOrUpdateModel.PaymentCNY - taxCNY; + + if (paymentModel == null) + { + var payment = _mapper.Map(addOrUpdateModel); + + //payment.BankTransferCNY = bankTransferCNY; + //payment.TaxCNY= taxCNY; + //payment.BankTransferCNY = bankTransferCNY; + payment.YearMonthDate = DateTime.Parse(payment.YearMonth); + + payment =await _paymentRepository.AddAsync(payment); + success =await _paymentRepository.SaveChangesAsync(); + return ResponseOutput.Result(success, payment.Id); + } + else + { + // 如果是 当月计算的工作量费用 和 调整费用都为0,则删除该行记录 + if (addOrUpdateModel.PaymentUSD == 0 && paymentModel.AdjustmentUSD == 0) + { + success =await _paymentRepository.DeleteFromQueryAsync(u => u.Id == paymentModel.Id); + //_paymentDetailRepository.Delete(u=>u.PaymentId==paymentModel.Id); + } + else + { + success = await _paymentRepository.UpdateFromQueryAsync(t => t.Id == paymentModel.Id, u => new Payment() + { + PaymentUSD = addOrUpdateModel.PaymentUSD, + CalculateTime = addOrUpdateModel.CalculateTime, + CalculateUser = addOrUpdateModel.CalculateUser, + //TaxCNY = taxCNY, + //ActuallyPaidCNY = actuallyPaidCNY, + //BankTransferCNY = bankTransferCNY, + PaymentCNY = addOrUpdateModel.PaymentCNY, + ExchangeRate = addOrUpdateModel.ExchangeRate + }); + } + + return ResponseOutput.Result(success, paymentModel.Id); + } + + } + + + + + /// + /// 保存费用计算的月度详情 + /// + private async Task AddOrUpdateMonthlyPaymentDetail(List addOrUpdateList, Guid paymentId) + { + //var paymentDetailIds = addOrUpdateList.Select(t => t.PaymentId).ToList(); + await _paymentDetailRepository.DeleteFromQueryAsync(t => t.PaymentId == paymentId); + await _paymentDetailRepository.AddRangeAsync(_mapper.Map>(addOrUpdateList)); + return await _paymentDetailRepository.SaveChangesAsync(); + } + + + + /// + /// 获取待计算费用的Reviewer对应的月份列表 + /// + public async Task> GetNeedCalculateReviewerList(Guid reviewerId, string yearMonth) + { + Expression> calculateLambda = u => !u.IsLock; + if (reviewerId != Guid.Empty) + { + calculateLambda = calculateLambda.And(u => u.DoctorId == reviewerId); + } + if (!string.IsNullOrWhiteSpace(yearMonth)) + { + calculateLambda = calculateLambda.And(u => u.YearMonth == yearMonth); + } + return await _paymentRepository.Where(calculateLambda).ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + } + + /// + /// 查询Reviewer某个月的费用是否被锁定 + /// + public async Task IsLock(Guid reviewerId, string yearMonth) + { + return await _paymentRepository.AnyAsync(u => u.DoctorId == reviewerId && u.YearMonth == yearMonth && u.IsLock); + } + + //public bool ResetMonthlyPayment(Guid reviewerId, Guid trialId, string yearMonth) + //{ + // var payment = _paymentRepository.FindSingleOrDefault(u => u.DoctorId == reviewerId && u.YearMonth == yearMonth); + // payment.PaymentCNY = 0; + // payment.PaymentUSD = 0; + // _paymentRepository.Update(payment); + // _paymentDetailRepository.Delete(u=>u.DoctorId==reviewerId && u.TrialId==trial) + //} + } +} diff --git a/IRaCIS.Core.Application/Service/Financial/DTO/AwardPriceModel.cs b/IRaCIS.Core.Application/Service/Financial/DTO/AwardPriceModel.cs new file mode 100644 index 00000000..2dfffd2f --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/DTO/AwardPriceModel.cs @@ -0,0 +1,37 @@ +using System; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public class AwardPriceDTO: AwardPriceCalculateDTO + { + public Guid Id { get; set; } + } + + public class AwardPriceCalculateDTO + { + public decimal Price { get; set; } + public int Max { get; set; } + public int Min { get; set; } + } + + public class AwardPriceCommand + { + //public Guid Id { get; set; } + public decimal Price { get; set; } + public int Min { get; set; } + public int Max { get; set; } + public Guid OptUserId { get; set; } + } + + public class AwardPriceQueryDTO : PageInput + { + } + + public class ExchangeRateQueryDTO : PageInput + { + public DateTime? SearchMonth { get; set; } + } + +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Financial/DTO/CalculateModel.cs b/IRaCIS.Core.Application/Service/Financial/DTO/CalculateModel.cs new file mode 100644 index 00000000..4d82f742 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/DTO/CalculateModel.cs @@ -0,0 +1,44 @@ +using System; + +namespace IRaCIS.Application.Contracts +{ + public class CalculateNeededDTO + { + public Guid Id { get; set; } + public Guid DoctorId { get; set; } + public string YearMonth { get; set; } = String.Empty; + public bool IsLock { get; set; } + } + + public class DoctorPrice + { + public decimal? AdjustmentMultiple { get; set; } + + public Guid? DoctorId { get; set; } + + public Guid? TrialId { get; set; } + + public bool? IsNewTrial { get; set; } + + public int? Training { get; set; } + + public int? RefresherTraining { get; set; } + + public int? Timepoint { get; set; } + + public int? Timepoint48H { get; set; } + + public int? Timepoint24H { get; set; } + + public int? Adjudication { get; set; } + + public int? Adjudication48H { get; set; } + + public int? Adjudication24H { get; set; } + + public int? Global { get; set; } + + + public int? Downtime { get; set; } + } +} diff --git a/IRaCIS.Core.Application/Service/Financial/DTO/ExchangeRateModel.cs b/IRaCIS.Core.Application/Service/Financial/DTO/ExchangeRateModel.cs new file mode 100644 index 00000000..a56b4c16 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/DTO/ExchangeRateModel.cs @@ -0,0 +1,13 @@ +using System; + +namespace IRaCIS.Application.Contracts +{ + public class ExchangeRateCommand + { + public Guid? Id { get; set; } + public string YearMonth { get; set; }=String.Empty; + public decimal Rate { get; set; } + + public DateTime UpdateTime { get; set; } + } +} diff --git a/IRaCIS.Core.Application/Service/Financial/DTO/PaymentAdjustmentModel.cs b/IRaCIS.Core.Application/Service/Financial/DTO/PaymentAdjustmentModel.cs new file mode 100644 index 00000000..1a399238 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/DTO/PaymentAdjustmentModel.cs @@ -0,0 +1,54 @@ +using System; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Contracts.Pay +{ + public class PaymentAdjustmentCommand + { + public Guid? Id { get; set; } + public Guid ReviewerId { get; set; } + public DateTime YearMonth { get; set; } + public decimal AdjustPaymentUSD { get; set; } + public decimal AdjustPaymentCNY { get; set; } + public string Note { get; set; } = string.Empty; + } + + public class PaymentAdjustmentDTO + { + public Guid Id { get; set; } + public Guid ReviewerId { get; set; } + public string YearMonth { get; set; }=String.Empty; + public DateTime YearMonthDate { get; set; } + public bool IsLock { get; set; } + public decimal AdjustPaymentUSD { get; set; } + public decimal AdjustPaymentCNY { get; set; } + public string Note { get; set; } = String.Empty; + } + + public class PaymentAdjustmentDetailDTO: PaymentAdjustmentDTO + { + public string ReviewerCode { get; set; } = String.Empty; + public string FirstName { get; set; } = String.Empty; + public string LastName { get; set; } = String.Empty; + public string FullName => LastName + " / " + FirstName; + public string ChineseName { get; set; } = String.Empty; + } + + public class PaymentAdjustmentQueryDTO:PageInput + { + public string TrialCode { get; set; } = string.Empty; + public string Reviewer { get; set; } = string.Empty; + public DateTime BeginMonth { get; set; } + public DateTime EndMonth { get; set; } + } + + public class DoctorSelectDTO + { + public Guid Id { get; set; } + public string Code { get; set; } = string.Empty; + public string FirstName { get; set; } = String.Empty; + public string LastName { get; set; } = String.Empty; + public string FullName => LastName + " / " + FirstName; + public string ChineseName { get; set; } = String.Empty; + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Financial/DTO/PaymentDetailModel.cs b/IRaCIS.Core.Application/Service/Financial/DTO/PaymentDetailModel.cs new file mode 100644 index 00000000..0a9821ad --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/DTO/PaymentDetailModel.cs @@ -0,0 +1,193 @@ +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Contracts.Pay +{ + public class PaymentDetailDTO + { + public Guid Id { get; set; } + public Guid PaymentId { get; set; } + public string YearMonth { get; set; } = String.Empty; + public Guid DoctorId { get; set; } + public Guid TrialId { get; set; } + public string TrialCode { get; set; } = String.Empty; + public string PaymentType { get; set; } = String.Empty; + public int Count { get; set; } + public decimal BasePrice { get; set; } + public decimal PersonalAdditional { get; set; } + + public decimal? NewPersonalAdditional { get; set; } + public decimal TrialAdditional { get; set; } + public int ShowTypeOrder { get; set; } + public int ShowCodeOrder { get; set; } + + public decimal ExchangeRate { get; set; } + + public decimal PaymentUSD { get; set; } + public decimal PaymentCNY { get; set; } + + public bool? IsNewTrial { get; set; } + + public decimal TotalUnitPrice => BasePrice + PersonalAdditional + TrialAdditional; + + public AdjustmentDTO AdjustmentView { get; set; } = new AdjustmentDTO(); + + } + + public class AdjustmentDTO + { + public decimal AdjustPaymentUSD { get; set; } + + public decimal AdjustPaymentCNY { get; set; } + + public string AdjustType + { + get + { + if (AdjustPaymentUSD > 0) + { + return "+"; + } + else if (AdjustPaymentUSD < 0) + { + return "-"; + } + else { return string.Empty; } + } + } + public string Note { get; set; } = String.Empty; + } + + + public class PaymentDetailCommand : PaymentDetailDTO + { + + } + + public class PayDetailDTO + { + public IEnumerable DetailList { get; set; } = new List(); + public DoctorPayInfo DoctorInfo { get; set; } = new DoctorPayInfo(); + } + + public class LockPaymentDTO + { + public List ReviewerIdList { get; set; }=new List(); + public DateTime Month { get; set; } + + public bool IsLock { get; set; } = true; + } + + public class DoctorPayInfo + { + public Guid DoctorId { get; set; } + public string ChineseName { get; set; } = String.Empty; + public string FirstName { get; set; } = String.Empty; + public string LastName { get; set; } = String.Empty; + public string Phone { get; set; } = String.Empty; + public string PayTitle { get; set; } = String.Empty; + public string Code { get; set; } = String.Empty; + public string YearMonth { get; set; } = String.Empty; + } + + + + + + + + public class ReviewerPaymentUSD + { + public Guid RecordId { get; set; } + public Guid DoctorId { get; set; } + public decimal PaymentUSD { get; set; } + } + + public class PaymentQueryDTO : PageInput + { + public string Reviewer { get; set; } = String.Empty; + public DateTime BeginMonth { get; set; } + public DateTime EndMonth { get; set; } + public int? Nation { get; set; } + } + public class MonthlyPaymentDTO + { + public Guid ReviewerId { get; set; } + public string ReviewerCode { get; set; } = String.Empty; + public string ChineseName { get; set; } = String.Empty; + public string FirstName { get; set; } = String.Empty; + public string LastName { get; set; } = String.Empty; + + public decimal AdjustmentUSD { get; set; } + public decimal AdjustmentCNY { get; set; } + public decimal PaymentUSD { get; set; } + public decimal PaymentCNY { get; set; } + + public decimal TotalUSD { get; set; } + public decimal TotalCNY { get; set; } + + } + + public class VolumeStatisticsDTO + { + public Guid StatisticsId { get; set; } + public string Month { get; set; } = String.Empty; + public decimal VolumeReward { get; set; } + public decimal ExchangeRate { get; set; } + + public decimal AdjustmentUSD { get; set; } + public decimal AdjustmentCNY { get; set; } + public decimal PaymentUSD { get; set; } + public decimal PaymentCNY { get; set; } + public decimal TotalCNY => AdjustmentCNY + PaymentCNY; + public decimal TotalUSD => AdjustmentUSD + PaymentUSD; + + public List TrialPaymentList = new List(); + } + + public class TrialPaymentDTO + { + public Guid TrialId { get; set; } + public string TrialCode { get; set; } = String.Empty; + public decimal TrialPayment { get; set; } + + } + public class VolumeQueryDTO + { + public DateTime BeginMonth { get; set; } + public DateTime EndMonth { get; set; } + public Guid ReviewerId { get; set; } + } + + public class RevenuesDTO + { + public List MissingTrialCodes = new List(); + public Guid Id { get; set; } + public string TrialCode { get; set; } = String.Empty; + public Guid TrialId { get; set; } + public string Indication { get; set; } = String.Empty; + public Guid? CroId { get; set; } + public string Cro { get; set; } = string.Empty; + + public int Expedited { get; set; } + public string ChineseName { get; set; } = string.Empty; + + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public string ReviewerCode { get; set; } = string.Empty; + + public decimal Training { get; set; } + public decimal Downtime { get; set; } + public decimal Timepoint { get; set; } + public decimal TimepointIn24H { get; set; } + public decimal TimepointIn48H { get; set; } + public decimal Adjudication { get; set; } + public decimal AdjudicationIn24H { get; set; } + public decimal AdjudicationIn48H { get; set; } + public decimal Global { get; set; } + public decimal Total { get; set; } + + public string YearMonth { get; set; } = String.Empty; + } + + +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Financial/DTO/PaymentModel.cs b/IRaCIS.Core.Application/Service/Financial/DTO/PaymentModel.cs new file mode 100644 index 00000000..77480739 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/DTO/PaymentModel.cs @@ -0,0 +1,178 @@ +using System; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Contracts +{ + public class PaymentDTO + { + public PageOutput CostList { get; set; }=new PageOutput(); + public decimal ExchangeRate { get; set; } + } + public class PaymentModel + { + public Guid Id { get; set; } + public string RankName { get; set; } = String.Empty; + public Guid DoctorId { get; set; } + public string YearMonth { get; set; } = String.Empty; + public DateTime YearMonthDate { get; set; } + public DateTime? CalculateTime { get; set; } + public string CalculateUser { get; set; } = String.Empty; + + //额外信息 + public string Code { get; set; } = String.Empty; + public string FirstName { get; set; } = String.Empty; + public string LastName { get; set; } = String.Empty; + + public string ChineseName { get; set; } = String.Empty; + public string Phone { get; set; } = String.Empty; + public string DoctorNameInBank { get; set; } = String.Empty; + public string IDCard { get; set; } = String.Empty; + public string BankCardNumber { get; set; } = String.Empty; + public string BankName { get; set; } = String.Empty; + + public bool IsLock { get; set; } = false; + + public decimal ExchangeRate { get; set; } + + public decimal PaymentCNY { get; set; } + + public decimal AdjustPaymentCNY { get; set; } + public decimal TotalPaymentCNY { get; set; } + public decimal PaymentUSD { get; set; } + + public decimal AdjustPaymentUSD { get; set; } + public decimal TotalPaymentUSD { get; set; } + } + + public class PaymentCommand + { + public Guid Id { get; set; } + public Guid DoctorId { get; set; } + public string YearMonth => new DateTime(Year, Month, 1).ToString("yyyy-MM"); + public int Year { get; set; } + public int Month { get; set; } + public decimal PaymentUSD { get; set; } + public DateTime CalculateTime { get; set; } + public decimal PaymentCNY { get; set; } + public decimal ExchangeRate { get; set; } + public string CalculateUser { get; set; } = String.Empty; + } + + public class MonthlyPaymentQueryDTO : PageInput + { + public DateTime StatisticsDate { get; set; } + public string KeyWord { get; set; } = String.Empty; + public int? Nation { get; set; } + } + + public class MonthlyPaymentDetailQuery + { + public Guid PaymentId { get; set; } + public Guid ReviewerId { get; set; } + + public DateTime YearMonth { get; set; } + } + + //public class LaborPaymentQuery + //{ + // public Guid PaymentId { get; set; } + // public Guid ReviewerId { get; set; } + + // public DateTime YearMonth { get; set; } + //} + + + public class TrialAnalysisDTO + { + public Guid TrialId { get; set; } + public string Indication { get; set; } = String.Empty; + + public string TrialCode { get; set; } = String.Empty; + public string Cro { get; set; } = string.Empty; + + public int Expedited { get; set; } + + public string Type { get; set; } = String.Empty; + + public decimal PaymentUSD { get; set; } + public decimal RevenusUSD { get; set; } + public decimal GrossProfit => RevenusUSD - PaymentUSD; + public decimal GrossProfitMargin + { + get { + if (RevenusUSD == 0) + return 0; + else + { + return GrossProfit / RevenusUSD; + } + } + } + + } + + public class LaborPayment + { + public string ChineseName { get; set; } = String.Empty; + public string FirstName { get; set; } = String.Empty; + public string LastName { get; set; } = String.Empty; + + public string ResidentId { get; set; } = String.Empty; + + public string Phone { get; set; } = String.Empty; + + public string AccountNumber { get; set; } = String.Empty; + + public string Bank { get; set; } = String.Empty; + public string YearMonth { get; set; } = String.Empty; + + public decimal PaymentCNY { get; set; } + public decimal TaxCNY { get; set; } + public decimal ActuallyPaidCNY { get; set; } + public decimal BankTransferCNY { get; set; } + } + public class ReviewerAnalysisDTO + { + public List MissingTrialCodes = new List(); + public string ChineseName { get; set; } = String.Empty; + public string FirstName { get; set; } = String.Empty; + public string LastName { get; set; } = String.Empty; + + public Guid ReviewerId { get; set; } + public string ReviewerCode { get; set; } = String.Empty; + + public decimal PaymentUSD { get; set; } + public decimal RevenusUSD { get; set; } + public decimal GrossProfit => RevenusUSD - PaymentUSD; + + public decimal GrossProfitMargin + { + get { + if (RevenusUSD == 0) + return 0; + else + { + return GrossProfit / RevenusUSD; + } + } + } + } + public class AnalysisQueryDTO + { + public string Reviewer { get; set; } = String.Empty; + public DateTime BeginDate { get; set; } + public DateTime EndDate { get; set; } + + public int? Nation { get; set; } + } + + public class TrialAnalysisQueryDTO + { + public Guid? CroId { get; set; } + public string TrialCode { get; set; } = String.Empty; + public DateTime BeginDate { get; set; } + public DateTime EndDate { get; set; } + + public int? AttendedReviewerType { get; set; } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Financial/DTO/RankPriceModel.cs b/IRaCIS.Core.Application/Service/Financial/DTO/RankPriceModel.cs new file mode 100644 index 00000000..4e39bdc0 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/DTO/RankPriceModel.cs @@ -0,0 +1,50 @@ +using System; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Contracts +{ + public class RankPriceDTO + { + public Guid Id { get; set; } + public string RankName { get; set; } = string.Empty; + public decimal Timepoint { get; set; } + public decimal TimepointIn24H { get; set; } + public decimal TimepointIn48H { get; set; } + public decimal Adjudication { get; set; } + public decimal AdjudicationIn24H { get; set; } + public decimal AdjudicationIn48H { get; set; } + public decimal Global { get; set; } + public decimal Training { get; set; } + public decimal Downtime { get; set; } + + public decimal RefresherTraining { get; set; } + public int ShowOrder { get; set; } + } + + public class RankDic + { + public Guid Id { get; set; } + public string RankName { get; set; } = string.Empty; + } + + public class RankPriceCommand + { + public Guid? Id { get; set; } + public string RankName { get; set; } = string.Empty; + public decimal Timepoint { get; set; } + public decimal TimepointIn24H { get; set; } + public decimal TimepointIn48H { get; set; } + public decimal Adjudication { get; set; } + public decimal AdjudicationIn24H { get; set; } + public decimal AdjudicationIn48H { get; set; } + public decimal Global { get; set; } + public decimal Training { get; set; } + public decimal Downtime { get; set; } + public decimal RefresherTraining { get; set; } + + + } + public class RankPriceQueryDTO : PageInput + { + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Financial/DTO/ReviewerPayInfoModel.cs b/IRaCIS.Core.Application/Service/Financial/DTO/ReviewerPayInfoModel.cs new file mode 100644 index 00000000..12e7de8b --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/DTO/ReviewerPayInfoModel.cs @@ -0,0 +1,40 @@ +using System; + +namespace IRaCIS.Application.Contracts +{ + public class ReviewerPayInfoQueryDTO + { + public Guid DoctorId { get; set; } + public string FirstName { get; set; } = String.Empty; + public string LastName { get; set; } = String.Empty; + public string ChineseName { get; set; } = String.Empty; + public string Code { get; set; } = String.Empty; + public string Phone { get; set; } = String.Empty; + public string DoctorNameInBank { get; set; } = String.Empty; + public string IDCard { get; set; } = String.Empty; + public string BankCardNumber { get; set; } = String.Empty; + public string BankName { get; set; } = String.Empty; + public Guid? RankId { get; set; } + public decimal? Additional { get; set; } + public DateTime? CreateTime { get; set; } + } + + public class DoctorPayInfoQueryListDTO : ReviewerPayInfoQueryDTO + { + public string Hospital { get; set; } = String.Empty; + public string RankName { get; set; } = String.Empty; + } + + public class ReviewerPayInfoCommand + { + //public Guid Id { get; set; } + public Guid DoctorId { get; set; } + public string DoctorNameInBank { get; set; } = String.Empty; + public string IDCard { get; set; } = String.Empty; + public string BankCardNumber { get; set; } = String.Empty; + public string BankName { get; set; } = String.Empty; + public Guid RankId { get; set; } + public decimal? Additional { get; set; } + } +} + diff --git a/IRaCIS.Core.Application/Service/Financial/DTO/TrialPaymentPriceModel.cs b/IRaCIS.Core.Application/Service/Financial/DTO/TrialPaymentPriceModel.cs new file mode 100644 index 00000000..f05cc3bc --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/DTO/TrialPaymentPriceModel.cs @@ -0,0 +1,46 @@ +using IRaCIS.Core.Domain.Share; +using System; + +namespace IRaCIS.Application.Contracts +{ + + public class DtoDoctorList + { + public Guid TrialId { get; set; } + + public string Name { get; set; } + } + + public class TrialPaymentPriceDTO + { + public Guid TrialId { get; set; } + public string TrialCode { get; set; } = String.Empty; + public string Indication { get; set; } = String.Empty; + public string Cro { get; set; } = String.Empty; + public int Expedited { get; set; } + + public bool? IsNewTrial { get; set; } + public decimal? TrialAdditional { get; set; } + public DateTime? CreateTime { get; set; } + public string SowName { get; set; } = String.Empty; + public string SowPath { get; set; } = String.Empty; + public string SowFullPath => SowPath; + public decimal AdjustmentMultiple { get; set; } = 1; + + public string DoctorsNames{ get; set; }=String.Empty; + + public string ReviewMode { get; set; } = String.Empty; + + + } + + public class TrialPaymentPriceCommand + { + public Guid TrialId { get; set; } + public decimal TrialAdditional { get; set; } + public decimal AdjustmentMultiple { get; set; } + + public bool? IsNewTrial { get; set; } + + } +} diff --git a/IRaCIS.Core.Application/Service/Financial/DTO/TrialRevenuesPriceModel.cs b/IRaCIS.Core.Application/Service/Financial/DTO/TrialRevenuesPriceModel.cs new file mode 100644 index 00000000..afe5207c --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/DTO/TrialRevenuesPriceModel.cs @@ -0,0 +1,37 @@ +using System; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Contracts +{ + public class TrialRevenuesPriceDTO + { + + public Guid TrialId { get; set; } + public decimal Timepoint { get; set; } + public decimal TimepointIn24H { get; set; } + public decimal TimepointIn48H { get; set; } + public decimal Adjudication { get; set; } + public decimal AdjudicationIn24H { get; set; } + public decimal AdjudicationIn48H { get; set; } + public decimal RefresherTraining { get; set; } + + public decimal Global { get; set; } + public decimal Training { get; set; } + public decimal Downtime { get; set; } + } + + public class TrialRevenuesPriceDetialDTO : TrialRevenuesPriceDTO + { + public Guid Id { get; set; } + public string TrialCode { get; set; } = String.Empty; + public string Indication { get; set; } = string.Empty; + public int Expedited { get; set; } + public string ReviewMode { get; set; } = String.Empty; + public string Cro { get; set; } = String.Empty; + } + public class TrialRevenuesPriceQueryDTO : PageInput + { + public string KeyWord { get; set; } = String.Empty; + public Guid? CroId { get; set; } + } +} diff --git a/IRaCIS.Core.Application/Service/Financial/DTO/TrialRevenuesPriceVerificationDTO.cs b/IRaCIS.Core.Application/Service/Financial/DTO/TrialRevenuesPriceVerificationDTO.cs new file mode 100644 index 00000000..4492ed0e --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/DTO/TrialRevenuesPriceVerificationDTO.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; + +namespace IRaCIS.Core.Application.Contracts +{ + public class RevenusVerifyQueryDTO + { + public DateTime BeginDate { get; set; } = DateTime.Now; + public DateTime EndDate { get; set; } = DateTime.Now; + + } + + public class AnalysisVerifyQueryDTO + { + public DateTime BeginDate { get; set; } = DateTime.Now; + public DateTime EndDate { get; set; } = DateTime.Now; + } + + + public class AnalysisNeedLockDTO + { + public string YearMonth { get; set; } = string.Empty; + + public string ReviewerCode { get; set; } = string.Empty; + + public string ReviewerName { get; set; } = string.Empty; + + public string ReviewerNameCN { get; set; } = string.Empty; + } + + public class AnalysisVerifyResultDTO + { + public List MonthVerifyResult = new List(); + + public List RevenuesVerifyList = new List(); + } + + + + public class MonthlyResult + { + public string YearMonth { get; set; } = string.Empty; + public List ReviewerNameList = new List(); + public List ReviewerNameCNList = new List(); + public List ReviewerCodeList = new List(); + } + + + public class RevenusVerifyDTO + { + public string TrialCode { get; set; } = string.Empty; + + public bool Training { get; set; } = false; + + public bool Downtime { get; set; } = false; + + public bool Global { get; set; } = false; + + public bool Timepoint { get; set; } = false; + + public bool TimepointIn24H { get; set; } = false; + + public bool TimepointIn48H { get; set; } = false; + + public bool Adjudication { get; set; } = false; + + public bool AdjudicationIn24H { get; set; } = false; + + public bool AdjudicationIn48H { get; set; } = false; + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Financial/ExchangeRateService.cs b/IRaCIS.Core.Application/Service/Financial/ExchangeRateService.cs new file mode 100644 index 00000000..5e81ce96 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/ExchangeRateService.cs @@ -0,0 +1,115 @@ +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Mvc; +using Panda.DynamicWebApi.Attributes; + +namespace IRaCIS.Application.Services +{ + + [ApiExplorerSettings(GroupName = "Financial")] + public class ExchangeRateService : BaseService, IExchangeRateService + { + private readonly IRepository _exchangeRateRepository; + private readonly IRepository _paymentRepository; + + public ExchangeRateService(IRepository exchangeRateRepository, IRepository paymentRepository) + { + _exchangeRateRepository = exchangeRateRepository; + _paymentRepository = paymentRepository; + } + + [NonDynamicMethod] + public async Task AddOrUpdateExchangeRate(ExchangeRateCommand model) + { + if (model.Id == Guid.Empty || model.Id == null) + { + var existItem = await _exchangeRateRepository.FirstOrDefaultAsync(u => u.YearMonth == model.YearMonth); + if (existItem != null) + { + return ResponseOutput.NotOk("The exchange rate of the same month already existed."); + } + var rate = _mapper.Map(model); + rate = await _exchangeRateRepository.AddAsync(rate); + if (await _exchangeRateRepository.SaveChangesAsync()) + { + return ResponseOutput.Ok(rate.Id.ToString()); + } + else + { + return ResponseOutput.NotOk(); + } + + } + else + { + var success = await _exchangeRateRepository.UpdateFromQueryAsync(t => t.Id == model.Id, u => new ExchangeRate() + { + //YearMonth = model.YearMonth, + Rate = model.Rate, + UpdateTime = DateTime.Now + }); + return ResponseOutput.Result(success); + } + } + + /// + /// 根据记录Id,删除汇率记录 + /// + /// 汇率记录Id + + [HttpDelete("{id:guid}")] + public async Task DeleteExchangeRate(Guid id) + { + var monthInfo = await _exchangeRateRepository.FirstOrDefaultAsync(t => t.Id == id); + + if (await _paymentRepository.AnyAsync(t => t.YearMonth == monthInfo.YearMonth)) + { + return ResponseOutput.NotOk("The exchange rate has been used in monthly payment"); + } + + + var success = await _exchangeRateRepository.DeleteFromQueryAsync(t => t.Id == id); + + return ResponseOutput.Ok(success); + } + + + [NonDynamicMethod] + public async Task GetExchangeRateByMonth(string month) + { + //var rate = _exchangeRateRepository.FindSingleOrDefault(u => u.YearMonth.Equals(month)); + //if (rate == null) + //{ + // return 0; + //} + //return rate.Rate; + + var rate = await _exchangeRateRepository.FirstOrDefaultAsync(t => t.YearMonth == month); + if (rate == null) + { + return 0; + } + return rate.Rate; + } + + [HttpPost] + public async Task> GetExchangeRateList(ExchangeRateQueryDTO queryParam) + { + + var yearMonth = queryParam.SearchMonth?.ToString("yyyy-MM"); + + var exchangeRateQueryable = _exchangeRateRepository.AsQueryable() + .WhereIf(queryParam.SearchMonth != null, o => o.YearMonth == yearMonth) + .ProjectTo(_mapper.ConfigurationProvider); + + return await exchangeRateQueryable.ToPagedListAsync(queryParam.PageIndex, queryParam.PageSize, "YearMonth", false); + + + + } + } +} diff --git a/IRaCIS.Core.Application/Service/Financial/FinancialService.cs b/IRaCIS.Core.Application/Service/Financial/FinancialService.cs new file mode 100644 index 00000000..b531e531 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/FinancialService.cs @@ -0,0 +1,1305 @@ +using IRaCIS.Core.Infrastructure.ExpressionExtend; +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Application.Contracts.Pay; +using IRaCIS.Core.Domain.Share; +using System.Linq.Expressions; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Mvc; +using Panda.DynamicWebApi.Attributes; + +namespace IRaCIS.Application.Services +{ + [ ApiExplorerSettings(GroupName = "Financial")] + public class FinancialService : BaseService, IPaymentService + { + private readonly IRepository _paymentRepository; + private readonly IRepository _doctorPayInfoRepository; + private readonly IRepository _TrialPaymentPriceRepository; + private readonly IRepository _trialRepository; + private readonly IRepository _doctorRepository; + private readonly IRepository _rankPriceRepository; + private readonly IRepository _paymentDetailRepository; + private readonly IRepository _croRepository; + private readonly IRepository _workloadRepository; + private readonly IRepository _trialRevenuePriceRepository; + private readonly IRepository _payAdjustmentRepository; + + private readonly IRepository _enrollRepository; + + private readonly IRepository _doctorWorkloadRepository; + + public FinancialService(IRepository costStatisticsRepository, + IRepository doctorPayInfoRepository, + IRepository trialRepository, + IRepository doctorRepository, + IRepository rankPriceRepository, + IRepository costStatisticsDetailRepository, + IRepository croCompanyRepository, + IRepository workloadRepository, + IRepository intoGroupRepository, + IRepository trialPaymentPriceRepository, + IRepository trialCostRepository, + IRepository costAdjustmentRepository, + IRepository doctoWorkloadRepository + ) + { + _TrialPaymentPriceRepository = trialPaymentPriceRepository; + _paymentRepository = costStatisticsRepository; + _doctorPayInfoRepository = doctorPayInfoRepository; + _trialRepository = trialRepository; + _doctorRepository = doctorRepository; + _rankPriceRepository = rankPriceRepository; + _paymentDetailRepository = costStatisticsDetailRepository; + _croRepository = croCompanyRepository; + _enrollRepository = intoGroupRepository; + _workloadRepository = workloadRepository; + _trialRevenuePriceRepository = trialCostRepository; + _payAdjustmentRepository = costAdjustmentRepository; + + _doctorWorkloadRepository = doctoWorkloadRepository; + } + + /// + /// Financials /MonthlyPayment 列表查询接口 + /// + [NonDynamicMethod] + public async Task> GetMonthlyPaymentList(MonthlyPaymentQueryDTO queryParam) + { + //var year = queryParam.StatisticsDate.Year; + //var month = queryParam.StatisticsDate.Month; + //var searchBeginDateTime = queryParam.StatisticsDate; + //Expression> workloadLambda = t => t.DataFrom == (int)WorkLoadFromStatus.FinalConfirm; + //workloadLambda = workloadLambda.And(t => + // t.WorkTime >= queryParam.StatisticsDate && t.WorkTime <= queryParam.StatisticsDate); + //var rate = _exchangeRateRepository.FindSingleOrDefault(u => u.YearMonth == yearMonth); + //var exchangeRate = rate == null ? 0 : rate.Rate; + + var yearMonth = queryParam.StatisticsDate.ToString("yyyy-MM"); + + Expression> doctorLambda = x => true; + if (!string.IsNullOrWhiteSpace(queryParam.KeyWord)) + { + var reviewer = queryParam.KeyWord.Trim(); + doctorLambda = doctorLambda.And(u => u.ChineseName.Contains(reviewer) + || u.FirstName.Contains(reviewer) + || u.LastName.Contains(reviewer) + || u.ReviewerCode.Contains(reviewer)); + } + + if (queryParam.Nation != null) + { + doctorLambda = doctorLambda.And(u => u.Nation == queryParam.Nation); + } + + var costStatisticsQueryable = + + from monthlyPayment in _paymentRepository.Where(t => t.YearMonth == yearMonth) + join payInfo in _doctorPayInfoRepository.AsQueryable() on monthlyPayment.DoctorId equals payInfo.DoctorId + into cc + from payInfo in cc.DefaultIfEmpty() + join doctor in _doctorRepository.Where(doctorLambda) on monthlyPayment.DoctorId equals doctor.Id + join rankPrice in _rankPriceRepository.AsQueryable() on payInfo.RankId equals rankPrice.Id into dd + from rankPriceItem in dd.DefaultIfEmpty() + select new PaymentModel() + { + Id = monthlyPayment.Id, + DoctorId = monthlyPayment.DoctorId, + YearMonth = monthlyPayment.YearMonth, + PaymentUSD = monthlyPayment.PaymentUSD, + CalculateTime = monthlyPayment.CalculateTime, + CalculateUser = monthlyPayment.CalculateUser, + IsLock = monthlyPayment.IsLock, + ExchangeRate = monthlyPayment.ExchangeRate, + PaymentCNY = monthlyPayment.PaymentCNY, + AdjustPaymentCNY = monthlyPayment.AdjustmentCNY, + AdjustPaymentUSD = monthlyPayment.AdjustmentUSD, + + TotalPaymentCNY = monthlyPayment.AdjustmentCNY + monthlyPayment.PaymentCNY, + TotalPaymentUSD = monthlyPayment.AdjustmentUSD + monthlyPayment.PaymentUSD, + + + Code = doctor.ReviewerCode, + FirstName = doctor.FirstName, + LastName = doctor.LastName, + ChineseName = doctor.ChineseName, + Phone = doctor.Phone, + DoctorNameInBank = payInfo.DoctorNameInBank, + RankName = rankPriceItem.RankName, + IDCard = payInfo.IDCard, + BankCardNumber = payInfo.BankCardNumber, + BankName = payInfo.BankName + }; + + var propName = string.IsNullOrWhiteSpace(queryParam.SortField) ? "Code" : queryParam.SortField; + costStatisticsQueryable = queryParam.Asc ? costStatisticsQueryable.OrderBy(propName).ThenBy(u => u.Code) + : costStatisticsQueryable.OrderByDescending(propName).ThenBy(u => u.Code); + var count = costStatisticsQueryable.Count(); + costStatisticsQueryable = costStatisticsQueryable.Skip((queryParam.PageIndex - 1) * queryParam.PageSize).Take(queryParam.PageSize); + var costStatisticsList = await costStatisticsQueryable.ToListAsync(); + + + #region 增加调整费用总计字段之前 + //var doctorIds = costStatisticsList.Select(t => t.DoctorId).ToList(); + + //var adjList = _payAdjustmentRepository.GetAll() + // .Where(t => t.YearMonth == yearMonth && doctorIds.Contains(t.ReviewerId)) + // .GroupBy(t => t.ReviewerId).Select(g => new + // { + // ReviewerId = g.Key, + // AdjustPaymentCNY = g.Sum(t => t.AdjustPaymentCNY), + // AdjustPaymentUSD = g.Sum(t => t.AdjustPaymentUSD) + // }).ToList(); + + //costStatisticsList.ForEach(t => + //{ + // var tempAdj = adjList.FirstOrDefault(u => u.ReviewerId == t.DoctorId); + // if (tempAdj != null) + // { + // t.AdjustPaymentUSD = tempAdj.AdjustPaymentUSD; + // t.AdjustPaymentCNY = tempAdj.AdjustPaymentCNY; + + // } + //}); + + + #endregion + + return new PageOutput(queryParam.PageIndex, queryParam.PageSize, count, costStatisticsList); + } + + /// + /// Financials /MonthlyPaymentDetail 详情查询接口 + /// + [HttpPost("{paymentId:guid}/{doctorId:guid}/{yearMonth:datetime}")] + public async Task GetMonthlyPaymentDetailList(Guid paymentId, Guid reviewerId, DateTime yearMonth) + { + var returnModel = new PayDetailDTO(); + var yearMonthStr = yearMonth.ToString("yyyy-MM"); + + List detailList = new List(); + //有详细表的数据 先查出来 + if (paymentId != Guid.Empty) + { + + //from enroll in _enrollRepository.Where(t => t.TrialId == challengeQuery.TrialId) + //join dociorc in _doctorRepository.Where() on enroll.DoctorId equals dociorc.Id + //join price in _TrialPaymentPriceRepository.Where() on enroll.TrialId equals price.TrialId + + + //select new EnrollViewModel() + //{ + // ChineseName = dociorc.ChineseName, + // AdjustmentMultiple = enroll.AdjustmentMultiple, + // FirstName = dociorc.FirstName, + // LastName = dociorc.LastName, + // DoctorId = dociorc.Id, + // TrialId = challengeQuery.TrialId + + //}; + //detailList = _paymentDetailRepository.AsQueryable() + // .Where(t => t.PaymentId == paymentId) + // .OrderBy(t => t.ShowCodeOrder).ThenBy(t => t.ShowTypeOrder).ProjectTo(_mapper.ConfigurationProvider).ToList(); + + + detailList = await (from pay in _paymentDetailRepository.Where(t => t.PaymentId == paymentId) + join enroll in _enrollRepository.Where() on new { pay.DoctorId, pay.TrialId } equals new { enroll.DoctorId, enroll.TrialId } + join price in _TrialPaymentPriceRepository.Where() on pay.TrialId equals price.TrialId + orderby pay.ShowCodeOrder + orderby pay.ShowTypeOrder + select new PaymentDetailDTO() + { + Id=pay.Id, + ShowCodeOrder = pay.ShowCodeOrder, + ShowTypeOrder = pay.ShowTypeOrder, + PaymentUSD = pay.PaymentUSD, + BasePrice = pay.BasePrice, + PersonalAdditional = pay.PersonalAdditional, + TrialAdditional = pay.TrialAdditional, + TrialCode = pay.TrialCode, + PaymentCNY = pay.PaymentCNY, + DoctorId = pay.DoctorId, + ExchangeRate = pay.ExchangeRate, + IsNewTrial = price.IsNewTrial, + NewPersonalAdditional= enroll.AdjustmentMultiple, + + }).ToListAsync(); + + } + //费用调整 + //_payAdjustmentRepository.Where(t => t.YearMonth == yearMonthStr && t.ReviewerId == reviewerId) + var adjList = + await (from costAdjustment in _payAdjustmentRepository.Where(t => t.YearMonth == yearMonthStr&& t.ReviewerId == paymentId) + join enroll in _enrollRepository.Where() on new { costAdjustment.ReviewerId, costAdjustment.TrialId } equals new { ReviewerId= enroll.DoctorId, enroll.TrialId } + join price in _TrialPaymentPriceRepository.Where() on costAdjustment.TrialId equals price.TrialId + + + select new PaymentDetailDTO() + { + Id=costAdjustment.Id, + TrialCode = "Adjustment", + PaymentCNY = costAdjustment.AdjustmentCNY, + PaymentUSD = costAdjustment.AdjustmentUSD, + DoctorId = costAdjustment.ReviewerId, + ExchangeRate = costAdjustment.ExchangeRate, + AdjustmentView = new AdjustmentDTO() + { + AdjustPaymentUSD = costAdjustment.AdjustmentUSD, + AdjustPaymentCNY = costAdjustment.AdjustmentCNY, + Note = costAdjustment.Note + }, + IsNewTrial = price.IsNewTrial, + NewPersonalAdditional = enroll.AdjustmentMultiple, + + }).ToListAsync(); + + + + + detailList.AddRange(adjList); + + detailList.ForEach(x => + { + x.PersonalAdditional = x.IsNewTrial == true && x.NewPersonalAdditional != null ? 0 : x.PersonalAdditional; + x.TrialAdditional = x.IsNewTrial == true && x.NewPersonalAdditional != null ? 0 : x.TrialAdditional; + x.BasePrice = x.IsNewTrial == true && x.NewPersonalAdditional != null ? x.NewPersonalAdditional.Value : x.BasePrice; + + }); + + returnModel.DetailList = detailList; + + + + var doctorId = returnModel.DetailList.FirstOrDefault()?.DoctorId; + var query = from doctor in _doctorRepository.AsQueryable() + .Where(t => t.Id == doctorId) + join payInfo in _doctorPayInfoRepository.AsQueryable() on doctor.Id equals payInfo.DoctorId + join rank in _rankPriceRepository.AsQueryable() on payInfo.RankId equals rank.Id + select new DoctorPayInfo() + { + Code = doctor.ReviewerCode, + ChineseName = doctor.ChineseName, + FirstName = doctor.FirstName, + LastName = doctor.LastName, + Phone = doctor.Phone, + DoctorId = doctor.Id, + PayTitle = rank.RankName, + YearMonth = yearMonthStr + }; + + returnModel.DoctorInfo =(await query.FirstOrDefaultAsync()).IfNullThrowException(); + + return returnModel; + } + + /// + /// NEW 导出Excel压缩包 数据获取 + /// + [HttpPost] + public async Task> GetReviewersMonthlyPaymentDetail(List manyReviewers) + { + List result = new List(); + foreach (var t in manyReviewers) + { + result.Add(await GetMonthlyPaymentDetailList(t.PaymentId, t.ReviewerId, t.YearMonth)); + } + + + return result; + + } + + + /// + /// Financials / Payment History 列表查询接口(已经支付锁定的数据,包含调整的)[New] + /// + [HttpPost] + public async Task> GetPaymentHistoryList(PaymentQueryDTO queryParam) + { + Expression> doctorLambda = x => true; + if (!string.IsNullOrWhiteSpace(queryParam.Reviewer)) + { + var reviewer = queryParam.Reviewer.Trim(); + doctorLambda = doctorLambda.And(u => u.ChineseName.Contains(reviewer) + || u.FirstName.Contains(reviewer) + || u.LastName.Contains(reviewer) + || u.ReviewerCode.Contains(reviewer)); + } + if (queryParam.Nation != null) + { + doctorLambda = doctorLambda.And(u => u.Nation == queryParam.Nation); + } + + Expression> paymentLambda = x => x.IsLock && x.YearMonthDate >= queryParam.BeginMonth && x.YearMonthDate <= queryParam.EndMonth; + + #region 在 payment表加 调整总计前 + + //var paymentQueryable = _paymentRepository.Find(paymentLambda) + // .Select( + // t => new + // { + // ReviewerId = t.DoctorId, + // t.YearMonth, + // t.PaymentUSD, + // t.PaymentCNY, + // t.AdjustPaymentCNY, + // t.AdjustPaymentUSD + // }); + + //Expression> payAdjustmentLambda = x => x.IsLock && x.AdjustedYearMonth >= param.BeginMonth && x.AdjustedYearMonth <= param.EndMonth; + + //var adjQueryable = _payAdjustmentRepository.GetAll() + // .Where(payAdjustmentLambda) + // .GroupBy(t => new { t.ReviewerId, t.YearMonth }).Select(g => new + // { + // g.Key.ReviewerId, + // g.Key.YearMonth, + // PaymentUSD = 0, + // PaymentCNY = 0, + // AdjustPaymentCNY = g.Sum(t => t.AdjustPaymentCNY), + // AdjustPaymentUSD = g.Sum(t => t.AdjustPaymentUSD) + // }); + + //var query = from pay in (from paymentCost in paymentQueryable + // join adjCost in adjQueryable on new { paymentCost.YearMonth, paymentCost.ReviewerId } equals new { adjCost.YearMonth, adjCost.ReviewerId } into a + // from adjCost in a.DefaultIfEmpty() + // select new + // { + // paymentCost.ReviewerId, + // paymentCost.PaymentUSD, + // paymentCost.PaymentCNY, + // AdjustmentCNY = adjCost == null ? 0 : adjCost.AdjustPaymentCNY, + // AdjustmentUSD = adjCost == null ? 0 : adjCost.AdjustPaymentUSD + // } + // ).Union( + // from adjCost in adjQueryable + // join paymentCost in paymentQueryable on new { adjCost.YearMonth, adjCost.ReviewerId } equals new { paymentCost.YearMonth, paymentCost.ReviewerId } into a + // from paymentCost in a.DefaultIfEmpty() + // select new + // { + // ReviewerId = adjCost.ReviewerId, + // PaymentUSD = paymentCost == null ? 0 : paymentCost.PaymentUSD, + // PaymentCNY = paymentCost == null ? 0 : paymentCost.PaymentCNY, + // AdjustmentCNY = adjCost.AdjustPaymentCNY, + // AdjustmentUSD = adjCost.AdjustPaymentUSD + // } + // ) + + #endregion + + var query = from monthlyPay in _paymentRepository.Where(paymentLambda) + .GroupBy(t => t.DoctorId).Select(g => new + { + ReviewerId = g.Key, + PaymentUSD = g.Sum(u => u.PaymentUSD), + PaymentCNY = g.Sum(u => u.PaymentCNY), + AdjustmentCNY = g.Sum(u => u.AdjustmentCNY), + AdjustmentUSD = g.Sum(u => u.AdjustmentCNY) + }) + join doctor in _doctorRepository.Where(doctorLambda) on monthlyPay.ReviewerId equals doctor.Id + select new MonthlyPaymentDTO + { + ReviewerId = doctor.Id, + ChineseName = doctor.ChineseName, + FirstName = doctor.FirstName, + LastName = doctor.LastName, + ReviewerCode = doctor.ReviewerCode, + PaymentUSD = monthlyPay.PaymentUSD, + PaymentCNY = monthlyPay.PaymentCNY, + AdjustmentCNY = monthlyPay.AdjustmentCNY, + AdjustmentUSD = monthlyPay.AdjustmentUSD, + TotalCNY = monthlyPay.AdjustmentCNY + monthlyPay.PaymentCNY, + TotalUSD = monthlyPay.AdjustmentUSD + monthlyPay.PaymentUSD + }; + var propName = queryParam.SortField == string.Empty ? "ReviewerCode" : queryParam.SortField; + query = queryParam.Asc ? query.OrderBy(propName).ThenBy(u => u.ReviewerCode) : query.OrderByDescending(propName).ThenBy(u => u.ReviewerCode); + if (propName == "FirstName" || propName == "LastName") + { + query = queryParam.Asc ? query.OrderBy(t => t.LastName).ThenBy(t => t.FirstName) + : query.OrderByDescending(t => t.LastName).ThenBy(t => t.FirstName); + } + var count = await query.CountAsync(); + query = query.Skip((queryParam.PageIndex - 1) * queryParam.PageSize).Take(queryParam.PageSize); + var list = await query.ToListAsync(); + return new PageOutput(queryParam.PageIndex, queryParam.PageSize, count, list); + + } + + /// + /// Financials / Payment History 详情接口[New] + /// + [HttpPost] + public async Task> GetPaymentHistoryDetailList(VolumeQueryDTO param) + { + var beginDate = new DateTime(param.BeginMonth.Year, param.BeginMonth.Month, 1); + var endDate = new DateTime(param.EndMonth.Year, param.EndMonth.Month, 1); + + Expression> monthlyPayLambda = x => x.IsLock && x.DoctorId == param.ReviewerId && x.YearMonthDate >= beginDate && x.YearMonthDate <= endDate; + + var query = + from monthlyPayment in _paymentRepository.Where(monthlyPayLambda) + + select new VolumeStatisticsDTO + { + StatisticsId = monthlyPayment.Id, + Month = monthlyPayment.YearMonth, + ExchangeRate = monthlyPayment.ExchangeRate, + PaymentUSD = monthlyPayment.PaymentUSD, + PaymentCNY = monthlyPayment.PaymentCNY, + AdjustmentCNY = monthlyPayment.AdjustmentCNY, + AdjustmentUSD = monthlyPayment.AdjustmentUSD, + VolumeReward = 0 + }; + + var payDetailList = await query.OrderBy(t => t.Month).ToListAsync(); + + List statisticsIdList = payDetailList.Select(t => t.StatisticsId).ToList(); + + var monthlyPayDetailList = await _paymentDetailRepository.Where(u => + statisticsIdList.Contains(u.PaymentId)).Select(t => new + { + PaymentId = t.PaymentId, + t.PaymentUSD, + t.TrialCode, + t.PaymentCNY, + t.YearMonth + }).ToListAsync(); + + payDetailList.ForEach(t => + { + t.VolumeReward = monthlyPayDetailList + .Where(u => u.PaymentId == t.StatisticsId && u.TrialCode == "Volume Reward") + .Sum(k => k.PaymentUSD); + + t.TrialPaymentList = monthlyPayDetailList + .Where(u => u.PaymentId == t.StatisticsId && u.TrialCode != "Volume Reward" && u.YearMonth == t.Month) + .GroupBy(u => u.TrialCode).Select(g => new TrialPaymentDTO + { + TrialPayment = g.Sum(k => k.PaymentUSD), + TrialCode = g.Key + }).ToList(); + }); + + + #region 改表之前 + + //var tempVolume = _costStatisticsDetailRepository.Find(u => + // statisticsIdList.Contains(u.PaymentId)).ToList(); + + //foreach (var item in payDetailList) + //{ + // var temp = tempVolume.Where(u => u.PaymentId == item.StatisticsId && u.TrialCode == "Volume Reward"); + // var Volume = 0.0; + // foreach (var t in temp) + // { + // Volume += (t.BasePrice * t.Count); + // } + // item.VolumeReward = Volume; + + // var trialCodes = tempVolume.Where(u => u.PaymentId == item.StatisticsId + // && u.TrialCode != "Volume Reward").GroupBy(u => u.TrialCode).Select(u => u.Key).Distinct(); + + // foreach (var trial in trialCodes) + // { + // var trialFee = 0.0; + // var aa = tempVolume.Where(u => u.PaymentId == item.StatisticsId && u.TrialCode == trial + // ).ToList(); + + // foreach (var a in aa) + // { + // trialFee += (a.Count * (a.BasePrice + a.PersonalAdditional + a.TrialAdditional)); + // } + // item.TrialFeeList.Add(new TrialFeeViewModel + // { + // TrialCode = trial, + // TrialFee = trialFee + // }); + // } + + //} + + #endregion + + return payDetailList; + } + + /// + /// Revenues列表接口,收入统计[New] 0是Detail 1是按照项目 2是按照人 3按照月份 + /// + [HttpPost] + public async Task> GetRevenuesStatistics(StatisticsQueryDTO queryParam) + { + var bDate = new DateTime(queryParam.BeginDate.Year, queryParam.BeginDate.Month, 1); + var eDate = new DateTime(queryParam.EndDate.Year, queryParam.EndDate.Month, 1); + Expression> workloadLambda = x => x.DataFrom == (int)WorkLoadFromStatus.FinalConfirm; + workloadLambda = workloadLambda.And(x => x.WorkTime >= bDate && x.WorkTime <= eDate); + + Expression> trialLambda = x => true; + if (Guid.Empty != queryParam.CroId && queryParam.CroId != null) + { + trialLambda = trialLambda.And(u => u.CROId == queryParam.CroId); + } + if (!string.IsNullOrWhiteSpace(queryParam.TrialCode)) + { + var trialCode = queryParam.TrialCode.Trim(); + trialLambda = trialLambda.And(u => u.TrialCode.Contains(trialCode)); + } + + if (queryParam.AttendedReviewerType != null) + { + trialLambda = trialLambda.And(u => u.AttendedReviewerType == queryParam.AttendedReviewerType); + } + + Expression> doctorLambda = x => true; + + if (!string.IsNullOrWhiteSpace(queryParam.Reviewer)) + { + var reviewer = queryParam.Reviewer.Trim(); + doctorLambda = doctorLambda.And(u => u.ChineseName.Contains(reviewer) + || u.FirstName.Contains(reviewer) + || u.LastName.Contains(reviewer) + || u.ReviewerCode.Contains(reviewer)); + } + if (queryParam.Nation != null) + { + doctorLambda = doctorLambda.And(u => u.Nation == queryParam.Nation); + } + + List incomeList = new List(); + IQueryable? query=null ; + var propName = string.Empty; + var count = 0; + + #region 按照详细维度 ReviewId TrialId + + if (queryParam.StatType == 0) + { + query = from workLoad in _workloadRepository.Where(workloadLambda) + join doctor in _doctorRepository.Where(doctorLambda) + on workLoad.DoctorId equals doctor.Id + join trial in _trialRepository.Where(trialLambda) + on workLoad.TrialId equals trial.Id + join cro in _croRepository.AsQueryable() on trial.CROId equals cro.Id into ttt + from croItem in ttt.DefaultIfEmpty() + join trialCost in _trialRevenuePriceRepository.AsQueryable() on workLoad.TrialId equals trialCost.TrialId into c + from trialCost in c.DefaultIfEmpty() + select new RevenuesDTO + { + + TrialId = workLoad.TrialId, + Cro = croItem.CROName, + ReviewerCode = doctor.ReviewerCode, + ChineseName = doctor.ChineseName, + FirstName = doctor.FirstName, + LastName = doctor.LastName, + + Indication = trial.Indication, + TrialCode = trial.TrialCode, + Expedited = trial.Expedited, + CroId = trial.CROId, + + Downtime = workLoad.Downtime * trialCost.Downtime, + Training = workLoad.Training * trialCost.Training, + Timepoint = workLoad.Timepoint * trialCost.Timepoint, + TimepointIn24H = workLoad.TimepointIn24H * trialCost.TimepointIn24H, + TimepointIn48H = workLoad.TimepointIn48H * trialCost.TimepointIn48H, + Global = workLoad.Global * trialCost.Global, + Adjudication = workLoad.Adjudication * trialCost.Adjudication, + AdjudicationIn24H = + workLoad.AdjudicationIn24H * trialCost.AdjudicationIn24H, + AdjudicationIn48H = workLoad.AdjudicationIn48H * trialCost.AdjudicationIn48H, + + YearMonth = workLoad.YearMonth, + + Total = (workLoad.Downtime * trialCost.Downtime + + workLoad.Training * trialCost.Training + + workLoad.Timepoint * trialCost.Timepoint + + workLoad.TimepointIn24H * trialCost.TimepointIn24H + + workLoad.TimepointIn48H * trialCost.TimepointIn48H + + workLoad.Global * trialCost.Global + + workLoad.Adjudication * trialCost.Adjudication + + workLoad.AdjudicationIn24H * trialCost.AdjudicationIn24H + + workLoad.AdjudicationIn48H * trialCost.AdjudicationIn48H + + workLoad.RefresherTraining * trialCost.RefresherTraining + ) + + }; + + //处理排序字段 + propName = queryParam.SortField == "" ? "ReviewerCode" : queryParam.SortField; + + } + //按照项目维度 + if (queryParam.StatType == 1) + { + + var workloadQuery = from workLoad in _workloadRepository.Where(workloadLambda) + + group workLoad by workLoad.TrialId + into gWorkLoad + select new + { + TrialId = gWorkLoad.Key, + Downtime = gWorkLoad.Sum(t => t.Downtime), + Training = gWorkLoad.Sum(t => t.Training), + Timepoint = gWorkLoad.Sum(t => t.Timepoint), + TimepointIn24H = gWorkLoad.Sum(t => t.TimepointIn24H), + TimepointIn48H = gWorkLoad.Sum(t => t.TimepointIn48H), + Global = gWorkLoad.Sum(t => t.Global), + Adjudication = gWorkLoad.Sum(t => t.Adjudication), + AdjudicationIn24H = gWorkLoad.Sum(t => t.AdjudicationIn24H), + AdjudicationIn48H = gWorkLoad.Sum(t => t.AdjudicationIn48H), + RefresherTraining = gWorkLoad.Sum(t => t.RefresherTraining), + }; + + query = from workLoad in workloadQuery + join trialCost in _trialRevenuePriceRepository.AsQueryable() on workLoad.TrialId equals trialCost.TrialId + into c + from trialCost in c.DefaultIfEmpty() + join trial in _trialRepository.Where(trialLambda) on workLoad.TrialId equals trial.Id + join cro in _croRepository.AsQueryable() on trial.CROId equals cro.Id into ttt + from croItem in ttt.DefaultIfEmpty() + select new RevenuesDTO + { + + TrialId = trial.Id, + Indication = trial.Indication, + TrialCode = trial.TrialCode, + Expedited = trial.Expedited, + CroId = trial.CROId, + Cro = croItem.CROName, + Training = workLoad.Training * trialCost.Training, + Timepoint = workLoad.Timepoint * trialCost.Timepoint, + TimepointIn24H = workLoad.TimepointIn24H * trialCost.TimepointIn24H, + TimepointIn48H = workLoad.TimepointIn48H * trialCost.TimepointIn48H, + Adjudication = workLoad.Adjudication * trialCost.Adjudication, + AdjudicationIn24H = workLoad.AdjudicationIn24H * trialCost.AdjudicationIn24H, + AdjudicationIn48H = workLoad.AdjudicationIn48H * trialCost.AdjudicationIn48H, + Global = workLoad.Global * trialCost.Global, + Downtime = workLoad.Downtime * trialCost.Downtime, + + Total = (workLoad.Downtime * trialCost.Downtime + + workLoad.Training * trialCost.Training + + workLoad.Timepoint * trialCost.Timepoint + + workLoad.TimepointIn24H * trialCost.TimepointIn24H + + workLoad.TimepointIn48H * trialCost.TimepointIn48H + + workLoad.Global * trialCost.Global + + workLoad.Adjudication * trialCost.Adjudication + + workLoad.AdjudicationIn24H * trialCost.AdjudicationIn24H + + workLoad.AdjudicationIn48H * trialCost.AdjudicationIn48H + + workLoad.RefresherTraining * trialCost.RefresherTraining + ) + }; + + //处理排序字段 + propName = queryParam.SortField == "" ? "TrialCode" : queryParam.SortField; + + } + + //按照人的维度 + if (queryParam.StatType == 2) + { + var workloadQuery = from workLoad in _workloadRepository.Where(workloadLambda) + join doctor in _doctorRepository.Where(doctorLambda) + on workLoad.DoctorId equals doctor.Id + join trial in _trialRepository.Where(trialLambda) on workLoad.TrialId equals trial.Id + join trialCost in _trialRevenuePriceRepository.AsQueryable() on workLoad.TrialId equals trialCost.TrialId into c + from trialCost in c.DefaultIfEmpty() + + select new + { + DoctorId = workLoad.DoctorId, + Downtime = workLoad.Downtime * trialCost.Downtime, + Training = workLoad.Training * trialCost.Training, + Timepoint = workLoad.Timepoint * trialCost.Timepoint, + TimepointIn24H = workLoad.TimepointIn24H * trialCost.TimepointIn24H, + TimepointIn48H = workLoad.TimepointIn48H * trialCost.TimepointIn48H, + Global = workLoad.Global * trialCost.Global, + Adjudication = workLoad.Adjudication * trialCost.Adjudication, + AdjudicationIn24H = + workLoad.AdjudicationIn24H * trialCost.AdjudicationIn24H, + AdjudicationIn48H = workLoad.AdjudicationIn48H * trialCost.AdjudicationIn48H, + RefresherTraining = workLoad.RefresherTraining * trialCost.RefresherTraining, + ReviewerCode = doctor.ReviewerCode, + ChineseName = doctor.ChineseName, + FirstName = doctor.FirstName, + LastName = doctor.LastName, + + }; + + + query = workloadQuery.GroupBy(t => new { t.DoctorId, t.ReviewerCode, t.ChineseName, t.FirstName, t.LastName }).Select(gWorkLoad => new RevenuesDTO + { + ReviewerCode = gWorkLoad.Key.ReviewerCode, + ChineseName = gWorkLoad.Key.ChineseName, + FirstName = gWorkLoad.Key.FirstName, + LastName = gWorkLoad.Key.LastName, + + Downtime = gWorkLoad.Sum(t => t.Downtime), + Training = gWorkLoad.Sum(t => t.Training), + Timepoint = gWorkLoad.Sum(t => t.Timepoint), + TimepointIn24H = gWorkLoad.Sum(t => t.TimepointIn24H), + TimepointIn48H = gWorkLoad.Sum(t => t.TimepointIn48H), + Global = gWorkLoad.Sum(t => t.Global), + Adjudication = gWorkLoad.Sum(t => t.Adjudication), + AdjudicationIn24H = gWorkLoad.Sum(t => t.AdjudicationIn24H), + AdjudicationIn48H = gWorkLoad.Sum(t => t.AdjudicationIn48H), + + Total = gWorkLoad.Sum(t => t.Downtime) + + gWorkLoad.Sum(t => t.Training) + + gWorkLoad.Sum(t => t.Timepoint) + + gWorkLoad.Sum(t => t.TimepointIn24H) + + gWorkLoad.Sum(t => t.TimepointIn48H) + + gWorkLoad.Sum(t => t.Global) + + gWorkLoad.Sum(t => t.Adjudication) + + gWorkLoad.Sum(t => t.AdjudicationIn24H) + + gWorkLoad.Sum(t => t.AdjudicationIn48H) + + gWorkLoad.Sum(t => t.RefresherTraining) + + }); + + propName = queryParam.SortField == string.Empty ? "ReviewerCode" : queryParam.SortField; + + + } + + //按照月份维度 + if (queryParam.StatType == 3) + { + var workloadQuery = from workLoad in _workloadRepository.Where(workloadLambda) + join trial in _trialRepository.Where(trialLambda) + on workLoad.TrialId equals trial.Id + join doctor in _doctorRepository.Where(doctorLambda) + on workLoad.DoctorId equals doctor.Id + join trialCost in _trialRevenuePriceRepository.AsQueryable() on workLoad.TrialId equals trialCost.TrialId into c + from trialCost in c.DefaultIfEmpty() + + select new + { + workLoad.YearMonth, + Downtime = workLoad.Downtime * trialCost.Downtime, + Training = workLoad.Training * trialCost.Training, + Timepoint = workLoad.Timepoint * trialCost.Timepoint, + TimepointIn24H = workLoad.TimepointIn24H * trialCost.TimepointIn24H, + TimepointIn48H = workLoad.TimepointIn48H * trialCost.TimepointIn48H, + Global = workLoad.Global * trialCost.Global, + Adjudication = workLoad.Adjudication * trialCost.Adjudication, + AdjudicationIn24H = + workLoad.AdjudicationIn24H * trialCost.AdjudicationIn24H, + AdjudicationIn48H = workLoad.AdjudicationIn48H * trialCost.AdjudicationIn48H, + RefresherTraining = workLoad.RefresherTraining * trialCost.RefresherTraining, + + }; + + query = workloadQuery.GroupBy(t => t.YearMonth).Select(gWorkLoad => new RevenuesDTO + { + + YearMonth = gWorkLoad.Key, + Downtime = gWorkLoad.Sum(t => t.Downtime), + Training = gWorkLoad.Sum(t => t.Training), + Timepoint = gWorkLoad.Sum(t => t.Timepoint), + TimepointIn24H = gWorkLoad.Sum(t => t.TimepointIn24H), + TimepointIn48H = gWorkLoad.Sum(t => t.TimepointIn48H), + Global = gWorkLoad.Sum(t => t.Global), + Adjudication = gWorkLoad.Sum(t => t.Adjudication), + AdjudicationIn24H = gWorkLoad.Sum(t => t.AdjudicationIn24H), + AdjudicationIn48H = gWorkLoad.Sum(t => t.AdjudicationIn48H), + Total = gWorkLoad.Sum(t => t.Downtime) + + gWorkLoad.Sum(t => t.Training) + + gWorkLoad.Sum(t => t.Timepoint) + + gWorkLoad.Sum(t => t.TimepointIn24H) + + gWorkLoad.Sum(t => t.TimepointIn48H) + + gWorkLoad.Sum(t => t.Global) + + gWorkLoad.Sum(t => t.Adjudication) + + gWorkLoad.Sum(t => t.AdjudicationIn24H) + + gWorkLoad.Sum(t => t.AdjudicationIn48H) + + gWorkLoad.Sum(t => t.RefresherTraining) + + }); + propName = queryParam.SortField == string.Empty ? "YearMonth" : queryParam.SortField; + } + + + #endregion + //if (propName == "FirstName" || propName == "LastName") + //{ + // query = param.Asc + // ? query.OrderBy(t => t.LastName).ThenBy(t => t.FirstName) + // : query.OrderByDescending(t => t.LastName).ThenBy(t => t.FirstName); + //} + + //非详细 只用单字段排序 + if (queryParam.StatType != 0) + { + query = queryParam.Asc ? query.OrderBy(propName) : query.OrderByDescending(propName); + } + //详情 多字段排序 + if (queryParam.StatType == 0) + { + query = queryParam.Asc ? query.OrderBy(propName).ThenBy(t => t.TrialId).ThenBy(t => t.YearMonth).ThenBy(t => t.ReviewerCode) : + query.OrderByDescending(propName).ThenBy(t => t.TrialId).ThenBy(t => t.YearMonth).ThenBy(t => t.ReviewerCode) + ; + } + + + count = query!.Count(); + query = query!.Skip((queryParam.PageIndex - 1) * queryParam.PageSize).Take(queryParam.PageSize); + + incomeList = await query.ToListAsync(); + + #region 处理缺失项目价格 按照医生 或者月份维度 + + if (queryParam.StatType == 2 || queryParam.StatType == 3) + { + var hasIncomePriceTrialIds = await _trialRevenuePriceRepository.Select(t => t.TrialId).ToListAsync(); + + workloadLambda = workloadLambda.And(t => !hasIncomePriceTrialIds.Contains(t.TrialId)); + var doctorMissingTrialCodesQuery = (from workLoad in _workloadRepository.Where(workloadLambda) + join trial in _trialRepository.AsQueryable() on workLoad.TrialId equals trial.Id + select new + { + DoctorId = workLoad.DoctorId, + Month = workLoad.YearMonth, + TrialCode = trial.TrialCode + }).Distinct(); + + var doctorMissingTrialCodes = ((await doctorMissingTrialCodesQuery.ToListAsync()).GroupBy(t => t.DoctorId).Select(g => new + { + DoctorId = g.Key, + TrialCodes = g.Select(u => u.TrialCode).Distinct().ToList() + })).ToList(); + + var monthMissingTrialCode = ((await doctorMissingTrialCodesQuery.ToListAsync()).GroupBy(t => t.Month).Select(g => new + { + Month = g.Key, + TrialCodes = g.Select(u => u.TrialCode).Distinct().ToList() + })).ToList(); + + incomeList.ForEach(income => + { + var doctor = doctorMissingTrialCodes.FirstOrDefault(t => t.DoctorId == income.Id); + var month = monthMissingTrialCode.FirstOrDefault(t => t.Month == income.YearMonth); + + if (queryParam.StatType == 2) + { + income.MissingTrialCodes = doctor == null ? new List() : doctor.TrialCodes; + } + else + { + income.MissingTrialCodes = month == null ? new List() : month.TrialCodes; + } + }); + } + + if (queryParam.StatType == 0 || queryParam.StatType == 1) + { + incomeList.ForEach(income => + { + List temp = new List(); + + + + if (Math.Abs(income.Total) < 0.001m) + { + temp.Add(income.TrialCode); + income.MissingTrialCodes = temp; + } + + }); + } + #endregion + + return new PageOutput(queryParam.PageIndex, queryParam.PageSize, count, incomeList); + + } + + /// + /// 收入支出分析接口,按照项目维度分析统计 + /// + [HttpPost] + public async Task> GetTrialAnalysisList(TrialAnalysisQueryDTO param) + { + var bDate = new DateTime(param.BeginDate.Year, param.BeginDate.Month, 1); + var eDate = new DateTime(param.EndDate.Year, param.EndDate.Month, 1); + + Expression> workloadLambda = x => x.DataFrom == (int)WorkLoadFromStatus.FinalConfirm; + + Expression> trialLambda = x => true; + if (Guid.Empty != param.CroId && param.CroId != null) + { + trialLambda = trialLambda.And(u => u.CROId == param.CroId); + } + if (!string.IsNullOrWhiteSpace(param.TrialCode)) + { + var trialCode = param.TrialCode.Trim(); + trialLambda = trialLambda.And(u => u.TrialCode.Contains(trialCode)); + } + if (param.AttendedReviewerType != null) + { + trialLambda = trialLambda.And(u => u.AttendedReviewerType == param.AttendedReviewerType); + } + + var lockedPaymentIdAndYearMonth = _paymentRepository.Where(t => t.IsLock && t.YearMonthDate >= bDate && t.YearMonthDate <= eDate).Select(t => new { PaymentId = t.Id, t.YearMonth, t.DoctorId }); + + var costStatisticsIds = await lockedPaymentIdAndYearMonth.Select(t => t.PaymentId).ToListAsync(); + + var lockedDoctorIdAndYearMonthQueryable = lockedPaymentIdAndYearMonth.Select(t => new { t.YearMonth, t.DoctorId }); + + + ////工作量过滤查询 锁定得月份 + //workloadLambda = workloadLambda.And(x => lockedDoctorIdAndYearMonthQueryable.Contains()); + + var trialPayQuery = from costStatisticsDetail in _paymentDetailRepository.AsQueryable() + .Where(t => costStatisticsIds.Contains(t.PaymentId) && t.TrialCode != "Volume Reward") + group costStatisticsDetail by costStatisticsDetail.TrialId + into g + select new + { + TrialId = g.Key, + PaymentUSD = g.Sum(t => t.PaymentUSD) + }; + + + var workloadQuery = from workLoad in _workloadRepository.Where(workloadLambda) + join lockedDoctorAndMonth in lockedDoctorIdAndYearMonthQueryable on new { workLoad.DoctorId, YearMonth = workLoad.YearMonth } equals new { lockedDoctorAndMonth.DoctorId, lockedDoctorAndMonth.YearMonth } + group workLoad by workLoad.TrialId + + into gWorkLoad + select new + { + TrialId = gWorkLoad.Key, + Downtime = gWorkLoad.Sum(t => t.Downtime), + Training = gWorkLoad.Sum(t => t.Training), + Timepoint = gWorkLoad.Sum(t => t.Timepoint), + TimepointIn24H = gWorkLoad.Sum(t => t.TimepointIn24H), + TimepointIn48H = gWorkLoad.Sum(t => t.TimepointIn48H), + Global = gWorkLoad.Sum(t => t.Global), + Adjudication = gWorkLoad.Sum(t => t.Adjudication), + AdjudicationIn24H = gWorkLoad.Sum(t => t.AdjudicationIn24H), + AdjudicationIn48H = gWorkLoad.Sum(t => t.AdjudicationIn48H) + + }; + + var workloadIncomeQuery = from workLoad in workloadQuery + join trialCost in _trialRevenuePriceRepository.AsQueryable() on workLoad.TrialId equals trialCost.TrialId + into c + from trialCost in c.DefaultIfEmpty() + join trial in _trialRepository.Where(trialLambda) on workLoad.TrialId equals trial.Id into t + from trial in t.DefaultIfEmpty() + join cro in _croRepository.AsQueryable() on trial.CROId equals cro.Id into ttt + from croItem in ttt.DefaultIfEmpty() + select new TrialAnalysisDTO + { + TrialId = trial.Id, + Indication = trial.Indication, + TrialCode = trial.TrialCode, + Expedited = trial.Expedited, + Cro = croItem == null ? "" : croItem.CROName, + + Type = "Trial", + + RevenusUSD = (workLoad.Downtime * trialCost.Downtime) + + (workLoad.Training * trialCost.Training) + + (workLoad.Timepoint * trialCost.Timepoint) + + (workLoad.TimepointIn24H * trialCost.TimepointIn24H) + + (workLoad.TimepointIn48H * trialCost.TimepointIn48H) + + (workLoad.Adjudication * trialCost.Adjudication) + + (workLoad.AdjudicationIn24H * trialCost.AdjudicationIn24H) + + (workLoad.AdjudicationIn48H * trialCost.AdjudicationIn48H) + + (workLoad.Global * trialCost.Global), + + }; + + + var trialPayList = await trialPayQuery.OrderBy(t => t.PaymentUSD).ToListAsync(); + var workloadIncomeList = await workloadIncomeQuery.ToListAsync(); + var returnList = workloadIncomeList; + returnList.ForEach(t => + { + var pay = trialPayList.FirstOrDefault(u => u.TrialId == t.TrialId); + t.PaymentUSD = pay == null ? 0 : pay.PaymentUSD; + }); + + if ((Guid.Empty == param.CroId || null == param.CroId) && string.IsNullOrWhiteSpace(param.TrialCode)) + { + var volumeReward = _paymentDetailRepository.AsQueryable() + .Where(t => costStatisticsIds.Contains(t.PaymentId) && t.TrialCode == "Volume Reward") + .Sum(t => (decimal?)t.PaymentUSD) ?? 0; + + var adjustment = _payAdjustmentRepository.AsQueryable() + .Where(t => t.YearMonthDate >= param.BeginDate + && t.YearMonthDate <= param.EndDate && t.IsLock) + .Sum(u => (decimal?)u.AdjustmentUSD) ?? 0; + + returnList.Add(new TrialAnalysisDTO() + { + Type = "Volume Reward", + RevenusUSD = 0, + PaymentUSD = volumeReward + }); + returnList.Add(new TrialAnalysisDTO() + { + Type = "Adjustment", + RevenusUSD = 0, + PaymentUSD = adjustment + }); + } + return returnList; + } + + /// + /// 收入支出分析接口,按照医生维度分析统计 + /// + [HttpPost] + public async Task> GetReviewerAnalysisList(AnalysisQueryDTO param) + { + var beginDate = new DateTime(param.BeginDate.Year, param.BeginDate.Month, 1); + var endDate = new DateTime(param.EndDate.Year, param.EndDate.Month, 1); + + Expression> workloadLambda = x => x.DataFrom == (int)WorkLoadFromStatus.FinalConfirm; + + Expression> doctorLambda = x => true; + if (!string.IsNullOrWhiteSpace(param.Reviewer)) + { + var reviewer = param.Reviewer.Trim(); + doctorLambda = doctorLambda.And(u => u.ChineseName.Contains(reviewer) + || u.FirstName.Contains(reviewer) + || u.LastName.Contains(reviewer) + || u.ReviewerCode.Contains(reviewer)); + } + if (param.Nation != null) + { + doctorLambda = doctorLambda.And(u => u.Nation == param.Nation); + } + + var lockedPaymentIdAndYearMonth = _paymentRepository.Where(t => t.IsLock && t.YearMonthDate >= param.BeginDate && t.YearMonthDate <= param.EndDate).Select(t => new { PaymentId = t.Id, t.YearMonth, t.DoctorId }); + + var lockedDoctorIdAndYearMonthQueryable = lockedPaymentIdAndYearMonth.Select(t => new { t.YearMonth, t.DoctorId }); + + + + Expression> monthlyPayLambda = x => x.IsLock && x.YearMonthDate >= beginDate && x.YearMonthDate <= endDate; + var payQuery = + from monthlyPayment in _paymentRepository.Where(monthlyPayLambda) + .GroupBy(t => t.DoctorId).Select(g => new + { + ReviewerId = g.Key, + PaymentUSD = g.Sum(u => u.PaymentUSD), + PaymentCNY = g.Sum(u => u.PaymentCNY), + AdjustmentCNY = g.Sum(u => u.AdjustmentCNY), + AdjustmentUSD = g.Sum(u => u.AdjustmentUSD) + }) + join doctor in _doctorRepository.Where(doctorLambda) on monthlyPayment.ReviewerId equals doctor.Id + select new MonthlyPaymentDTO + { + ReviewerId = doctor.Id, + ChineseName = doctor.ChineseName, + FirstName = doctor.FirstName, + LastName = doctor.LastName, + ReviewerCode = doctor.ReviewerCode, + PaymentUSD = monthlyPayment.PaymentUSD, + PaymentCNY = monthlyPayment.PaymentCNY, + AdjustmentCNY = monthlyPayment.AdjustmentCNY, + AdjustmentUSD = monthlyPayment.AdjustmentUSD, + TotalUSD = monthlyPayment.AdjustmentUSD + monthlyPayment.PaymentUSD, + TotalCNY = monthlyPayment.AdjustmentCNY + monthlyPayment.PaymentCNY, + }; + + //获取工作量收入 workloadLambda 已经加入过滤 payment锁定月份 + + ////工作量过滤查询 锁定得月份 + + var workloadIncomeQuery = from workLoad in _workloadRepository.Where(workloadLambda) + join doctor in _doctorRepository.Where(doctorLambda) + on workLoad.DoctorId equals doctor.Id + join lockedDoctorAndMonth in lockedDoctorIdAndYearMonthQueryable on new { workLoad.DoctorId, workLoad.YearMonth } equals new { lockedDoctorAndMonth.DoctorId, lockedDoctorAndMonth.YearMonth } + join trialCost in _trialRevenuePriceRepository.AsQueryable() on workLoad.TrialId equals trialCost.TrialId into c + from trialCost in c.DefaultIfEmpty() + + select new + { + DoctorId = workLoad.DoctorId, + Downtime = workLoad.Downtime * trialCost.Downtime, + Training = workLoad.Training * trialCost.Training, + Timepoint = workLoad.Timepoint * trialCost.Timepoint, + TimepointIn24H = workLoad.TimepointIn24H * trialCost.TimepointIn24H, + TimepointIn48H = workLoad.TimepointIn48H * trialCost.TimepointIn48H, + Global = workLoad.Global * trialCost.Global, + Adjudication = workLoad.Adjudication * trialCost.Adjudication, + AdjudicationIn24H = workLoad.AdjudicationIn24H * trialCost.AdjudicationIn24H, + AdjudicationIn48H = workLoad.AdjudicationIn48H * trialCost.AdjudicationIn48H + + + }; + + //工作量收入按照人分组统计 + var workloadIncomeList = await workloadIncomeQuery.GroupBy(t => t.DoctorId).Select(gWorkLoad => new + { + + DoctorId = gWorkLoad.Key, + //TrialId = Guid.Empty, + Downtime = gWorkLoad.Sum(t => t.Downtime), + Training = gWorkLoad.Sum(t => t.Training), + Timepoint = gWorkLoad.Sum(t => t.Timepoint), + TimepointIn24H = gWorkLoad.Sum(t => t.TimepointIn24H), + TimepointIn48H = gWorkLoad.Sum(t => t.TimepointIn48H), + Global = gWorkLoad.Sum(t => t.Global), + Adjudication = gWorkLoad.Sum(t => t.Adjudication), + AdjudicationIn24H = gWorkLoad.Sum(t => t.AdjudicationIn24H), + AdjudicationIn48H = gWorkLoad.Sum(t => t.AdjudicationIn48H), + }).ToListAsync(); + var workloadPayList = payQuery.ToList(); + //var workloadIncomeList = workloadIncomeQuery.ToList(); + + var returnList = new List(); + + #region 处理找到缺失的项目 + + //有项目价格的trial Id 集合 + var hasIncomePriceTrialIds = await _trialRevenuePriceRepository.Select(t => t.TrialId).ToListAsync(); + + workloadLambda = workloadLambda.And(t => !hasIncomePriceTrialIds.Contains(t.TrialId)); + var doctorMissingTrialCodesQuery = (from workLoad in _workloadRepository.Where(workloadLambda) + join trial in _trialRepository.AsQueryable() on workLoad.TrialId equals trial.Id + select new + { + DoctorId = workLoad.DoctorId, + TrialCode = trial.TrialCode + }).Distinct(); + + var doctorMissingTrialCodes = ((await doctorMissingTrialCodesQuery.ToListAsync()).GroupBy(t => t.DoctorId).Select(g => new + { + DoctorId = g.Key, + TrialCodes = g.Select(u => u.TrialCode).ToList() + })).ToList(); + + + #endregion + + workloadPayList.ForEach(pay => + { + var doctor = workloadIncomeList.FirstOrDefault(t => t.DoctorId == pay.ReviewerId); + + var doctorMissingCode = doctorMissingTrialCodes.FirstOrDefault(t => t.DoctorId == pay.ReviewerId); + + returnList.Add(new ReviewerAnalysisDTO() + { + ReviewerId = pay.ReviewerId, + ChineseName = pay.ChineseName, + FirstName = pay.FirstName, + LastName = pay.LastName, + ReviewerCode = pay.ReviewerCode, + PaymentUSD = pay.PaymentUSD + pay.AdjustmentUSD, + + //付费表的医生数量肯定事全的 工作量的不一定全 (只有调整) + RevenusUSD = doctor != null + ? doctor.Adjudication + doctor.AdjudicationIn24H + doctor.AdjudicationIn48H + doctor.Global + + doctor.Downtime + doctor.Training + + doctor.Timepoint + doctor.TimepointIn24H + doctor.TimepointIn48H + : 0, + MissingTrialCodes = doctorMissingCode != null ? doctorMissingCode.TrialCodes : new List() + }); + }); + return returnList.OrderBy(t => t.ReviewerCode).ToList(); + } + + /// + /// 获取劳务费用列表 + /// + [HttpPost] + public async Task> GetLaborPaymentList(List paymentIds) + { + var query = from payment in _paymentRepository.Where(t => paymentIds.Contains(t.Id)) + join reviewer in _doctorRepository.AsQueryable() on payment.DoctorId equals reviewer.Id + join payInfo in _doctorPayInfoRepository.AsQueryable() on payment.DoctorId equals payInfo.DoctorId + select new LaborPayment() + { + + ChineseName = reviewer.ChineseName, + FirstName = reviewer.FirstName, + LastName = reviewer.LastName, + ResidentId = payInfo.IDCard, + PaymentCNY = payment.PaymentCNY + payment.AdjustmentCNY, + YearMonth = payment.YearMonth, + Phone = reviewer.Phone, + AccountNumber = payInfo.BankCardNumber, + Bank = payInfo.BankName + + //TaxCNY = payment.TaxCNY, + //ActuallyPaidCNY = payment.ActuallyPaidCNY, + //BankTransferCNY = payment.BankTransferCNY, + }; + var result = await query.ToListAsync(); + + result.ForEach(t => + { + t.TaxCNY =GetTax2(t.PaymentCNY); + + t.ActuallyPaidCNY = t.PaymentCNY - t.TaxCNY; + t.BankTransferCNY = t.PaymentCNY - t.TaxCNY; + }); + + return result; + + } + + /// + /// 锁定医生费用,锁定后,无法变更该医生对应月份的费用和工作量[New] + /// + [HttpPost] + public async Task LockMonthlyPayment(LockPaymentDTO param) + { + var reviewerIds = param.ReviewerIdList; + var yearMonth = param.Month.ToString("yyyy-MM"); + var isLock = param.IsLock; + + + var paymentLockSuccess = await _paymentRepository.UpdateFromQueryAsync(u => reviewerIds.Contains(u.DoctorId) && u.YearMonth == yearMonth, t => new Payment + { + IsLock = isLock + }); + + var adjustmentLockSuccess = await _payAdjustmentRepository.UpdateFromQueryAsync( + t => t.YearMonth == yearMonth && reviewerIds.Contains(t.ReviewerId), u => + new PaymentAdjustment() + { + IsLock = true + }); + await _doctorWorkloadRepository.UpdateFromQueryAsync(u => reviewerIds.Contains(u.DoctorId) && u.YearMonth == yearMonth, + t => new Workload { IsLock = true }); + + return ResponseOutput.Result(paymentLockSuccess || adjustmentLockSuccess); + } + + [NonDynamicMethod] + private static decimal GetTax2(decimal paymentCNY) + { + if (paymentCNY >= 0 && paymentCNY <= 800) + { + return 0; + } + + var calculateTaxPart = paymentCNY <= 4000 ? paymentCNY - 800 : paymentCNY * (1 - 0.2m); + + + if (calculateTaxPart <= 20000) + { + return calculateTaxPart * 0.2m; + } + + else if (calculateTaxPart > 20000 && calculateTaxPart <= 50000) + { + return calculateTaxPart * 0.3m - 2000; + } + + else + { + return calculateTaxPart * 0.4m - 7000; + } + + + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Financial/Interface/ICalculateService.cs b/IRaCIS.Core.Application/Service/Financial/Interface/ICalculateService.cs new file mode 100644 index 00000000..0e4ab8ee --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/Interface/ICalculateService.cs @@ -0,0 +1,16 @@ +using IRaCIS.Application.Contracts; +using System; +using System.Collections.Generic; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface ICalculateService + { + Task CalculateMonthlyPayment(CalculateDoctorAndMonthDTO param, string token); + //IResponseOutput LockMonthlyPayment(LockPaymentDTO param); + Task> GetNeedCalculateReviewerList(Guid reviewerId, string yearMonth); + Task IsLock(Guid reviewerId, string yearMonth); + //bool ResetMonthlyPayment(Guid reviewerId, Guid trialId,string yearMonth); + } +} diff --git a/IRaCIS.Core.Application/Service/Financial/Interface/IDoctorPayInfoService.cs b/IRaCIS.Core.Application/Service/Financial/Interface/IDoctorPayInfoService.cs new file mode 100644 index 00000000..87aae6d3 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/Interface/IDoctorPayInfoService.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface IReviewerPayInfoService + { + Task AddOrUpdateReviewerPayInfo(ReviewerPayInfoCommand addOrUpdateModel, Guid userId); + Task> GetReviewerPayInfoList(DoctorPaymentInfoQueryDTO queryParam); + Task GetReviewerPayInfo(Guid doctorId); + Task> GetReviewerIdByRankId(Guid rankId); + } +} diff --git a/IRaCIS.Core.Application/Service/Financial/Interface/IExchangeRateService.cs b/IRaCIS.Core.Application/Service/Financial/Interface/IExchangeRateService.cs new file mode 100644 index 00000000..de5b56b9 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/Interface/IExchangeRateService.cs @@ -0,0 +1,15 @@ +using IRaCIS.Application.Contracts; +using System; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface IExchangeRateService + { + Task AddOrUpdateExchangeRate(ExchangeRateCommand model); + Task GetExchangeRateByMonth(string month); + Task> GetExchangeRateList(ExchangeRateQueryDTO queryParam); + + Task DeleteExchangeRate(Guid id); + } +} diff --git a/IRaCIS.Core.Application/Service/Financial/Interface/IPaymentAdjustmentService.cs b/IRaCIS.Core.Application/Service/Financial/Interface/IPaymentAdjustmentService.cs new file mode 100644 index 00000000..4cf96408 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/Interface/IPaymentAdjustmentService.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using IRaCIS.Application.Contracts.Pay; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface IPaymentAdjustmentService + { + Task> GetPaymentAdjustmentList(PaymentAdjustmentQueryDTO queryParam); + Task AddOrUpdatePaymentAdjustment(PaymentAdjustmentCommand addOrUpdateModel); + Task DeletePaymentAdjustment(Guid id); + Task CalculateCNY(string yearMonth, decimal rate); + Task> GetReviewerSelectList(); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Financial/Interface/IPaymentService.cs b/IRaCIS.Core.Application/Service/Financial/Interface/IPaymentService.cs new file mode 100644 index 00000000..8ddbf2b4 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/Interface/IPaymentService.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using IRaCIS.Application.Contracts; +using IRaCIS.Application.Contracts.Pay; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface IPaymentService + { + Task LockMonthlyPayment(LockPaymentDTO param); + + Task> GetMonthlyPaymentList(MonthlyPaymentQueryDTO queryParam); + Task GetMonthlyPaymentDetailList(Guid PaymentId, Guid doctorId, DateTime yearMonth); + + Task> GetLaborPaymentList(List paymentId); + + //导出多个医生的付费详细 + Task> GetReviewersMonthlyPaymentDetail(List manyReviewers); + + Task> GetPaymentHistoryList(PaymentQueryDTO param); + Task> GetPaymentHistoryDetailList(VolumeQueryDTO param); + + Task> GetRevenuesStatistics(StatisticsQueryDTO param); + Task> GetTrialAnalysisList(TrialAnalysisQueryDTO param); + Task> GetReviewerAnalysisList(AnalysisQueryDTO param); + + + + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Financial/Interface/IRankPriceService.cs b/IRaCIS.Core.Application/Service/Financial/Interface/IRankPriceService.cs new file mode 100644 index 00000000..93ddbdbc --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/Interface/IRankPriceService.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface IRankPriceService + { + Task AddOrUpdateRankPrice(RankPriceCommand addOrUpdateModel, Guid userId); + + Task> GetRankPriceList(RankPriceQueryDTO queryParam); + + Task DeleteRankPrice( Guid id); + + Task> GetRankDic(); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Financial/Interface/ITrialPaymentPriceService.cs b/IRaCIS.Core.Application/Service/Financial/Interface/ITrialPaymentPriceService.cs new file mode 100644 index 00000000..6ce2a42c --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/Interface/ITrialPaymentPriceService.cs @@ -0,0 +1,21 @@ +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface ITrialPaymentPriceService + { + Task AddOrUpdateTrialPaymentPrice(TrialPaymentPriceCommand addOrUpdateModel);//新增也不需要返回Id,TrialId 也是唯一 + Task> GetTrialPaymentPriceList(TrialPaymentPriceQueryDTO queryParam); + + + /// + /// 上传入组后的Ack-SOW + /// + Task UploadTrialSOW( TrialSOWPathDTO trialSowPath); + + + + Task DeleteTrialSOW( DeleteSowPathDTO trialSowPath); + } +} diff --git a/IRaCIS.Core.Application/Service/Financial/Interface/ITrialRevenuesPriceService.cs b/IRaCIS.Core.Application/Service/Financial/Interface/ITrialRevenuesPriceService.cs new file mode 100644 index 00000000..f4d0c44f --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/Interface/ITrialRevenuesPriceService.cs @@ -0,0 +1,14 @@ +using IRaCIS.Application.Contracts; +using System; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface ITrialRevenuesPriceService + { + Task AddOrUpdateTrialRevenuesPrice(TrialRevenuesPriceDTO model); + Task DeleteTrialCost(Guid Id); + Task> GetTrialRevenuesPriceList(TrialRevenuesPriceQueryDTO param); + + } +} diff --git a/IRaCIS.Core.Application/Service/Financial/Interface/ITrialRevenuesPriceVerificationService.cs b/IRaCIS.Core.Application/Service/Financial/Interface/ITrialRevenuesPriceVerificationService.cs new file mode 100644 index 00000000..19355c7f --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/Interface/ITrialRevenuesPriceVerificationService.cs @@ -0,0 +1,17 @@ + +using IRaCIS.Core.Application.Contracts; +using System.Collections.Generic; + + + +namespace IRaCIS.Application.Interfaces +{ + public interface ITrialRevenuesPriceVerificationService + { + //List GetRevenuesVerifyResultList(RevenusVerifyQueryDTO param); + + Task GetAnalysisVerifyList(RevenusVerifyQueryDTO param); + + Task> GetRevenuesVerifyList(RevenusVerifyQueryDTO param); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Financial/Interface/IVolumeRewardService.cs b/IRaCIS.Core.Application/Service/Financial/Interface/IVolumeRewardService.cs new file mode 100644 index 00000000..fa25acd5 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/Interface/IVolumeRewardService.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface IVolumeRewardService + { + Task AddOrUpdateVolumeRewardPriceList(IEnumerable addOrUpdateModels); + Task> GetVolumeRewardPriceList(AwardPriceQueryDTO queryParam); + Task> GetVolumeRewardPriceList(); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Financial/PaymentAdjustmentService.cs b/IRaCIS.Core.Application/Service/Financial/PaymentAdjustmentService.cs new file mode 100644 index 00000000..6e312d04 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/PaymentAdjustmentService.cs @@ -0,0 +1,292 @@ +using AutoMapper; +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts.Pay; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Mvc; +using Panda.DynamicWebApi.Attributes; + +namespace IRaCIS.Application.Services +{ + [ApiExplorerSettings(GroupName = "Financial")] + public class PaymentAdjustmentService : BaseService, IPaymentAdjustmentService + { + private readonly IRepository _payAdjustmentRepository; + private readonly IRepository _doctorRepository; + private readonly IRepository _exchangeRateRepository; + private readonly IRepository _paymentRepository; + + + public PaymentAdjustmentService(IRepository costAdjustmentRepository, IRepository doctorRepository, + IRepository exchangeRateRepository, IRepository paymentRepository, IMapper mapper) + { + _payAdjustmentRepository = costAdjustmentRepository; + _doctorRepository = doctorRepository; + _exchangeRateRepository = exchangeRateRepository; + _paymentRepository = paymentRepository; + + } + + /// + /// 添加或更新费用调整[AUTH] + /// + + [HttpPost] + public async Task AddOrUpdatePaymentAdjustment(PaymentAdjustmentCommand addOrUpdateModel) + { + + var yearMonthDate = new DateTime(addOrUpdateModel.YearMonth.Year, addOrUpdateModel.YearMonth.Month, 1); + var yearMonth = addOrUpdateModel.YearMonth.ToString("yyyy-MM"); + + var payment = await _paymentRepository.FirstOrDefaultAsync(u => u.DoctorId == addOrUpdateModel.ReviewerId + && u.YearMonth == yearMonth); + + //判断付费表中是否有记录 + if (payment == null) + { + //没有 添加仅有的调整费用记录 + payment = new Payment + { + DoctorId = addOrUpdateModel.ReviewerId, + YearMonth = yearMonth, + YearMonthDate = yearMonthDate, + PaymentCNY = 0, + PaymentUSD = 0, + AdjustmentCNY = 0, + AdjustmentUSD = 0 + }; + await _paymentRepository.AddAsync(payment); + await _paymentRepository.SaveChangesAsync(); + } + else + { + if (payment.IsLock) + { + return ResponseOutput.NotOk("Doctor payment has confirmed lock"); + } + } + + var exchangeRate = await _exchangeRateRepository.FirstOrDefaultAsync(t => t.YearMonth == yearMonth); + if (addOrUpdateModel.Id == Guid.Empty || addOrUpdateModel.Id == null) + { + var costAdjustment = _mapper.Map(addOrUpdateModel); + + //视图模型和领域模型没对应 重新赋值 + costAdjustment.ExchangeRate = exchangeRate?.Rate ?? 0; + costAdjustment.AdjustmentCNY = addOrUpdateModel.AdjustPaymentUSD * (exchangeRate?.Rate ?? 0); + costAdjustment.AdjustmentUSD = addOrUpdateModel.AdjustPaymentUSD; + + await _payAdjustmentRepository.AddAsync(costAdjustment); + + //添加的时候,每个月调整汇总费用 需要加上本次调整的费用 + payment.AdjustmentCNY += costAdjustment.AdjustmentCNY; + payment.AdjustmentUSD += costAdjustment.AdjustmentUSD; + + await _paymentRepository.UpdateAsync(payment); + + await _payAdjustmentRepository.SaveChangesAsync(); + + + return ResponseOutput.Ok(costAdjustment.Id.ToString()); + + } + else + { + // 更新的时候,先查出来,更新前的调整费用数据 + var paymentAdjust = await _payAdjustmentRepository.FirstOrDefaultAsync(t => t.Id == addOrUpdateModel.Id); + + + _mapper.Map(addOrUpdateModel, paymentAdjust); + + paymentAdjust.ExchangeRate = exchangeRate?.Rate ?? 0; + paymentAdjust.AdjustmentUSD = addOrUpdateModel.AdjustPaymentUSD; + paymentAdjust.AdjustmentCNY = addOrUpdateModel.AdjustPaymentUSD * (exchangeRate?.Rate ?? 0); + + await _payAdjustmentRepository.UpdateAsync(paymentAdjust); + var success = await _payAdjustmentRepository.SaveChangesAsync(); + + if (success) + { + + var adjustmentList = await _payAdjustmentRepository.Where(u => u.ReviewerId == addOrUpdateModel.ReviewerId && u.YearMonth == yearMonth).ToListAsync(); + + payment.AdjustmentCNY = adjustmentList.Sum(t => t.AdjustmentCNY); + payment.AdjustmentUSD = adjustmentList.Sum(t => t.AdjustmentUSD); + + + await _paymentRepository.UpdateAsync(payment); + + await _paymentRepository.SaveChangesAsync(); + } + + //查询得到历史汇总 + + + + return ResponseOutput.Ok(success); + + #region 逻辑存在错误 问题待查 + + //// 更新的时候,先查出来,更新前的调整费用数据 + //var paymentAdjust = _payAdjustmentRepository.FindSingleOrDefault(t => t.Id == addOrUpdateModel.Id); + + ////减去数据库本条记录的值 + //payment.AdjustmentCNY = -paymentAdjust.AdjustmentCNY; + //payment.AdjustmentUSD = -paymentAdjust.AdjustmentUSD; + + //_mapper.Map(addOrUpdateModel, paymentAdjust); + + //paymentAdjust.ExchangeRate = exchangeRate?.Rate ?? 0; + //paymentAdjust.AdjustmentUSD = addOrUpdateModel.AdjustPaymentUSD; + //paymentAdjust.AdjustmentCNY = addOrUpdateModel.AdjustPaymentUSD * (exchangeRate?.Rate ?? 0); + + //_payAdjustmentRepository.Update(paymentAdjust); + + ////查询得到历史汇总 + //var adjustment = _payAdjustmentRepository.Find(u => u.ReviewerId == addOrUpdateModel.ReviewerId && u.YearMonth == yearMonth) + // .GroupBy(u => new { u.ReviewerId, u.YearMonth }).Select(g => new + // { + // AdjustCNY = g.Sum(t => t.AdjustmentCNY), + // AdjustUSD = g.Sum(t => t.AdjustmentUSD) + // }).FirstOrDefault(); + + ////最终的值 等于历史汇总 减去更新前的加上当前更新的值 + //payment.AdjustmentCNY += (adjustment.AdjustCNY + paymentAdjust.AdjustmentCNY); + //payment.AdjustmentUSD += (adjustment.AdjustUSD + paymentAdjust.AdjustmentUSD); + + //_paymentRepository.Update(payment); + + //var success = _payAdjustmentRepository.SaveChanges(); + //return ResponseOutput.Result(success, success ? string.Empty : StaticData.UpdateFailed); + + #endregion + + + + } + } + /// + /// 删除费用调整记录 + /// + + [HttpDelete("{id:guid}")] + public async Task DeletePaymentAdjustment(Guid id) + { + var adjustPayment = await _payAdjustmentRepository.FirstOrDefaultAsync(u => u.Id == id); + + var monthPay = await _paymentRepository.FirstOrDefaultAsync(t => + t.DoctorId == adjustPayment.ReviewerId && t.YearMonth == adjustPayment.YearMonth); + + await _payAdjustmentRepository.DeleteAsync(new PaymentAdjustment() { Id = id }); + + var success = await _payAdjustmentRepository.SaveChangesAsync(); + + if (success) + { + + var adjustmentList = await _payAdjustmentRepository.Where(u => + u.ReviewerId == adjustPayment.ReviewerId && u.YearMonth == adjustPayment.YearMonth).ToListAsync(); + + monthPay.AdjustmentCNY = adjustmentList.Sum(t => t.AdjustmentCNY); + monthPay.AdjustmentUSD = adjustmentList.Sum(t => t.AdjustmentUSD); + + + + await _paymentRepository.UpdateAsync(monthPay); + + await _paymentRepository.SaveChangesAsync(); + } + + + + return ResponseOutput.Result(success); + } + + /// + /// 获取费用调整列表 + /// + [HttpPost] + public async Task> GetPaymentAdjustmentList(PaymentAdjustmentQueryDTO queryParam) + { + + var beginYearMonth = queryParam.BeginMonth.AddDays(1 - queryParam.BeginMonth.Day); + var endYearMonth = queryParam.EndMonth.AddDays(1 - queryParam.EndMonth.Day).AddMonths(1).AddDays(-1); + + var costAdjustmentQueryable = from costAdjustment in _payAdjustmentRepository + .Where(t => t.YearMonthDate >= beginYearMonth && t.YearMonthDate <= endYearMonth) + join doctor in _doctorRepository.AsQueryable(). + WhereIf(!string.IsNullOrWhiteSpace(queryParam.Reviewer), + u => u.ChineseName.Contains(queryParam.Reviewer) || + (u.LastName + u.FirstName).Contains(queryParam.Reviewer) || + u.ReviewerCode.Contains(queryParam.Reviewer)) + on costAdjustment.ReviewerId equals doctor.Id + + select new PaymentAdjustmentDetailDTO() + { + AdjustPaymentCNY = costAdjustment.AdjustmentCNY, + AdjustPaymentUSD = costAdjustment.AdjustmentUSD, + IsLock = costAdjustment.IsLock, + Id = costAdjustment.Id, + YearMonth = costAdjustment.YearMonth, + YearMonthDate = costAdjustment.YearMonthDate, + Note = costAdjustment.Note, + ReviewerId = costAdjustment.ReviewerId, + ReviewerCode = doctor.ReviewerCode, + FirstName = doctor.FirstName, + LastName = doctor.LastName, + ChineseName = doctor.ChineseName + }; + + return await costAdjustmentQueryable.ToPagedListAsync(queryParam.PageIndex, queryParam.PageSize, string.IsNullOrWhiteSpace(queryParam.SortField) ? "YearMonthDate" : queryParam.SortField, queryParam.Asc); + + + } + + public async Task> GetReviewerSelectList() + { + return await _doctorRepository.Where(t => t.CooperateStatus == ContractorStatusEnum.Cooperation && t.ResumeStatus == ResumeStatusEnum.Pass).ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + } + + [NonDynamicMethod] + public async Task CalculateCNY(string yearMonth, decimal rate) + { + + //如果是double 不会保留两位小数 + await _payAdjustmentRepository.UpdateFromQueryAsync(u => u.YearMonth == yearMonth && + !u.IsLock, t => new PaymentAdjustment + { + AdjustmentCNY = t.AdjustmentUSD * rate, + ExchangeRate = rate, + UpdateTime = DateTime.Now + }); + + var adjustList = await _payAdjustmentRepository.Where(u => u.YearMonth == yearMonth && + !u.IsLock).ToListAsync(); + + + + var needUpdatePayment = adjustList.GroupBy(t => t.ReviewerId).Select(g => new + { + ReviewerId = g.Key, + AdjustCNY = g.Sum(t => t.AdjustmentCNY), + AdjustUSD = g.Sum(t => t.AdjustmentUSD) + }); + + foreach (var reviewer in needUpdatePayment) + { + await _paymentRepository.UpdateFromQueryAsync(u => u.YearMonth == yearMonth && + !u.IsLock && u.DoctorId == reviewer.ReviewerId, t => new Payment() + { + AdjustmentUSD = reviewer.AdjustUSD, + AdjustmentCNY = reviewer.AdjustCNY + + }); + } + + + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Financial/RankPriceService.cs b/IRaCIS.Core.Application/Service/Financial/RankPriceService.cs new file mode 100644 index 00000000..29179102 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/RankPriceService.cs @@ -0,0 +1,104 @@ +using AutoMapper; +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Mvc; +using Panda.DynamicWebApi.Attributes; + +namespace IRaCIS.Application.Services +{ + [ ApiExplorerSettings(GroupName = "Financial")] + public class RankPriceService : BaseService, IRankPriceService + { + private readonly IRepository _rankPriceRepository; + private readonly IRepository _reviewerPayInfoRepository; + + + public RankPriceService(IRepository rankPriceRepository, IRepository reviewerPayInfoRepository,IMapper mapper) + { + _rankPriceRepository = rankPriceRepository; + _reviewerPayInfoRepository = reviewerPayInfoRepository; + + } + + [NonDynamicMethod] + public async Task AddOrUpdateRankPrice(RankPriceCommand addOrUpdateModel, Guid userId) + { + if (addOrUpdateModel.Id == Guid.Empty|| addOrUpdateModel.Id ==null) + { + var rankPrice = _mapper.Map(addOrUpdateModel); + rankPrice = await _rankPriceRepository.AddAsync(rankPrice); + if (await _rankPriceRepository.SaveChangesAsync()) + { + return ResponseOutput.Ok(rankPrice.Id.ToString()); + } + else + { + return ResponseOutput.NotOk(); + } + + } + else + { + var success =await _rankPriceRepository.UpdateFromQueryAsync(t => t.Id == addOrUpdateModel.Id, u => new RankPrice() + { + UpdateUserId = userId, + UpdateTime = DateTime.Now, + RefresherTraining=addOrUpdateModel.RefresherTraining, + RankName = addOrUpdateModel.RankName, + Timepoint = addOrUpdateModel.Timepoint, + TimepointIn24H = addOrUpdateModel.TimepointIn24H, + TimepointIn48H = addOrUpdateModel.TimepointIn48H, + Adjudication = addOrUpdateModel.Adjudication, + AdjudicationIn24H = addOrUpdateModel.AdjudicationIn24H, + AdjudicationIn48H = addOrUpdateModel.AdjudicationIn48H, + Global = addOrUpdateModel.Global, + Training = addOrUpdateModel.Training, + Downtime = addOrUpdateModel.Downtime + + }); + + + return ResponseOutput.Result(success); + } + } + + + [HttpDelete("{id:guid}")] + public async Task DeleteRankPrice(Guid id) + { + + if (await _reviewerPayInfoRepository.AnyAsync(t => t.RankId == id)) + { + return ResponseOutput.NotOk("This title has been used by reviewer payment information"); + } + + var success = await _rankPriceRepository.DeleteFromQueryAsync(t => t.Id == id); + + return ResponseOutput.Result(success); + } + + /// + /// 获取职称单价列表 + /// + [HttpPost] + public async Task> GetRankPriceList(RankPriceQueryDTO queryParam) + { + var rankPriceQueryable = _rankPriceRepository.ProjectTo(_mapper.ConfigurationProvider); + return await rankPriceQueryable.ToPagedListAsync(queryParam.PageIndex, queryParam.PageSize, "ShowOrder", queryParam.Asc); + + } + + + public async Task> GetRankDic() + { + var rankQueryable = _rankPriceRepository.ProjectTo(_mapper.ConfigurationProvider); + return await rankQueryable.ToListAsync(); + + } + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Financial/ReviewerPayInfoService.cs b/IRaCIS.Core.Application/Service/Financial/ReviewerPayInfoService.cs new file mode 100644 index 00000000..f95713b3 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/ReviewerPayInfoService.cs @@ -0,0 +1,147 @@ +using AutoMapper; +using IRaCIS.Core.Infrastructure.ExpressionExtend; +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using System.Linq.Expressions; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Mvc; +using Panda.DynamicWebApi.Attributes; + +namespace IRaCIS.Application.Services +{ + [ ApiExplorerSettings(GroupName = "Financial")] + public class ReviewerPayInfoService : BaseService, IReviewerPayInfoService + { + private readonly IRepository _doctorPayInfoRepository; + private readonly IRepository _doctorRepository; + private readonly IRepository _rankPriceRepository; + private readonly IRepository _hospitalRepository; + + + public ReviewerPayInfoService(IRepository doctorRepository, IRepository doctorPayInfoRepository, + IRepository rankPriceRepository, IRepository hospitalRepository, IMapper mapper) + { + _doctorPayInfoRepository = doctorPayInfoRepository; + _doctorRepository = doctorRepository; + _rankPriceRepository = rankPriceRepository; + _hospitalRepository = hospitalRepository; + + } + [NonDynamicMethod] + public async Task AddOrUpdateReviewerPayInfo(ReviewerPayInfoCommand addOrUpdateModel, Guid userId) + { + var success = false; + var doctorPayInfoExistedItem = await _doctorPayInfoRepository.FirstOrDefaultAsync(u => u.DoctorId == addOrUpdateModel.DoctorId); + if (doctorPayInfoExistedItem == null)//insert + { + var doctorPayInfo = _mapper.Map(addOrUpdateModel); + //doctorPayInfo.CreateTime = DateTime.Now; + //doctorPayInfo.CreateUserId = userId; + await _doctorPayInfoRepository.AddAsync(doctorPayInfo); + + } + else//update + { + await _doctorPayInfoRepository.UpdateAsync(_mapper.Map(addOrUpdateModel, doctorPayInfoExistedItem)); + + } + success = await _doctorPayInfoRepository.SaveChangesAsync(); + return ResponseOutput.Result(success); + } + + /// + /// 获取医生支付信息列表 + /// + [HttpPost] + public async Task> GetReviewerPayInfoList(DoctorPaymentInfoQueryDTO queryParam) + { + + + var doctorQueryable = from doctor in _doctorRepository.AsQueryable() + .WhereIf(queryParam.HospitalId != null, o => o.HospitalId == queryParam.HospitalId) + .WhereIf(!string.IsNullOrEmpty(queryParam.SearchName), + u => u.ChineseName.Contains(queryParam.SearchName)|| (u.LastName+ u.FirstName).Contains(queryParam.SearchName)) + join hospitalItem in _hospitalRepository.AsQueryable() on doctor.HospitalId equals hospitalItem.Id into gt + from hospital in gt.DefaultIfEmpty() + join trialPayInfo in _doctorPayInfoRepository.Where() + on doctor.Id equals trialPayInfo.DoctorId into payInfo + from doctorPayInfo in payInfo.DefaultIfEmpty() + join rankPrice in _rankPriceRepository.Where() + on doctorPayInfo.RankId equals rankPrice.Id into rankPriceInfo + from rankPrice in rankPriceInfo.DefaultIfEmpty() + select new DoctorPayInfoQueryListDTO + { + //Id = doctorPayInfo.Id, + DoctorId = doctor.Id, + Code = doctor.ReviewerCode, + LastName = doctor.LastName, + FirstName = doctor.FirstName, + ChineseName = doctor.ChineseName, + Phone = doctor.Phone, + DoctorNameInBank = doctorPayInfo.DoctorNameInBank, + IDCard = doctorPayInfo.IDCard, + BankCardNumber = doctorPayInfo.BankCardNumber, + BankName = doctorPayInfo.BankName, + RankId = doctorPayInfo.RankId, + RankName = rankPrice.RankName, + Additional = doctorPayInfo.Additional, + Hospital = hospital.HospitalName, + CreateTime = doctor.CreateTime + }; + + return await doctorQueryable.ToPagedListAsync(queryParam.PageIndex, queryParam.PageSize, "Code", queryParam.Asc); + + + } + + + /// + /// 根据医生Id获取支付信息 + /// + /// 医生Id + /// + [HttpGet("{doctorId:guid}")] + public async Task GetReviewerPayInfo(Guid doctorId) + { + var doctorQueryable = from doctor in _doctorRepository.Where(u => u.Id == doctorId) + join trialPayInfo in _doctorPayInfoRepository.Where() + on doctor.Id equals trialPayInfo.DoctorId into payInfo + from doctorPayInfo in payInfo.DefaultIfEmpty() + join rankPrice in _rankPriceRepository.Where() + on doctorPayInfo.RankId equals rankPrice.Id into rankPriceInfo + from rankPrice in rankPriceInfo.DefaultIfEmpty() + select new DoctorPayInfoQueryListDTO + { + //Id = doctorPayInfo.Id, + DoctorId = doctor.Id, + Code = doctor.ReviewerCode, + LastName = doctor.LastName, + FirstName = doctor.FirstName, + ChineseName = doctor.ChineseName, + Phone = doctor.Phone, + DoctorNameInBank = doctorPayInfo.DoctorNameInBank, + IDCard = doctorPayInfo.IDCard, + BankCardNumber = doctorPayInfo.BankCardNumber, + BankName = doctorPayInfo.BankName, + RankId = doctorPayInfo.RankId, + RankName = rankPrice.RankName, + Additional = doctorPayInfo.Additional, + CreateTime = doctor.CreateTime + }; + + return (await doctorQueryable.FirstOrDefaultAsync()).IfNullThrowException(); + } + + /// + /// 根据rankId 获取ReviewerId,用于当Rank的单价信息改变时,触发费用计算 + /// + /// + /// + public async Task> GetReviewerIdByRankId(Guid rankId) + { + return await _doctorPayInfoRepository.Where(u => u.RankId == rankId).Select(u => u.DoctorId).ToListAsync(); + } + } +} diff --git a/IRaCIS.Core.Application/Service/Financial/TrialPaymentPriceService.cs b/IRaCIS.Core.Application/Service/Financial/TrialPaymentPriceService.cs new file mode 100644 index 00000000..9cae7565 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/TrialPaymentPriceService.cs @@ -0,0 +1,189 @@ +using AutoMapper; +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Mvc; +using Panda.DynamicWebApi.Attributes; + +namespace IRaCIS.Application.Services +{ + [ApiExplorerSettings(GroupName = "Financial")] + public class TrialPaymentPriceService : BaseService, ITrialPaymentPriceService + { + private readonly IRepository _trialExtRepository; + private readonly IRepository _enrollRepository; + private readonly IRepository _doctorRepository; + private readonly IRepository _croRepository; + + + private readonly IRepository _trialRepository; + public TrialPaymentPriceService(IRepository trialRepository, IRepository trialExtRepository, + IRepository enrollRepository, + IRepository doctorRepository, + IRepository croCompanyRepository, IMapper mapper) + { + _trialExtRepository = trialExtRepository; + _croRepository = croCompanyRepository; + _enrollRepository = enrollRepository; + _doctorRepository = doctorRepository; + + _trialRepository = trialRepository; + } + + /// + /// 添加或更新项目支付价格信息 + /// + [NonDynamicMethod] + public async Task AddOrUpdateTrialPaymentPrice(TrialPaymentPriceCommand addOrUpdateModel) + { + + var trialExistedItem = await _trialExtRepository.FirstOrDefaultAsync(u => u.TrialId == addOrUpdateModel.TrialId); + if (trialExistedItem == null)//insert + { + var trialExt = _mapper.Map(addOrUpdateModel); + + await _trialExtRepository.AddAsync(trialExt); + + } + else//update + { + await _trialExtRepository.UpdateAsync(_mapper.Map(addOrUpdateModel, trialExistedItem)); + + } + var success = await _trialExtRepository.SaveChangesAsync(); + return ResponseOutput.Result(success); + } + + + [HttpPost] + public async Task UploadTrialSOW(TrialSOWPathDTO trialSowPath) + { + var trialPaymentPrice = await _trialExtRepository.FirstOrDefaultAsync(u => u.TrialId == trialSowPath.TrialId); + + if (trialPaymentPrice == null)//添加 + { + await _trialExtRepository.AddAsync(_mapper.Map(trialSowPath)); + } + else//更新 + { + await _trialExtRepository.UpdateAsync(_mapper.Map(trialSowPath, trialPaymentPrice)); + } + + var success = await _trialExtRepository.SaveChangesAsync(); + + return ResponseOutput.Result(success); + } + + + [HttpPost] + public async Task DeleteTrialSOW(DeleteSowPathDTO trialSowPath) + { + var success = await _trialExtRepository.UpdateFromQueryAsync(u => u.TrialId == trialSowPath.TrialId, s => new TrialPaymentPrice + { + SowPath = "", + SowName = "", + UpdateTime = DateTime.Now, + UpdateUserId = _userInfo.Id + }); + + return ResponseOutput.Result(success); + } + + /// + /// 获取项目支付价格信息列表 + /// + [HttpPost] + public async Task> GetTrialPaymentPriceList(TrialPaymentPriceQueryDTO queryParam) + { + #region hwt + //var trialQueryable = from trial in _trialRepository.AsQueryable() + // .WhereIf(queryParam.CroId != null, o => o.CROId == queryParam.CroId) + // .WhereIf(!string.IsNullOrEmpty(queryParam.KeyWord), o => o.TrialCode.Contains(queryParam.KeyWord) || o.Indication.Contains(queryParam.KeyWord)) + // join cro in _croRepository.AsQueryable() on trial.CROId equals cro.Id into CRO + // from croInfo in CRO.DefaultIfEmpty() + // join trialExt in _trialExtRepository.Where() + // on trial.Id equals trialExt.TrialId into trialInfo + // from trialExt in trialInfo.DefaultIfEmpty() + // select new TrialPaymentPriceDTO + // { + // //Id = trialExt.Id , + // IsNewTrial = trialExt.IsNewTrial, + // TrialId = trial.Id, + // TrialCode = trial.TrialCode, + // Cro = croInfo.CROName, + // Indication = trial.Indication, + // Expedited = trial.Expedited, + // TrialAdditional = trialExt.TrialAdditional, + // AdjustmentMultiple = trialExt.AdjustmentMultiple, + // SowName = trialExt.SowName, + // SowPath = trialExt.SowPath, + // CreateTime = trial.CreateTime, + // }; + + //var namelist = (from enroll in _enrollRepository.AsQueryable() + // join doctor in _doctorRepository.Where() on enroll.DoctorId equals doctor.Id + // select new DtoDoctorList() + // { + // TrialId = enroll.TrialId, + // Name = doctor.ChineseName + // }).ToList().GroupBy(x => new { x.TrialId }, + //(key, lst) => new DtoDoctorList + //{ + // TrialId = key.TrialId, + // Name = string.Join(',', lst.Select(x => x.Name)) + //}); + + //var returndata = trialQueryable.ToPagedList(queryParam.PageIndex, queryParam.PageSize, "CreateTime", queryParam.Asc); + + //returndata.CurrentPageData.ForEach(x => { + // x.DoctorsNames = namelist.Where(y => y.TrialId == x.TrialId).Select(y => y.Name).FirstOrDefault() ?? string.Empty; + //}); + //return returndata; + + #endregion + + #region byzhouhang 方式一 + + //var trialQueryable = _trialExtRepository.Where(t => t.Trial.IsDeleted == false) + // .WhereIf(queryParam.CroId != null, o => o.Trial.CROId == queryParam.CroId) + // .WhereIf(!string.IsNullOrEmpty(queryParam.KeyWord), o => o.Trial.TrialCode.Contains(queryParam.KeyWord) || o.Trial.Indication.Contains(queryParam.KeyWord)) + // .Select(trialExt => new TrialPaymentPriceDTO() + // { + // TrialCode = trialExt.Trial.TrialCode, + // Cro = trialExt.Trial.CRO.CROName, + // Indication = trialExt.Trial.Indication, + // Expedited = trialExt.Trial.Expedited, + + // TrialId = trialExt.TrialId, + // IsNewTrial = trialExt.IsNewTrial, + // SowName = trialExt.SowName, + // SowPath = trialExt.SowPath, + // TrialAdditional = trialExt.TrialAdditional, + // AdjustmentMultiple = trialExt.AdjustmentMultiple, + // CreateTime = trialExt.CreateTime, + // DoctorsNames = string.Join(',', trialExt.Trial.EnrollList.Select(t => t.Doctor.ChineseName)) + // }); + + //return trialQueryable.ToPagedList(queryParam.PageIndex, queryParam.PageSize, string.IsNullOrEmpty(queryParam.SortField) ? "CreateTime" : queryParam.SortField, queryParam.Asc); + + #endregion + + + #region byzhouhang 方式二 + + var trialQueryable2 = _trialExtRepository.Where(t => t.Trial.IsDeleted == false) + .WhereIf(queryParam.CroId != null, o => o.Trial.CROId == queryParam.CroId) + .WhereIf(!string.IsNullOrEmpty(queryParam.KeyWord), o => o.Trial.TrialCode.Contains(queryParam.KeyWord) || o.Trial.Indication.Contains(queryParam.KeyWord)) + .ProjectTo(_mapper.ConfigurationProvider); + + return await trialQueryable2.ToPagedListAsync(queryParam.PageIndex, queryParam.PageSize, string.IsNullOrEmpty(queryParam.SortField) ? "CreateTime" : queryParam.SortField, queryParam.Asc); + + #endregion + + + + } + + + } +} diff --git a/IRaCIS.Core.Application/Service/Financial/TrialRevenuesPriceService.cs b/IRaCIS.Core.Application/Service/Financial/TrialRevenuesPriceService.cs new file mode 100644 index 00000000..672bf755 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/TrialRevenuesPriceService.cs @@ -0,0 +1,152 @@ +using AutoMapper; +using IRaCIS.Core.Infrastructure.ExpressionExtend; +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using System.Linq.Expressions; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Mvc; +using Panda.DynamicWebApi.Attributes; + +namespace IRaCIS.Application.Services +{ + [ ApiExplorerSettings(GroupName = "Financial")] + public class TrialRevenuesPriceService : BaseService, ITrialRevenuesPriceService + { + private readonly IRepository _trialRepository; + private readonly IRepository _trialRevenuesPriceRepository; + private readonly IRepository _croRepository; + private readonly IRepository _dictionaryRepository; + private readonly IRepository _trialRevenuesPriceVerificationRepository; + + + public TrialRevenuesPriceService(IRepository trialRepository, IRepository trialCostRepository, IRepository croCompanyRepository, IRepository dictionaryRepository, IRepository trialRevenuesPriceVerificationRepository, IMapper mapper) + { + _trialRepository = trialRepository; + _trialRevenuesPriceRepository = trialCostRepository; + _croRepository = croCompanyRepository; + _dictionaryRepository = dictionaryRepository; + _trialRevenuesPriceVerificationRepository = trialRevenuesPriceVerificationRepository; + + } + + public async Task AddOrUpdateTrialRevenuesPrice(TrialRevenuesPriceDTO model) + { + var count = model.Timepoint + + model.TimepointIn24H + + model.TimepointIn48H + + model.Adjudication + + model.AdjudicationIn24H + + model.AdjudicationIn48H + + model.Downtime + + model.Global + + model.Training; + + if (count <= 0) + { + return ResponseOutput.NotOk("Please add meaningful data"); + } + var trialExistedItem = await _trialRevenuesPriceRepository.FirstOrDefaultAsync(u => u.TrialId == model.TrialId); + if (trialExistedItem == null)//insert + { + var trialCost = _mapper.Map(model); + await _trialRevenuesPriceRepository.AddAsync(trialCost); + var success = await _trialRevenuesPriceRepository.SaveChangesAsync(); + return ResponseOutput.Result(success, trialCost.Id.ToString()); + } + else//update + { + var trialRevenuesPrice = (await _trialRevenuesPriceRepository.AsQueryable().FirstOrDefaultAsync(u => u.TrialId == model.TrialId)).IfNullThrowException(); + + await _trialRevenuesPriceRepository.UpdateAsync(_mapper.Map(model, trialRevenuesPrice)); + + // 完善价格的 将对应的列设置为true 变更为有价格了 + + var aaa = await _trialRevenuesPriceVerificationRepository.UpdateFromQueryAsync(t => t.TrialId == model.TrialId, u => new TrialRevenuesPriceVerification() + { + //有价格 则设置为true 否则 该列不变 + Timepoint = model.Timepoint > 0 || u.Timepoint, + TimepointIn24H = model.TimepointIn24H > 0 || u.TimepointIn24H, + TimepointIn48H = model.TimepointIn48H > 0 || u.TimepointIn48H, + Adjudication = model.Adjudication > 0 || u.Adjudication, + AdjudicationIn24H = + model.AdjudicationIn24H > 0 || u.AdjudicationIn24H, + AdjudicationIn48H = + model.AdjudicationIn48H > 0 || u.AdjudicationIn48H, + Global = model.Global > 0 || u.Global, + Downtime = model.Downtime > 0 || u.Downtime, + Training = model.Training > 0 || u.Training, + RefresherTraining = model.RefresherTraining > 0 || u.RefresherTraining, + }); + + //删除所有有价格的记录 为true 表示有价格或者不需要价格 缺价格的为false + await _trialRevenuesPriceVerificationRepository.DeleteFromQueryAsync(t => t.TrialId == model.TrialId && + t.Timepoint&& + t.TimepointIn24H&& + t.TimepointIn48H && + t.Adjudication && + t.AdjudicationIn24H && + t.AdjudicationIn48H && + t.Global && + t.Training &&t.RefresherTraining); + + + + var success = await _trialRevenuesPriceRepository.SaveChangesAsync(); + return ResponseOutput.Result(success); + } + } + + [NonDynamicMethod] + public async Task DeleteTrialCost(Guid id) + { + return await _trialRevenuesPriceRepository.DeleteFromQueryAsync(u => u.Id == id); + } + + /// + /// 获取项目收入费用信息列表[New] + /// + [HttpPost] + public async Task> GetTrialRevenuesPriceList(TrialRevenuesPriceQueryDTO queryParam) + { + + var trialQueryable = from trial in _trialRepository.AsQueryable() + .Where(u => u.TrialCode.Contains(queryParam.KeyWord)|| u.Indication.Contains(queryParam.KeyWord)) + .WhereIf(queryParam.CroId != null, o => o.CROId == queryParam.CroId) + join cro in _croRepository.AsQueryable() on trial.CROId equals cro.Id into CRO + from croInfo in CRO.DefaultIfEmpty() + join dic in _dictionaryRepository.AsQueryable() on trial.ReviewModeId equals dic.Id into dict + from dic in dict.DefaultIfEmpty() + join trialCost in _trialRevenuesPriceRepository.AsQueryable() + on trial.Id equals trialCost.TrialId into trialInfo + from trialCostItem in trialInfo.DefaultIfEmpty() + select new TrialRevenuesPriceDetialDTO + { + Id = trialCostItem == null ? Guid.Empty : trialCostItem.Id, + TrialId = trial.Id, + TrialCode = trial.TrialCode, + Indication = trial.Indication, + Cro = croInfo == null ? string.Empty : croInfo.CROName, + ReviewMode = dic == null ? string.Empty : dic.Value, + Timepoint = trialCostItem == null ? 0 : trialCostItem.Timepoint, + TimepointIn24H = trialCostItem == null ? 0 : trialCostItem.TimepointIn24H, + TimepointIn48H = trialCostItem == null ? 0 : trialCostItem.TimepointIn48H, + Adjudication = trialCostItem == null ? 0 : trialCostItem.Adjudication, + AdjudicationIn24H = trialCostItem == null ? 0 : trialCostItem.AdjudicationIn24H, + AdjudicationIn48H = trialCostItem == null ? 0 : trialCostItem.AdjudicationIn48H, + Downtime = trialCostItem == null ? 0 : trialCostItem.Downtime, + Global = trialCostItem == null ? 0 : trialCostItem.Global, + Training = trialCostItem == null ? 0 : trialCostItem.Training, + RefresherTraining= trialCostItem == null ? 0 : trialCostItem.RefresherTraining, + Expedited = trial.Expedited + + }; + + return await trialQueryable.ToPagedListAsync(queryParam.PageIndex, queryParam.PageSize, "TrialCode", queryParam.Asc); + + } + + + } +} diff --git a/IRaCIS.Core.Application/Service/Financial/TrialRevenuesPriceVerificationService.cs b/IRaCIS.Core.Application/Service/Financial/TrialRevenuesPriceVerificationService.cs new file mode 100644 index 00000000..f197f8be --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/TrialRevenuesPriceVerificationService.cs @@ -0,0 +1,197 @@ +using IRaCIS.Application.Interfaces; +using IRaCIS.Core.Domain.Models; +using System.Linq.Expressions; +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Application.Contracts; + +namespace IRaCIS.Core.Application.Services +{ + /// + /// Financial---项目收入价格验证 + /// + [ ApiExplorerSettings(GroupName = "Financial")] + public class TrialRevenuesPriceVerificationService : BaseService, ITrialRevenuesPriceVerificationService + { + private readonly IRepository _trialRevenuesPriceVerificationRepository; + private readonly IRepository _trialRepository; + private readonly IRepository _doctorRepository; + private readonly IRepository _paymentRepository; + + public TrialRevenuesPriceVerificationService(IRepository trialRevenuesPriceVerificationRepository, + IRepository trialRepository,IRepository doctorRepository,IRepository paymentRepository) + { + _trialRevenuesPriceVerificationRepository = trialRevenuesPriceVerificationRepository; + _trialRepository = trialRepository; + _doctorRepository = doctorRepository; + _paymentRepository = paymentRepository; + } + [HttpPost] + public async Task GetAnalysisVerifyList(RevenusVerifyQueryDTO param) + { + AnalysisVerifyResultDTO result=new AnalysisVerifyResultDTO(); + result.RevenuesVerifyList = await GetRevenuesVerifyList(param); + + + var bDate = new DateTime(param.BeginDate.Year, param.BeginDate.Month, 1); + var eDate = new DateTime(param.EndDate.Year, param.EndDate.Month, 1); + eDate = eDate.AddMonths(1).AddSeconds(-1); + + Expression> paymentLambda = x => x.YearMonthDate >= bDate && x.YearMonthDate <= eDate&&!x.IsLock; + + var query = from payment in _paymentRepository.Where(paymentLambda) + join doctor in _doctorRepository.AsQueryable() on payment.DoctorId equals doctor.Id + select new AnalysisNeedLockDTO() + { + YearMonth = payment.YearMonth, + ReviewerCode = doctor.ReviewerCode, + ReviewerName = doctor.LastName + " / " + doctor.FirstName, + ReviewerNameCN = doctor.ChineseName + }; + + result.MonthVerifyResult = (await query.ToListAsync()).GroupBy(t => t.YearMonth).Select(g => new MonthlyResult + { + YearMonth = g.Key, + ReviewerNameList = g.Select(t => t.ReviewerName).ToList(), + ReviewerNameCNList = g.Select(t => t.ReviewerNameCN).ToList(), + ReviewerCodeList = g.Select(t => t.ReviewerCode).ToList() + + }).OrderBy(t=>t.YearMonth).ToList(); + + return result; + } + + + [HttpPost] + public async Task> GetRevenuesVerifyList(RevenusVerifyQueryDTO param) + { + var bDate = new DateTime(param.BeginDate.Year, param.BeginDate.Month, 1); + var eDate = new DateTime(param.EndDate.Year, param.EndDate.Month, 1); + Expression> trialRevenuesPriceVerificationLambda = x => x.WorkLoadDate >= bDate && x.WorkLoadDate <= eDate; + + var query = (from trialVerify in _trialRevenuesPriceVerificationRepository.Where(trialRevenuesPriceVerificationLambda) + join trail in _trialRepository.AsQueryable() on trialVerify.TrialId equals trail.Id + select new RevenusVerifyDTO + { + TrialCode = trail.TrialCode, + Timepoint = trialVerify.Timepoint, + TimepointIn24H = trialVerify.TimepointIn24H, + TimepointIn48H = trialVerify.TimepointIn48H, + Adjudication = trialVerify.Adjudication, + AdjudicationIn24H = trialVerify.AdjudicationIn24H, + AdjudicationIn48H = trialVerify.AdjudicationIn48H, + Global = trialVerify.Global, + Downtime = trialVerify.Downtime, + Training = trialVerify.Training + }).Distinct(); + + + return await query.ToListAsync(); + } + + + + //废弃 + [Obsolete] + public async Task> GetRevenuesVerifyResultList(RevenusVerifyQueryDTO param) + { + var bDate = new DateTime(param.BeginDate.Year, param.BeginDate.Month, 1); + var eDate = new DateTime(param.EndDate.Year, param.EndDate.Month, 1); + Expression> trialRevenuesPriceVerificationLambda = x => x.WorkLoadDate >= bDate && x.WorkLoadDate <= eDate; + + var query = (from trialVerify in _trialRevenuesPriceVerificationRepository.Where(trialRevenuesPriceVerificationLambda) + join trail in _trialRepository.AsQueryable() on trialVerify.TrialId equals trail.Id + select new RevenusVerifyDTO + { + TrialCode = trail.TrialCode, + Timepoint = trialVerify.Timepoint, + TimepointIn24H = trialVerify.TimepointIn24H, + TimepointIn48H = trialVerify.TimepointIn48H, + Adjudication = trialVerify.Adjudication, + AdjudicationIn24H = trialVerify.AdjudicationIn24H, + AdjudicationIn48H = trialVerify.AdjudicationIn48H, + Global = trialVerify.Global, + Downtime = trialVerify.Downtime, + Training = trialVerify.Training + }).Distinct(); + + + return await query.ToListAsync(); + #region 提示 old + //query = from trialVerify in _trialRevenuesPriceVerificationRepository.GetAll() + // join trail in _trialRepository.GetAll() on trialVerify.TrialId equals trail.Id + // join reviewer in _doctorRepository.GetAll() on trialVerify.ReviewerId equals reviewer.Id + // select new RevenusVerifyDTO() + // { + // ReviewerCode = reviewer.Code, + // TrialCode = trail.Code, + // YearMonth = trialVerify.YearMonth + // }; + + + + + + + + + + + ////0是Detail 1是按照项目 2是按照人 3按照月份 + //if (param.StatType == 0) + //{ + // query = from trialVerify in _trialRevenuesPriceVerificationRepository.GetAll() + // join trail in _trialRepository.GetAll() on trialVerify.TrialId equals trail.Id + // join reviewer in _doctorRepository.GetAll() on trialVerify.ReviewerId equals reviewer.Id + // select new RevenusVerifyDTO() + // { + // ReviewerCode = reviewer.Code, + // TrialCode = trail.Code, + // YearMonth = trialVerify.YearMonth + // }; + + //} + //else if (param.StatType == 1) + //{ + // query = (from trialVerify in _trialRevenuesPriceVerificationRepository.GetAll() + // join trail in _trialRepository.GetAll() on trialVerify.TrialId equals trail.Id + // select new RevenusVerifyDTO() + // { + // ReviewerCode = "", + // TrialCode = trail.Code, + // YearMonth = "" + // }).Distinct(); + //} + //else if (param.StatType == 2) + //{ + // query = (from trialVerify in _trialRevenuesPriceVerificationRepository.GetAll() + // join trail in _trialRepository.GetAll() on trialVerify.TrialId equals trail.Id + // join reviewer in _doctorRepository.GetAll() on trialVerify.ReviewerId equals reviewer.Id + // select new RevenusVerifyDTO() + // { + // ReviewerCode = reviewer.Code, + // TrialCode = trail.Code, + // YearMonth = "" + // }).Distinct(); + //} + //else + //{ + // query = from trialVerify in _trialRevenuesPriceVerificationRepository.GetAll() + // join trail in _trialRepository.GetAll() on trialVerify.TrialId equals trail.Id + // join reviewer in _doctorRepository.GetAll() on trialVerify.ReviewerId equals reviewer.Id + // select new RevenusVerifyDTO() + // { + // ReviewerCode = reviewer.Code, + // TrialCode = trail.Code, + // YearMonth = trialVerify.YearMonth + // }; + //} + + + #endregion + + + + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Financial/VolumeRewardService.cs b/IRaCIS.Core.Application/Service/Financial/VolumeRewardService.cs new file mode 100644 index 00000000..e72f3d43 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/VolumeRewardService.cs @@ -0,0 +1,62 @@ +using AutoMapper; +using AutoMapper.QueryableExtensions; +using IRaCIS.Core.Infrastructure.ExpressionExtend; +using IRaCIS.Application.Interfaces; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Mvc; +using Panda.DynamicWebApi.Attributes; + +namespace IRaCIS.Application.Services +{ + [ ApiExplorerSettings(GroupName = "Financial")] + public class VolumeRewardService : BaseService, IVolumeRewardService + { + private readonly IRepository _volumeRewardRepository; + + + public VolumeRewardService(IRepository volumeRewardRepository,IMapper mapper) + { + _volumeRewardRepository = volumeRewardRepository; + + } + /// + /// 批量添加或更新奖励费用单价 + /// + [NonDynamicMethod] + public async Task AddOrUpdateVolumeRewardPriceList(IEnumerable addOrUpdateModel) + { + await _volumeRewardRepository.DeleteFromQueryAsync(t => t.Id != Guid.Empty); + var temp = _mapper.Map>(addOrUpdateModel); + + await _volumeRewardRepository.AddRangeAsync(temp); + var success = await _volumeRewardRepository.SaveChangesAsync(); + + return ResponseOutput.Result(success); + } + + + /// + /// 获取所有奖励单价列表-用于计算时,一次性获取所有 + /// + [NonDynamicMethod] + public async Task> GetVolumeRewardPriceList() + { + return await _volumeRewardRepository.ProjectTo(_mapper.ConfigurationProvider).OrderBy(t => t.Min).ToListAsync(); + } + + /// + /// 分页获取奖励单价列表 + /// + [HttpPost] + public async Task> GetVolumeRewardPriceList(AwardPriceQueryDTO queryParam) + { + var awardPriceQueryable = _volumeRewardRepository.ProjectTo(_mapper.ConfigurationProvider); + + return await awardPriceQueryable.ToPagedListAsync(queryParam.PageIndex, queryParam.PageSize, "Min"); + + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Financial/_MapConfig.cs b/IRaCIS.Core.Application/Service/Financial/_MapConfig.cs new file mode 100644 index 00000000..16d342dd --- /dev/null +++ b/IRaCIS.Core.Application/Service/Financial/_MapConfig.cs @@ -0,0 +1,62 @@ +using AutoMapper; +using IRaCIS.Application.Contracts; +using IRaCIS.Application.Contracts.Pay; +using IRaCIS.Application.Interfaces; +using IRaCIS.Core.Domain.Models; + +namespace IRaCIS.Core.Application.Service +{ + public class FinancialConfig : Profile + { + public FinancialConfig() + { + CreateMap() + .ForMember(t => t.YearMonthDate, u => u.MapFrom(t => t.YearMonth)) + .ForMember(t => t.YearMonth, u => u.MapFrom(t => t.YearMonth.ToString("yyyy-MM"))); + + CreateMap(); + CreateMap(); + + CreateMap(); + + CreateMap(); + + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap(); + + CreateMap(); + + CreateMap(); + + + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap(); + + + CreateMap() + .ForMember(t => t.TrialCode, u => u.MapFrom(t => t.Trial.Code)) + .ForMember(t => t.ReviewMode, u => u.MapFrom(t => t.Trial.ReviewMode.Value)) + .ForMember(t => t.Cro, u => u.MapFrom(t => t.Trial.CRO.CROName)) + .ForMember(t => t.Indication, u => u.MapFrom(t => t.Trial.Indication)) + .ForMember(t => t.Expedited, u => u.MapFrom(t => t.Trial.Expedited)) + .ForMember(t => t.DoctorsNames, u => u.MapFrom(t => string.Join(',', t.Trial.EnrollList.Select(t => t.Doctor.ChineseName)))) + + + ; + + } + } + +} diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomArchiveResult.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomArchiveResult.cs new file mode 100644 index 00000000..1e2089d1 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomArchiveResult.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace IRaCIS.Core.Application.Contracts.Dicom.DTO +{ + public sealed class DicomArchiveResult + { + public int ReceivedFileCount { get; set; } + public ICollection ErrorFiles { get; set; } + public ICollection ArchivedDicomStudies { get; set; } + + public Guid ReuploadNewStudyId { get; set; } = Guid.Empty; + + public DicomArchiveResult() + { + ReceivedFileCount = 0; + ErrorFiles = new List(); + ArchivedDicomStudies = new List(); + } + } +} diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomInstanceModel.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomInstanceModel.cs new file mode 100644 index 00000000..ff4135eb --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomInstanceModel.cs @@ -0,0 +1,58 @@ +namespace IRaCIS.Core.Application.Contracts +{ + public class DicomInstanceDTO + { + public Guid Id { get; set;} + public Guid StudyId { get; set; } + public Guid SeriesId { get; set; } + public string StudyInstanceUid { get; set; } = string.Empty; + public string SeriesInstanceUid { get; set; } = string.Empty; + public string SopInstanceUid { get; set; } = string.Empty; + public int InstanceNumber { get; set; } + public DateTime InstanceTime { get; set; } + public bool CPIStatus { get; set; } + public int ImageRows { get; set; } + public int ImageColumns { get; set; } + public int SliceLocation { get; set; } + + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public Guid SubjectId { get; set; } + public Guid SubjectVisitId { get; set; } + + //public Guid CreateUserId { get; set; } + //public DateTime CreateTime { get; set; } + //public Guid UpdateUserId { get; set; } + //public DateTime UpdateTime { get; set; } + } + + + public class DicomTrialSiteSubjectInfo + { + public string TrialSiteCode { get; set; } = string.Empty; + public string SiteCode { get; set; } = string.Empty; + public string SiteName { get; set; } = string.Empty; + public string SubjectCode { get; set; } = string.Empty; + public int? SubjectAge { get; set; } + public string SubjectSex { get; set; } = string.Empty; + public string TrialCode { get; set; } = string.Empty; + public string ResearchProgramNo { get; set; } = string.Empty; + public string TrialIndication { get; set; } = string.Empty; + + + + public decimal VisitNum { get; set; } + public string SVUPDES { get; set; } = string.Empty; + public string VisitName { get; set; } = string.Empty; + public string Sponsor { get; set; } = string.Empty; + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public Guid SubjectId { get; set; } + public Guid SubjectVisitId { get; set; } + + //public string SubjectName => LastName + " / " + FirstName; + //public string FirstName { get; set; } = string.Empty; + //public string LastName { get; set; } = string.Empty; + //public bool IsDoubleReview { get; set; } + } +} diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomSeriesModel.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomSeriesModel.cs new file mode 100644 index 00000000..66ebca99 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomSeriesModel.cs @@ -0,0 +1,34 @@ +namespace IRaCIS.Core.Application.Contracts.Dicom.DTO +{ + public class DicomSeriesDTO + { + public Guid Id { get; set; } + public Guid StudyId { get; set; } + public string StudyInstanceUid { get; set; } = String.Empty; + public string SeriesInstanceUid { get; set; } = String.Empty; + public int SeriesNumber { get; set; } + public DateTime SeriesTime { get; set; } + public string Modality { get; set; } = String.Empty; + public string Description { get; set; }=String.Empty; + public int InstanceCount { get; set; } + public string SliceThickness { get; set; } = String.Empty; + + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public Guid SubjectId { get; set; } + public Guid SubjectVisitId { get; set; } + + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + + public List InstanceList { get; set; } = new List(); + } + + public class DicomSeriesWithLabelDTO : DicomSeriesDTO + { + public bool HasLabel { get; set; } = false; + public bool KeySeries { get; set; } = false; + } +} diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomStudyModel.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomStudyModel.cs new file mode 100644 index 00000000..088f2ef1 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomStudyModel.cs @@ -0,0 +1,91 @@ +namespace IRaCIS.Core.Application.Contracts +{ + public class DicomStudyBasicDTO + { + public Guid Id { get; set; } + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + + public Guid SubjectId { get; set; } + public Guid SubjectVisitId { get; set; } + + public string StudyCode { get; set; } = string.Empty; + + public DateTime StudyTime { get; set; } + } + + public class DicomStudyDTO + { + public Guid Id { get; set; } + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + + public Guid SubjectId { get; set; } + public Guid SubjectVisitId { get; set; } + + public string StudyCode { get; set; } = string.Empty; + + public int Status { get; set; } = 1; + + public string StudyInstanceUid { get; set; } = string.Empty; + public DateTime StudyTime { get; set; } + public string Modalities { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + public int SeriesCount { get; set; } = 0; + public int InstanceCount { get; set; } = 0; + + public bool SoftDelete { get; set; } = false; + + public string InstitutionName { get; set; } = string.Empty; + public string PatientId { get; set; } = string.Empty; + public string PatientName { get; set; } = string.Empty; + public string PatientAge { get; set; } = string.Empty; + public string PatientSex { get; set; } = string.Empty; + + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } + } + + public class RelationVisitDTO + { + public string VisitName { get; set; } = string.Empty; + public string TpCode { get; set; } = string.Empty; + public Guid StudyId { get; set; } + } + + public class RelationStudyDTO + { + public string VisitName { get; set; } = string.Empty; + public string StudyCode { get; set; } = string.Empty; + public Guid StudyId { get; set; } + public int SeriesCount { get; set; } + public string Modalities { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + } + + + public class UploadViewInitDto + { + public Guid SubjectVisitId { get; set; } + + public Guid SubjectId { get; set; } + + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + //public string SubjectName { get; set; } + + public string SubjectCode { get; set; } = string.Empty; + + public string TrialSiteCode { get; set; } = string.Empty; + + public decimal VisitNum { get; set; } + + public string VisitName { get; set; } = string.Empty; + + + + } +} diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/ImageLabelModel.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/ImageLabelModel.cs new file mode 100644 index 00000000..419304ed --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/ImageLabelModel.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace IRaCIS.Core.Application.Contracts.Dicom.DTO +{ + public class ImageLabelDTO + { + public Guid Id { get; set; } = Guid.Empty; + public string TpCode { get; set; } = string.Empty; + public Guid StudyId { get; set; } = Guid.Empty; + public Guid SeriesId { get; set; } = Guid.Empty; + public Guid InstanceId { get; set; } = Guid.Empty; + public string LabelValue { get; set; } = string.Empty; + } + public class ImageLabelInfo + { + public Guid StudyId { get; set; } = Guid.Empty; + public Guid SeriesId { get; set; } = Guid.Empty; + public Guid InstanceId { get; set; } = Guid.Empty; + public string LabelValue { get; set; } = string.Empty; + } + + public class ImageLabelCommand + { + public string TpCode { get; set; } = string.Empty; + public List ImageLabelList { get; set; } = new List(); + } +} diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/ImageShareModel.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/ImageShareModel.cs new file mode 100644 index 00000000..f5ce0524 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/ImageShareModel.cs @@ -0,0 +1,23 @@ +using System; + +namespace IRaCIS.Core.Application.Contracts.Dicom.DTO +{ + public class ImageShareCommand + { + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public Guid SubjectId { get; set; } + public Guid? StudyId { get; set; } + + public DateTime? ExpireTime { get; set; } + + public string Password { get; set; } = string.Empty; + } + + public class ResourceInfo + { + public Guid StudyId { get; set; } + + public string Token { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/StudyDTFModel.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/StudyDTFModel.cs new file mode 100644 index 00000000..385bf04d --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/StudyDTFModel.cs @@ -0,0 +1,30 @@ +namespace IRaCIS.Core.Application.Contracts.Dicom.DTO +{ + public class StudyDTFDTO + { + public Guid Id { get; set; } + public Guid StudyId { get; set; } + + public string FileName { get; set; } = string.Empty; + + public string Path { get; set; } = string.Empty; + public Guid CreateUserId { get; set; } + + public string UserName { get; set; } = string.Empty; + + public string FirstName { get; set; } = string.Empty; + + public string LastName { get; set; } = string.Empty; + public DateTime CreateTime { get; set; } + } + + public class StudyDTFAddOrUpdateCommand + { + public Guid? Id { get; set; } + public Guid StudyId { get; set; } + public string FileName { get; set; } = string.Empty; + public string Path { get; set; } = string.Empty; + + } + +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/StudyReviewerModel.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/StudyReviewerModel.cs new file mode 100644 index 00000000..ce32d3f0 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/StudyReviewerModel.cs @@ -0,0 +1,46 @@ +namespace IRaCIS.Core.Application.Contracts.Dicom.DTO +{ + public class StudyReviewerDTO + { + public Guid Id { get; set; } + public Guid StudyId { get; set; } + public Guid ReviewerId { get; set; } + + } + + public class StudyDistributeInfo + { + public Guid StudyId { get; set; } + public int Status { get; set; } + //public int ReviewerCount { get; set; } + } + + public class StudyReviewerCommand + { + public List StudyList { get; set; } = new List(); + //public List StudyIdList { get; set; } + public Guid ReviewerId { get; set; } + //public int WorkloadType { get; set; } + public Guid TrialId { get; set; } + //public bool IsDoubleReview { get; set; } + } + + public class StudyReviewerEditCommand + { + public Guid TrialId { get; set; } + public Guid StudyId { get; set; } + public bool IsDoubleReview { get; set; } + + public Guid? ReviewerId1 { get; set; } + public Guid? ReviewerId2 { get; set; } + public Guid? ReviewerIdForAD { get; set; } + } + + public class ReviewerDistributionDTO + { + public Guid ReviewerId { get; set; } + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public string ReviewerCode { get; set; } = string.Empty; + } +} diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/StudyStatusDetailModel.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/StudyStatusDetailModel.cs new file mode 100644 index 00000000..b8e56430 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/StudyStatusDetailModel.cs @@ -0,0 +1,27 @@ +using System; + +namespace IRaCIS.Core.Application.Contracts.Dicom.DTO +{ + public class StudyStatusDetailDTO + { + public Guid Id { get; set; } + public Guid StudyId { get; set; } + public int Status { get; set; } + public string OptUserName { get; set; }=string.Empty; + public DateTime OptTime { get; set; } + public string Note { get; set; } = string.Empty; + } + + public class StudyStatusDetailCommand + { + public Guid StudyId { get; set; } + + public int Status { get; set; } + + public DateTime? DeadlineTime { get; set; } + public string Note { get; set; } = string.Empty; + + //QA不通过的时候传递参数 + public string QAComment { get; set; } = string.Empty; + } +} diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/SystemAnonymizationViewModel.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/SystemAnonymizationViewModel.cs new file mode 100644 index 00000000..55da414b --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/SystemAnonymizationViewModel.cs @@ -0,0 +1,61 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-03-03 15:28:20 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using System; +using System.Collections.Generic; +namespace IRaCIS.Core.Application.ViewModel +{ + /// SystemAnonymizationView 列表视图模型 + public class SystemAnonymizationView: SystemAnonymizationAddOrEdit + { + + public Guid CreateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid UpdateUserId { get; set; } + + public DateTime CreateTime { get; set; } + + } + + ///SystemAnonymizationQuery 列表查询参数模型 + public class SystemAnonymizationQuery:PageInput + { + public string Group { get; set; } = string.Empty; + + public bool? IsAdd { get; set; } + + public string Element { get; set; } = string.Empty; + + public string TagDescription { get; set; } = string.Empty; + + public string ValueRepresentation { get; set; } = string.Empty; + + + + } + + /// SystemAnonymizationAddOrEdit 列表查询参数模型 + public class SystemAnonymizationAddOrEdit + { + public Guid? Id { get; set; } + public string Group { get; set; } = String.Empty; + public string Element { get; set; } = String.Empty; + public string TagDescription { get; set; } = String.Empty; + public string TagDescriptionCN { get; set; } = String.Empty; + public string ReplaceValue { get; set; } = String.Empty; + + public string ValueRepresentation { get; set; } = String.Empty; + + public bool IsAdd { get; set; } + + public bool IsEnable { get; set; } + + public bool IsFixed { get; set; } + } + + +} + + diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/UnionStudyViewDodel.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/UnionStudyViewDodel.cs new file mode 100644 index 00000000..e0c2d734 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/UnionStudyViewDodel.cs @@ -0,0 +1,109 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IRaCIS.Core.Application.Contracts +{ + + public class UnionStudyBaseModel + { + public Guid TrialId { get; set; } + + public Guid SiteId { get; set; } + + public Guid SubjectId { get; set; } + + public Guid SubjectVisitId { get; set; } + + + public string SubjectCode { get; set; } + + public string VisitName { get; set; } = string.Empty; + + public decimal VisitNum { get; set; } + + public string TrialSiteCode { get; set; } = string.Empty; + + public string TrialSiteAliasName { get; set; } = string.Empty; + + public string Uploader { get; set; } = string.Empty; + + public DateTime UploadTime { get; set; } + + public string StudyCode => IsDicom ? DicomStudyCode : "NST" + NoneDicomCode.ToString("D5"); + + + [JsonIgnore] + public string DicomStudyCode { get; set; } = string.Empty; + [JsonIgnore] + public int NoneDicomCode { get; set; } + + public bool IsDicom { get; set; } + } + + public class UnionStudyMonitorModel : UnionStudyBaseModel + { + public Guid StudyId { get; set; } + + + public DateTime UploadStartTime { get; set; } + + + public DateTime UploadFinishedTime { get; set; } + + + public decimal FileSize { get; set; } + + public string IP { get; set; } + + + public bool IsDicomReUpload { get; set; } + + public bool IsDicom { get; set; } + + public int FileCount { get; set; } + + } + + + public class UnionStudyViewModel:UnionStudyBaseModel + { + + public Guid Id { get; set; } + + + + public int? Count { get; set; } + + + public string Modalities { get; set; } = string.Empty; + + public string Bodypart { get; set; } = string.Empty; + + public DateTime? StudyTime { get; set; } + + + } + + + + public class StudyQuery:PageInput + { + [NotDefault] + public Guid TrialId { get; set; } + + public Guid? SubjectId { get; set; } + + public Guid? SiteId { get; set; } + public Guid? SubjectVisitId { get; set; } + + public string SubjectInfo { get; set; } = String.Empty; + + public string VisitPlanInfo { get; set; } = String.Empty; + + } +} diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/DicomArchiveService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DicomArchiveService.cs new file mode 100644 index 00000000..5524b207 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/DicomArchiveService.cs @@ -0,0 +1,447 @@ +using Dicom; +using Dicom.Imaging.Codec; +using EasyCaching.Core; +using IRaCIS.Core.Application.Contracts.Dicom; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Share; +using Microsoft.Extensions.Hosting; +using System.Text; +using IRaCIS.Core.Application.Contracts; + +namespace IRaCIS.Core.Application.Services +{ + public class DicomArchiveService : IDicomArchiveService + { + private readonly IRepository _studyRepository; + private readonly IRepository _seriesRepository; + private readonly IRepository _instanceRepository; + + private readonly IEasyCachingProvider _provider; + private readonly DicomFileStoreHelper _dicomFileStoreHelper; + + + private static object lockCodeGenerate = new object(); + + public DicomArchiveService(IRepository studyRepository, + IRepository seriesRepository, + IRepository instanceRepository, + IHostEnvironment hostEnvironment, + DicomFileStoreHelper dicomFileStoreHelper, + IEasyCachingProvider provider) + { + _dicomFileStoreHelper = dicomFileStoreHelper; + + _studyRepository = studyRepository; + + _seriesRepository = seriesRepository; + + _instanceRepository = instanceRepository; + + _provider = provider; + + } + + public async Task DicomDBDataSaveChange() + { + var success = await _studyRepository.SaveChangesAsync(); + return success; + } + + public async Task ArchiveDicomStreamAsync(Stream dicomStream, + DicomTrialSiteSubjectInfo addtionalInfo, List seriesInstanceUidList, List instanceUidList) + { + + + DicomFile dicomFile = await DicomFile.OpenAsync(dicomStream, Encoding.Default); + + DicomDataset dataset = dicomFile.Dataset; + + //如果数据库存在该instance 记录 那么就不处理 + string sopInstanceUid = dataset.GetString(DicomTag.SOPInstanceUID); + string studyInstanceUid = dataset.GetString(DicomTag.StudyInstanceUID); + + if (instanceUidList.Any(t => t == sopInstanceUid)) + { + return IdentifierHelper.CreateGuid(studyInstanceUid, addtionalInfo.TrialId.ToString()); + } + + + var anonymize_AddFixedFiledList = _provider.Get>(StaticData.Anonymize_AddFixedFiled).Value; + var anonymize_AddIRCInfoFiled = _provider.Get>(StaticData.Anonymize_AddIRCInfoFiled).Value; + var anonymize_FixedField = _provider.Get>(StaticData.Anonymize_FixedField).Value; + var anonymize_IRCInfoField = _provider.Get>(StaticData.Anonymize_IRCInfoField).Value; + + + + foreach (var item in anonymize_AddFixedFiledList.Union(anonymize_FixedField)) + { + + var dicomTag = new DicomTag(Convert.ToUInt16(item.Group, 16), Convert.ToUInt16(item.Element, 16)); + + dataset.AddOrUpdate(dicomTag, item.ReplaceValue); + } + + foreach (var item in anonymize_AddIRCInfoFiled.Union(anonymize_IRCInfoField)) + { + + var dicomTag = new DicomTag(Convert.ToUInt16(item.Group, 16), Convert.ToUInt16(item.Element, 16)); + + if (dicomTag == DicomTag.ClinicalTrialProtocolID) + { + dataset.AddOrUpdate(dicomTag, addtionalInfo.TrialCode); + + } + if (dicomTag == DicomTag.ClinicalTrialSiteID) + { + dataset.AddOrUpdate(dicomTag, addtionalInfo.TrialSiteCode); + + } + if (dicomTag == DicomTag.ClinicalTrialSubjectID) + { + dataset.AddOrUpdate(dicomTag, addtionalInfo.SubjectCode); + + } + if (dicomTag == DicomTag.ClinicalTrialTimePointID) + { + dataset.AddOrUpdate(dicomTag, addtionalInfo.VisitNum.ToString()); + + } + if (dicomTag == DicomTag.PatientID) + { + dataset.AddOrUpdate(dicomTag, addtionalInfo.TrialCode+"_"+ addtionalInfo.SubjectCode); + + } + + } + + + #region 调整废弃 + ////按照配置文件 匿名化 + //foreach (var anonymizeItem in SystemConfig.AnonymizeTagList) + //{ + // if (anonymizeItem.Enable) + // { + // ushort group = Convert.ToUInt16(anonymizeItem.Group, 16); + // ushort element = Convert.ToUInt16(anonymizeItem.Element, 16); + // dataset.AddOrUpdate(new DicomTag(group, element), anonymizeItem.ReplaceValue); + // } + //} + + + //if (AppSettings.AddClinicalInfo) //是否需要写入临床信息 + //{ + // //Dicom 文件中写入临床信息 + // dataset.AddOrUpdate(DicomTag.ClinicalTrialProtocolID, addtionalInfo.TrialCode); //Trial + // dataset.AddOrUpdate(DicomTag.ClinicalTrialProtocolName, addtionalInfo.TrialIndication); //indication + // dataset.AddOrUpdate(DicomTag.ClinicalTrialSponsorName, addtionalInfo.Sponsor);//sponsor + // dataset.AddOrUpdate(DicomTag.ClinicalTrialSiteID, addtionalInfo.SiteCode); //SiteId + // dataset.AddOrUpdate(DicomTag.ClinicalTrialSiteName, addtionalInfo.SiteName);//SiteName + // dataset.AddOrUpdate(DicomTag.ClinicalTrialSubjectID, addtionalInfo.SubjectCode + " " + addtionalInfo.SubjectSex);//SubjectId + // dataset.AddOrUpdate(DicomTag.ClinicalTrialTimePointID, addtionalInfo.VisitNum.ToString()); // TimePoint + // dataset.AddOrUpdate(DicomTag.ClinicalTrialTimePointDescription, addtionalInfo.VisitName + " " + addtionalInfo.SVUPDES); + //} + + //DicomStudy dicomStudy = null; + //DicomSeries dicomSeries = null; + //DicomInstance dicomInstance = null; + + //lock (lockTest) + //{ + // dicomStudy = CreateDicomStudy(dataset, addtionalInfo, out bool isStudyNeedAdd); + // dicomSeries = CreateDicomSeries(dataset, dicomStudy, out bool isSeriesNeedAdd); + // dicomInstance = CreateDicomInstance(dataset, dicomStudy, dicomSeries); + + // if (isStudyNeedAdd) _studyRepository.Add(dicomStudy); + // if (isSeriesNeedAdd) _seriesRepository.Add(dicomSeries); + // _instanceRepository.Add(dicomInstance); + //} + #endregion + + + + + + DicomStudy dicomStudy = CreateDicomStudy(dataset, addtionalInfo, out bool isStudyNeedAdd); + DicomSeries dicomSeries = CreateDicomSeries(dataset, dicomStudy, out bool isSeriesNeedAdd); + DicomInstance dicomInstance = CreateDicomInstance(dataset, dicomStudy, dicomSeries); + + if (isStudyNeedAdd) await _studyRepository.AddAsync(dicomStudy); + if (isSeriesNeedAdd) await _seriesRepository.AddAsync(dicomSeries); + await _instanceRepository.AddAsync(dicomInstance); + + + + string filePath = _dicomFileStoreHelper.CreateInstanceFilePath(dicomStudy, dicomSeries.Id, dicomInstance.Id); + + var samplesPerPixel = dataset.GetSingleValueOrDefault(DicomTag.SamplesPerPixel, string.Empty); + var photometricInterpretation = dataset.GetSingleValueOrDefault(DicomTag.PhotometricInterpretation, string.Empty); + if (samplesPerPixel == "1" && (photometricInterpretation.ToUpper() == "MONOCHROME2" || photometricInterpretation.ToUpper() == "MONOCHROME1"))//MONOCHROME2 + { + if (dataset.InternalTransferSyntax.IsEncapsulated) + { + await dicomFile.SaveAsync(filePath); + } + else + { + await dicomFile.Clone(DicomTransferSyntax.JPEGLSLossless).SaveAsync(filePath); + } + } + else + { + if (dataset.InternalTransferSyntax.IsEncapsulated) await dicomFile.SaveAsync(filePath); + else await dicomFile.Clone(DicomTransferSyntax.RLELossless).SaveAsync(filePath); //RLELossless + } + return dicomInstance.StudyId; + } + + + private DicomStudy CreateDicomStudy(DicomDataset dataset, DicomTrialSiteSubjectInfo addtionalInfo, out bool isStudyNeedAdd) + { + + string studyInstanceUid = dataset.GetString(DicomTag.StudyInstanceUID); + Guid studyId = IdentifierHelper.CreateGuid(studyInstanceUid, addtionalInfo.TrialId.ToString()); + + + + // 每个线程都查询数据库最大的,和缓存中最大的,取最大值为基数生成Code + //var id = Thread.CurrentThread.ManagedThreadId.ToString("00"); + + //虽然每个文件都会进来,但是只要查询过,就会跟踪,不会再次查询数据库 这里线程并发会有问题,得加锁,不然生成Code 出错 + DicomStudy dicomStudy = _studyRepository.ImageFind(studyId, typeof(DicomStudy)); + + if (dicomStudy != null) + { + isStudyNeedAdd = false; + return dicomStudy; + } + + //_logger.LogWarning($"Thread {id} ,studyUid{studyInstanceUid}, 生成StudyId:{studyId}"); + + isStudyNeedAdd = true; + + dicomStudy = new DicomStudy + { + Id = studyId, + StudyInstanceUid = studyInstanceUid, + StudyTime = dataset.GetSingleValueOrDefault(DicomTag.StudyDate, DateTime.Now).Add(dataset.GetSingleValueOrDefault(DicomTag.StudyTime, DateTime.Now).TimeOfDay),//dataset.GetDateTime(DicomTag.StudyDate, DicomTag.StudyTime), + Modalities = dataset.GetSingleValueOrDefault(DicomTag.Modality, string.Empty), + Description = dataset.GetSingleValueOrDefault(DicomTag.StudyDescription, string.Empty), + InstitutionName = dataset.GetSingleValueOrDefault(DicomTag.InstitutionName, string.Empty), + PatientId = dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty), + PatientName = dataset.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty), + PatientAge = dataset.GetSingleValueOrDefault(DicomTag.PatientAge, string.Empty), + PatientSex = dataset.GetSingleValueOrDefault(DicomTag.PatientSex, string.Empty), + BodyPartExamined = dataset.GetSingleValueOrDefault(DicomTag.BodyPartExamined, string.Empty), + + StudyId = dataset.GetSingleValueOrDefault(DicomTag.StudyID, string.Empty), + AccessionNumber = dataset.GetSingleValueOrDefault(DicomTag.AccessionNumber, string.Empty), + + //需要特殊处理 + PatientBirthDate = dataset.GetSingleValueOrDefault(DicomTag.PatientBirthDate, string.Empty), + + + AcquisitionTime = dataset.GetSingleValueOrDefault(DicomTag.AcquisitionTime, string.Empty), + AcquisitionNumber = dataset.GetSingleValueOrDefault(DicomTag.AcquisitionNumber, string.Empty), + TriggerTime = dataset.GetSingleValueOrDefault(DicomTag.TriggerTime, string.Empty), + + + SiteId = addtionalInfo.SiteId, + TrialId = addtionalInfo.TrialId, + SubjectId = addtionalInfo.SubjectId, + SubjectVisitId = addtionalInfo.SubjectVisitId, + //IsDoubleReview = addtionalInfo.IsDoubleReview, + SeriesCount = 0, + InstanceCount = 0 + }; + + if (dicomStudy.PatientBirthDate.Length == 8) + { + dicomStudy.PatientBirthDate = $"{dicomStudy.PatientBirthDate[0]}{dicomStudy.PatientBirthDate[1]}{dicomStudy.PatientBirthDate[2]}{dicomStudy.PatientBirthDate[3]}-{dicomStudy.PatientBirthDate[4]}{dicomStudy.PatientBirthDate[5]}-{dicomStudy.PatientBirthDate[6]}{dicomStudy.PatientBirthDate[7]}"; + } + + lock (lockCodeGenerate) + { + + //查询数据库获取最大的Code 没有记录则为0 + var dbStudyCodeIntMax = _studyRepository.Where(s => s.TrialId == addtionalInfo.TrialId).Select(t => t.Code).DefaultIfEmpty().Max(); + + //获取缓存中的值 并发的时候,需要记录,已被占用的值 这样其他线程在此占用的最大的值上递增 + var cacheMaxCodeInt = _provider.Get($"{addtionalInfo.TrialId }_{ StaticData.StudyMaxCode}").Value; + + int currentNextCodeInt = cacheMaxCodeInt > dbStudyCodeIntMax ? cacheMaxCodeInt + 1 : dbStudyCodeIntMax + 1; + + dicomStudy.Code = currentNextCodeInt; + + dicomStudy.StudyCode = "ST" + currentNextCodeInt.ToString("D5"); + + _provider.Set($"{addtionalInfo.TrialId }_{ StaticData.StudyMaxCode}", dicomStudy.Code, TimeSpan.FromMinutes(30)); + + } + + #region Setting Code old + + //var studyCode = _studyRepository.Where(s => s.TrialId == addtionalInfo.TrialId).Select(t => t.StudyCode).OrderByDescending(c => c).FirstOrDefault(); + + //var cacheMaxCode = _provider.Get($"{addtionalInfo.TrialId }_{ StaticData.StudyMaxCode}").Value; + + //if (studyCode == null && string.IsNullOrEmpty(cacheMaxCode)) + //{ + // dicomStudy.StudyCode = "ST" + 1.ToString().PadLeft(5, '0'); + + // _logger.LogWarning($"Thread {id} DB:{studyCode} 生成{ dicomStudy.StudyCode}"); + //} + //else + //{ + // int dbNum = 0; + + // int cacheNum = 0; + + // if (studyCode != null) + // { + // int.TryParse(studyCode.Substring(studyCode.Length - 5, 5), out dbNum); + // } + + // if (!string.IsNullOrEmpty(cacheMaxCode)) + // { + // int.TryParse(cacheMaxCode.Substring(cacheMaxCode.Length - 5, 5), out cacheNum); + // } + + // dbNum = cacheNum > dbNum ? cacheNum : dbNum; + + + // dicomStudy.StudyCode = "ST" + (++dbNum).ToString().PadLeft(5, '0'); + + // _logger.LogWarning($" Thread {id} DB:{studyCode} cache:{cacheNum} 生成{ dicomStudy.StudyCode}"); + //} + + //_provider.Set($"{addtionalInfo.TrialId }_{ StaticData.StudyMaxCode}", dicomStudy.StudyCode, TimeSpan.FromMinutes(30)); + + + #endregion + + + + return dicomStudy; + + + + + + } + + private DicomSeries CreateDicomSeries(DicomDataset dataset, DicomStudy dicomStudy, out bool isSeriesNeedAdd) + { + string seriesInstanceUid = dataset.GetString(DicomTag.SeriesInstanceUID); + Guid seriesId = IdentifierHelper.CreateGuid(dicomStudy.StudyInstanceUid, seriesInstanceUid, dicomStudy.TrialId.ToString()); + + //有几个序列会查询几次 + + DicomSeries dicomSeries = _seriesRepository.ImageFind(seriesId, typeof(DicomSeries)); + + if (dicomSeries != null) + { + isSeriesNeedAdd = false; + return dicomSeries; + } + else + { + //var id = Thread.CurrentThread.ManagedThreadId.ToString("00"); + + isSeriesNeedAdd = true; + dicomSeries = new DicomSeries + { + Id = seriesId, + StudyId = dicomStudy.Id, + + StudyInstanceUid = dicomStudy.StudyInstanceUid, + SeriesInstanceUid = seriesInstanceUid, + SeriesNumber = dataset.GetSingleValueOrDefault(DicomTag.SeriesNumber, 1), + SeriesTime = dataset.GetSingleValueOrDefault(DicomTag.SeriesDate, DateTime.Now).Add(dataset.GetSingleValueOrDefault(DicomTag.SeriesTime, DateTime.Now).TimeOfDay), // dataset.GetDateTime(DicomTag.SeriesDate, DicomTag.SeriesTime), + Modality = dataset.GetSingleValueOrDefault(DicomTag.Modality, string.Empty), + Description = dataset.GetSingleValueOrDefault(DicomTag.SeriesDescription, string.Empty), + SliceThickness = dataset.GetSingleValueOrDefault(DicomTag.SliceThickness, string.Empty), + + ImagePositionPatient = dataset.GetSingleValueOrDefault(DicomTag.ImagePositionPatient, string.Empty), + ImageOrientationPatient = dataset.GetSingleValueOrDefault(DicomTag.ImageOrientationPatient, string.Empty), + BodyPartExamined = dataset.GetSingleValueOrDefault(DicomTag.BodyPartExamined, string.Empty), + SequenceName = dataset.GetSingleValueOrDefault(DicomTag.SequenceName, string.Empty), + ProtocolName = dataset.GetSingleValueOrDefault(DicomTag.ProtocolName, string.Empty), + ImagerPixelSpacing = dataset.GetSingleValueOrDefault(DicomTag.ImagerPixelSpacing, string.Empty), + + AcquisitionTime = dataset.GetSingleValueOrDefault(DicomTag.ImagerPixelSpacing, string.Empty), + AcquisitionNumber = dataset.GetSingleValueOrDefault(DicomTag.ImagerPixelSpacing, string.Empty), + TriggerTime = dataset.GetSingleValueOrDefault(DicomTag.ImagerPixelSpacing, string.Empty), + + SiteId = dicomStudy.SiteId, + TrialId = dicomStudy.TrialId, + SubjectId = dicomStudy.SubjectId, + SubjectVisitId = dicomStudy.SubjectVisitId, + + InstanceCount = 0 + }; + + ++dicomStudy.SeriesCount; + + + //_logger.LogWarning($"线程:{id},sericeId:{seriesId},count:{SeriesDic.Keys.Count}"); + + return dicomSeries; + } + + + } + + private DicomInstance CreateDicomInstance(DicomDataset dataset, DicomStudy dicomStudy, DicomSeries dicomSeries) + { + string sopInstanceUid = dataset.GetString(DicomTag.SOPInstanceUID); + Guid instanceId = IdentifierHelper.CreateGuid(dicomStudy.StudyInstanceUid, dicomSeries.SeriesInstanceUid, sopInstanceUid, dicomStudy.TrialId.ToString()); + + DicomInstance dicomInstance = new DicomInstance + { + Id = instanceId, + StudyId = dicomStudy.Id, + SeriesId = dicomSeries.Id, + + SiteId = dicomStudy.SiteId, + TrialId = dicomStudy.TrialId, + SubjectId = dicomStudy.SubjectId, + SubjectVisitId = dicomStudy.SubjectVisitId, + + StudyInstanceUid = dicomStudy.StudyInstanceUid, + SeriesInstanceUid = dicomSeries.SeriesInstanceUid, + SopInstanceUid = sopInstanceUid, + InstanceNumber = dataset.GetSingleValueOrDefault(DicomTag.InstanceNumber, 1), + InstanceTime = dataset.GetSingleValueOrDefault(DicomTag.ContentDate, DateTime.Now).Add(dataset.GetSingleValueOrDefault(DicomTag.ContentTime, DateTime.Now).TimeOfDay), + //dataset.GetSingleValueOrDefault(DicomTag.ContentDate,DateTime.Now);//, DicomTag.ContentTime) + CPIStatus = false, + ImageRows = dataset.GetSingleValueOrDefault(DicomTag.Rows, 0), + ImageColumns = dataset.GetSingleValueOrDefault(DicomTag.Columns, 0), + SliceLocation = dataset.GetSingleValueOrDefault(DicomTag.SliceLocation, 0), + + SliceThickness = dataset.GetSingleValueOrDefault(DicomTag.SliceThickness, string.Empty), + NumberOfFrames = dataset.GetSingleValueOrDefault(DicomTag.NumberOfFrames, 0), + PixelSpacing = dataset.GetSingleValueOrDefault(DicomTag.PixelSpacing, string.Empty), + ImagerPixelSpacing = dataset.GetSingleValueOrDefault(DicomTag.ImagerPixelSpacing, string.Empty), + FrameOfReferenceUID = dataset.GetSingleValueOrDefault(DicomTag.FrameOfReferenceUID, string.Empty), + WindowCenter = dataset.GetSingleValueOrDefault(DicomTag.WindowCenter, string.Empty), + WindowWidth = dataset.GetSingleValueOrDefault(DicomTag.WindowWidth, string.Empty), + }; + + ++dicomStudy.InstanceCount; + ++dicomSeries.InstanceCount; + + return dicomInstance; + + + + } + + + + + + + + + } +} diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/Helper/DicomFileStoreHelper.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/Helper/DicomFileStoreHelper.cs new file mode 100644 index 00000000..6e47afc7 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/Helper/DicomFileStoreHelper.cs @@ -0,0 +1,81 @@ +using Microsoft.Extensions.Hosting; +using System; +using System.IO; +using IRaCIS.Core.Domain.Models; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Core.Application +{ + public class DicomFileStoreHelper + { + private readonly IWebHostEnvironment _hostEnvironment; + private static string _fileStorePath = string.Empty; + + private readonly ILogger _logger; + + public DicomFileStoreHelper(IWebHostEnvironment hostEnvironment, ILogger logger) + { + _logger = logger; + _hostEnvironment = hostEnvironment; + var rootPath = Directory.GetParent(_hostEnvironment.ContentRootPath.TrimEnd('\\')).IfNullThrowException().FullName; + + //上传根路径 + _fileStorePath = Path.Combine(rootPath, StaticData.TrialDataFolder); + + } + + public string GetInstanceFilePath(DicomStudy dicomStudy, Guid seriesId, string instanceId) + { + return Path.Combine(_fileStorePath, dicomStudy.TrialId.ToString(), + dicomStudy.SiteId.ToString(), dicomStudy.SubjectId.ToString(), dicomStudy.SubjectVisitId.ToString(), StaticData.DicomFolder, dicomStudy.Id.ToString(), instanceId.ToString() + ".dcm"); + } + + + public string CreateInstanceFilePath(DicomStudy dicomStudy, Guid seriesId, Guid instanceId) + { + + //加入访视层级 和Data + var path = Path.Combine(_fileStorePath, dicomStudy.TrialId.ToString(), + dicomStudy.SiteId.ToString(), dicomStudy.SubjectId.ToString(), dicomStudy.SubjectVisitId.ToString(), StaticData.DicomFolder, dicomStudy.Id.ToString()); + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + return Path.Combine(path, instanceId.ToString() + ".dcm"); + } + + + public string GetSubjectVisitPath(Guid trialId,Guid siteId,Guid subjectId, Guid subjectVisitId) + { + return Path.Combine(_fileStorePath, trialId.ToString(), + siteId.ToString(), subjectId.ToString(), subjectVisitId.ToString(), StaticData.DicomFolder); + } + + //public static string CreateInstanceFilePath(DicomStudy dicomStudy, Guid seriesId, Guid instanceId) + //{ + // string path = Path.Combine(_fileStorePath, "Dicom", dicomStudy.CreateTime.Year.ToString(), dicomStudy.TrialId.ToString(), + // dicomStudy.SiteId.ToString(), dicomStudy.SubjectId.ToString(), dicomStudy.Id.ToString()); + // if (!Directory.Exists(path)) Directory.CreateDirectory(path); + + // return Path.Combine(path, instanceId.ToString() + ".dcm"); + //} + + //public static string GetInstanceFilePath(DicomStudy dicomStudy, Guid seriesId, Guid instanceId) + //{ + // return Path.Combine(_fileStorePath, "Dicom", dicomStudy.CreateTime.Year.ToString(), dicomStudy.TrialId.ToString(), + // dicomStudy.SiteId.ToString(), dicomStudy.SubjectId.ToString(), dicomStudy.Id.ToString(), instanceId.ToString() + ".dcm"); + //} + + + //public static void RemoveStudyDirectory(DicomStudy dicomStudy) + //{ + // string path = Path.Combine(_fileStorePath, "Dicom", dicomStudy.CreateTime.Year.ToString(), dicomStudy.TrialId.ToString(), + // dicomStudy.SiteId.ToString(), dicomStudy.SubjectId.ToString(), dicomStudy.Id.ToString()); + // if (Directory.Exists(path)) Directory.Delete(path, true); + //} + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/Helper/DicomRenderingHelper.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/Helper/DicomRenderingHelper.cs new file mode 100644 index 00000000..811a0fa3 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/Helper/DicomRenderingHelper.cs @@ -0,0 +1,37 @@ +using FellowOakDicom.Imaging; +using SixLabors.ImageSharp.Formats.Jpeg; + +namespace IRaCIS.Core.Application.Dicom +{ + public static class DicomRenderingHelper + { + public static Stream RenderPreviewJpeg(string filePath) + { + string jpegPath = filePath + ".preview.jpg"; + + if (!File.Exists(jpegPath)) + { + using (Stream stream = new FileStream(jpegPath, FileMode.Create)) + { + DicomImage image = new DicomImage(filePath); + //image.ShowOverlays = false; + //image.Scale = Math.Min(Math.Min(128.0 / image.Width, 128.0 / image.Height), 1.0); + //image.RenderImage().AsClonedBitmap().Save(stream, ImageFormat.Jpeg); + + var sharpimage = image.RenderImage().AsSharpImage(); + + sharpimage.Save(stream, new JpegEncoder()); + + } + } + + return new FileStream(jpegPath, FileMode.Open); + } + + public static void RemovePreviewJpeg(string filePath) + { + string jpegPath = filePath + ".preview.jpg"; + if (File.Exists(jpegPath)) File.Delete(jpegPath); + } + } +} diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/Helper/IdentifierHelper.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/Helper/IdentifierHelper.cs new file mode 100644 index 00000000..a5c82f6d --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/Helper/IdentifierHelper.cs @@ -0,0 +1,24 @@ + +using System.Security.Cryptography; +using System.Text; + +namespace IRaCIS.Core.Application.Services +{ + static class IdentifierHelper + { + //private static MD5 md5 = new MD5CryptoServiceProvider + // + private static MD5 md5 = MD5.Create(); + + private static object lockObj =new object(); + + public static Guid CreateGuid(params string[] parts) + { + lock (lockObj) + { + return new Guid(md5.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(parts)))); + } + + } + } +} diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/ImageShareService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/ImageShareService.cs new file mode 100644 index 00000000..13534c72 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/ImageShareService.cs @@ -0,0 +1,133 @@ +using IRaCIS.Core.Application.Contracts.Dicom; +using IRaCIS.Core.Application.Contracts.Dicom.DTO; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using IRaCIS.Core.Application.Auth; + +namespace IRaCIS.Core.Application.Services +{ + [AllowAnonymous, ApiExplorerSettings(GroupName = "Image")] + public class ImageShareService : BaseService, IImageShareService + { + + private readonly IRepository _imageShareRepository; + private readonly IRepository _studyRepository; + private readonly IConfiguration _configuration; + private readonly ITokenService _tokenService; + + public ImageShareService(IRepository imageShareRepository, IRepository studyRepository, IConfiguration configuration, ITokenService tokenService) + { + + _imageShareRepository = imageShareRepository; + _studyRepository = studyRepository; + _configuration = configuration; + _tokenService = tokenService; + } + + [HttpPost] + public async Task CreateImageShare(ImageShareCommand imageShareCommand) + { + if (imageShareCommand.StudyId == null) + { + + #region 上传不按照访视上传,基线没传,数据可能出错 + + //var subjectVisit1 = _subjectVisitRepository.FirstOrDefault(t => + // t.TrialId == imageShareCommand.TrialId && t.SubjectId == imageShareCommand.SubjectId && + // t.VisitNum == 1); + + //if (subjectVisit1 == null) + //{ + // return ResponseOutput.NotOk("当前无影像数据,无法分享!"); + //} + + //imageShareCommand.StudyId = + // _studyRepository.GetAll().First(t => t.SubjectVisitId == subjectVisit1.Id&&t.Status != (int)StudyStatus.Abandon).Id; + + #endregion + + var studyIds = await _studyRepository.AsQueryable() + .Where(t => t.TrialId == imageShareCommand.TrialId && t.SubjectId == imageShareCommand.SubjectId && + t.SiteId == imageShareCommand.SiteId) + .Select(u => u.Id).ToListAsync(); + + if (!studyIds.Any()) + { + return ResponseOutput.NotOk("There is no image in the current study and cannot be shared! "); + } + + imageShareCommand.StudyId = studyIds.First(); + + } + + //验证码 4位 + int verificationPassWord = new Random().Next(1000, 10000); + + imageShareCommand.Password = verificationPassWord.ToString(); + + //配置文件读取过期时间 + var days = int.Parse(_configuration.GetSection("imageShare:ExpireDays").Value); + + imageShareCommand.ExpireTime = DateTime.Now.AddDays(days); + + var imageShare = _mapper.Map(imageShareCommand); + + await _imageShareRepository.AddAsync(imageShare); + + var success = await _imageShareRepository.SaveChangesAsync(); + + return ResponseOutput.Result(success, new { ResourceId = imageShare.Id, Password = verificationPassWord.ToString() }); + + + } + + [HttpGet, Route("{resourceId:guid}/{password}")] + public async Task VerifyShareImage(Guid resourceId, string password) + { + var pWord = password.Trim(); + var imageShare = await _imageShareRepository.FirstOrDefaultAsync(t => t.Id == resourceId); + + if (imageShare == null) + { + return ResponseOutput.NotOk("The resource does not exist! "); + } + + if (pWord != imageShare.Password.Trim()) + { + return ResponseOutput.NotOk("Shared password error!"); + } + + if (DateTime.Now > imageShare.ExpireTime) + { + return ResponseOutput.NotOk("Resource sharing has expired!"); + } + + var resource = new ResourceInfo() + { + StudyId = imageShare.StudyId, + Token = _tokenService.GetToken(IRaCISClaims.Create(new UserBasicInfo() + { + Id = Guid.Empty, + IsReviewer = false, + IsAdmin = false, + RealName = "Share001", + UserName = "Share001", + Sex = 0, + //UserType = "ShareType", + UserTypeEnum = UserTypeEnum.ShareImage, + Code = "ShareCode001", + })) + }; + + + return ResponseOutput.Ok(resource); + + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/InstanceService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/InstanceService.cs new file mode 100644 index 00000000..29177124 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/InstanceService.cs @@ -0,0 +1,109 @@ +using IRaCIS.Core.Application.Contracts.Dicom; +using IRaCIS.Core.Application.Contracts.Dicom.DTO; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Models; +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Application.Dicom; +using Microsoft.AspNetCore.Authorization; +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Core.Application.Services +{ + [ApiExplorerSettings(GroupName = "Image")] + [AllowAnonymous] + public class InstanceService : BaseService, IInstanceService + { + private readonly IRepository _instanceRepository; + private readonly IRepository _studyRepository; + private readonly IRepository _keyInstanceRepository; + private readonly DicomFileStoreHelper _dicomFileStoreHelper; + public InstanceService(IRepository instanceRepository, IRepository studyRepository, + IRepository keyInstanceRepository, DicomFileStoreHelper dicomFileStoreHelper) + { + _dicomFileStoreHelper = dicomFileStoreHelper; + _instanceRepository = instanceRepository; + _studyRepository = studyRepository; + _keyInstanceRepository = keyInstanceRepository; + } + + /// 指定资源Id,获取Dicom序列所属的实例信息列表 + /// Dicom序列的Id + [HttpGet("{seriesId:guid}")] + public async Task> List(Guid seriesId) + { + return await _instanceRepository.Where(s => s.SeriesId == seriesId).OrderBy(s => s.InstanceNumber). + ThenBy(s => s.InstanceTime).ThenBy(s => s.CreateTime) + .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + } + + + /// 指定资源Id,获取Dicom序列所属的实例Id列表 + /// Dicom序列的Id + /// + /// + [HttpGet, Route("{seriesId:guid}/{tpCode?}/{key?}")] + public IEnumerable List(Guid seriesId, string tpCode, bool? key) + { + if (key != null && key.HasValue && key.Value) + { + return _keyInstanceRepository.Where(s => s.TpCode == tpCode).Select(t => t.InstanceId).Distinct(); + } + else + return _instanceRepository.Where(s => s.SeriesId == seriesId).OrderBy(s => s.InstanceNumber).Select(t => t.Id); + } + + [AllowAnonymous] + [HttpGet, Route("{instanceId:guid}")] + public async Task Preview(Guid instanceId) + { + var path = string.Empty; + + DicomInstance dicomInstance = await _instanceRepository.FirstOrDefaultAsync(s => s.Id == instanceId).IfNullThrowException(); + + DicomStudy dicomStudy = await _studyRepository.FirstOrDefaultAsync(s => s.Id == dicomInstance.StudyId).IfNullThrowException(); + + path = _dicomFileStoreHelper.GetInstanceFilePath(dicomStudy, dicomInstance.SeriesId, dicomInstance.Id.ToString()); + + + + using (var sw = DicomRenderingHelper.RenderPreviewJpeg(path)) + { + var bytes = new byte[sw.Length]; + sw.Read(bytes, 0, bytes.Length); + sw.Close(); + return new FileContentResult(bytes, "image/jpeg"); + } + } + + [AllowAnonymous] + [HttpGet, Route("{instanceId:guid}")] + public async Task Content(Guid instanceId) + { + var filePath = string.Empty; + DicomInstance dicomInstance = await _instanceRepository.FirstOrDefaultAsync(s => s.Id == instanceId).IfNullThrowException(); + + DicomStudy dicomStudy = await _studyRepository.FirstOrDefaultAsync(s => s.Id == dicomInstance.StudyId).IfNullThrowException(); + + + if (dicomInstance.Anonymize) //被匿名化 + { + filePath = _dicomFileStoreHelper.GetInstanceFilePath(dicomStudy, dicomInstance.SeriesId, dicomInstance.Id + ".Anonymize"); + } + + else filePath = _dicomFileStoreHelper.GetInstanceFilePath(dicomStudy, dicomInstance.SeriesId, dicomInstance.Id.ToString()); + + + + using (var sw = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + var bytes = new byte[sw.Length]; + sw.Read(bytes, 0, bytes.Length); + sw.Close(); + return new FileContentResult(bytes, "application/octet-stream"); + } + } + + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/Interface/IDicomArchiveService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/Interface/IDicomArchiveService.cs new file mode 100644 index 00000000..372561d8 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/Interface/IDicomArchiveService.cs @@ -0,0 +1,24 @@ +namespace IRaCIS.Core.Application.Contracts.Dicom +{ + public interface IDicomArchiveService + { + Task ArchiveDicomStreamAsync(Stream dicomStream, DicomTrialSiteSubjectInfo addtionalInfo, List seriesInstanceUidList, List instanceUidList); + //ICollection GetArchivedStudyList(List archivedStudyIds); + + Task DicomDBDataSaveChange(); + + + //[EasyCachingAble(Expiration = 6000)] + + + + //IEnumerable GetSeriesList(Guid studyId); + //IEnumerable GetSeriesWithLabelList(Guid studyId,string tpCode); + + ////[EasyCachingAble(Expiration = 6000)] + //string GetSeriesPreview(Guid seriesId); + + + + } +} diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/Interface/IImageShareService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/Interface/IImageShareService.cs new file mode 100644 index 00000000..ec47c6d2 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/Interface/IImageShareService.cs @@ -0,0 +1,10 @@ +using IRaCIS.Core.Application.Contracts.Dicom.DTO; + +namespace IRaCIS.Core.Application.Services +{ + public interface IImageShareService + { + Task CreateImageShare(ImageShareCommand imageShareCommand); + Task VerifyShareImage(Guid resourceId, string password); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/Interface/IInstanceService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/Interface/IInstanceService.cs new file mode 100644 index 00000000..24a3f630 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/Interface/IInstanceService.cs @@ -0,0 +1,13 @@ +using IRaCIS.Core.Application.Contracts; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Core.Application.Services +{ + public interface IInstanceService + { + Task Content(Guid instanceId); + Task> List(Guid seriesId); + IEnumerable List(Guid seriesId, string tpCode, bool? key); + Task Preview(Guid instanceId); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/Interface/IStudyDTFService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/Interface/IStudyDTFService.cs new file mode 100644 index 00000000..a29b14fa --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/Interface/IStudyDTFService.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using IRaCIS.Core.Application.Contracts.Dicom.DTO; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Core.Application.Contracts.Image +{ + public interface IStudyDTFService + { + IResponseOutput AddStudyDTF(StudyDTFAddOrUpdateCommand studyDtfAddOrUpdate); + IResponseOutput DeleteStudyDTF(Guid studyDTFId ); + + List GetStudyDtfdtos(Guid triaId, string studyInstanceUid); + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/Interface/IStudyService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/Interface/IStudyService.cs new file mode 100644 index 00000000..697f1768 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/Interface/IStudyService.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using IRaCIS.Core.Application.Contracts.Dicom.DTO; +using System.Threading.Tasks; +using IRaCIS.Core.Infrastructure.Extention; +using IRaCIS.Core.Application.Contracts; + +namespace IRaCIS.Application.Interfaces +{ + public interface IStudyService + { + + + + + //IResponseOutput ForwardStudy(Guid studyId); + + DicomTrialSiteSubjectInfo GetSaveToDicomInfo(Guid subjctVisitId); + + void GetHasUploadSeriesAndInstance(Guid studyId, ref List seriesInstanceUidList, ref List instanceUidList); + + + void UploadOrReUploadNeedTodo(ArchiveStudyCommand archiveStudyCommand, List archiveStudyIds, ref DicomArchiveResult reusult, StudyMonitor monitor); + + + + //PageOutput GetStudyList(StudyQueryDTO queryDto); + + + //Task DicomAnonymize(Guid studyId, string optuserName); + + + //List GetStudyStatusDetailList(Guid studyId); + + + //IEnumerable GetRelationVisitList(decimal visitNum, string tpCode); + //List GetSubjectVisitStudyList(Guid trialId, Guid siteId, Guid subjectId, Guid subjectVisitId); + + //PageOutput GetDistributeStudyList(StudyStatusQueryDTO studyStatusQueryDto); + + + //IEnumerable GetAllRelationStudyList(Guid studyId); + + /// 根据项目Id,医生Id,获取受试者访视点(不分页) + //IEnumerable GetStudyStatList(Guid trialId, Guid subjectId, Guid siteId); + + + //bool UpdateReUploadNewStudyCode(Guid studyId, Guid newStudyId); + + + //void ReUploadSameStudy(Guid subjectVisitId, Guid studyId); + + + //VerifyStudyUploadResult VerifyStudyAllowUpload(string studyInstanceUid, Guid trialId, Guid? studyId); + + //List< VerifyStudyUploadResult> VerifyStudyAllowUpload(VerifyUploadOrReupload verifyList); + + //void ClearStudyInstanceAndSeriseData(Guid studyId); + + //IResponseOutput DeleteStudy(Guid id); + + + + //Guid AddSubjectVisit(Guid subjectId, Guid siteId, Guid visitStageId, DateTime? SVSTDTC, DateTime? SVENDTC); + //void UpdateSubjectLatestInfo(Guid subjectVisitId); + + //void UpdateSubjectVisit(Guid subjectVisitId, Guid studyId); + + //bool DistributeStudy(StudyReviewerCommand studyReviewer); + + //IResponseOutput EditStudyReviewer(StudyReviewerEditCommand studyReviewerEditCommand); + + + //void UploadStudyDeal(Guid studyId); + /// + /// 更新Study 表中的Status,并且往 StudyStatusDetail详情表中插入一条状态变化记录 + /// + /// + + //List GetReviewerListByTrialId(Guid trialId); + + //IResponseOutput UpdateStudyStatus(StudyStatusDetailCommand studyStatusDetailDTO); + + //string GetStudyPreview(Guid studyId); + //DicomStudyDTO GetStudyItem(Guid studyId); + + ////bool SaveImageLabel(ImageLabelDTO label); + //bool SaveImageLabelList(ImageLabelCommand imageLabelCommand); + //IEnumerable GetImageLabel(string tpCode); + + //IResponseOutput DealNonDicomFile(Dictionary filePathDic, ArchiveStudyCommand archiveStudyCommand); + //bool ReUploadDifferentStudy(Guid subjectVisitId, Guid abandonStudyId, Guid newStudyId); + + + //List GetQANoticeList(Guid studyId); + + + + } +} diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/Interface/ISystemAnonymizationService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/Interface/ISystemAnonymizationService.cs new file mode 100644 index 00000000..3beb86ab --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/Interface/ISystemAnonymizationService.cs @@ -0,0 +1,24 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-03-03 15:28:27 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using IRaCIS.Core.Application.ViewModel; +namespace IRaCIS.Core.Application.Interfaces +{ + /// + /// ISystemAnonymizationService + /// + public interface ISystemAnonymizationService + { + + + Task> GetSystemAnonymizationList(SystemAnonymizationQuery querySystemAnonymization); + + Task AddOrUpdateSystemAnonymization(SystemAnonymizationAddOrEdit addOrEditSystemAnonymization); + + Task DeleteSystemAnonymization(Guid systemAnonymizationId); + + + } +} diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/SeriesService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/SeriesService.cs new file mode 100644 index 00000000..d676635e --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/SeriesService.cs @@ -0,0 +1,118 @@ +using IRaCIS.Core.Application.Contracts.Dicom; +using IRaCIS.Core.Application.Contracts.Dicom.DTO; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Models; +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Application.Dicom; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Authorization; + +namespace IRaCIS.Core.Application.Services +{ + [ApiExplorerSettings(GroupName = "Image")] + [AllowAnonymous] + public class SeriesService : BaseService + { + private readonly IRepository _imageLabelRepository; + private readonly IRepository _seriesRepository; + private readonly IRepository _instanceRepository; + private readonly IRepository _studyRepository; + private readonly IRepository _keyInstanceRepository; + private readonly DicomFileStoreHelper _dicomFileStoreHelper; + public SeriesService(IRepository instanceRepository, IRepository studyRepository, + IRepository keyInstanceRepository, DicomFileStoreHelper dicomFileStoreHelper, IRepository seriesRepository, IRepository imageLabelRepository) + { + _seriesRepository = seriesRepository; + _dicomFileStoreHelper = dicomFileStoreHelper; + _instanceRepository = instanceRepository; + _studyRepository = studyRepository; + _keyInstanceRepository = keyInstanceRepository; + _imageLabelRepository = imageLabelRepository; + } + + + + //医生读片那一块有耦合,关键序列 这里暂时留存 + /// 指定资源Id,获取Dicom检查所属序列信息列表 + /// Dicom检查的Id + /// + [HttpGet, Route("{studyId:guid}/{tpCode?}")] + public async Task>> List(Guid studyId, string? tpCode) + { + + var seriesList = await _seriesRepository.Where(s => s.StudyId == studyId).OrderBy(s => s.SeriesNumber). + ThenBy(s => s.SeriesTime).ThenBy(s => s.CreateTime) + .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + + bool hasKeyInstance = false; + var SeriesIdList = _imageLabelRepository.Where(u => u.TpCode == tpCode).Select(s => s.SeriesId).Distinct().ToList(); + var instanceIdList = _imageLabelRepository.Where(u => u.TpCode == tpCode).Select(s => s.InstanceId).Distinct().ToList(); + foreach (var item in seriesList) + { + if (SeriesIdList.Contains(item.Id)) + { + item.HasLabel = true; + hasKeyInstance = true; + } + else item.HasLabel = false; + } + if (hasKeyInstance) + { + seriesList.Add(new DicomSeriesWithLabelDTO + { + KeySeries = true, + Id = SeriesIdList[0], + InstanceCount = instanceIdList.Count, + HasLabel = true, + Modality = seriesList[0].Modality, + Description = "Key Series" + }); + } + + var idList = await _instanceRepository.Where(s => s.StudyId == studyId).ToListAsync();//.GroupBy(u => u.SeriesId); + + foreach (var item in seriesList) + { + if (item.KeySeries) + { + item.InstanceList = instanceIdList; + } + else + { + item.InstanceList = idList.Where(s => s.SeriesId == item.Id).OrderBy(t => t.InstanceNumber) + .ThenBy(s => s.InstanceTime).ThenBy(s => s.CreateTime).Select(u => u.Id).ToList(); + } + } + + return ResponseOutput.Ok(seriesList); + } + + + + + + /// 指定资源Id,渲染Dicom序列的Jpeg预览图像 + /// Dicom序列的Id + [HttpGet, Route("{seriesId:guid}")] + [AllowAnonymous] + public async Task Preview(Guid seriesId) + { + string path = string.Empty; + DicomInstance dicomInstance = await _instanceRepository.FirstOrDefaultAsync(s => s.SeriesId == seriesId).IfNullThrowException(); + + DicomStudy dicomStudy = await _studyRepository.FirstOrDefaultAsync(s => s.Id == dicomInstance.StudyId).IfNullThrowException(); + + path = _dicomFileStoreHelper.GetInstanceFilePath(dicomStudy, dicomInstance.SeriesId, dicomInstance.Id.ToString()); + + using (var sw = DicomRenderingHelper.RenderPreviewJpeg(path)) + { + var bytes = new byte[sw.Length]; + sw.Read(bytes, 0, bytes.Length); + sw.Close(); + return new FileContentResult(bytes, "image/jpeg"); + } + } + + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/StudyDTFService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/StudyDTFService.cs new file mode 100644 index 00000000..6bfc0a9c --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/StudyDTFService.cs @@ -0,0 +1,108 @@ +//using IRaCIS.Core.Application.Contracts.Dicom.DTO; +//using IRaCIS.Core.Application.Contracts.Image; +//using IRaCIS.Core.Infrastructure.Extention; +//using IRaCIS.Core.Infra.EFCore; +//using IRaCIS.Core.Domain.Models; +//using Microsoft.AspNetCore.Authorization; +//using Microsoft.AspNetCore.Mvc; + +//namespace IRaCIS.Core.Application.Services +//{ +// [ ApiExplorerSettings(GroupName = "Image")] +// public class StudyDTFService : BaseService, IStudyDTFService +// { +// private readonly IRepository _studyDtfRepository; + +// private readonly IRepository _userRepository; + + +// public StudyDTFService(IRepository studyDtfRepository, IRepository userRepository) +// { +// _studyDtfRepository = studyDtfRepository; + +// _userRepository = userRepository; +// } + +// public IResponseOutput AddStudyDTF(StudyDTFAddOrUpdateCommand studyDtfAddOrUpdate) +// { +// var studyDtf = _mapper.Map(studyDtfAddOrUpdate); + +// _studyDtfRepository.Add(studyDtf); + +// var success = _studyDtfRepository.SaveChanges(); + +// return ResponseOutput.Result(success); +// } + +// //[NonDynamicMethod] +// //[AllowAnonymous] +// //public async Task UpdateStudyDTF(StudyDTFAddOrUpdateCommand studyDtfAddOrUpdate) +// //{ +// // studyDtfAddOrUpdate.Id.IfNullThrowException(); +// // var studyDTF = await _studyDtfRepository.FindAsync( studyDtfAddOrUpdate.Id.Value ); + +// // studyDTF.Path = "9999"; + +// // //var success= await _efStudyDtfRepository.UpdateAsync(studyDTF,true); + +// // var success = await _studyDtfRepository.SaveChangesAsync(); + +// // //_efStudyDtfRepository.UpdateAsync(); + + +// // return ResponseOutput.Ok(success); +// //} + +// [HttpDelete("{trialId:guid}/{studyDTFId:guid}")] +// public IResponseOutput DeleteStudyDTF(Guid studyDTFId) +// { +// return ResponseOutput.Result(_studyDtfRepository.Delete(t => t.Id == studyDTFId)); +// } + +// [HttpGet("{trialId:guid}/{studyInstanceUid}")] +// public List GetStudyDtfdtos(Guid trialId, string studyInstanceUid) +// { +// var studyId = IdentifierHelper.CreateGuid(studyInstanceUid, trialId.ToString()); + +// var query = from studyDtf in _studyDtfRepository.Where(t => t.StudyId == studyId) +// join user in _userRepository.AsQueryable() on studyDtf.CreateUserId equals user.Id +// select new StudyDTFDTO() +// { +// CreateTime = studyDtf.CreateTime, +// CreateUserId = studyDtf.CreateUserId, +// FileName = studyDtf.FileName, +// Id = studyDtf.Id, +// FirstName = user.FirstName, +// LastName = user.LastName, +// UserName = user.UserName, +// Path = studyDtf.Path +// }; + +// return query.ToList(); +// } + + +// [HttpGet("{trialId:guid}/{studyId:guid}")] +// public List GetStudyDtfList(Guid trialId, Guid studyId) +// { +// //var studyId = IdentifierHelper.CreateGuid(studyInstanceUid, trialId.ToString()); + +// var query = from studyDtf in _studyDtfRepository.Where(t => t.StudyId == studyId) +// join user in _userRepository.AsQueryable() on studyDtf.CreateUserId equals user.Id +// select new StudyDTFDTO() +// { +// CreateTime = studyDtf.CreateTime, +// CreateUserId = studyDtf.CreateUserId, +// FileName = studyDtf.FileName, +// Id = studyDtf.Id, +// FirstName = user.FirstName, +// LastName = user.LastName, +// UserName = user.UserName, +// Path = studyDtf.Path +// }; + +// return query.ToList(); +// } + +// } +//} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/StudyListService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/StudyListService.cs new file mode 100644 index 00000000..a02d0ee2 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/StudyListService.cs @@ -0,0 +1,417 @@ +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Domain.Share; +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Application.Dicom; +using Microsoft.AspNetCore.Authorization; +using IRaCIS.Core.Application.Services; +using EasyCaching.Core; + +namespace IRaCIS.Core.Application.Service.ImageAndDoc +{ + [ApiExplorerSettings(GroupName = "Image")] + public class StudyService : BaseService + { + + private readonly DicomFileStoreHelper _dicomFileStoreHelper; + private readonly IEasyCachingProvider _provider; + + public StudyService(DicomFileStoreHelper dicomFileStoreHelper, IEasyCachingProvider provider) + { + _dicomFileStoreHelper = dicomFileStoreHelper; + _provider = provider; + } + + + [HttpPost] + public async Task> GetDicomAndNoneDicomStudyList(StudyQuery studyQuery) + { + + var dicomStudyQuery = _repository.Where(t => t.TrialId == studyQuery.TrialId) + .WhereIf(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ClinicalResearchCoordinator, t => t.TrialSite.CRCUserList.Any(t => t.UserId == _userInfo.Id)) + .WhereIf(!string.IsNullOrEmpty(studyQuery.VisitPlanInfo), studyQuery.VisitPlanInfo.Contains('.') ? t => t.SubjectVisit.VisitNum.ToString().Contains(".") : t => t.SubjectVisit.VisitNum == decimal.Parse(studyQuery.VisitPlanInfo)) + .WhereIf(!string.IsNullOrWhiteSpace(studyQuery.SubjectInfo), t => t.Subject.Code.Contains(studyQuery.SubjectInfo)) + .Select(t => new UnionStudyViewModel() + { + TrialId = t.TrialId, + SiteId = t.SiteId, + SubjectId = t.SubjectId, + SubjectVisitId = t.SubjectVisitId, + VisitName = t.SubjectVisit.VisitName, + VisitNum = t.SubjectVisit.VisitNum, + + IsDicom = true, + + SubjectCode = t.Subject.Code, + + Id = t.Id, + + Bodypart = t.BodyPartExamined, + + Modalities = t.Modalities, + + Count = t.SeriesCount, + + DicomStudyCode = t.StudyCode, + NoneDicomCode = 0, + + StudyTime = t.StudyTime, + + TrialSiteAliasName = t.TrialSite.TrialSiteAliasName, + + TrialSiteCode = t.TrialSite.TrialSiteCode, + + Uploader = t.Uploader.LastName + " / " + t.Uploader.LastName, + + UploadTime = t.CreateTime + + }); + + + //.ProjectTo(_mapper.ConfigurationProvider); + + + + var nodeDicomStudyQuery = _repository.Where(t => t.TrialId == studyQuery.TrialId) + .WhereIf(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ClinicalResearchCoordinator, t => t.TrialSite.CRCUserList.Any(t => t.UserId == _userInfo.Id)) + .WhereIf(!string.IsNullOrEmpty(studyQuery.VisitPlanInfo), studyQuery.VisitPlanInfo.Contains('.') ? t => t.SubjectVisit.VisitNum.ToString().Contains(".") : t => t.SubjectVisit.VisitNum == decimal.Parse(studyQuery.VisitPlanInfo)) + .WhereIf(!string.IsNullOrWhiteSpace(studyQuery.SubjectInfo), t => t.Subject.Code.Contains(studyQuery.SubjectInfo)) + + .Select(t => new UnionStudyViewModel() + { + TrialId = t.TrialId, + SiteId = t.SiteId, + SubjectId = t.SubjectId, + SubjectVisitId = t.SubjectVisitId, + VisitName = t.SubjectVisit.VisitName, + VisitNum = t.SubjectVisit.VisitNum, + + IsDicom = false, + + SubjectCode = t.Subject.Code, + + Id = t.Id, + + Bodypart = t.BodyPart, + + Modalities = t.Modality, + + Count = t.NoneDicomFileList.Count(), + + NoneDicomCode = t.Code, + DicomStudyCode = string.Empty, + + StudyTime = t.ImageDate, + + TrialSiteAliasName = t.TrialSite.TrialSiteAliasName, + + TrialSiteCode = t.TrialSite.TrialSiteCode, + + Uploader = t.CreateUser.LastName + " / " + t.CreateUser.LastName, + + UploadTime = t.CreateTime + + }); + + //.ProjectTo(_mapper.ConfigurationProvider); + + + var unionQuery = dicomStudyQuery.Union(nodeDicomStudyQuery) + .WhereIf(studyQuery.SubjectId != null, t => t.SubjectId == studyQuery.SubjectId) + .WhereIf(studyQuery.SubjectVisitId != null, t => t.SubjectId == studyQuery.SubjectVisitId) + .WhereIf(studyQuery.SiteId != null, t => t.SiteId == studyQuery.SiteId); + + return await unionQuery.ToPagedListAsync(studyQuery.PageIndex, studyQuery.PageSize, studyQuery.SortField, studyQuery.Asc); + } + + + [HttpPost] + public async Task> GetDicomAndNoneDicomStudyMonitorList(StudyQuery studyQuery) + { + + var dicomStudyQuery = _repository.Where(t => t.TrialId == studyQuery.TrialId && t.IsDicom) + .WhereIf(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ClinicalResearchCoordinator, t => t.TrialSite.CRCUserList.Any(t => t.UserId == _userInfo.Id)) + .WhereIf(!string.IsNullOrEmpty(studyQuery.VisitPlanInfo), studyQuery.VisitPlanInfo.Contains('.') ? t => t.SubjectVisit.VisitNum.ToString().Contains(".") : t => t.SubjectVisit.VisitNum == decimal.Parse(studyQuery.VisitPlanInfo)) + .WhereIf(!string.IsNullOrWhiteSpace(studyQuery.SubjectInfo), t => t.Subject.Code.Contains(studyQuery.SubjectInfo)) + .Select(t => new UnionStudyMonitorModel() + { + TrialId = t.TrialId, + SiteId = t.SiteId, + SubjectId = t.SubjectId, + SubjectVisitId = t.SubjectVisitId, + VisitName = t.SubjectVisit.VisitName, + VisitNum = t.SubjectVisit.VisitNum, + + + + SubjectCode = t.Subject.Code, + + + TrialSiteAliasName = t.TrialSite.TrialSiteAliasName, + + TrialSiteCode = t.TrialSite.TrialSiteCode, + + Uploader = t.Uploader.LastName + " / " + t.Uploader.LastName, + + UploadTime = t.CreateTime, + + + IP = t.IP, + FileCount = t.FileCount, + FileSize = t.FileSize, + UploadFinishedTime = t.UploadFinishedTime, + UploadStartTime = t.UploadStartTime, + IsDicomReUpload = t.IsDicomReUpload, + StudyId = t.Id, + IsDicom = t.IsDicom, + + DicomStudyCode = t.DicomStudy.StudyCode, + NoneDicomCode = 0, + + }); + + + //.ProjectTo(_mapper.ConfigurationProvider); + + + + var nodeDicomStudyQuery = _repository.Where(t => t.TrialId == studyQuery.TrialId && t.IsDicom == false) + .WhereIf(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ClinicalResearchCoordinator, t => t.TrialSite.CRCUserList.Any(t => t.UserId == _userInfo.Id)) + .WhereIf(!string.IsNullOrEmpty(studyQuery.VisitPlanInfo), studyQuery.VisitPlanInfo.Contains('.') ? t => t.SubjectVisit.VisitNum.ToString().Contains(".") : t => t.SubjectVisit.VisitNum == decimal.Parse(studyQuery.VisitPlanInfo)) + .WhereIf(!string.IsNullOrWhiteSpace(studyQuery.SubjectInfo), t => t.Subject.Code.Contains(studyQuery.SubjectInfo)) + + .Select(t => new UnionStudyMonitorModel() + { + + TrialId = t.TrialId, + SiteId = t.SiteId, + SubjectId = t.SubjectId, + SubjectVisitId = t.SubjectVisitId, + VisitName = t.SubjectVisit.VisitName, + VisitNum = t.SubjectVisit.VisitNum, + SubjectCode = t.Subject.Code, + TrialSiteAliasName = t.TrialSite.TrialSiteAliasName, + TrialSiteCode = t.TrialSite.TrialSiteCode, + Uploader = t.Uploader.LastName + " / " + t.Uploader.LastName, + UploadTime = t.CreateTime, + + IP = t.IP, + FileCount = t.FileCount, + FileSize = t.FileSize, + UploadFinishedTime = t.UploadFinishedTime, + UploadStartTime = t.UploadStartTime, + IsDicomReUpload = t.IsDicomReUpload, + StudyId = t.Id, + IsDicom = t.IsDicom, + + DicomStudyCode = string.Empty, + NoneDicomCode = t.NoneDicomStudy.Code, + + }); + + //.ProjectTo(_mapper.ConfigurationProvider); + + + var unionQuery = dicomStudyQuery.Union(nodeDicomStudyQuery) + .WhereIf(studyQuery.SubjectId != null, t => t.SubjectId == studyQuery.SubjectId) + .WhereIf(studyQuery.SubjectVisitId != null, t => t.SubjectId == studyQuery.SubjectVisitId) + .WhereIf(studyQuery.SiteId != null, t => t.SiteId == studyQuery.SiteId); + + return await unionQuery.ToPagedListAsync(studyQuery.PageIndex, studyQuery.PageSize, string.IsNullOrEmpty(studyQuery.SortField) ? "UploadTime" : studyQuery.SortField, studyQuery.Asc); + } + + + /// 指定资源Id,渲染Dicom检查的Jpeg预览图像 + /// Dicom检查的Id + [HttpGet("{studyId:guid}")] + public async Task Preview(Guid studyId) + { + string path = String.Empty; + + DicomInstance dicomInstance = await _repository.FirstOrDefaultAsync(s => s.StudyId == studyId); + + if (dicomInstance != null) + { + DicomStudy dicomStudy = await _repository.FirstOrDefaultAsync(s => s.Id == dicomInstance.StudyId); + if (dicomStudy != null) + { + path = _dicomFileStoreHelper.GetInstanceFilePath(dicomStudy, dicomInstance.SeriesId, dicomInstance.Id.ToString()); + } + } + + using (var sw = DicomRenderingHelper.RenderPreviewJpeg(path)) + { + var bytes = new byte[sw.Length]; + sw.Read(bytes, 0, bytes.Length); + sw.Close(); + return new FileContentResult(bytes, "image/jpeg"); + } + } + + + /// + /// 获取某个检查的关联检查列表(该受试者在这个想项目下的所有检查) + /// 点击检查检查列表中的一个检查获取对应的序列列表(调用之前的接口:/series/list/,根据StudyId,获取访视的序列列表) + /// + /// + [HttpGet("{subjectVisitId:guid}")] + [AllowAnonymous] + public IResponseOutput> GetAllRelationStudyList(Guid subjectVisitId) + { + #region 废弃 + //var studylist = _studyRepository.Where(u => u.SubjectVisitId == subjectVisitId && u.IsDeleted == false).Select(t => new { StudyId = t.Id, t.SubjectId, t.TrialId }).ToList(); + //var subjectId = studylist.FirstOrDefault().SubjectId; + //var trialId = studylist.FirstOrDefault().TrialId; + //var studyIds = studylist.Select(t => t.StudyId).ToList(); + + + //var query = from studyItem in _studyRepository.Where(u => u.SubjectId == subjectId + // && u.TrialId == trialId && u.IsDeleted == false && + // !studyIds.Contains(u.Id) + // /* && u.Status != (int)StudyStatus.Abandon*/) + // join visitItem in _subjectVisitRepository.AsQueryable() + // on studyItem.SubjectVisitId equals visitItem.Id + // select new RelationStudyDTO + // { + // StudyId = studyItem.Id, + // StudyCode = studyItem.StudyCode, + // VisitName = visitItem.VisitName, + // Modalities = studyItem.Modalities, + // Description = studyItem.Description, + // SeriesCount = studyItem.SeriesCount + // }; + #endregion + + var studyInfo = _repository.Where(u => u.SubjectVisitId == subjectVisitId).Select(t => new { t.SubjectId, t.TrialId }).FirstOrDefault().IfNullThrowException(); + + var query = _repository.Where(t => t.SubjectVisitId != subjectVisitId && t.TrialId == studyInfo.TrialId && t.SubjectId == studyInfo.SubjectId) + .ProjectTo(_mapper.ConfigurationProvider).ToList(); + + var list = query.OrderBy(u => u.VisitName).ThenBy(s => s.StudyCode).ToList(); + + return ResponseOutput.Ok(list); + } + + + /// 指定资源Id,获取Dicom检查信息 + /// Dicom检查的Id + [HttpGet, Route("{studyId:guid}")] + public IResponseOutput Item(Guid studyId) + { + return ResponseOutput.Ok(_mapper.Map(_repository.Where().FirstOrDefault(s => s.Id == studyId))); + } + + + + /// + /// 批量验证 检查是否可以上传 并告知原因 + /// + [HttpPost] + public IResponseOutput> VerifyStudyAllowUpload(VerifyUploadOrReupload verifyInfo) + { + var trialInfo = _repository.Where().FirstOrDefault(t => t.Id == verifyInfo.TrialId).IfNullThrowException(); + + var result = new List(); + + var visitList = _repository.Where(t => t.SubjectId == verifyInfo.SubjectId).Select(t => new { t.VisitNum, t.EarliestScanDate, t.LatestScanDate, t.Id }).ToList(); + + + verifyInfo.StudyInstanceUidList.ForEach(waitUploadItem => + { + + if (trialInfo.IsVerifyVisitImageDate) + { + + //小于当前访视 最近的最早拍片日期 + var before = visitList.Where(u => u.VisitNum < verifyInfo.VisitNum).Max(k => k.EarliestScanDate); + + if (before != null && before > waitUploadItem.StudyDate) + { + result.Add(new VerifyStudyUploadResult() { ErrorMesseage = "当前访视检查时间不能小于该访视之前检查的时间,请核对检查数据是否有误", StudyInstanceUid = waitUploadItem.StudyInstanceUid }); + return; + } + + //大于当前访视 最近的最晚拍片日期 + var after = visitList.Where(u => u.VisitNum > verifyInfo.VisitNum).Min(k => k.LatestScanDate); + + if (after != null && after < waitUploadItem.StudyDate) + { + result.Add(new VerifyStudyUploadResult() { ErrorMesseage = "当前访视检查时间不能大于该访视之后的检查时间,请核对检查数据是否有误", StudyInstanceUid = waitUploadItem.StudyInstanceUid }); + return; + } + } + + var temp = VerifyStudyUpload(waitUploadItem.StudyInstanceUid, verifyInfo.TrialId, verifyInfo.SubjectVisitId, verifyInfo.SubjectId); + + result.Add(temp); + }); + + return ResponseOutput.Ok(result); + } + + + private VerifyStudyUploadResult VerifyStudyUpload(string studyInstanceUid, Guid trialId, Guid currentSubjectVisitId, Guid SubjectId) + { + + var result = new VerifyStudyUploadResult(); + + if (_provider.Exists("StudyUid_" + studyInstanceUid)) + { + result.AllowUpload = false; + + result.AllowReUpload = false; + result.StudyInstanceUid = studyInstanceUid; + result.ErrorMesseage = "当前有人正在上传归档该检查!"; + + return result; + } + + if (_repository.Where(t => t.Id == SubjectId).Select(t => t.Status).FirstOrDefault() == SubjectStatus.EndOfVisit) + { + result.AllowUpload = false; + + result.AllowReUpload = false; + result.StudyInstanceUid = studyInstanceUid; + result.ErrorMesseage = "受试者访视结束,不允许上传!"; + + return result; + } + + Guid expectStudyId = IdentifierHelper.CreateGuid(studyInstanceUid.Trim(), trialId.ToString()); + + + + var verifyStudyInfo = _repository.Where(t => t.TrialId == trialId && t.Id == expectStudyId).ProjectTo(_mapper.ConfigurationProvider).FirstOrDefault(); + result.StudyInfo = verifyStudyInfo; + + + //数据库不存在该检查 允许上传 + if (verifyStudyInfo == null) + { + result.AllowUpload = true; + } + //数据库该项目有该检查 看是否支持重传 + else + { + //是同一个受试者 支持重传 + if (verifyStudyInfo.SubjectId == SubjectId && verifyStudyInfo.SubjectVisitId == currentSubjectVisitId) + { + result.AllowReUpload = true; + } + //不是同一个受试者 + else + { + //有默认值,其实不用写,这里为了好理解 + result.AllowUpload = false; + + result.AllowReUpload = false; + + result.ErrorMesseage = $"{ verifyStudyInfo.StudyTime }: This DICOM images have been uploaded and allocate to the {verifyStudyInfo.VisitName} of the subject {verifyStudyInfo.SubjectCode} (Study ID: {verifyStudyInfo.StudyCode}), which cannot continue to upload and assign it to others."; + } + } + result.StudyInstanceUid = studyInstanceUid; + return result; + } + + + } +} diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/StudyService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/StudyService.cs new file mode 100644 index 00000000..0890e9da --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/StudyService.cs @@ -0,0 +1,1062 @@ +using AutoMapper; +using IRaCIS.Application.Interfaces; +using IRaCIS.Core.Application.Contracts.Dicom.DTO; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Share; + +using IRaCIS.Core.Application.Contracts; + +namespace IRaCIS.Application.Services +{ + //[Intercept(typeof(QANoticeAOP))] + public class StudyService : IStudyService + { + private readonly IRepository _studyRepository; + + private readonly IRepository _subjectVisitRepository; + private readonly IRepository _dicomInstanceRepository; + private readonly IRepository _dicomSeriesRepository; + + private readonly IUserInfo _userInfo; + + private readonly IMapper _mapper; + + + private static string _fileStorePath = string.Empty; + + private readonly IRepository _dicomStudyMonitorRepository; + + + + + + public StudyService(IRepository studyRepository, + IRepository subjectVisitRepository, + IRepository dicomInstanceRepository, + IRepository dicomSeriesRepository, + + IUserInfo userInfo, + + IRepository dicomStudyMonitorRepository, + + IMapper mapper) + { + _dicomStudyMonitorRepository = dicomStudyMonitorRepository; + + + _userInfo = userInfo; + _studyRepository = studyRepository; + + _subjectVisitRepository = subjectVisitRepository; + _dicomInstanceRepository = dicomInstanceRepository; + _dicomSeriesRepository = dicomSeriesRepository; + _mapper = mapper; + } + + + + + + /// + /// 获取保存到Dicom文件中的信息 + /// + /// + /// + public DicomTrialSiteSubjectInfo GetSaveToDicomInfo(Guid subjectVisitId) + { + //6表连接 subject trial trialSite sponsor subjectVisit site + var info = _subjectVisitRepository.Where(t => t.Id == subjectVisitId).ProjectTo(_mapper.ConfigurationProvider).FirstOrDefault().IfNullThrowException(); + + return info; + } + + public void GetHasUploadSeriesAndInstance(Guid studyId, ref List seriesInstanceUidList, ref List sopInstanceUidList) + { + seriesInstanceUidList = _dicomSeriesRepository.Where(t => t.StudyId == studyId).Select(t => t.SeriesInstanceUid).ToList(); + + sopInstanceUidList = _dicomInstanceRepository.Where(t => t.StudyId == studyId).Select(t => t.SopInstanceUid).ToList(); + } + + public void UploadOrReUploadNeedTodo(ArchiveStudyCommand archiveStudyCommand, List archiveStudyIds, ref DicomArchiveResult result,StudyMonitor monitor) + { + + + var archivedStudyId = archiveStudyIds[0]; + + result.ArchivedDicomStudies = _studyRepository.Where(t => t.SubjectVisitId == archiveStudyCommand.SubjectVisitId) + .ProjectTo(_mapper.ConfigurationProvider).ToList(); + + #region 更新受试者访视信息 及访视状态为待提交 最早最晚拍片时间 访视已执行 + + var subjectVisit = _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == archiveStudyCommand.SubjectVisitId).Result; + + //正常情况下 上传或者重传(待提交之前 要改为待提交 ) QA后上传 不能改为待提交 + if (subjectVisit.SubmitState == SubmitStateEnum.None) + { + subjectVisit.SubmitState = SubmitStateEnum.ToSubmit; + } + + subjectVisit.VisitExecuted = VisitExecutedEnum.Executed; + + + + //处理拍片日期 + var svTime = _subjectVisitRepository.Where(t => t.Id == archiveStudyCommand.SubjectVisitId).Select(t => new + { + DicomStudyMinStudyTime = t.StudyList.Min(t => (DateTime?)t.StudyTime), + DicomStudyMaxStudyTime = t.StudyList.Max(t => (DateTime?)t.StudyTime), + NoneDicomStudyMinStudyTime = t.NoneDicomStudyList.Min(t => (DateTime?)t.ImageDate), + NoneDicomStudyMaxStudyTime = t.NoneDicomStudyList.Max(t => (DateTime?)t.ImageDate) + }).FirstOrDefault(); + + var minArray = new DateTime?[] { svTime.DicomStudyMinStudyTime, svTime.NoneDicomStudyMinStudyTime }; + var maxArray = new DateTime?[] { svTime.DicomStudyMaxStudyTime, svTime.NoneDicomStudyMaxStudyTime }; + + subjectVisit.EarliestScanDate = minArray.Min(); + subjectVisit.LatestScanDate = maxArray.Max(); + + #endregion + + + //按照访视维度,上传只存在重传同一份,和上传新的(不存在之前检查维度 重传同一份 和重传不同的) + + if (archiveStudyCommand.AbandonStudyId != null && archiveStudyCommand.AbandonStudyId == archivedStudyId) + { + result.ReuploadNewStudyId = archivedStudyId; + + + // 1、普通上传后重传 + // 2、QC后重传 + //_studyService.ReUploadSameStudy(archiveStudyCommand.SubjectVisitId, archivedDicomStudy.Id); + + }//上传 + else + { + //汇总 Series的Modality 更新到检查里面 检查状态 上传人 + var seriesModalityList = _dicomSeriesRepository.Where(t => t.StudyId == archivedStudyId).Select(u => u.Modality).Distinct(); + string ModaliyStr = string.Join('、', seriesModalityList.ToList()); + + var study = _studyRepository.FirstOrDefaultAsync(t => t.Id == archivedStudyId).Result.IfNullThrowException(); + study.Status = (int)StudyStatus.Uploaded; + study.Modalities = ModaliyStr; + + } + _ = _dicomStudyMonitorRepository.AddAsync(monitor).Result; + _ = _studyRepository.SaveChangesAsync().Result; + + + + + } + + + + + + //public virtual IResponseOutput ForwardStudy(Guid studyId) + //{ + + // var study = _studyRepository.FirstOrDefault(u => u.Id == studyId); + + + + // //数据库状态,其他人已经操作过了,此时提醒 + // if (study.Status != (int)StudyStatus.Anonymized) + // { + // ResponseOutput.NotOk($"当前界面Study:{study.StudyCode} 状态被其他人已变更,不允许该操作,请刷新界面"); + // } + + // _studyRepository.Update(t => t.Id == studyId, u => new DicomStudy() + // { + // Status = (int)StudyStatus.Forwarding + // }); + + // var subject = _subjectRepository.FirstOrDefault(u => u.Id == study.SubjectId); + // var trial = _trialRepository.FirstOrDefault(u => u.Id == study.TrialId); + // var subjectVisit = _subjectVisitRepository.FirstOrDefault(u => u.Id == study.SubjectVisitId); + // var targetPath = Path.Combine("/IMPORT-IMAGES", + // trial.TrialCode + "_" + subject.Code + "_" + subjectVisit.VisitName + "_" + study.StudyCode); + // var path = Path.Combine(_fileStorePath, study.CreateTime.Year.ToString(), study.TrialId.ToString(), + // study.SiteId.ToString(), study.SubjectId.ToString(), study.SubjectVisitId.ToString(), study.Id.ToString(), "Data"); + // try + // { + // // 主机及端口信息后面可以改到 配置文件 + // SessionOptions sessionOptions = new SessionOptions + // { + // Protocol = Protocol.Sftp, + // PortNumber = 8022, + // HostName = "CS-690-sftp.mint-imaging.com", + // UserName = "zdong", + // Password = "Everest@2021", + // //GiveUpSecurityAndAcceptAnySshHostKey = true, + // SshHostKeyFingerprint = @"ecdsa-sha2-nistp384 384 59gkjJ5lMwv3jsB8Wz2B35tBAIor5pSd8PcJYtoamPo=" + // }; + // using (Session session = new Session()) + // { + // session.Open(sessionOptions); + // if (!session.FileExists(targetPath)) + // { + // session.CreateDirectory(targetPath); + // } + + // var files = (new DirectoryInfo(path)).GetFiles(); + // foreach (var file in files) + // { + // if (file.Name.Contains("Anonymize") && file.Extension.Contains("dcm")) + // { + // string remoteFilePath = + // RemotePath.TranslateLocalPathToRemote(file.FullName, path, targetPath); + // var result = session.PutFiles(file.FullName, remoteFilePath, false); + // if (!result.IsSuccess) + // { + // return ResponseOutput.NotOk("Forward Failed"); + // } + // } + // } + + // //_studyStatusDetailRepository.Add(new StudyStatusDetail + // //{ + // // Status = (int)StudyStatus.Forwarded, + // // StudyId = studyId, + // // Note = string.Empty, + // // OptUserName = _userInfo.RealName, + // // OptTime = DateTime.Now + // //}); + + // study.Status = (int)StudyStatus.Forwarded; + // _studyRepository.Update(study); + + // //_workloadTPRepository.Add(new WorkloadTP + // //{ + // // SiteId = study.SiteId, + // // StudyId = study.Id, + // // SubjectId = study.SubjectId, + // // SubjectVisitId = study.SubjectVisitId, + // // Status = 0, + // // ReviewerId = Guid.Empty, + // // TrialId = study.TrialId, + // // UpdateTime = DateTime.Now, + // // TimepointCode = study.StudyCode + "_T01" + // //}); + // //if (study.IsDoubleReview) //双重阅片,则再添加一条,编号变为T02 + // //{ + // // _workloadTPRepository.Add(new WorkloadTP + // // { + // // SiteId = study.SiteId, + // // StudyId = study.Id, + // // SubjectId = study.SubjectId, + // // SubjectVisitId = study.SubjectVisitId, + // // Status = 0, + // // ReviewerId = Guid.Empty, + // // TrialId = study.TrialId, + // // UpdateTime = DateTime.Now, + // // TimepointCode = study.StudyCode + "_T02" + // // }); + // //} + + + // return ResponseOutput.Result(_studyRepository.SaveChanges()); + // } + // } + // catch (Exception e) + // { + // _studyRepository.Update(t => t.Id == studyId, u => new DicomStudy() + // { + // Status = (int)StudyStatus.ForwardFailed + // }); + // return ResponseOutput.NotOk("Forward Failed " + e.Message); + // } + //} + + + + + + + + + #region 暂存 读片 + + //public IEnumerable GetSeriesList(Guid studyId) + //{ + // return _dicomSeriesRepository.Where(s => s.StudyId == studyId).OrderBy(s => s.SeriesNumber). + // ThenBy(s => s.SeriesTime).ThenBy(s => s.CreateTime) + // .ProjectTo(_mapper.ConfigurationProvider); + //} + + + //public IEnumerable GetImageLabel(string tpCode) + //{ + // return _imageLabelRepository.Where(s => s.TpCode == tpCode) + // .ProjectTo(_mapper.ConfigurationProvider); + //} + + //public bool SaveImageLabelList(ImageLabelCommand imageLabelCommand) + //{ + // var success = _imageLabelRepository.Delete(u => u.TpCode == imageLabelCommand.TpCode); + // _keyInstanceRepository.Delete(u => u.TpCode == imageLabelCommand.TpCode); + // if (imageLabelCommand.ImageLabelList.Count == 0) + // { + // return true; + // } + // else + // { + + // foreach (var label in imageLabelCommand.ImageLabelList) + // { + // _imageLabelRepository.Add(new ImageLabel + // { + // TpCode = imageLabelCommand.TpCode, + // StudyId = label.StudyId, + // SeriesId = label.SeriesId, + // InstanceId = label.InstanceId, + // LabelValue = label.LabelValue + // }); + // _keyInstanceRepository.Add(new KeyInstance + // { + // TpCode = imageLabelCommand.TpCode, + // SeriesId = label.SeriesId, + // InstanceId = label.InstanceId, + // }); + // } + // success = _imageLabelRepository.SaveChanges(); + // return success; + // } + //} + + + #endregion + + + + + #region 废弃 + //public DicomStudyDTO GetStudyItem(Guid studyId) + //{ + // return _mapper.Map(_studyRepository.FirstOrDefault(s => s.Id == studyId)); + //} + //public virtual async Task DicomAnonymize(Guid studyId, string optUserName) + //{ + // var study = _studyRepository.FirstOrDefault(t => t.Id == studyId); + + // //数据库状态,其他人已经操作过了,此时提醒 + // if (study.Status != (int)StudyStatus.QAFinish) + // { + // ResponseOutput.NotOk($"当前界面Study:{study.StudyCode} 状态被其他人已变更,不允许该操作,请刷新界面"); + // } + + // var subject = _subjectRepository.Select(t => new { Id = t.Id, Code = t.Code }).FirstOrDefault(u => u.Id == study.SubjectId).IfNullThrowException(); + // var trial = _trialRepository.Select(t => new { Id = t.Id, Code = t.TrialCode }).FirstOrDefault(u => u.Id == study.TrialId).IfNullThrowException(); + + // study.Status = (int)StudyStatus.Anonymizing; + + + // // 查询受试者 信息 + // //string subjectCde = string.Empty; + // //string subjectSex = string.Empty; + + // //读取Dicom 文件,匿名化 + + // //按照配置文件 匿名化 + // var dicomSeries = _dicomSeriesRepository.Where(s => s.StudyId == studyId).ToList(); + // foreach (var seriesItem in dicomSeries) + // { + // var dicomInstances = _dicomInstanceRepository.Where(s => s.SeriesId == seriesItem.Id).ToList(); + + // foreach (var instanceItem in dicomInstances) + // { + // _dicomInstanceRepository.Attach(instanceItem); + + // try + // { + // string filePath = _dicomFileStoreHelper.CreateInstanceFilePath(study, seriesItem.Id, instanceItem.Id); + // DicomFile dicomFile = await DicomFile.OpenAsync(filePath, Encoding.Default); + + // #region 废弃 + + // //if (SystemConfig.AddClinicalInfo) //是否需要写入临床信息 + // //{ + // // dicomFile.Dataset.AddOrUpdate(DicomTag.ClinicalTrialSubjectID, subjectCde + " " + subjectSex);//SubjectId + // //} + // //DicomDataset dataset = dicomFile.Dataset; + + // #endregion + + // foreach (var anonymizeItem in AppSettings.AnonymizeTagList) + // { + // if (anonymizeItem.Enable) + // { + // ushort group = Convert.ToUInt16(anonymizeItem.Group, 16); + // ushort element = Convert.ToUInt16(anonymizeItem.Element, 16); + // dicomFile.Dataset.AddOrUpdate(new DicomTag(group, element), anonymizeItem.ReplaceValue); + // } + // } + + // dicomFile.Dataset.AddOrUpdate(DicomTag.PatientID, trial.Code + "_" + subject.Code); + + // //_dicomFileStoreHelper.CreateInstanceFilePath(study, seriesItem.Id, instanceItem.Id); + + // string path = Path.Combine(_fileStorePath, study.CreateTime.Year.ToString(), + // study.TrialId.ToString(), + // study.SiteId.ToString(), study.SubjectId.ToString(), study.SubjectVisitId.ToString(), study.Id.ToString(), "Data"); + + // //await dicomFile.SaveAsync(Path.Combine(path, instanceItem.Id.ToString() + ".dcm")); + // await dicomFile.SaveAsync( + // Path.Combine(path, instanceItem.Id.ToString() + ".Anonymize" + ".dcm")); + + // instanceItem.Anonymize = true; + + // //_dicomInstanceRepository.Update(instanceItem); + // } + // catch (Exception ex) + // { + + // study.Status = (int)StudyStatus.AnonymizeFailed; + + // _dicomInstanceRepository.SaveChanges(); + + // //这个地方返回没用 + // return ResponseOutput.NotOk(ex.Message); + // } + // } + + // } + + // study.Status = (int)StudyStatus.Anonymized; + + // var success = _dicomInstanceRepository.SaveChanges(); + + // return ResponseOutput.Result(success); + //} + //public string GetStudyPreview(Guid studyId) + //{ + // DicomInstance dicomInstance = _dicomInstanceRepository.FirstOrDefault(s => s.StudyId == studyId); + // if (dicomInstance != null) + // { + // DicomStudy dicomStudy = _studyRepository.FirstOrDefault(s => s.Id == dicomInstance.StudyId); + // if (dicomStudy != null) + // { + // return _dicomFileStoreHelper.GetInstanceFilePath(dicomStudy, dicomInstance.SeriesId, dicomInstance.Id.ToString()); + // } + // } + // return string.Empty; + //} + + //public IEnumerable GetAllRelationStudyList(Guid subjectVisitId) + //{ + // #region 废弃 + // //var studylist = _studyRepository.Where(u => u.SubjectVisitId == subjectVisitId && u.IsDeleted == false).Select(t => new { StudyId = t.Id, t.SubjectId, t.TrialId }).ToList(); + // //var subjectId = studylist.FirstOrDefault().SubjectId; + // //var trialId = studylist.FirstOrDefault().TrialId; + // //var studyIds = studylist.Select(t => t.StudyId).ToList(); + + + // //var query = from studyItem in _studyRepository.Where(u => u.SubjectId == subjectId + // // && u.TrialId == trialId && u.IsDeleted == false && + // // !studyIds.Contains(u.Id) + // // /* && u.Status != (int)StudyStatus.Abandon*/) + // // join visitItem in _subjectVisitRepository.AsQueryable() + // // on studyItem.SubjectVisitId equals visitItem.Id + // // select new RelationStudyDTO + // // { + // // StudyId = studyItem.Id, + // // StudyCode = studyItem.StudyCode, + // // VisitName = visitItem.VisitName, + // // Modalities = studyItem.Modalities, + // // Description = studyItem.Description, + // // SeriesCount = studyItem.SeriesCount + // // }; + // #endregion + + // var studyInfo = _studyRepository.Where(u => u.SubjectVisitId == subjectVisitId).Select(t => new { t.SubjectId, t.TrialId }).FirstOrDefault().IfNullThrowException(); + + // var query = _studyRepository.Where(t => t.SubjectVisitId != subjectVisitId && t.TrialId == studyInfo.TrialId && t.SubjectId == studyInfo.SubjectId) + // .ProjectTo(_mapper.ConfigurationProvider).ToList(); + + // return query.OrderBy(u => u.VisitName).ThenBy(s => s.StudyCode).ToList(); + //} + + + ///// + ///// 检查列表 + ///// + ///// + ///// + //public PageOutput GetStudyList(StudyQueryDTO queryDto) + //{ + // var query = _studyRepository.Where(x => x.TrialId == queryDto.TrialId) + // .WhereIf(queryDto.SubjectId != null, t => t.SubjectId == queryDto.SubjectId) + // .WhereIf(queryDto.SubjectVisitId != null, t => t.SubjectId == queryDto.SubjectVisitId) + // .WhereIf(!string.IsNullOrEmpty(queryDto.VisitPlanInfo), queryDto.VisitPlanInfo.Contains('.') ? t => t.SubjectVisit.VisitNum.ToString().Contains(".") : t => t.SubjectVisit.VisitNum == decimal.Parse(queryDto.VisitPlanInfo)) + // .WhereIf(queryDto.StudyTimeBegin != null, t => t.StudyTime >= queryDto.StudyTimeBegin) + // .WhereIf(queryDto.StudyTimeEnd != null, t => t.StudyTime <= queryDto.StudyTimeEnd) + // .WhereIf(queryDto.UpdateTimeBegin != null, t => t.StudyTime >= queryDto.UpdateTimeBegin) + // .WhereIf(queryDto.UpdateTimeEnd != null, t => t.StudyTime <= queryDto.UpdateTimeEnd) + // .WhereIf(queryDto.SiteId != null, t => t.SiteId == queryDto.SiteId) + // .WhereIf(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ClinicalResearchCoordinator, t => t.TrialSite.CRCUserList.Any(t => t.UserId == _userInfo.Id)) + // .WhereIf(!string.IsNullOrWhiteSpace(queryDto.SubjectInfo), t => t.Subject.Code.Contains(queryDto.SubjectInfo)) + // .ProjectTo(_mapper.ConfigurationProvider); + + + // var list = query.ToPagedList(queryDto.PageIndex, queryDto.PageSize, queryDto.SortField == string.Empty ? "StudyCode" : queryDto.SortField, queryDto.Asc); + + + // foreach (var item in list.CurrentPageData) + // { + // if (item.PatientBirthDate.Length == 8) + // { + // item.PatientBirthDate = + // $"{item.PatientBirthDate[0]}{item.PatientBirthDate[1]}{item.PatientBirthDate[2]}{item.PatientBirthDate[3]}-{item.PatientBirthDate[4]}{item.PatientBirthDate[5]}-{item.PatientBirthDate[6]}{item.PatientBirthDate[7]}"; + // } + // } + + // return list; + //} + + + + //public IResponseOutput DeleteStudy(Guid id) + //{ + // var needDelete = _studyRepository.FirstOrDefault(t => t.Id == id); + + // if (needDelete == null) + // { + // return ResponseOutput.DBNotExistIfNUll(needDelete); + // } + // else //移除内存中缓存的StudyCode 这样刚上传的,删除 再上传还是用同样的Code + // { + + // if (_provider.Get($"{needDelete.TrialId }_{ StaticData.StudyMaxCode}").Value == needDelete.StudyCode) + // { + // _provider.Remove($"{needDelete.TrialId }_{ StaticData.StudyMaxCode}"); + // } + // } + + + + // //if (study.Status != (int)StudyStatus.Uploaded) + // //{ + // // return ResponseOutput.NotOk("This study has been quality controlled and couldn't be deleted "); + // //} + + // #region 废弃的直接删除了 这里不用处理了 + + // ////处理废弃的Study 如果有的话 + // //var oldStudy = _studyRepository.FirstOrDefault(t => t.StudyCode == study.StudyCode && t.Id != id); + // //if (oldStudy != null) + // //{ + // // _studyRepository.Delete(t => t.Id == oldStudy.Id); + // // _dicomInstanceRepository.Delete(t => t.StudyId == oldStudy.Id); + // // _dicomSeriesRepository.Delete(t => t.StudyId == oldStudy.Id); + // //} + // //处理废弃的Study + // //var success5 = _studyRepository.Delete(t => t.StudyCode == study.StudyCode); + // #endregion + + + // //清除现有Study记录 + // var success1 = _studyRepository.Delete(t => t.Id == id); + // var succeess2 = _dicomInstanceRepository.Delete(t => t.StudyId == id); + // var success3 = _dicomSeriesRepository.Delete(t => t.StudyId == id); + + + // //一个访视下面有多个检查,所以需要检测 没有的时候才清空 + // if (_studyRepository.Count(t => t.SubjectVisitId == needDelete.SubjectVisitId) == 0) + // { + // _subjectVisitRepository.Update(t => t.Id == needDelete.SubjectVisitId, + + // u => new SubjectVisit() { VisitExecuted = VisitExecutedEnum.UnExecuted, SVENDTC = null, SVSTDTC = null }); + + // _qcChallengeRepository.Delete(t => t.SubjectVisitId == needDelete.SubjectVisitId); + + // //_qcChallengeRepository.Delete(t => t.SubjectVisitId == subjectVisitId); + + // //_trialQCQuestionAnswerRepository.Delete(t => t.SubjectVisitId == subjectVisitId); + // } + + + + + + // return ResponseOutput.Result(success1 || succeess2 || success3); + //} + + //public void ClearStudyInstanceAndSeriseData(Guid studyId) + //{ + // //重新开始计数,怕数据库又脏数据,影响数量 + // _studyRepository.Update(t => t.Id == studyId, u => new DicomStudy() + // { + // InstanceCount = 0, + // SeriesCount = 0 + // }); + + // _dicomSeriesRepository.Delete(t => t.StudyId == studyId); + // _dicomInstanceRepository.Delete(t => t.StudyId == studyId); + //} + + + + //public bool ReUploadDifferentStudy(Guid subjectVisitId, Guid abandonStudyId, Guid newStudyId) + //{ + // //_qcChallengeRepository.Update(t => t.SubjectVisitId == subjectVisitId && t.NeedReUpload == true, u => new QCChallenge() { ReUploadedTime = DateTime.Now, ReUploader = _userInfo.RealName }); + + // //注意这里不是用SeqId 主键查询,因为Seq id 是后续加的,为了不改变代码和前端接口参数 + // var study = _studyRepository.FirstOrDefault(t => t.Id == abandonStudyId); + + // var code = study.StudyCode; + + // //清理以前的数据 + // _studyRepository.Delete(t => t.Id == abandonStudyId); + // _dicomInstanceRepository.Delete(t => t.StudyId == abandonStudyId); + // _dicomSeriesRepository.Delete(t => t.StudyId == abandonStudyId); + + // ////将以前的qa记录数据绑定到现在新生成的Study上 + // //_qaNoticeRepository.Update(t => t.SubjectVisitId == abandonStudyId, u => new QANotice() { SubjectVisitId = newStudyId }); + // //_qaNoticeUserRepository.Update(t => t.SubjectVisitId == abandonStudyId, + // // u => new QANoticeUser() { SubjectVisitId = newStudyId }); + // //_qaRecordRepository.Update(t => t.SubjectVisitId == abandonStudyId, u => new QARecord() { SubjectVisitId = newStudyId }); + + + // var success = false; + // if (study.Status == (int)StudyStatus.Uploading) + // { + // success = _studyRepository.Update(t => t.Id == newStudyId, u => new DicomStudy() + // { + // Status = (int)StudyStatus.Uploaded, + // StudyCode = code + // }); + // } + // else + // { + // //新一份的影像状态为旧影像的状态 不改变 + // success = _studyRepository.Update(t => t.Id == newStudyId, u => new DicomStudy() + // { + // Status = study.Status, + // StudyCode = code + // }); + // } + + + + // #region 废弃之前的Study记录 讲新产生的记录的StudyCode 变更为老的 这样可以关联在一起 + + // //var realName = _userInfo.RealName; + // //var code = _studyRepository.GetAll().First(t => t.Id == abandonStudyId).StudyCode; + // //_studyRepository.Update(t => t.Id == abandonStudyId, u => new DicomStudy() + // //{ + // // Status = (int)StudyStatus.Abandon + // //}); + // //_studyRepository.Update(t => t.Id == newStudyId, u => new DicomStudy() + // //{ + // // Status = (int)StudyStatus.Uploaded, + // // StudyCode = code + // //}); + // //_studyStatusDetailRepository.Add(new StudyStatusDetail + // //{ + // // Status = (int)StudyStatus.Abandon, + // // StudyId = abandonStudyId, + // // Note = string.Empty, + // // OptUserName = realName, + // // OptTime = DateTime.Now + // //}); + // //_studyStatusDetailRepository.Add(new StudyStatusDetail + // //{ + // // Status = (int)StudyStatus.Uploaded, + // // StudyId = newStudyId, + // // Note = string.Empty, + // // OptUserName = realName, + // // OptTime = DateTime.Now + // //}); + + // //_studyStatusDetailRepository.SaveChanges(); + + // #endregion + + + // return success; + + //} + + + //public virtual void ReUploadSameStudy(Guid subjectVisitId, Guid studyId) + //{ + // //_qcChallengeRepository.Update(t => t.SubjectVisitId == subjectVisitId && t.NeedReUpload == true, u => new QCChallenge() { ReUploadedTime = DateTime.Now, ReUploader = _userInfo.RealName }); + + // #region 之前整个上传过程操作没有放在一个整体事务 会有脏数据 现在整体一个事务,不会产生,废弃 但是切入AOP 所以保留空方法 + // //var study = _studyRepository.FirstOrDefault(t=>t.Id== studyId); + // //var status = study.Status; + + // //if (status == (int)StudyStatus.Uploading) + // //{ + // // _studyRepository.Update(t => t.Id == studyId, u => new DicomStudy() + // // { + // // Status = (int)StudyStatus.Uploaded + // // }); + // //} + // #endregion + //} + ///// + ///// 上传完检查后,处理检查 汇总 Series的Modality 更新到检查里面 检查状态 上传人 + ///// + ///// + //public void UploadStudyDeal(Guid studyId) + //{ + + // var seriesModalityList = _dicomSeriesRepository.Where(t => t.StudyId == studyId).Select(u => u.Modality).Distinct(); + // string ModaliyStr = string.Join('、', seriesModalityList.ToList()); + + // var study = _studyRepository.FirstOrDefault(t => t.Id == studyId); + // study.Status = (int)StudyStatus.Uploaded; + // study.Modalities = ModaliyStr; + // _studyRepository.SaveChanges(); + //} + + + //public List GetSubjectVisitStudyList(Guid trialId, Guid siteId, Guid subjectId, Guid subjectVisitId) + //{ + // var query = _studyRepository.Where(t => t.TrialId == trialId && t.SiteId == siteId && t.SubjectId == t.SubjectId && t.SubjectVisitId == subjectVisitId) + // .Select(u => new SubjectVisitStudyDTO() { Modalities = u.Modalities, StudyCode = u.StudyCode, StudyId = u.Id, Status = u.Status }); + + // return query.ToList(); + //} + + ////此处有AOP操作,不要更改为动态Api + //public virtual IResponseOutput UpdateStudyStatus(StudyStatusDetailCommand studyStatus) + //{ + + // //注意这里不是用SeqId 主键查询,因为Seq id 是后续加的,为了不改变代码和前端接口参数 + // var study = _studyRepository.FirstOrDefault(t => t.Id == studyStatus.StudyId); + + // //数据库状态,其他人已经操作过了,此时提醒 + // if (study.Status > studyStatus.Status) + // { + // ResponseOutput.NotOk($"当前界面Study:{study.StudyCode} 状态被其他人已变更,不允许该操作,请刷新界面"); + // } + + // ////防止多次调用,更新 没必要 + // //if (study.Status == studyStatus.Status) + // //{ + // // return ResponseOutput.Ok(); + // //} + + // study.Status = studyStatus.Status; + // study.UpdateTime = DateTime.Now; + + // //if (studyStatus.Status == (int)StudyStatus.Uploaded) + // //{ + // // study.UploadedTime = DateTime.Now; + // // study.Uploader = _userInfo.RealName; + + + // //} + // //if (study.Status == (int)StudyStatus.QAing) + // //{ + // // study.DeadlineTime = studyStatus.DeadlineTime; + // //} + + // if (studyStatus.Status == (int)StudyStatus.QAFInishNotPass) + // { + // study.QAComment = studyStatus.QAComment; + // } + + + // return ResponseOutput.Ok(_studyRepository.SaveChanges()); + //} + + //public List GetStudyStatusDetailList(Guid studyId) + //{ + // return _studyStatusDetailRepository.Where(s => s.StudyId == studyId).OrderByDescending(s => s.OptTime) + // .ProjectTo(_mapper.ConfigurationProvider).ToList(); + //} + + + //public bool DistributeStudy(StudyReviewerCommand studyReviewer) + //{ + // //更新Study表中,总的状态 + // // 插入中间表 + // studyReviewer.StudyList.ForEach(study => + // { + // var workloadType = 1; + // StudyStatus studyStatus = StudyStatus.Distributed; + + // if (study.Status >= (int)StudyStatus.NeedAd) + // { + // workloadType = 2; + // studyStatus = StudyStatus.AdDistributed; + // } + + // var tempStudy = _studyRepository.FirstOrDefault(s => s.Id == study.StudyId); + // tempStudy.Status = (int)studyStatus; + // _studyRepository.Update(tempStudy); + + // _studyReviewerRepository.Add(new StudyReviewer() + // { + // ReviewerId = studyReviewer.ReviewerId, + // StudyId = study.StudyId, + // WorkloadType = workloadType, + // TrialId = studyReviewer.TrialId, + // Status = (int)studyStatus + // }); + // }); + + // return _studyReviewerRepository.SaveChanges(); + //} + + + //public IResponseOutput EditStudyReviewer(StudyReviewerEditCommand studyReviewerEditCommand) + //{ + // _studyReviewerRepository.Delete(t => studyReviewerEditCommand.StudyId == t.StudyId); + + // if (studyReviewerEditCommand.IsDoubleReview) + // { + // if (studyReviewerEditCommand.ReviewerId1 != null) + // { + // _studyReviewerRepository.Add( + // new StudyReviewer() + // { + // StudyId = studyReviewerEditCommand.StudyId, + // TrialId = studyReviewerEditCommand.TrialId, + // ReviewerId = studyReviewerEditCommand.ReviewerId1.Value, + // WorkloadType = 1 + // }); + // } + + // if (studyReviewerEditCommand.ReviewerId2 != null) + // { + // _studyReviewerRepository.Add( + // new StudyReviewer() + // { + // StudyId = studyReviewerEditCommand.StudyId, + // TrialId = studyReviewerEditCommand.TrialId, + // ReviewerId = studyReviewerEditCommand.ReviewerId2.Value, + // WorkloadType = 1 + // }); + // } + + // if (studyReviewerEditCommand.ReviewerIdForAD != null) + // { + // _studyReviewerRepository.Add( + // new StudyReviewer() + // { + // StudyId = studyReviewerEditCommand.StudyId, + // TrialId = studyReviewerEditCommand.TrialId, + // ReviewerId = studyReviewerEditCommand.ReviewerIdForAD.Value, + // WorkloadType = 2 + // }); + // } + + // if (studyReviewerEditCommand.ReviewerId2 == null || studyReviewerEditCommand.ReviewerId1 == null) + // { + // _studyRepository.Update(t => t.Id == studyReviewerEditCommand.StudyId, + // u => new DicomStudy() + // { + // Status = (int)StudyStatus.QAFinish + // }); + // } + // } + // else + // { + // if (studyReviewerEditCommand.ReviewerId1 != null) + // { + // _studyReviewerRepository.Add( + // new StudyReviewer() + // { + // StudyId = studyReviewerEditCommand.StudyId, + // TrialId = studyReviewerEditCommand.TrialId, + // ReviewerId = studyReviewerEditCommand.ReviewerId1.Value, + // WorkloadType = 1 + // }); + // } + // else + // { + // _studyRepository.Update(t => t.Id == studyReviewerEditCommand.StudyId, + // u => new DicomStudy() + // { + // Status = (int)StudyStatus.QAFinish + // }); + // } + // } + + // var success = _studyReviewerRepository.SaveChanges(); + // return ResponseOutput.Result(success); + //} + + //public List GetReviewerListByTrialId(Guid trialId) + //{ + // var query = from enrollItem in _enrollRepository + // .Where(t => t.TrialId == trialId && t.EnrollStatus >= 10) + // join doctorItem in _doctorRepository.AsQueryable() + // on enrollItem.DoctorId equals doctorItem.Id into g + // from doctor in g.DefaultIfEmpty() + // select new + // { + // ReviewerCode = doctor.ReviewerCode, + // FirstName = doctor.FirstName, + // LastName = doctor.LastName, + // ReviewerId = enrollItem.DoctorId, + // ActivelyReading = doctor.ActivelyReading, + // }; + + // return query.Where(s => s.ActivelyReading).Select(s => new ReviewerDistributionDTO + // { + // ReviewerCode = s.ReviewerCode, + // FirstName = s.FirstName, + // LastName = s.LastName, + // ReviewerId = s.ReviewerId, + + // }).ToList(); + //} + + + //public PageOutput GetDistributeStudyList( + // StudyStatusQueryDTO studyStatusQueryDto) + //{ + // Expression> studyReviewerLambda = x => x.TrialId == studyStatusQueryDto.TrialId; + + // if (studyStatusQueryDto.ReviewerId != null) + // { + // studyReviewerLambda = studyReviewerLambda.And(t => t.ReviewerId == studyStatusQueryDto.ReviewerId); + // } + + // if (studyStatusQueryDto.StudyStatus != null) + // { + // studyReviewerLambda = studyReviewerLambda.And(t => t.Status == studyStatusQueryDto.StudyStatus); + // } + + // var query = from studyReviewer in _studyReviewerRepository.Where(studyReviewerLambda) + // join doctor in _doctorRepository.AsQueryable() on studyReviewer.ReviewerId equals doctor.Id + // join study in _studyRepository.AsQueryable() on studyReviewer.StudyId equals study.Id + // select new DistributeReviewerStudyStatusDTO() + // { + // Name = doctor.LastName + " / " + doctor.FirstName, + // NameCN = doctor.ChineseName, + // ReviewerCode = doctor.ReviewerCode, + // Status = studyReviewer.Status, + // StudyCode = study.StudyCode + // }; + + // return query.ToPagedList(studyStatusQueryDto.PageIndex, + // studyStatusQueryDto.PageSize, studyStatusQueryDto.SortField == string.Empty + // ? "ReviewerCode" + // : studyStatusQueryDto.SortField, studyStatusQueryDto.Asc); + + + //} + + //public IEnumerable GetRelationVisitList(decimal visitNum, string tpCode) + //{ + // string tpGroup = tpCode.Substring(tpCode.Length - 3, 3); + // var tp = _workloadTPRepository.FirstOrDefault(u => u.TimepointCode == tpCode); + // var query = from workloadTp in _workloadTPRepository.Where(u => u.SubjectId == tp.SubjectId + // && u.TrialId == tp.TrialId && + // u.TimepointCode.Contains(tpGroup)) + // join subjectVisit in _subjectVisitRepository.Where(u => u.VisitNum <= visitNum) + // on workloadTp.SubjectVisitId equals subjectVisit.Id + // select new RelationVisitDTO + // { + // StudyId = workloadTp.StudyId, + // TpCode = workloadTp.TimepointCode, + // VisitName = subjectVisit.VisitName + // }; + // return query.ToList(); + //} + + //public List GetQANoticeList(Guid subjectVisitId) + //{ + + // var query = from noticeUser in _qaNoticeUserRepository.AsQueryable() + // .Where(t => t.ToUserId == _userInfo.Id && t.SubjectVisitId == subjectVisitId) + // join notice in _qaNoticeRepository.Where(u => u.NeedDeal == true && u.SubjectVisitId == subjectVisitId) on + // noticeUser.QANoticeId equals notice.Id + // select notice.Id; + + // var noticeUserList = query.ToList(); + + // //待处理的消息数量 + // var count = noticeUserList.Count; + + + // var list = _qaNoticeRepository.Where(t => t.SubjectVisitId == subjectVisitId) + // .ProjectTo(_mapper.ConfigurationProvider).OrderByDescending(t => t.SendTime).ToList(); + + // list.ForEach(t => t.IsMessageReceiver = count > 0 && noticeUserList.Contains(t.Id) ? t.NeedDeal : false); + + + // return list; + //} + + //public IResponseOutput DealNonDicomFile(Dictionary filePathDic, + // ArchiveStudyCommand archiveStudyCommand) + //{ + + + // var savedInfo = GetSaveToDicomInfo(archiveStudyCommand.SubjectVisitId); + + // var dicomStudy = new DicomStudy + // { + // Id = Guid.NewGuid(), + + // //SiteId = addtionalInfo.SiteId, + // //TrialId = addtionalInfo.TrialId, + // //SubjectId = addtionalInfo.SubjectId, + // //SubjectVisitId = addtionalInfo.SubjectVisitId, + // IsDoubleReview = savedInfo.IsDoubleReview, + // Comment = savedInfo.Comment + // }; + + + + // #region Setting Code + + // //var last = _studyRepository.Where(s => s.TrialId == addtionalInfo.TrialId) + // // .OrderByDescending(c => c.StudyCode).FirstOrDefault(); + // //if (last != null) + // //{ + // // var len = last.StudyCode.Length; + // // if (len > 5 && int.TryParse(last.StudyCode.Substring(len - 5, 5), out var num)) + // // { + // // dicomStudy.StudyCode = "ST" + (++num).ToString().PadLeft(5, '0'); + // // } + // // else + // // { + // // return ResponseOutput.NotOk("Generate StudyCode failed"); + // // } + // //} + // //else + // //{ + // // dicomStudy.StudyCode = "ST" + 1.ToString().PadLeft(5, '0'); + // //} + + // #endregion + + // var study = _studyRepository.Add(dicomStudy); + // var saveFileList = new List(); + + // foreach (var key in filePathDic.Keys) + // { + // saveFileList.Add(new NoneDicomFile() { FileName = filePathDic[key], StudyId = study.Id, Path = key }); + // } + + + // _noneDicomFileRepository.AddRange(saveFileList); + + + + // var success = _noneDicomFileRepository.SaveChanges(); + + // UpdateStudyStatus(new StudyStatusDetailCommand + // { + // StudyId = study.Id, + // Status = (int)StudyStatus.Uploaded, + // Note = string.Empty + // }); + + + // return ResponseOutput.Result(success); + + //} + #endregion + + + } +} diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/SystemAnonymizationService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/SystemAnonymizationService.cs new file mode 100644 index 00000000..d25f2fe3 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/SystemAnonymizationService.cs @@ -0,0 +1,67 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-03-03 15:28:32 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using IRaCIS.Core.Application.Interfaces; +using IRaCIS.Core.Application.MediatR.Handlers; +using IRaCIS.Core.Application.ViewModel; +using IRaCIS.Core.Infra.EFCore; +using MediatR; +using Microsoft.AspNetCore.Mvc; +namespace IRaCIS.Core.Application.Service +{ + /// + /// SystemAnonymizationService + /// + [ApiExplorerSettings(GroupName = "Image")] + public class SystemAnonymizationService : BaseService, ISystemAnonymizationService + { + private readonly IMediator _mediator; + private readonly IRepository systemAnonymizationRepository; + + public SystemAnonymizationService(IMediator mediator, IRepository systemAnonymizationRepository) + { + _mediator = mediator; + this.systemAnonymizationRepository = systemAnonymizationRepository; + } + + [HttpPost] + public async Task> GetSystemAnonymizationList(SystemAnonymizationQuery querySystemAnonymization) + { + + var systemAnonymizationQueryable = systemAnonymizationRepository + .WhereIf(!string.IsNullOrEmpty(querySystemAnonymization.Group), t => t.Group.Contains(querySystemAnonymization.Group)) + .WhereIf(!string.IsNullOrEmpty(querySystemAnonymization.Element), t => t.Element.Contains(querySystemAnonymization.Element)) + .WhereIf(querySystemAnonymization.IsAdd != null, t => t.IsAdd == querySystemAnonymization.IsAdd) + .WhereIf(!string.IsNullOrEmpty(querySystemAnonymization.ValueRepresentation), t => t.ValueRepresentation.Contains(querySystemAnonymization.ValueRepresentation)) + .ProjectTo(_mapper.ConfigurationProvider); + + return await systemAnonymizationQueryable.ToPagedListAsync(querySystemAnonymization.PageIndex, querySystemAnonymization.PageSize, querySystemAnonymization.SortField, querySystemAnonymization.Asc); + } + + + public async Task AddOrUpdateSystemAnonymization(SystemAnonymizationAddOrEdit addOrEditSystemAnonymization) + { + + var entity = await _repository.InsertOrUpdateAsync(addOrEditSystemAnonymization, true); + + await _mediator.Send(new AnonymizeCacheRequest()); + + return ResponseOutput.Ok(entity.Id.ToString()); + + } + + + [HttpDelete("{systemAnonymizationId:guid}")] + public async Task DeleteSystemAnonymization(Guid systemAnonymizationId) + { + var success = await systemAnonymizationRepository.DeleteFromQueryAsync(t => t.Id == systemAnonymizationId); + + return ResponseOutput.Result(success); + } + + + } +} diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/_MapConfig.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/_MapConfig.cs new file mode 100644 index 00000000..e1c2f37c --- /dev/null +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/_MapConfig.cs @@ -0,0 +1,116 @@ +using AutoMapper; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Application.Contracts.Dicom.DTO; +using IRaCIS.Core.Application.Contracts.DTO; +using IRaCIS.Core.Application.ViewModel; +using IRaCIS.Core.Domain.Models; + +namespace IRaCIS.Core.Application.Service +{ + public class ImageAndDocConfig : Profile + { + public ImageAndDocConfig() + { + CreateMap(); + CreateMap(); + CreateMap(); + + //影像上传 检查 + CreateMap() + .ForMember(d => d.SubjectId, u => u.MapFrom(s => s.Subject.Id)) + .ForMember(d => d.VisitName, u => u.MapFrom(s => s.SubjectVisit.VisitName)) + .ForMember(d => d.SubjectCode, u => u.MapFrom(s => s.Subject.Code)) + .ForMember(d => d.FirstName, u => u.MapFrom(s => s.Subject.FirstName)) + .ForMember(d => d.LastName, u => u.MapFrom(s => s.Subject.LastName)); + + + + CreateMap().IncludeMembers(t => t.Subject, u => u.SubjectVisit) + .ForMember(d => d.UploaderFirstName, u => u.MapFrom(s => s.Site.SiteName)) + .ForMember(d => d.SiteName, u => u.MapFrom(s => s.Site.SiteName)) + .ForMember(d => d.UploaderFirstName, u => u.MapFrom(s => s.Uploader.FirstName)) + .ForMember(d => d.UploaderLastName, u => u.MapFrom(s => s.Uploader.LastName)) + .ForMember(d => d.StudyStatus, u => u.MapFrom(s => s.Status)) + .ForMember(d => d.UploadedTime, u => u.MapFrom(s => s.CreateTime)) + + .ForMember(d => d.DTFCount, u => u.MapFrom(s => s.StudyDTFList.Count())); + + CreateMap(); + + CreateMap(); + + CreateMap() + .ForMember(d => d.StudyId, u => u.MapFrom(s => s.Id)) + .ForMember(d => d.VisitName, u => u.MapFrom(s => s.SubjectVisit.VisitName)); + + CreateMap(); + + + CreateMap() + .ForMember(o => o.StudyCode, t => t.MapFrom(u => u.DicomStudy.StudyCode)) + .ForMember(o => o.InstanceList, t => t.MapFrom(u => u.DicomInstanceList.Select(t=>t.Id).ToArray())) + ; + + CreateMap() + .ForMember(o => o.UploadedTime, t => t.MapFrom(u => u.CreateTime)) + .ForMember(o => o.Uploader, t => t.MapFrom(u => u.Uploader.LastName + " / " + u.Uploader.FirstName)) + .ForMember(o => o.StudyId, t => t.MapFrom(u => u.Id)); + + + + CreateMap(); + + //CreateMap(); + + CreateMap(); + + CreateMap(); + + + + CreateMap(); + + CreateMap(); + CreateMap(); + CreateMap(); + + + + CreateMap().ReverseMap(); + + CreateMap(); + + + + CreateMap() + .ForMember(d => d.DicomStudyCode, u => u.MapFrom(s => s.StudyCode)) + //.ForMember(d => d.Modality, u => u.MapFrom(s => s.Modalities)) + .ForMember(d => d.Bodypart, u => u.MapFrom(s => s.BodyPartExamined)) + .ForMember(d => d.StudyTime, u => u.MapFrom(s => s.StudyTime)) + .ForMember(d => d.TrialSiteCode, u => u.MapFrom(s => s.TrialSite.TrialSiteCode)) + .ForMember(d => d.TrialSiteAliasName, u => u.MapFrom(s => s.TrialSite.TrialSiteAliasName)) + .ForMember(d => d.IsDicom, u => u.MapFrom(s => true)) + .ForMember(d => d.Count, u => u.MapFrom(s => s.SeriesCount)) + .ForMember(d => d.VisitNum, u => u.MapFrom(s => s.SubjectVisit.VisitNum)) + .ForMember(d => d.VisitName, u => u.MapFrom(s => s.SubjectVisit.VisitName)); + + CreateMap() + .ForMember(d => d.NoneDicomCode, u => u.MapFrom(s => s.Code)) + //.ForMember(d => d.Modality, u => u.MapFrom(s => s.Modalities)) + .ForMember(d => d.Bodypart, u => u.MapFrom(s => s.BodyPart)) + .ForMember(d => d.StudyTime, u => u.MapFrom(s => s.ImageDate)) + .ForMember(d => d.TrialSiteCode, u => u.MapFrom(s => s.TrialSite.TrialSiteCode)) + .ForMember(d => d.TrialSiteAliasName, u => u.MapFrom(s => s.TrialSite.TrialSiteAliasName)) + .ForMember(d => d.IsDicom, u => u.MapFrom(s => false)) + .ForMember(d => d.Count, u => u.MapFrom(s => s.NoneDicomFileList.Count())) + .ForMember(d => d.VisitNum, u => u.MapFrom(s => s.SubjectVisit.VisitNum)) + .ForMember(d => d.VisitName, u => u.MapFrom(s => s.SubjectVisit.VisitName)); + + + + + } + } + +} diff --git a/IRaCIS.Core.Application/Service/Institution/CROService.cs b/IRaCIS.Core.Application/Service/Institution/CROService.cs new file mode 100644 index 00000000..441d9b41 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Institution/CROService.cs @@ -0,0 +1,75 @@ +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Application.Services +{ + [ ApiExplorerSettings(GroupName = "Institution")] + public class CroService : BaseService, ICroService + { + private readonly IRepository _croRepository; + public CroService(IRepository croRepository) + { + _croRepository = croRepository; + } + + /// 分页获取CRO列表 + [HttpPost] + public async Task> GetCroList(CROCompanyQueryDTO croCompanySearchModel) + { + var croQueryable = _croRepository.WhereIf(!string.IsNullOrEmpty(croCompanySearchModel.CROName), t => t.CROName.Contains(croCompanySearchModel.CROName)) + .ProjectTo(_mapper.ConfigurationProvider); + + return await croQueryable.ToPagedListAsync(croCompanySearchModel.PageIndex, croCompanySearchModel.PageSize, + string.IsNullOrWhiteSpace(croCompanySearchModel.SortField) ? "CROName": croCompanySearchModel.SortField, croCompanySearchModel.Asc); + + + } + + /// 根据CRO 名称查询所有CRO 列表 + public async Task> GetAllCROList() + { + return await _croRepository.ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + } + + + + /// 添加CRO信息 + + public async Task AddOrUpdateCro(CROCompanyDTO addCroCompanyCommand) + { + + var exp = new EntityVerifyExp() + { + VerifyExp = cro => cro.CROName.Equals(addCroCompanyCommand.CROName) , + VerifyMsg = "A CRO with the same name already existed in the table. Please confirm." + }; + + var cro = await _croRepository.InsertOrUpdateAsync(addCroCompanyCommand, true, exp); + + + + return ResponseOutput.Ok( cro.Id.ToString()); + + } + + /// 删除CRO信息 + + [HttpDelete("{croCompanyId:guid}")] + public async Task DeleteCro(Guid cROCompanyId) + { + if (await _repository.GetQueryable().AnyAsync(t => t.CROId == cROCompanyId)) + { + return ResponseOutput.NotOk("This CRO has participated in the trial and couldn't be deleted."); + } + //if (_userRepository.Find().Any(t => t.OrganizationId == cROCompanyId)) + //{ + // return ResponseOutput.NotOk("该CRO下存在用户,暂时无法删除。"); + //} + var success = await _croRepository.DeleteFromQueryAsync(x=>x.Id== cROCompanyId); + + return ResponseOutput.Result(success); + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Institution/DTO/CROCompanyModel.cs b/IRaCIS.Core.Application/Service/Institution/DTO/CROCompanyModel.cs new file mode 100644 index 00000000..f4745f49 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Institution/DTO/CROCompanyModel.cs @@ -0,0 +1,29 @@ +using System; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Contracts +{ + public class CROCompanyDTO + { + public Guid? Id { get; set; } + public string CROName { get; set; } = string.Empty; + public string CROCode { get; set; } = string.Empty; + } + + public class CroSelectDTO + { + public Guid Id { get; set; } + public string CROName { get; set; } = string.Empty; + public string CROCode { get; set; } = string.Empty; + } + + public class CROCompanyQueryDTO : PageInput + { + public string CROName { get; set; } = string.Empty; + } + + public class CROCompanySelectSearchDTO + { + public string CROName { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Institution/DTO/HospitalModel.cs b/IRaCIS.Core.Application/Service/Institution/DTO/HospitalModel.cs new file mode 100644 index 00000000..32d933d9 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Institution/DTO/HospitalModel.cs @@ -0,0 +1,34 @@ +using System; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Contracts +{ + public class HospitalDTO : HospitalCommand + { + + } + + public class HospitalCommand + { + public Guid? Id { get; set; } + public string HospitalName { get; set; } = String.Empty; + public string UniversityAffiliated { get; set; } = String.Empty; + public string Country { get; set; } = String.Empty; + public string Province { get; set; } = String.Empty; + public string City { get; set; } = String.Empty; + + + public string HospitalNameCN { get; set; } = String.Empty; + public string UniversityAffiliatedCN { get; set; } = String.Empty; + public string CountryCN { get; set; } = String.Empty; + public string ProvinceCN { get; set; } = String.Empty; + public string CityCN { get; set; } = String.Empty; + } + + public class HospitalQueryDTO : PageInput + { + public string HospitalName { get; set; } = string.Empty; + public string Province { get; set; } = string.Empty; + public string City { get; set; } = string.Empty; + } +} diff --git a/IRaCIS.Core.Application/Service/Institution/DTO/SiteModel.cs b/IRaCIS.Core.Application/Service/Institution/DTO/SiteModel.cs new file mode 100644 index 00000000..de32f964 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Institution/DTO/SiteModel.cs @@ -0,0 +1,47 @@ +using System; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Contracts +{ + public class SiteDTO: SiteCommand + { + + } + + public class SiteCommand + { + public Guid? Id { get; set; } + public string SiteName { get; set; } = String.Empty; + public string SiteCode { get; set; } = String.Empty; + + public string Address { get; set; } = String.Empty; + public string UniqueCode { get; set; } = string.Empty; + public string City { get; set; } = string.Empty; + public string Country { get; set; } = string.Empty; + public Guid? HospitalId { get; set; } + public string DirectorName { get; set; } = string.Empty; + public string DirectorPhone { get; set; } = string.Empty; + public string ContactName { get; set; } = string.Empty; + public string ContactPhone { get; set; } = string.Empty; + } + + public class SiteSelectionDTO + { + public Guid Id { get; set; } + public string SiteName { get; set; } = String.Empty; + public string City { get; set; } = String.Empty; + public string Country { get; set; } = String.Empty; + } + + public class SiteSelectDTO : SiteDTO + { + public string HospitalName { get; set; } = String.Empty; + public bool IsSelect { get; set; } + } + + public class SiteQueryParam : PageInput + { + public string SiteName { get; set; } = String.Empty; + public Guid UserId { get; set; } = Guid.Empty; + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Institution/DTO/SponsorModel.cs b/IRaCIS.Core.Application/Service/Institution/DTO/SponsorModel.cs new file mode 100644 index 00000000..78a67fbf --- /dev/null +++ b/IRaCIS.Core.Application/Service/Institution/DTO/SponsorModel.cs @@ -0,0 +1,28 @@ +using System; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Contracts +{ + public class SponsorDTO + { + public Guid Id { get; set; } + public string SponsorName { get; set; } = string.Empty; + } + + public class SponsorCommand + { + public Guid? Id { get; set; } + public string SponsorName { get; set; } = string.Empty; + } + + public class SponsorQueryDTO : PageInput + { + public string SponsorName { get; set; } = string.Empty; + } + + public class SponsorSelectDTO + { + public Guid Id { get; set; } + public string SponsorName { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Institution/HospitalService.cs b/IRaCIS.Core.Application/Service/Institution/HospitalService.cs new file mode 100644 index 00000000..8dbbfd27 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Institution/HospitalService.cs @@ -0,0 +1,80 @@ +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Application.Services +{ + [ ApiExplorerSettings(GroupName = "Institution")] + public class HospitalService : BaseService, IHospitalService + { + private readonly IRepository _hospitalRepository; + + public HospitalService(IRepository hospitalRepository) + { + _hospitalRepository = hospitalRepository; + } + + /// 获取所有医院列表 + public async Task> GetHospitalList() + { + return await _hospitalRepository.ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + } + + /// 添加医院 + [HttpPost] + public async Task AddOrUpdateHospital(HospitalCommand hospitalCommand) + { + var exp = new EntityVerifyExp() + { + VerifyExp = h => h.HospitalName.Equals(hospitalCommand.HospitalName), + VerifyMsg = "A hospital with the same name already existed in the table. Please confirm." + }; + + var hospital = await _hospitalRepository.InsertOrUpdateAsync(hospitalCommand, true, exp); + + return ResponseOutput.Ok(hospital.Id.ToString()); + + } + + + + /// 删除医院信息 + [HttpDelete("{hospitalId:guid}")] + public async Task DeleteHospital(Guid hospitalId) + { + if (await _repository.GetQueryable().AnyAsync(t => t.Id == hospitalId)) + { + return ResponseOutput.NotOk("There are already doctors registered in this hospital,and it couldn't be deleted"); + } + //if (_userRepository.Find().Any(t => t.OrganizationId == hospitalId)) + //{ + // return ResponseOutput.NotOk("该医院下存在用户,暂时无法删除。"); + //} + + var success = await _hospitalRepository.DeleteFromQueryAsync(x => x.Id == hospitalId); + + return ResponseOutput.Result(success); + } + + /// 分页获取医院列表 + [HttpPost] + public async Task> GetHospitalPageList(HospitalQueryDTO hospitalSearchModel) + { + + var hospitalQueryable = + _hospitalRepository + .WhereIf(hospitalSearchModel.HospitalName!=null, t => t.HospitalName.Contains(hospitalSearchModel.HospitalName!) || t.HospitalNameCN.Contains(hospitalSearchModel.HospitalName!)) + .WhereIf(hospitalSearchModel.City != null, t => t.City.Contains(hospitalSearchModel.City!) || t.HospitalNameCN.Contains(hospitalSearchModel.City!)) + .WhereIf(hospitalSearchModel.Province != null, t => t.Province.Contains(hospitalSearchModel.Province!) || t.HospitalNameCN.Contains(hospitalSearchModel.Province!)) + .ProjectTo(_mapper.ConfigurationProvider); + + //优化后 + return await hospitalQueryable.ToPagedListAsync(hospitalSearchModel.PageIndex, hospitalSearchModel.PageSize, string.IsNullOrWhiteSpace(hospitalSearchModel.SortField) ? "HospitalName" : hospitalSearchModel.SortField, + hospitalSearchModel.Asc); + + + + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Institution/Interface/ICROService.cs b/IRaCIS.Core.Application/Service/Institution/Interface/ICROService.cs new file mode 100644 index 00000000..961a70b2 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Institution/Interface/ICROService.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface ICroService + { + Task> GetCroList(CROCompanyQueryDTO queryModel); + Task> GetAllCROList(); + Task AddOrUpdateCro(CROCompanyDTO addCroCompanyViewModel); + Task DeleteCro(Guid croCompanyId); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Institution/Interface/IHospitalService.cs b/IRaCIS.Core.Application/Service/Institution/Interface/IHospitalService.cs new file mode 100644 index 00000000..c3694a93 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Institution/Interface/IHospitalService.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface IHospitalService + { + /// 获取医院列表 + Task> GetHospitalList(); + Task> GetHospitalPageList(HospitalQueryDTO queryModel); + Task AddOrUpdateHospital(HospitalCommand model); + Task DeleteHospital(Guid hospitalId); + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Institution/Interface/ISiteService.cs b/IRaCIS.Core.Application/Service/Institution/Interface/ISiteService.cs new file mode 100644 index 00000000..2d392e5b --- /dev/null +++ b/IRaCIS.Core.Application/Service/Institution/Interface/ISiteService.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface ISiteService + { + Task> GetSiteList(SiteQueryParam queryModel); + Task> GetAllSiteList(); + Task AddOrUpdateSite(SiteCommand AddModel); + Task DeleteSite(Guid researchCenterId); + + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Institution/Interface/ISponsorService.cs b/IRaCIS.Core.Application/Service/Institution/Interface/ISponsorService.cs new file mode 100644 index 00000000..6020d07a --- /dev/null +++ b/IRaCIS.Core.Application/Service/Institution/Interface/ISponsorService.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface ISponsorService + { + Task> GetSponsorList(SponsorQueryDTO queryParam); + Task> GetAllSponsorList(); + Task AddOrUpdateSponsor(SponsorCommand model); + Task DeleteSponsor(Guid sponsorId); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Institution/SiteService.cs b/IRaCIS.Core.Application/Service/Institution/SiteService.cs new file mode 100644 index 00000000..2003394f --- /dev/null +++ b/IRaCIS.Core.Application/Service/Institution/SiteService.cs @@ -0,0 +1,74 @@ +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Application.Services +{ + [ ApiExplorerSettings(GroupName = "Institution")] + public class SiteService : BaseService, ISiteService + { + private readonly IRepository _siteRepository; + + public SiteService(IRepository siteRepository) + { + _siteRepository = siteRepository; + } + + /// 分页获取研究中心列表 + [HttpPost] + public async Task> GetSiteList(SiteQueryParam searchModel) + { + + var siteQueryable = _siteRepository + .WhereIf(!string.IsNullOrWhiteSpace(searchModel.SiteName), t => t.SiteName.Contains(searchModel.SiteName)) + .ProjectTo(_mapper.ConfigurationProvider); + + + return await siteQueryable.ToPagedListAsync(searchModel.PageIndex, searchModel.PageSize, string.IsNullOrWhiteSpace(searchModel.SortField) ? "SiteName" : searchModel.SortField, searchModel.Asc); + + + } + + public async Task> GetAllSiteList() + { + return await _siteRepository.ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + } + + + /// 添加研究中心 + + public async Task AddOrUpdateSite(SiteCommand siteCommand) + { + + var exp = new EntityVerifyExp() + { + VerifyExp = h => h.SiteName.Equals(siteCommand.SiteName)|| h.SiteCode.Equals(siteCommand.SiteCode), + VerifyMsg = "A site with the same name or code already existed in the table. Please confirm." + }; + + var site = await _siteRepository.InsertOrUpdateAsync(siteCommand, true, exp); + + + + return ResponseOutput.Ok(site.Id.ToString()); + + + } + + /// 删除研究中心 + + [HttpDelete("{siteId:guid}")] + public async Task DeleteSite(Guid siteId) + { + + if (_repository.GetQueryable().Any(t => t.SiteId == siteId)) + { + return ResponseOutput.NotOk("This site has participated in the trial and couldn't be deleted."); + } + + var success = await _siteRepository.DeleteFromQueryAsync(x => x.Id == siteId); + return ResponseOutput.Result(success); + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Institution/SponsorService.cs b/IRaCIS.Core.Application/Service/Institution/SponsorService.cs new file mode 100644 index 00000000..44a1010b --- /dev/null +++ b/IRaCIS.Core.Application/Service/Institution/SponsorService.cs @@ -0,0 +1,80 @@ +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Application.Services +{ + [ ApiExplorerSettings(GroupName = "Institution")] + public class SponsorService : BaseService, ISponsorService + { + private readonly IRepository _sponsorRepository; + + public SponsorService(IRepository sponsorRepository) + { + _sponsorRepository = sponsorRepository; + } + + /// 分页获取申办方列表 + [HttpPost] + public async Task> GetSponsorList(SponsorQueryDTO sponsorSearchModel) + { + + var sponsorQueryable = _sponsorRepository + .WhereIf(!string.IsNullOrWhiteSpace(sponsorSearchModel.SponsorName),t => t.SponsorName.Contains(sponsorSearchModel.SponsorName)) + .ProjectTo(_mapper.ConfigurationProvider); + + return await sponsorQueryable.ToPagedListAsync(sponsorSearchModel.PageIndex, + sponsorSearchModel.PageSize, string.IsNullOrWhiteSpace(sponsorSearchModel.SortField) ? "Id" : sponsorSearchModel.SortField, sponsorSearchModel.Asc); + + } + + /// 分页获取申办方列表 + public async Task> GetAllSponsorList() + { + //Expression> sponsorLambda = x => true; + //if (!string.IsNullOrWhiteSpace(sponsorSearchModel.SponsorName)) + //{ + // sponsorLambda = sponsorLambda.And(t => t.SponsorName.Contains(sponsorSearchModel.SponsorName.Trim())); + //} + var sponsorQueryable = _sponsorRepository.ProjectTo(_mapper.ConfigurationProvider); + return await sponsorQueryable.ToListAsync(); + } + + /// 添加申办方 + public async Task AddOrUpdateSponsor(SponsorCommand sponsorCommand) + { + var exp = new EntityVerifyExp() + { + VerifyExp = h => h.SponsorName.Equals(sponsorCommand.SponsorName), + VerifyMsg = "A sponsor with the same name already existed in the table. Please confirm." + }; + + var sponsor = await _sponsorRepository.InsertOrUpdateAsync(sponsorCommand, true, exp); + + return ResponseOutput.Ok(sponsor.Id.ToString()); + + + + } + + /// 删除申办方 + [HttpDelete("{sponsorId:guid}")] + + public async Task DeleteSponsor(Guid sponsorId) + { + if (await _repository.GetQueryable().AnyAsync(t => t.CROId == sponsorId)) + { + return ResponseOutput.NotOk("This sponsor has participated in the trial and couldn't be deleted."); + } + //if (_userRepository.Find().Any(t => t.OrganizationId == sponsorId)) + //{ + // return ResponseOutput.NotOk("该申办方下存在用户,暂时无法删除。"); + //} + + + var success = await _sponsorRepository.DeleteFromQueryAsync(x => x.Id == sponsorId); + return ResponseOutput.Result(success); + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Institution/_MapConfig.cs b/IRaCIS.Core.Application/Service/Institution/_MapConfig.cs new file mode 100644 index 00000000..605901c9 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Institution/_MapConfig.cs @@ -0,0 +1,34 @@ +using AutoMapper; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Domain.Models; + +namespace IRaCIS.Core.Application.Service +{ + public class InstitutionConfig : Profile + { + public InstitutionConfig() + { + CreateMap(); + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap().ReverseMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + + + //CreateMap() + // .ForMember(o => o.InstitutionName, t => t.MapFrom(u => u.CROName)); + //CreateMap() + // .ForMember(o => o.InstitutionName, t => t.MapFrom(u => u.SponsorName)); + //CreateMap() + // .ForMember(o => o.InstitutionName, t => t.MapFrom(u => u.HospitalName)); + //CreateMap() + // .ForMember(o => o.InstitutionName, t => t.MapFrom(u => u.SiteName)); + } + } + +} diff --git a/IRaCIS.Core.Application/Service/Management/DTO/MenuModel.cs b/IRaCIS.Core.Application/Service/Management/DTO/MenuModel.cs new file mode 100644 index 00000000..747675cb --- /dev/null +++ b/IRaCIS.Core.Application/Service/Management/DTO/MenuModel.cs @@ -0,0 +1,100 @@ +namespace IRaCIS.Application.Contracts +{ + public class MenuCommand + { + public Guid? MenuId { get; set; } + + //上级菜单 + public Guid? ParentId { get; set; } = Guid.Empty; + + // 类型(M目录 C菜单 F按钮 L链接) + public string MenuType { get; set; } = string.Empty; + + public string MenuIcon { get; set; } = String.Empty; + + public string MenuName { get; set; } = string.Empty; + + //路由地址 + public string Path { get; set; } = string.Empty; + + //组件路径 + public string Component { get; set; } = string.Empty; + + public int ShowOrder { get; set; } + + //启用 禁用 + public bool IsEnable { get; set; } = true; + + public bool IsCache { get; set; } = false; + + public bool IsDisplay { get; set; } + + public bool IsInTabDisplay { get; set; } + + public bool IsExternalLink { get; set; } + + //权限点 + public string PermissionStr { get; set; } = String.Empty; + + //Api 接口地址 + public string ApiPath { get; set; } = String.Empty; + + public string Meta { get; set; } = string.Empty; + + public string Note { get; set; } + + public string Redirect { get; set; } = string.Empty; + } + + + public class MenuDTO : MenuCommand + { + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid UpdateUserId { get; set; } + } + + public class MenuQueyDTO + { + public Guid? ParentId { get; set; } + + public string? MenuType { get; set; } + + public bool? IsEnable { get; set; } + + public bool? IsCache { get; set; } + + public bool? IsDisplay { get; set; } + + public bool? IsInTabDisplay { get; set; } + + public bool? IsExternalLink { get; set; } + } + + + public class MenuFunctionDTO:MenuCommand + { + public List Children { get; set; }=new List(); + + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid UpdateUserId { get; set; } + } + + public class RoleMenuFunctionSelectDTO + { + public Guid RoleId { get; set; } + public List MenuFunctionId { get; set; }=new List(); + } + + + public class FunctionSelectDTO + { + public Guid RoleId { get; set; } + public Guid FunctionId { get; set; } + public bool IsSelect { get; set; } + } + +} diff --git a/IRaCIS.Core.Application/Service/Management/DTO/RoleModel.cs b/IRaCIS.Core.Application/Service/Management/DTO/RoleModel.cs new file mode 100644 index 00000000..c34b4695 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Management/DTO/RoleModel.cs @@ -0,0 +1,30 @@ +using System; + +namespace IRaCIS.Application.Contracts +{ + public class RoleDTO + { + public Guid? Id { get; set; } = Guid.Empty; + public string RoleName { get; set; } = string.Empty; + public string RoleDescription { get; set; } = string.Empty; + public int PrivilegeLevel { get; set; } + } + + public class UserRoleSelectDTO + { + public Guid userId { get; set; } + public Guid roleId { get; set; } + public bool isSelect { get; set; } + } + + + public class UserSelectRoleDTO : RoleDTO + { + public bool IsSelect { get; set; } + } + + public class RoleCommand : RoleDTO + { + + } +} diff --git a/IRaCIS.Core.Application/Service/Management/DTO/TreeNode.cs b/IRaCIS.Core.Application/Service/Management/DTO/TreeNode.cs new file mode 100644 index 00000000..3d5e440d --- /dev/null +++ b/IRaCIS.Core.Application/Service/Management/DTO/TreeNode.cs @@ -0,0 +1,26 @@ +namespace IRaCIS.Application.Contracts +{ + + public class DictionaryTreeNode + { + public Guid Id { get; set; } + public string KeyName { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + public List Children { get; set; }=new List(); + } + + public class MenuTreeNodeSelect: MenuCommand + { + + public List Children { get; set; } = new List(); + public bool IsSelect { get; set; } = false; + } + + + public class MenuTreeNode : MenuCommand + { + + public List Children { get; set; } = new List(); + } + +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Management/DTO/UserModel.cs b/IRaCIS.Core.Application/Service/Management/DTO/UserModel.cs new file mode 100644 index 00000000..3a6547ec --- /dev/null +++ b/IRaCIS.Core.Application/Service/Management/DTO/UserModel.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using IRaCIS.Core.Infrastructure.Extention; +using IRaCIS.Core.Domain.Share; +using Newtonsoft.Json; + + +namespace IRaCIS.Application.Contracts +{ + + public class UserTypeViewModel + { + public Guid Id { get; set; } + + public string UserType { get; set; } = string.Empty; + public UserTypeEnum UserTypeEnum { get; set; } + public string Description { get; set; } = string.Empty; + } + + public class UserLoginDTO + { + public string UserName { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + } + + public class LoginReturnDTO + { + public UserBasicInfo BasicInfo { get; set; } = new UserBasicInfo(); + public string JWTStr { get; set; }=string.Empty; + + } + + public class UserBasicInfo + { + public Guid Id { get; set; } + public string UserName { get; set; } = string.Empty; + public string RealName { get; set; } = string.Empty; + public int Sex { get; set; } // 1-男 2-女 + + public UserTypeEnum UserTypeEnum { get; set; } + + public bool IsAdmin { get; set; } = false; + public string UserTypeShortName { get; set; } = string.Empty; + public bool PasswordChanged { get; set; } + public int Status { get; set; } + public Guid UserTypeId { get; set; } + + public string Code { get; set; } = String.Empty; + + public string PermissionStr { get; set; } = String.Empty; + + + public bool IsFirstAdd { get; set; } + public bool IsReviewer { get; set; } = false; + + } + + public class MenuFuncTreeNodeView + { + [JsonIgnore] + public Guid Id { get; set; } + [JsonIgnore] + public Guid ParentId { get; set; } = Guid.Empty; + [JsonIgnore] + public int ShowOrder { get; set; } + + public string routeName { get; set; } = string.Empty; + public string component { get; set; } = string.Empty; + public string redirect { get; set; } = string.Empty; + public string path { get; set; } = string.Empty; + public Meta meta { get; set; } = new Meta(); + public bool hidden { get; set; } + public List Childrens { get; set; }=new List(); + } + + public class Meta + { + public string MetaTitle { get; set; } = string.Empty; + public bool MetaBreadcrumb { get; set; } = false; + public string MetaIcon { get; set; } = string.Empty; + public string MetaActiveMenu { get; set; } = string.Empty; + } + + + public class FunctionTreeNodeDTO + { + [JsonIgnore] + public Guid Id { get; set; } + [JsonIgnore] + public Guid ParentId { get; set; } = Guid.Empty; + [JsonIgnore] + public int ShowOrder { get; set; } + + public string RouteName { get; set; } = string.Empty; + public string FunctionName { get; set; } = string.Empty; + + public List Childrens { get; set; } = new List(); + } + + + + public class UserDetailDTO : UserInfo + { + public bool CanEditUserType { get; set; } + } + + public class UserInfo + { + public Guid Id { get; set; } + public string UserName { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string RealName { get; set; } = string.Empty; + + + public string FirstName { get; set; } = string.Empty; + + public string LastName { get; set; } = string.Empty; + public int Sex { get; set; } // 1-男 2-女 + + public int Status { get; set; } = 1; // 0-已删除 1-正常 + + public string Phone { get; set; } = string.Empty; + public string EMail { get; set; } = string.Empty; + public Guid UserTypeId { get; set; } = Guid.Empty; + + public string UserCode { get; set; } = string.Empty; + + public bool IsZhiZhun { get; set; } + + public string UserType { get; set; } = string.Empty; + + public string UserTypeShortName { get; set; } = string.Empty; + + + public UserTypeEnum UserTypeEnum { get; set; } + + + + //public Guid OrganizationTypeId { get; set; } = Guid.Empty; + //public string OrganizationType { get; set; } = String.Empty; + //public Guid OrganizationId { get; set; } + public string OrganizationName { get; set; } = string.Empty; + + + + public string DepartmentName { get; set; } = String.Empty; + public string PositionName { get; set; } = String.Empty; + } + + /// + /// 添加用户是的返回模型 + /// + public class UserAddedReturnDTO + { + public Guid Id { get; set; } + public string UserCode { get; set; } = string.Empty; + + public int VerificationCode { get; set; } + } + + + public class UserCommand : UserInfo + { + public bool IsFirstAdd { get; set; } = true; + + //public string FirstName { get; set; } + //public string LastName { get; set; } + } + + public class EditPasswordCommand + { + public string NewPassWord { get; set; } = string.Empty; + public string OldPassWord { get; set; } = string.Empty; + } + + public class UserListQueryDTO : PageInput + { + public string UserName { get; set; } = string.Empty; + public string Phone { get; set; } = string.Empty; + public string OrganizationName { get; set; } = string.Empty; + public Guid? UserType { get; set; } + public UserStateEnum? UserState { get; set; } + } + + public class UserRoleInfoDTO + { + public List RoleList { get; set; } = new List(); + public int MaxPrivilegeLevel { get; set; } + } + + public class UserListDTO : UserInfo + { + [JsonIgnore] + public Guid testGuid { get; set; } + public bool CanEditUserType { get; set; } + + public List RoleNameArray { get; set; } = new List(); + public IEnumerable RoleNameList { get; set; } = new List(); + } + + + public class UserIdRoleName : RoleDTO + { + public Guid UserId { get; set; } + } + public class UserIdRoleNameList + { + public Guid UserId { get; set; } + public IEnumerable RoleList { get; set; }=new List(); + } + public class ResetPasswordCommand + { + public string EmailOrPhone { get; set; } = string.Empty; + public VerifyType VerificationType { get; set; } + public string VerificationCode { get; set; } = string.Empty; + public string NewPwd { get; set; } = string.Empty; + + public bool IsReviewer { get; set; } = false; + } +} diff --git a/IRaCIS.Core.Application/Service/Management/DTO/UserTypeRoleModel.cs b/IRaCIS.Core.Application/Service/Management/DTO/UserTypeRoleModel.cs new file mode 100644 index 00000000..2ca7b8a0 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Management/DTO/UserTypeRoleModel.cs @@ -0,0 +1,87 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-03 09:38:51 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using IRaCIS.Core.Domain.Share; +namespace IRaCIS.Core.Application.Contracts +{ + /// UserTypeRoleView 列表视图模型 + public class UserTypeRoleView : UserTypeMenuAddOrEdit + { + public List UserTypeGroupList { get; set; } = new List(); + + public new List UserTypeGroupIdList => UserTypeGroupList.Select(t=>t.DictionaryId).ToList(); + + } + + public class UserTypeGroupInfo + { + public Guid UserTypeId { get; set; } + + public Guid DictionaryId { get; set; } + + public string GroupName { get; set; }=string.Empty; + + public string GroupNameCN { get; set; } = string.Empty; + } + + ///UserTypeRoleQuery 列表查询参数模型 + public class UserTypeQuery + { + public Guid? GroupId { get; set; } + + public string? SearchFilter { get; set; } = string.Empty; + + } + + public class UserTypeMenuAddOrEdit: UserTypeRoleAddOrEdit + { + public List MenuIds { get; set; } = new List(); + + } + + public class UserTypeRoleAddOrEdit + { + + public Guid? Id { get; set; } + public string UserTypeName { get; set; } = string.Empty; + public string UserTypeShortName { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string PermissionStr { get; set; } = string.Empty; + public int Order { get; set; } + + + public List UserTypeGroupIdList { get; set; } = new List(); + + + /// 用户类型名称简写/// + //public bool IsInternal { get; set; } + //public UserTypeGroup Type { get; set; } = UserTypeGroup.TrialUser; + + + } + + + public class TrialUserType + { + public Guid Id { get; set; } + public string UserType { get; set; } = string.Empty; + public UserTypeEnum UserTypeEnum { get; set; } + public string UserTypeShortName { get; set; } = string.Empty; + public string PermissionStr { get; set; } = String.Empty; + + + + //public UserTypeGroup Type { get; set; } + + //public bool IsInternal { get; set; } + + + + } + + +} + + diff --git a/IRaCIS.Core.Application/Service/Management/Interface/IMenuService.cs b/IRaCIS.Core.Application/Service/Management/Interface/IMenuService.cs new file mode 100644 index 00000000..7f001dce --- /dev/null +++ b/IRaCIS.Core.Application/Service/Management/Interface/IMenuService.cs @@ -0,0 +1,13 @@ +using IRaCIS.Application.Contracts; + +namespace IRaCIS.Application.Services +{ + public interface IMenuService + { + Task AddOrUpdateMenu(MenuCommand menuAddOrUpdateModel); + Task DeleteMenu(Guid menuId); + Task> GetMenuList(MenuQueyDTO menuQueyDTO); + Task> GetMenuTree(); + Task> GetUserTypeMenuTree(Guid userTypeId); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Management/Interface/IUserService.cs b/IRaCIS.Core.Application/Service/Management/Interface/IUserService.cs new file mode 100644 index 00000000..1cce7f51 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Management/Interface/IUserService.cs @@ -0,0 +1,20 @@ +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Domain.Share; + +namespace IRaCIS.Application.Services +{ + public interface IUserService + { + Task> AddUser(UserCommand userAddModel); + Task DeleteUser(Guid userId); + Task GetUser(Guid id); + Task> GetUserList(UserListQueryDTO param); + Task> Login(string userName, string password); + Task ModifyPassword(EditPasswordCommand editPwModel); + Task ResetPassword(Guid userId); + Task SendVerificationCode(string emailOrPhone, VerifyType verificationType, bool isReviewer = false); + Task SetNewPassword(ResetPasswordCommand resetPwdModel); + Task UpdateUser(UserCommand model); + Task UpdateUserState(Guid userId, UserStateEnum state); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Management/Interface/IUserTypeService.cs b/IRaCIS.Core.Application/Service/Management/Interface/IUserTypeService.cs new file mode 100644 index 00000000..f91b2a33 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Management/Interface/IUserTypeService.cs @@ -0,0 +1,18 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-03 09:38:11 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using IRaCIS.Core.Domain.Share; + +namespace IRaCIS.Core.Application.Contracts +{ + public interface IUserTypeService + { + Task AddOrUpdateUserTypeRole(UserTypeMenuAddOrEdit addOrEditUserTypeRole); + Task DeleteUserTypeRole(Guid userTypeRoleId); + Task> GetTrialUserTypeList(); + Task> GetUserTypeList(UserTypeSelectEnum userTypeSelectEnum); + Task> GetUserTypeRoleList(UserTypeQuery userTypeQuery); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Management/MenuService.cs b/IRaCIS.Core.Application/Service/Management/MenuService.cs new file mode 100644 index 00000000..591f6181 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Management/MenuService.cs @@ -0,0 +1,357 @@ +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Application.Services +{ + [ApiExplorerSettings(GroupName = "Management")] + public class MenuService : BaseService, IMenuService + { + private readonly IRepository menuRepository; + + public MenuService(IRepository menuRepository) + { + this.menuRepository = menuRepository; + } + + /// + /// 添加菜单 New + /// + /// + /// + public async Task AddOrUpdateMenu(MenuCommand menuAddOrUpdateModel) + { + var exp = new EntityVerifyExp() + { + VerifyExp = u => u.ParentId == menuAddOrUpdateModel.ParentId && u.MenuName == menuAddOrUpdateModel.MenuName, + VerifyMsg = "A son node with the same name already existed under this parent node." + }; + + var menu = await _repository.InsertOrUpdateAsync(menuAddOrUpdateModel, true, exp); + + return ResponseOutput.Ok(menu.Id.ToString()); + + #region 废弃前 + + //if (menuAddOrUpdateModel.Id == null) + //{ + // bool exist = _menuFunctionRepository.Any(u => u.ParentId == menuAddOrUpdateModel.ParentId && u.MenuName == menuAddOrUpdateModel.MenuName); + // if (exist) + // { + // return ResponseOutput.NotOk("A son node with the same name already existed under this parent node."); + // } + + // if (!string.IsNullOrWhiteSpace(menuAddOrUpdateModel.FunctionName)) + // { + // menuAddOrUpdateModel.IsFunction = true; + // } + + // var result = _menuFunctionRepository.Add(_mapper.Map(menuAddOrUpdateModel)); + + // if (_menuFunctionRepository.SaveChanges()) + // { + // return ResponseOutput.Ok(result.Id.ToString()); + // } + // return ResponseOutput.NotOk(); + //} + //else + //{ + // var menuUpdateModel = menuAddOrUpdateModel; + // bool exist = _menuFunctionRepository.Any(u => u.ParentId == menuUpdateModel.ParentId && + // u.MenuName == menuUpdateModel.MenuName && u.Id != menuUpdateModel.Id); + // if (exist) + // { + // return ResponseOutput.NotOk("A son node with the same name already existed under this parent node."); + // } + // var updateModel = _mapper.Map(menuUpdateModel); + + // if (!string.IsNullOrWhiteSpace(updateModel.FunctionName)) + // { + // updateModel.IsFunction = true; + // } + // _menuFunctionRepository.Update(updateModel); + + // var success = _menuFunctionRepository.SaveChanges(); + // return ResponseOutput.Result(success); + //} + #endregion + + } + + /// + /// 删除菜单 已验证 父节点不允许删除 New + /// + /// + /// + [HttpDelete("{menuId:guid}")] + public async Task DeleteMenu(Guid menuId) + { + if (await menuRepository.AnyAsync(t => t.ParentId == menuId)) + { + return ResponseOutput.NotOk("不允许直接删除父节点"); + } + + var success =await menuRepository.DeleteFromQueryAsync(u => u.Id == menuId); + + return ResponseOutput.Result(success); + } + + + /// + /// 所有的菜单树 包括禁用的 New + /// + /// + [HttpGet] + public async Task> GetMenuTree() + { + var allMenuList = (await menuRepository.ProjectTo(_mapper.ConfigurationProvider).ToListAsync()).IfNullThrowException(); + + var list = allMenuList.ToTree((root, child) => child.ParentId == Guid.Empty, + (root, child) => child.ParentId == root.MenuId, + (child, datalist) => + { + child.Children ??= new List(); + child.Children.AddRange(datalist); + }, item => item.ShowOrder); + + return list; + } + + /// + /// 菜单列表 参数都是可传的 根据需要传递 New + /// + /// + /// + [HttpPost] + public async Task> GetMenuList(MenuQueyDTO menuQueyDTO) + { + return await menuRepository + .WhereIf(menuQueyDTO.ParentId != null, t => t.ParentId == menuQueyDTO.ParentId) + .WhereIf(menuQueyDTO.IsEnable != null, t => t.IsEnable == menuQueyDTO.IsEnable) + .WhereIf(menuQueyDTO.IsCache != null, t => t.IsCache == menuQueyDTO.IsCache) + .WhereIf(menuQueyDTO.IsDisplay != null, t => t.IsDisplay == menuQueyDTO.IsDisplay) + .WhereIf(menuQueyDTO.IsExternalLink != null, t => t.IsExternalLink == menuQueyDTO.IsExternalLink) + .WhereIf(menuQueyDTO.IsInTabDisplay != null, t => t.IsInTabDisplay == menuQueyDTO.IsInTabDisplay) + .WhereIf(menuQueyDTO.MenuType != null, t => t.MenuType == menuQueyDTO.MenuType) + + .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + } + + + /// + /// 获取某用户类型(角色) 菜单树 勾选情况 New + /// + /// + /// + [HttpGet("{userTypeId:guid}")] + public async Task> GetUserTypeMenuTree(Guid userTypeId) + { + var menuFunctionlist = (await menuRepository + .ProjectTo(_mapper.ConfigurationProvider, new { userTypeId = userTypeId }).ToListAsync()).IfNullThrowException(); + + + var list = menuFunctionlist.ToTree((root, child) => child.ParentId == Guid.Empty, + (root, child) => child.ParentId == root.MenuId, + (child, datalist) => + { + child.Children ??= new List(); + child.Children.AddRange(datalist); + }, item => item.ShowOrder); + + return list; + + } + + public async Task> GetUserMenuTree() + { + + var menuFunctionlist = await _repository.GetQueryable() + .Where(t => t.UserTypeId == _userInfo.UserTypeId && (t.Menu.MenuType == "M" || t.Menu.MenuType == "C") && t.Menu.IsEnable) + .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + + var list = menuFunctionlist.ToTree((root, child) => child.ParentId == Guid.Empty, + (root, child) => child.ParentId == root.MenuId, + (child, datalist) => + { + child.Children ??= new List(); + child.Children.AddRange(datalist); + }, item => item.ShowOrder); + + return list; + } + + + public async Task> GetUserPermissions() + { + + var list = await _repository.GetQueryable() + .Where(t => t.UserTypeId == _userInfo.UserTypeId && t.Menu.MenuType == "F") + .Select(t => t.Menu.PermissionStr).ToListAsync(); + + //var str = await _repository.Where(t => t.Id == _userInfo.UserTypeId).Select(t => t.PermissionStr).FirstOrDefaultAsync(); + + + list.Add(_userInfo.PermissionStr); + + return list; + + } + + + + #region 生成树废弃前 + + + + + //public List GetMenuFunction() + //{ + // var allMenuList = _menuFunctionRepository.ProjectTo(_mapper.ConfigurationProvider).ToList(); + + // return GetTreeList(Guid.Empty, allMenuList); + //} + + ///// + ///// 根据父节点ID递归生成树 + ///// + ///// + ///// + ///// + //[NonDynamicMethod] + //public List GetTreeList(Guid? parentId, List allMenuList) + //{ + // //树节点集合 每个节点包含一个菜单项 和一个子菜单集合 + // List treeList = new List(); + + // // 根据父菜单节点获取子菜单节点 并且进行排序 + // List menuList = allMenuList.Where(x => x.ParentId == parentId).OrderBy(t => t.ShowOrder).ToList(); + + // foreach (var menuItem in menuList) + // { + // MenuTreeNode treeItem = new MenuTreeNode() + // { + // Id = menuItem.Id, + // ParentId = menuItem.ParentId, + // RouteName = menuItem.RouteName, + // MenuName = menuItem.MenuName, + // Path = menuItem.Path, + // Hidden = menuItem.Hidden, + // Component = menuItem.Component, + // FunctionName = menuItem.FunctionName, + // IsFunction = menuItem.IsFunction, + // MetaActiveMenu = menuItem.MetaActiveMenu, + // MetaBreadcrumb = menuItem.MetaBreadcrumb, + // MetaIcon = menuItem.MetaIcon, + // MetaTitle = menuItem.MetaTitle, + // Redirect = menuItem.Redirect, + // ShowOrder = menuItem.ShowOrder, + // Note = menuItem.Note, + // Status = menuItem.Status, + + // Children = GetTreeList(menuItem.Id, allMenuList) + // }; + + // treeList.Add(treeItem); + // } + // return treeList; + //} + + + ///// + ///// 获取某角色 菜单树 勾选情况 和 功能勾选情况 + ///// + ///// + ///// + //[HttpGet("{roleId:guid}")] + //public List GetRoleMenuFunction(Guid roleId) + //{ + + + + // var menuFuncQuery = from function in _menuFunctionRepository.Where(u => !u.IsFunction) + // join roleMenuFunctionItem in _roleMenuFunctionRepository.AsQueryable() + // .Where(t => t.UserTypeId == roleId) + // on function.Id equals roleMenuFunctionItem.MenuFunctionId into t + // from roleFunction in t.DefaultIfEmpty() + // select new MenuTreeNodeSelect() + // { + // IsSelect = roleFunction != null, + // ShowOrder = function.ShowOrder, + // MenuName = function.MenuName, + // Note = function.Note, + // FunctionName = function.FunctionName, + // ParentId = function.ParentId, + // Id = function.Id, + // }; + // var list = menuFuncQuery.ToList(); + + // return GetSelectTreeList(Guid.Empty, list); + //} + + //[NonDynamicMethod] + //private List GetSelectTreeList(Guid parentId, List allMenuList) + //{ + // List TreeList = new List(); + // List menuList = allMenuList.Where(x => x.ParentId == parentId).OrderBy(t => t.ShowOrder).ToList(); + + // foreach (var menuItem in menuList) + // { + // MenuTreeNodeSelect treeItem = new MenuTreeNodeSelect() + // { + // Id = menuItem.Id, + // ParentId = menuItem.ParentId, + // RouteName = menuItem.RouteName, + // MenuName = menuItem.MenuName, + // Note = menuItem.Note, + // IsSelect = menuItem.IsSelect, + // ShowOrder = menuItem.ShowOrder, + // Children = GetSelectTreeList(menuItem.Id, allMenuList) + // }; + + // TreeList.Add(treeItem); + // } + // return TreeList; + //} + + //[NonDynamicMethod] + //private List GetTreeList(Guid? parentId, List allMenuList) + //{ + // List TreeList = new List(); + // List menuList = allMenuList.Where(x => x.ParentId == parentId).OrderBy(t => t.ShowOrder).ToList(); + + // foreach (var menuItem in menuList) + // { + // MenuTreeNode treeItem = new MenuTreeNode() + // { + // Id = menuItem.Id, + // ParentId = menuItem.ParentId, + // RouteName = menuItem.RouteName, + // MenuName = menuItem.MenuName, + // Path = menuItem.Path, + // Hidden = menuItem.Hidden, + // Component = menuItem.Component, + // FunctionName = menuItem.FunctionName, + // IsFunction = menuItem.IsFunction, + // MetaActiveMenu = menuItem.MetaActiveMenu, + // MetaBreadcrumb = menuItem.MetaBreadcrumb, + // MetaIcon = menuItem.MetaIcon, + // MetaTitle = menuItem.MetaTitle, + // Redirect = menuItem.Redirect, + // ShowOrder = menuItem.ShowOrder, + // Note = menuItem.Note, + // Status = menuItem.Status, + + // Children = GetTreeList(menuItem.Id, allMenuList) + // }; + + // TreeList.Add(treeItem); + // } + // return TreeList; + //} + + #endregion + + + + } +} diff --git a/IRaCIS.Core.Application/Service/Management/UserService.cs b/IRaCIS.Core.Application/Service/Management/UserService.cs new file mode 100644 index 00000000..d63e1322 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Management/UserService.cs @@ -0,0 +1,597 @@ +using IRaCIS.Core.Infrastructure.ExpressionExtend; +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infrastructure; +using System.Text.RegularExpressions; +using Autofac.Extras.DynamicProxy; +using IRaCIS.Core.API.Utility.AOP; + +using Microsoft.AspNetCore.Mvc; +using Panda.DynamicWebApi.Attributes; + +namespace IRaCIS.Application.Services +{ + [ApiExplorerSettings(GroupName = "Management")] + [Intercept(typeof(UserAddAOP))] + public class UserService : BaseService, IUserService + { + private readonly IRepository _userRepository; + private readonly IMailVerificationService _mailVerificationService; + private readonly IRepository _verificationCodeRepository; + private readonly IRepository _doctorRepository; + private readonly IRepository _userTrialRepository; + public UserService(IRepository userRepository, + + IMailVerificationService mailVerificationService, + IRepository verificationCodeRepository, + IRepository doctorRepository, + IRepository userTrialRepository + + ) + { + _userRepository = userRepository; + _mailVerificationService = mailVerificationService; + _verificationCodeRepository = verificationCodeRepository; + _doctorRepository = doctorRepository; + _userTrialRepository = userTrialRepository; + } + + /// 发送验证码 邮箱或者手机号 New + + [HttpGet("{email}")] + public async Task SendVerificationCode(string email) + { + + //检查手机或者邮箱是否有效 + if (!Regex.IsMatch(email, @"^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$")) + { + + return ResponseOutput.NotOk("Please input a legal email"); + + } + + ////查找改邮箱或者手机的用户 + //var exist = await _userRepository.AnyAsync(t => t.EMail == email); + + //if (!exist) + //{ + // return ResponseOutput.NotOk("No user with this email exists."); + + //} + //var user = await _userRepository.FirstOrDefaultAsync(t => t.EMail == email); + + + //验证码 6位 + int verificationCode = new Random().Next(100000, 1000000); + + await _mailVerificationService.SendMailEditEmail(_userInfo.Id, _userInfo.RealName , email, verificationCode); + + return ResponseOutput.Ok(); + + } + + + [HttpPut("{newEmail}/{verificationCode}")] + public async Task SetNewEmail( string newEmail,string verificationCode) + { + + + var verificationRecord = await _verificationCodeRepository + .FirstOrDefaultAsync(t => t.UserId == _userInfo.Id && t.Code == verificationCode && t.CodeType == 0); + + //检查数据库是否存在该验证码 + if (verificationRecord == null) + { + + return ResponseOutput.NotOk("Verification code error"); + + } + else + { + //检查验证码是否失效 + if (verificationRecord.ExpirationTime < DateTime.Now) + { + return ResponseOutput.NotOk("The verification code has expired"); + + } + else //验证码正确 并且 没有超时 + { + //更新密码 + //var pwd = MD5Helper.Md5(newPwd); + //var count = _doctorRepository.Update().Where(t => t.Id == doctor.Id).Set(d => d.Password == pwd).ExecuteAffrows(); + + + if (await _userRepository.AnyAsync(t => (t.EMail == newEmail && t.UserTypeId == _userInfo.UserTypeId && t.Id != _userInfo.Id))) + { + return ResponseOutput.NotOk("The mailbox for this user type already exists"); + } + + var success = await _userRepository.UpdateFromQueryAsync(t => t.Id == _userInfo.Id, u => new User() + { + EMail= newEmail + }); + + //删除验证码历史记录 + await _verificationCodeRepository.DeleteFromQueryAsync(t => t.UserId == _userInfo.Id && t.CodeType ==0); + + return ResponseOutput.Result(success); + + } + } + } + + + [HttpPut("{newPhone}")] + public async Task SetNewPhone( string newPhone) + { + + + var success = await _userRepository.UpdateFromQueryAsync(t => t.Id == _userInfo.Id, u => new User() + { + Phone = newPhone + }); + + return ResponseOutput.Ok(); + } + + + [HttpPut("{newUserName}")] + public async Task SetNewUserName( string newUserName) + { + + if (await _userRepository.AnyAsync(t => t.UserName == newUserName && t.Id != _userInfo.Id)) + { + return ResponseOutput.NotOk("UserId already exists"); + } + var success = await _userRepository.UpdateFromQueryAsync(t => t.Id == _userInfo.Id, u => new User() + { + UserName = newUserName + }); + + return ResponseOutput.Ok(); + } + + + + + + + /// + /// 发送验证码 邮箱或者手机号 + /// + /// + /// + /// + /// + [HttpGet("{emailOrPhone}/{verificationType:int}")] + public async Task SendVerificationCode(string emailOrPhone, VerifyType verificationType, bool isReviewer = false) + { + if (string.IsNullOrEmpty(emailOrPhone)) + { + return ResponseOutput.NotOk(verificationType == VerifyType.Email ? "Please input email" : "Please input phone"); + + } + //防止输入前后有空格 + var emailOrPhoneStr = emailOrPhone.Trim(); + + //检查手机或者邮箱是否有效 + if (!Regex.IsMatch(emailOrPhoneStr, @"/^1[34578]\d{9}$/") && !Regex.IsMatch(emailOrPhoneStr, @"^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$")) + { + + return ResponseOutput.NotOk(verificationType == VerifyType.Email + ? "Please input a legal email" + : "Please input a legal phone"); + + } + + //医生登录 + if (isReviewer) + { + var exist = await _doctorRepository.AnyAsync(t => t.EMail == emailOrPhoneStr || t.Phone == emailOrPhoneStr); + + if (!exist) + { + return ResponseOutput.NotOk(verificationType == VerifyType.Email + ? "No user with this email exists." + : "No user with this phone exists."); + + } + + var user = await _doctorRepository.FirstOrDefaultAsync(t => t.EMail == emailOrPhoneStr || t.Phone == emailOrPhoneStr); + //邮箱 + if (verificationType == VerifyType.Email) + { + //验证码 6位 + int verificationCode = new Random().Next(100000, 1000000); + + await _mailVerificationService.SendMail(user.Id, user.ChineseName, emailOrPhoneStr, + verificationCode); + } + //手机短信 + else + { + + } + + } + else//管理用户登录 + { + //查找改邮箱或者手机的用户 + var exist = await _userRepository.AnyAsync(t => t.EMail == emailOrPhoneStr || t.Phone == emailOrPhoneStr); + + if (!exist) + { + return ResponseOutput.NotOk(verificationType == VerifyType.Email + ? "No user with this email exists." + : "No user with this phone exists."); + + } + var user = await _userRepository.FirstOrDefaultAsync(t => t.EMail == emailOrPhoneStr || t.Phone == emailOrPhoneStr); + //邮箱 + if (verificationType == VerifyType.Email) + { + //验证码 6位 + int verificationCode = new Random().Next(100000, 1000000); + + await _mailVerificationService.SendMail(user.Id, user.LastName + ' ' + user.FirstName, emailOrPhoneStr, + verificationCode); + } + //手机短信 + else + { + + } + } + + return ResponseOutput.Ok(); + } + + + /// + /// 验证设置新密码 + /// + /// + /// + [HttpPost] + public async Task SetNewPassword(ResetPasswordCommand resetPwdModel) + { + if (resetPwdModel.IsReviewer) + { + var emailOrPhoneStr = resetPwdModel.EmailOrPhone.Trim(); + var verificationCodeStr = resetPwdModel.VerificationCode.Trim(); + var user = await _doctorRepository.FirstOrDefaultAsync(t => t.EMail == emailOrPhoneStr || t.Phone == emailOrPhoneStr); + + var verificationRecord = await _verificationCodeRepository + .FirstOrDefaultAsync(t => t.UserId == user.Id && t.Code == verificationCodeStr && t.CodeType == resetPwdModel.VerificationType); + + //检查数据库是否存在该验证码 + if (verificationRecord == null) + { + + return ResponseOutput.NotOk("Verification code error"); + + } + else + { + //检查验证码是否失效 + if (verificationRecord.ExpirationTime < DateTime.Now) + { + return ResponseOutput.NotOk("The verification code has expired"); + + } + else //验证码正确 并且 没有超时 + { + //更新密码 + var success = await _doctorRepository.UpdateFromQueryAsync(t => t.Id == user.Id, u => new Doctor() + { + Password = resetPwdModel.NewPwd + }); + + //删除验证码历史记录 + await _verificationCodeRepository.DeleteFromQueryAsync(t => t.UserId == user.Id && t.CodeType == resetPwdModel.VerificationType); + + return ResponseOutput.Result(success); + + } + } + } + else + { + var emailOrPhoneStr = resetPwdModel.EmailOrPhone.Trim(); + var verificationCodeStr = resetPwdModel.VerificationCode.Trim(); + + var user = await _userRepository.FirstOrDefaultAsync(t => t.EMail == emailOrPhoneStr || t.Phone == emailOrPhoneStr); + + + var verificationRecord = await _verificationCodeRepository + .FirstOrDefaultAsync(t => t.UserId == user.Id && t.Code == verificationCodeStr && t.CodeType == resetPwdModel.VerificationType); + + //检查数据库是否存在该验证码 + if (verificationRecord == null) + { + + return ResponseOutput.NotOk("Verification code error"); + + } + else + { + //检查验证码是否失效 + if (verificationRecord.ExpirationTime < DateTime.Now) + { + return ResponseOutput.NotOk("The verification code has expired"); + + } + else //验证码正确 并且 没有超时 + { + //更新密码 + //var pwd = MD5Helper.Md5(newPwd); + //var count = _doctorRepository.Update().Where(t => t.Id == doctor.Id).Set(d => d.Password == pwd).ExecuteAffrows(); + + var success = await _userRepository.UpdateFromQueryAsync(t => t.Id == user.Id, u => new User() + { + Password = resetPwdModel.NewPwd, + PasswordChanged = true + }); + + //删除验证码历史记录 + await _verificationCodeRepository.DeleteFromQueryAsync(t => t.UserId == user.Id && t.CodeType == resetPwdModel.VerificationType); + + return ResponseOutput.Result(success); + + } + } + } + + } + + + + + + + + + + + + + + + + /// + /// 获取用户列表 + /// + /// + /// + [HttpPost] + public async Task> GetUserList(UserListQueryDTO param) + { + var userQueryable = _userRepository.Where(x => x.UserTypeEnum != UserTypeEnum.SuperAdmin) + .WhereIf(!string.IsNullOrWhiteSpace(param.UserName), t => t.UserName.Contains(param.UserName) || (t.LastName + ' ' + t.FirstName).Contains(param.UserName)) + .WhereIf(!string.IsNullOrWhiteSpace(param.Phone), t => t.Phone.Contains(param.Phone)) + .WhereIf(!string.IsNullOrWhiteSpace(param.OrganizationName), t => t.OrganizationName.Contains(param.OrganizationName)) + .WhereIf(param.UserType != null, t => t.UserTypeId == param.UserType) + .WhereIf(param.UserState != null, t => t.Status == param.UserState) + .ProjectTo(_mapper.ConfigurationProvider); + + return await userQueryable.ToPagedListAsync(param.PageIndex, param.PageSize, param.SortField == string.Empty ? "UserName" : param.SortField, param.Asc); + + + } + + /// + /// 根据用户Id获取用户详细信息[New] + /// + /// + /// + [HttpGet("{id:guid}")] + public async Task GetUser(Guid id) + { + var userQuery = _userRepository.Where(t => t.Id == id).ProjectTo(_mapper.ConfigurationProvider); + return await (userQuery.FirstOrDefaultAsync()).IfNullThrowException(); + } + + /// + /// 添加用户 + /// + /// + /// + public async Task> AddUser(UserCommand userAddModel) + { + if (await _userRepository.AnyAsync(t => t.UserName == userAddModel.UserName ||(t.EMail == userAddModel.EMail && t.UserTypeId == userAddModel.UserTypeId))) + { + return ResponseOutput.NotOk(" UserId or The mailbox for this user type already exists", new UserAddedReturnDTO()); + } + + var saveItem = _mapper.Map(userAddModel); + + saveItem.Code = await _userRepository.Select(t => t.Code).DefaultIfEmpty().MaxAsync() + 1; + + saveItem.UserCode = AppSettings.UserCodePrefix + saveItem.Code.ToString("D4"); + + if (saveItem.IsZhiZhun) + { + saveItem.OrganizationName = "Zhizhun"; + } + + //验证码 6位 + int verificationCode = new Random().Next(100000, 1000000); + + saveItem.Password = MD5Helper.Md5("123456"); + + await _userRepository.AddAsync(saveItem); + + var success = await _userRepository.SaveChangesAsync(); + + return ResponseOutput.Result(success, new UserAddedReturnDTO { Id = saveItem.Id, UserCode = saveItem.UserCode, VerificationCode = verificationCode }); + + } + + /// + /// 更新用户 + /// + /// + /// + public async Task UpdateUser(UserCommand model) + { + + // 判断当前用户名是否已经存在 + if (await _userRepository.AnyAsync(t => (t.UserName == model.UserName && t.Id != model.Id) || (t.EMail == model.EMail && t.UserTypeId==model.UserTypeId && t.Id != model.Id))) + { + return ResponseOutput.NotOk("UserId or The mailbox for this user type already exists"); + } + + var user = await _userRepository.FirstOrDefaultAsync(t => t.Id == model.Id); + + if (user == null) return Null404NotFound(user); + + _mapper.Map(model, user); + + if (user.IsZhiZhun) + { + user.OrganizationName = "Zhizhun"; + } + var success = await _userRepository.SaveChangesAsync(); + + return ResponseOutput.Result(success); + + } + + /// + /// 删除用户 + /// + /// + /// + [HttpDelete("{userId:guid}")] + public async Task DeleteUser(Guid userId) + { + if (await _userTrialRepository.AnyAsync(t => t.Id == userId)) + { + return ResponseOutput.NotOk("This user has participated in the trial and couldn't be deleted"); + } + + var success = await _userRepository.DeleteFromQueryAsync(t => t.Id == userId); + + return ResponseOutput.Result(success); + } + + /// + /// 禁用或者启用账户 + /// + /// + /// + /// + + [HttpPost("{userId:guid}/{state:int}")] + public async Task UpdateUserState(Guid userId, UserStateEnum state) + { + var success = await _userRepository.UpdateFromQueryAsync(u => u.Id == userId, t => new User + { + Status = state + }); + return ResponseOutput.Result(success); + } + + /// + /// 重置密码为 默认密码 + /// + /// + /// + [HttpGet("{userId:guid}")] + + public async Task ResetPassword(Guid userId) + { + var success = await _userRepository.UpdateFromQueryAsync(t => t.Id == userId, u => new User() + { + Password = MD5Helper.Md5(StaticData.DefaultPassword), + PasswordChanged = false + }); + + return ResponseOutput.Result(success); + } + + /// + /// 修改密码,当前支持旧密码修改密码,手机及邮箱验证码后续支持[New] + /// + /// + /// + [HttpPost] + public async Task ModifyPassword(EditPasswordCommand editPwModel) + { + if (await _userRepository.FirstOrDefaultAsync(t => t.Id == _userInfo.Id && t.Password == editPwModel.OldPassWord) != null) + { + var success = await _userRepository.UpdateFromQueryAsync(t => t.Id == _userInfo.Id, u => new User() + { + Password = editPwModel.NewPassWord, + IsFirstAdd = false + }); + + return ResponseOutput.Result(success); + } + + if (await _doctorRepository.AnyAsync(t => t.Id == _userInfo.Id && t.Password == editPwModel.OldPassWord)) + { + var success = await _doctorRepository.UpdateFromQueryAsync(t => t.Id == _userInfo.Id, u => new Doctor() + { + Password = editPwModel.NewPassWord + }); + + return ResponseOutput.Result(success); + } + + return ResponseOutput.NotOk("Old password is wrong."); + + } + + + /// + /// 用户登陆 + /// + /// + /// + /// + [NonDynamicMethod] + public async Task> Login(string userName, string password) + { + var userLoginReturnModel = new LoginReturnDTO(); + + + var loginUser = await _userRepository.Where(u => u.UserName == userName && u.Password == password).ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync(); + + if (loginUser == null) + { + //此处下面 代码 为了支持医生也能登录 而且前端不加选择到底是管理用户 还是医生用户 奇怪的需求 无法理解 + + var loginDoctor = await _doctorRepository.Where(u => u.Phone == userName && u.Password == password).ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync(); + + if (loginDoctor == null) + { + return ResponseOutput.NotOk("Please check the user name or password.", new LoginReturnDTO()); + + } + + userLoginReturnModel.BasicInfo = loginDoctor; + + + return ResponseOutput.Ok(userLoginReturnModel); + + } + + if (loginUser.Status == 0) + { + return ResponseOutput.NotOk("The user has been disabled!", new LoginReturnDTO()); + } + + userLoginReturnModel.BasicInfo = loginUser; + + + return ResponseOutput.Ok(userLoginReturnModel); + + } + + + + } +} diff --git a/IRaCIS.Core.Application/Service/Management/UserTypeService.cs b/IRaCIS.Core.Application/Service/Management/UserTypeService.cs new file mode 100644 index 00000000..de5bd889 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Management/UserTypeService.cs @@ -0,0 +1,152 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-03 09:38:11 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infrastructure.ExpressionExtend; +using IRaCIS.Core.Infra.EFCore; + +namespace IRaCIS.Core.Application.Contracts +{ + /// + /// UserTypeRoleService + /// + [ApiExplorerSettings(GroupName = "Management")] + public class UserTypeRoleService : BaseService, IUserTypeService + { + private readonly IRepository userTypeServiceRepository; + + public UserTypeRoleService(IRepository userTypeServiceRepository) + { + this.userTypeServiceRepository = userTypeServiceRepository; + } + + [HttpPost] + public async Task> GetUserTypeRoleList(UserTypeQuery userTypeQuery) + { + + var userTypeRoleQueryable = userTypeServiceRepository + .WhereIf(!string.IsNullOrWhiteSpace(userTypeQuery.SearchFilter), t => t.Description.Contains(userTypeQuery.SearchFilter!) || t.UserTypeName.Contains(userTypeQuery.SearchFilter!) || t.UserTypeShortName.Contains(userTypeQuery.SearchFilter!)) + + .WhereIf(userTypeQuery.GroupId!=null,t=>t.UserTypeGroupList.Any(t=>t.DictionaryId== userTypeQuery.GroupId)) + .OrderBy(t => t.UserTypeEnum).ProjectTo(_mapper.ConfigurationProvider); + + return await userTypeRoleQueryable.ToListAsync(); + } + + + + public async Task AddOrUpdateUserTypeRole(UserTypeMenuAddOrEdit addOrEditUserTypeRole) + { + + var entity = new UserType(); + + if (addOrEditUserTypeRole.Id == null) + { + entity = _mapper.Map(addOrEditUserTypeRole); + + entity.UserTypeEnum = userTypeServiceRepository.Select(t => t.UserTypeEnum).DefaultIfEmpty().Max() + 1; + + await _repository.AddAsync(entity); + + + } + else + { + + if (addOrEditUserTypeRole.MenuIds.Count > 0) + { + entity = userTypeServiceRepository.Where(t => t.Id == addOrEditUserTypeRole.Id, true).Include(t => t.UserTypeMenuList).Include(t=>t.UserTypeGroupList).FirstOrDefault(); + + } + else + { + entity = userTypeServiceRepository.Where(t => t.Id == addOrEditUserTypeRole.Id, true).FirstOrDefault(); + } + _mapper.Map(addOrEditUserTypeRole, entity); + } + + var success = await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok( entity.Id.ToString()); + + } + + + [HttpDelete("{userTypeId:guid}")] + public async Task DeleteUserTypeRole(Guid userTypeId) + { + if ( await _repository.AnyAsync(t => t.UserTypeId == userTypeId)) + { + return ResponseOutput.NotOk("该用户类型,被某些用户已使用,不能删除"); + } + + var success = await userTypeServiceRepository.DeleteFromQueryAsync(t => t.Id == userTypeId); + + return ResponseOutput.Result(success); + } + + + + + /// + /// 通过传递场景枚举 返回对应的下拉框数据 1:是外部 2:是内部 3:是Site调研 + /// + /// + /// + [HttpGet("{userTypeSelectEnum:int}")] + public async Task> GetUserTypeList(UserTypeSelectEnum userTypeSelectEnum) + { + var userTypeEnums = new List(); + + if (userTypeSelectEnum == UserTypeSelectEnum.ExternalUser) + { + userTypeEnums = new List() { UserTypeEnum.CPM, UserTypeEnum.SPM, UserTypeEnum.CPM, UserTypeEnum.SMM, UserTypeEnum.CMM, UserTypeEnum.EA }; + } + + + if (userTypeSelectEnum == UserTypeSelectEnum.InnerUser) + { + userTypeEnums = new List() { UserTypeEnum.IQC, UserTypeEnum.APM, UserTypeEnum.MIM, UserTypeEnum.MW }; + + if (_userInfo.UserTypeEnumInt == (int)UserTypeEnum.SuperAdmin) + { + userTypeEnums.Add(UserTypeEnum.ProjectManager); + } + } + + + if (userTypeSelectEnum == UserTypeSelectEnum.SiteSurvey) + { + userTypeEnums = new List() { UserTypeEnum.CRA, UserTypeEnum.ClinicalResearchCoordinator }; + } + + + var query = userTypeServiceRepository.Where(x => x.UserTypeEnum != UserTypeEnum.SuperAdmin) + .WhereIf(userTypeSelectEnum != UserTypeSelectEnum.None, t => userTypeEnums.Contains(t.UserTypeEnum)) + .OrderBy(t => t.Order).ProjectTo(_mapper.ConfigurationProvider); + + return await query.ToListAsync(); + } + + + + + + /// + /// 项目文档 配置 哪里用户类型下拉 + /// + /// + public async Task> GetTrialUserTypeList() + { + var query = userTypeServiceRepository.Where(x => x.UserTypeEnum != UserTypeEnum.SuperAdmin) + //.Where(t => t.Type == UserTypeGroup.TrialUser) + .OrderBy(t => t.Order).ProjectTo(_mapper.ConfigurationProvider); + + return await query.ToListAsync(); + } + + } +} diff --git a/IRaCIS.Core.Application/Service/Management/_MapConfig.cs b/IRaCIS.Core.Application/Service/Management/_MapConfig.cs new file mode 100644 index 00000000..79daeeb2 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Management/_MapConfig.cs @@ -0,0 +1,96 @@ +using AutoMapper; +using AutoMapper.EquivalencyExpression; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Domain.Models; + +namespace IRaCIS.Core.Application.Service +{ + public class ManagementConfig : Profile + { + public ManagementConfig() + { + + CreateMap().IncludeMembers(t => t.Menu); + + + + + CreateMap(); + + CreateMap().ForMember(d => d.UserCode, x => x.Ignore()); + CreateMap().ReverseMap() + .ForMember(t => t.UserTypeMenuList, u => u.MapFrom(c => c.MenuIds)) + .ForMember(t => t.UserTypeGroupList, u => u.MapFrom(c => c.UserTypeGroupIdList)); + + CreateMap().ForMember(t => t.DictionaryId, u => u.MapFrom(c => c)) + .EqualityComparison((odto, o) => odto == o.DictionaryId); + + CreateMap().ForMember(t=>t.MenuId,u=>u.MapFrom(c=>c)) + .EqualityComparison((odto, o) => odto == o.MenuId); + + CreateMap() + .ForMember(t => t.GroupName, u => u.MapFrom(c => c.Group.Value)) + .ForMember(t => t.GroupNameCN, u => u.MapFrom(c => c.Group.ValueCN)); + + + CreateMap().ForMember(d => d.Id, u => u.MapFrom(t => t.MenuId)).ReverseMap(); + + CreateMap() + .ForMember(d => d.MenuId, u => u.MapFrom(t => t.Id)); + + CreateMap().ForMember(d => d.MenuId, u => u.MapFrom(t => t.Id)); + + + CreateMap() + .ForMember(t => t.MenuIds, u => u.MapFrom(c => c.UserTypeMenuList.Select(t=>t.MenuId))); + + CreateMap(); + CreateMap(); + + CreateMap(); + + CreateMap(); + + + //用户类型菜单勾选 + var userTypeId = Guid.Empty; + CreateMap() + .ForMember(d => d.IsSelect, u => u.MapFrom(s => s.UserTypeMenuList.Any(t => t.UserTypeId == userTypeId))); + + //菜单树 + CreateMap() + /*.ForMember(d => d.meta, u => u.MapFrom(s => new Meta() { MetaTitle = s.MetaTitle, MetaBreadcrumb = s.MetaBreadcrumb, MetaIcon = s.MetaIcon, MetaActiveMenu = s.MetaActiveMenu }))*/; + + //功能树 + CreateMap(); + + //普通用户菜单树 + CreateMap().IncludeMembers(t => t.Menu) + .ForMember(d => d.Id, u => u.MapFrom(t => t.Menu.Id)); + //.ForMember(d => d.meta, u => u.MapFrom(s => new Meta() { MetaTitle = s.MenuFunction.MetaTitle, MetaBreadcrumb = s.MenuFunction.MetaBreadcrumb, MetaIcon = s.MenuFunction.MetaIcon, MetaActiveMenu = s.MenuFunction.MetaActiveMenu })); + + + CreateMap().IncludeMembers(t => t.Menu) + .ForMember(d => d.Id, u => u.MapFrom(t => t.Menu.Id)); + + CreateMap() + .ForMember(d => d.UserType, u => u.MapFrom(t => t.UserTypeName)); + + CreateMap() + .ForMember(d => d.RealName, u => u.MapFrom(s => s.LastName + " / " + s.FirstName)) + .ForMember(d => d.UserTypeId, u => u.MapFrom(s => s.UserTypeRole.Id)) + .ForMember(d => d.UserType, u => u.MapFrom(s => s.UserTypeRole.UserTypeName)) + .ForMember(d => d.UserTypeShortName, u => u.MapFrom(s => s.UserTypeRole.UserTypeShortName)) + .ForMember(d => d.CanEditUserType, u => u.MapFrom(s => !s.UserTrials.Any())); + + CreateMap() + .ForMember(d => d.RealName, u => u.MapFrom(s => s.LastName + " / " + s.FirstName)) + .ForMember(d => d.UserTypeId, u => u.MapFrom(s => s.UserTypeRole.Id)) + .ForMember(d => d.UserType, u => u.MapFrom(s => s.UserTypeRole.UserTypeShortName)) + .ForMember(d => d.CanEditUserType, u => u.MapFrom(s => !s.UserTrials.Any())); + + } + } + +} diff --git a/IRaCIS.Core.Application/Service/QC/ClinicalDataService.cs b/IRaCIS.Core.Application/Service/QC/ClinicalDataService.cs new file mode 100644 index 00000000..13e4776b --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/ClinicalDataService.cs @@ -0,0 +1,253 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-22 11:29:44 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Hosting; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infra.EFCore; + +namespace IRaCIS.Core.Application.Contracts +{ + /// + ///受试者临床信息 + /// + [ApiExplorerSettings(GroupName = "Image")] + public class ClinicalDataService : BaseService, IClinicalDataService + { + public IRepository _previousOtherRepository { get; } + private readonly IRepository _previousHistoryRepository; + private readonly IRepository _previousSurgeryRepository; + private readonly IRepository _previousPdfRepository; + + + + public ClinicalDataService(IRepository previousHistoryRepository, + IRepository previousOtherRepository, + IRepository previousSurgeryRepository, + IRepository previousPDFRepository) + { + _previousOtherRepository = previousOtherRepository; + _previousHistoryRepository = previousHistoryRepository; + _previousSurgeryRepository = previousSurgeryRepository; + _previousPdfRepository = previousPDFRepository; + } + + + /// + /// 上传临床数据 + /// + /// + /// + /// + /// + [HttpPost("{trialId:guid}/{subjectVisitId:guid}")] + [DisableRequestSizeLimit] + public async Task UploadVisitClinicalData(IFormCollection formCollection, Guid subjectVisitId, [FromServices] IWebHostEnvironment _hostEnvironment) + { + + + var rootPath = Directory.GetParent(_hostEnvironment.ContentRootPath.TrimEnd('\\')).IfNullThrowException().FullName; + + //上传根路径 + var _fileStorePath = Path.Combine(rootPath, StaticData.TrialDataFolder); + + var sv = _repository.Where(t => t.Id == subjectVisitId).Select(t => new { t.TrialId, t.SiteId, t.SubjectId }).FirstOrDefault().IfNullThrowException(); + + string uploadFolderPath = Path.Combine(_fileStorePath, sv.TrialId.ToString(), + sv.SiteId.ToString(), sv.SubjectId.ToString(), subjectVisitId.ToString(), StaticData.TreatmenthistoryFolder); + + if (!Directory.Exists(uploadFolderPath)) + { + Directory.CreateDirectory(uploadFolderPath); + } + + foreach (IFormFile file in formCollection.Files) + { + var realName = file.FileName; + var fileNameEX = Path.GetExtension(realName); + var trustedFileNameForFileStorage = Guid.NewGuid().ToString() + fileNameEX; + + var relativePath = $"/{StaticData.TrialDataFolder}/{sv.TrialId}/{sv.SiteId}/{sv.SubjectId}/{subjectVisitId}/{StaticData.TreatmenthistoryFolder}/{trustedFileNameForFileStorage}"; + + var filePath = Path.Combine(uploadFolderPath, trustedFileNameForFileStorage); + + using (FileStream fs = System.IO.File.Create(filePath)) + { + await file.CopyToAsync(fs); + await fs.FlushAsync(); + } + + //插入临床pdf 路径 + await _previousPdfRepository.AddAsync(new PreviousPDF() { SubjectVisitId = subjectVisitId, Path = relativePath, FileName = realName }); + } + + + await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(); + + //return ResponseOutput.Ok(new + //{ + // FilePath = relativePath, + // FullFilePath = relativePath + "?access_token=" + _userInfo.UserToken + + //}); + } + + + + /// + /// 获取访视+受试者级别的数据 + /// + /// + /// + [HttpGet("{subjectVisitId:guid}")] + public async Task GetSubjectVisitClinicalData(Guid subjectVisitId) + { + + // 最完美方式 其中TrialSite 通过导航属性 两个字段连接出来 + + var clinicalObj = await _repository.Where(t => t.Id == subjectVisitId) + .ProjectTo(_mapper.ConfigurationProvider, new { subjectVisitId = subjectVisitId, token = _userInfo.UserToken }).FirstOrDefaultAsync().IfNullThrowException(); + + return clinicalObj; + + #region 老方式 linq join 而且需要映射 + + + //var query = from sv in _subjectVisitRepository.GetAll() + // join subject in _subjectRepository.GetAll() on sv.SubjectId equals subject.Id + // join trialSite in _trialSiteRepository.Find(t => t.TrialId == subjectClinicalDataQuery.TrialId) on sv.SiteId equals trialSite.SiteId + // select new SubjectClinicalDataDto() + // { + // SubjectId = sv.SubjectId, + // SubjectCode = subject.Code, + // SubjectVisitId = sv.Id, + // VisitName = sv.VisitName, + // VisitNum = sv.VisitNum, + // TrialSiteCode = trialSite.TrialSiteCode + // }; + + //var clinicalObj = query.FirstOrDefault(t => t.SubjectVisitId == subjectClinicalDataQuery.SubjectVisitId); + + //clinicalObj.PreviousHistoryList = _previousHistoryRepository.Find(t => t.SubjectVisitId == subjectClinicalDataQuery.SubjectVisitId) + // .ProjectTo(_mapper.ConfigurationProvider).ToList(); + //clinicalObj.PreviousOtherList = _previousOtherRepository.Find(t => t.SubjectVisitId == subjectClinicalDataQuery.SubjectVisitId) + // .ProjectTo(_mapper.ConfigurationProvider).ToList(); + //clinicalObj.PreviousSurgeryList = _previousSurgeryRepository.Find(t => t.SubjectVisitId == subjectClinicalDataQuery.SubjectVisitId) + // .ProjectTo(_mapper.ConfigurationProvider).ToList(); + + #endregion + + } + + + public async Task> GetPreviousHistoryList(PreviousHistoryQuery queryPreviousHistory) + { + + var previousHistoryQueryable = _previousHistoryRepository.WhereIf(queryPreviousHistory.IsSubjectLevel != null, t => t.IsSubjectLevel == queryPreviousHistory.IsSubjectLevel) + .ProjectTo(_mapper.ConfigurationProvider, new { token = _userInfo.UserToken }); + + return await previousHistoryQueryable.ToListAsync(); + } + + [HttpPost("{trialId:guid}")] + public async Task AddOrUpdatePreviousHistory(PreviousHistoryAddOrEdit addOrEditPreviousHistory) + { + + var entity = await _previousHistoryRepository.InsertOrUpdateAsync(addOrEditPreviousHistory, true); + return ResponseOutput.Ok(entity.Id); + } + + + [HttpDelete("{trialId:guid}/{previousHistoryId:guid}")] + public async Task DeletePreviousHistory(Guid previousHistoryId) + { + var success = await _previousHistoryRepository.DeleteFromQueryAsync(t => t.Id == previousHistoryId); + return ResponseOutput.Result(success); + } + + + public async Task> GetPreviousOtherList(PreviousOtherQuery queryPreviousOther) + { + var previousOtherQueryable = _previousOtherRepository.WhereIf(queryPreviousOther.IsSubjectLevel != null, t => t.IsSubjectLevel == queryPreviousOther.IsSubjectLevel) + .ProjectTo(_mapper.ConfigurationProvider, new { token = _userInfo.UserToken }); + return await previousOtherQueryable.ToListAsync(); + } + + + [HttpPost("{trialId:guid}")] + public async Task AddOrUpdatePreviousOther(PreviousOtherAddOrEdit addOrEditPreviousOther) + { + var entity = await _previousOtherRepository.InsertOrUpdateAsync(addOrEditPreviousOther, true); + return ResponseOutput.Ok(entity.Id); + + } + + + [HttpDelete("{trialId:guid}/{previousOtherId:guid}")] + public async Task DeletePreviousOther(Guid previousOtherId) + { + var success = await _previousOtherRepository.DeleteFromQueryAsync(t => t.Id == previousOtherId); + return ResponseOutput.Result(success); + } + + + public async Task> GetPreviousSurgeryList(PreviousSurgeryQuery queryPreviousSurgery) + { + var previousSurgeryQueryable = _previousSurgeryRepository.WhereIf(queryPreviousSurgery.IsSubjectLevel != null, t => t.IsSubjectLevel == queryPreviousSurgery.IsSubjectLevel) + .ProjectTo(_mapper.ConfigurationProvider, new { token = _userInfo.UserToken }); + + return await previousSurgeryQueryable.ToListAsync(); + } + + [HttpPost("{trialId:guid}")] + public async Task AddOrUpdatePreviousSurgery(PreviousSurgeryAddOrEdit addOrEditPreviousSurgery) + { + + var entity = await _previousSurgeryRepository.InsertOrUpdateAsync(addOrEditPreviousSurgery, true); + return ResponseOutput.Ok(entity.Id); + } + + + [HttpDelete("{trialId:guid}/{previousSurgeryId:guid}")] + public async Task DeletePreviousSurgery(Guid previousSurgeryId) + { + var success = await _previousSurgeryRepository.DeleteFromQueryAsync(t => t.Id == previousSurgeryId); + return ResponseOutput.Result(success); + } + + [HttpGet("{subjectVisitId:guid}")] + public async Task> GetPreviousPDFList(Guid subjectVisitId) + { + + var previousPDFQueryable = _previousPdfRepository.Where(t => t.SubjectVisitId == subjectVisitId).ProjectTo(_mapper.ConfigurationProvider); + + return await previousPDFQueryable.ToListAsync(); + } + + + public async Task AddOrUpdatePreviousPDF(PreviousPDFAddOrEdit addOrEditPreviousPDF) + { + + var entity = await _previousPdfRepository.InsertOrUpdateAsync(addOrEditPreviousPDF, true); + return ResponseOutput.Ok(entity.Id); + + } + + [HttpDelete("{trialId:guid}/{previousPDFId:guid}")] + public async Task DeletePreviousPDF(Guid previousPDFId) + { + + var success = await _previousPdfRepository.DeleteFromQueryAsync(t => t.Id == previousPDFId); + return ResponseOutput.Result(success); + } + + + + + } +} diff --git a/IRaCIS.Core.Application/Service/QC/DTO/NoneDicomStudyFileViewModel.cs b/IRaCIS.Core.Application/Service/QC/DTO/NoneDicomStudyFileViewModel.cs new file mode 100644 index 00000000..d79a8f58 --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/DTO/NoneDicomStudyFileViewModel.cs @@ -0,0 +1,55 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-06 10:56:50 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +namespace IRaCIS.Core.Application.Contracts +{ + /// NoneDicomStudyFileView 列表视图模型 + public class NoneDicomStudyFileView + { + public Guid Id { get; set; } + public string Path { get; set; } = string.Empty; + public string FileName { get; set; } = string.Empty; + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public Guid NoneDicomStudyId { get; set; } + + public string FullFilePath { get; set; } = string.Empty; + + } + + public class NoneDicomStudyAndFile + { + public string NoneDicomStudyCode { get; set; } = string.Empty; + + public List NoneDicomStudyFileList { get; set; } = new List(); + + } + + ///NoneDicomStudyFileQuery 列表查询参数模型 + public class NoneDicomStudyFileQuery + { + /// Path + public string Path { get; set; } = string.Empty; + + /// FileName + public string FileName { get; set; } = string.Empty; + + } + + /// NoneDicomStudyFileAddOrEdit 列表查询参数模型 + public class NoneDicomStudyFileAddOrEdit + { + public Guid Id { get; set; } + public string Path { get; set; } = string.Empty; + public string FileName { get; set; } = string.Empty; + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public Guid NoneDicomStudyId { get; set; } + } + + +} + + diff --git a/IRaCIS.Core.Application/Service/QC/DTO/NoneDicomStudyViewModel.cs b/IRaCIS.Core.Application/Service/QC/DTO/NoneDicomStudyViewModel.cs new file mode 100644 index 00000000..f4786ca1 --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/DTO/NoneDicomStudyViewModel.cs @@ -0,0 +1,68 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-06 10:56:50 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using System; +using System.Collections.Generic; +namespace IRaCIS.Core.Application.Contracts +{ + /// NoneDicomStudyView 列表视图模型 + public class NoneDicomStudyView + { + public string CodeView => "NST" + Code.ToString("D5"); + public int Code {get;set;} + public Guid Id { get; set; } + + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public Guid SubjectId { get; set; } + public Guid SubjectVisitId { get; set; } + public string BodyPart { get; set; } = string.Empty; + public string Modality { get; set; } = string.Empty; + public DateTime ImageDate { get; set; } + public DateTime UpdateTime { get; set; } + public Guid UpdateUserId { get; set; } + public string Description { get; set; } = string.Empty; + + public int FileCount { get; set; } + + public List NoneDicomStudyFileList { get; set; } = new List(); + + } + + ///NoneDicomStudyQuery 列表查询参数模型 + public class NoneDicomStudyQuery + { + /// BodyPart + public string BodyPart { get; set; } = string.Empty; + + /// Modality + public string Modality { get; set; } = string.Empty; + + /// Description + public string Description { get; set; } = string.Empty; + + } + + /// NoneDicomStudyAddOrEdit 列表查询参数模型 + public class NoneDicomStudyAddOrEdit + { + public Guid? Id { get; set; } + + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public Guid SubjectId { get; set; } + public Guid SubjectVisitId { get; set; } + public string BodyPart { get; set; } = string.Empty; + public string Modality { get; set; } = string.Empty; + public DateTime ImageDate { get; set; } + public string Description { get; set; } = string.Empty; + } + + +} + + diff --git a/IRaCIS.Core.Application/Service/QC/DTO/PreviousHistoryViewModel.cs b/IRaCIS.Core.Application/Service/QC/DTO/PreviousHistoryViewModel.cs new file mode 100644 index 00000000..6a061363 --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/DTO/PreviousHistoryViewModel.cs @@ -0,0 +1,106 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-22 11:27:53 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +namespace IRaCIS.Core.Application.Contracts +{ + + public class SubjectClinicalDataDto + { + public string SubjectCode { get; set; } = String.Empty; + + //public string SubjectName { get; set; } + + public string TrialSiteCode { get; set; } = String.Empty; + + public Guid SubjectVisitId { get; set; } + + public decimal VisitNum { get; set; } + + public string VisitName { get; set; } = String.Empty; + + public Guid SubjectId { get; set; } + + + public List PreviousHistoryList { get; set; } = new List(); + + public List PreviousOtherList { get; set; } = new List(); + + public List PreviousSurgeryList { get; set; } = new List(); + + public List PreviousPDFList { get; set; } = new List(); + } + + + public class SubjectClinicalDataQuery + { + + //public Guid TrialId { get; set; } + + //public Guid SubjectId { get; set; } + + //public Guid SubjectId { get; set; } + + + //public bool? IsSubjectLevel { get; set; } + + + public Guid SubjectVisitId { get; set; } + } + + + + + /// PreviousHistoryView 列表视图模型 + public class PreviousHistoryView + { + public Guid Id { get; set; } + public DateTime CreateTime { get; set; } + public string CreateUser { get; set; } = String.Empty; + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } + public int? IsPD { get; set; } + public Guid? SubjectVisitId { get; set; } + public bool IsSubjectLevel { get; set; } + public string Path { get; set; } = String.Empty; + public string FileName { get; set; } = String.Empty; + public string Position { get; set; } = String.Empty; + //public Guid SubjectId { get; set; } + public Guid CreateUserId { get; set; } + + public string FullFilePath { get; set; } = String.Empty; + + + } + + ///PreviousHistoryQuery 列表查询参数模型 + public class PreviousHistoryQuery + { + public bool? IsSubjectLevel { get; set; } + + public string Position { get; set; } = String.Empty; + + } + + public class PreviousHistoryAddOrEdit + { + + + public Guid? Id { get; set; } + + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } + public bool IsPD { get; set; } + public Guid SubjectVisitId { get; set; } + public bool IsSubjectLevel { get; set; } = true; + public string Path { get; set; } = String.Empty; + public string FileName { get; set; } = String.Empty; + public string Position { get; set; } = String.Empty; + //public Guid SubjectId { get; set; } + } + + +} + + diff --git a/IRaCIS.Core.Application/Service/QC/DTO/PreviousOtherViewModel.cs b/IRaCIS.Core.Application/Service/QC/DTO/PreviousOtherViewModel.cs new file mode 100644 index 00000000..392b53d1 --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/DTO/PreviousOtherViewModel.cs @@ -0,0 +1,57 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-22 11:27:53 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using System; +using System.Collections.Generic; +namespace IRaCIS.Core.Application.Contracts +{ + /// PreviousOtherView 列表视图模型 + public class PreviousOtherView + { + public Guid Id { get; set; } + public DateTime CreateTime { get; set; } + public string CreateUser { get; set; } = String.Empty; + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } + public bool IsPD { get; set; } + public Guid SubjectVisitId { get; set; } + public bool IsSubjectLevel { get; set; } + public string Path { get; set; } = String.Empty; + public string FileName { get; set; } = String.Empty; + public string TreatmentType { get; set; } = String.Empty; + //public Guid SubjectId { get; set; } + public Guid CreateUserId { get; set; } + + public string FullFilePath { get; set; } = String.Empty; + } + + ///PreviousOtherQuery 列表查询参数模型 + public class PreviousOtherQuery + { + public bool? IsSubjectLevel { get; set; } + + public string TreatmentType { get; set; } = String.Empty; + + } + + public class PreviousOtherAddOrEdit + { + public Guid? Id { get; set; } + + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } + public bool IsPD { get; set; } + public Guid? SubjectVisitId { get; set; } + public bool IsSubjectLevel { get; set; } = true; + public string Path { get; set; }=String.Empty; + public string FileName { get; set; } = String.Empty; + public string TreatmentType { get; set; } = String.Empty; + //public Guid SubjectId { get; set; } + } + + +} + + diff --git a/IRaCIS.Core.Application/Service/QC/DTO/PreviousSurgeryViewModel.cs b/IRaCIS.Core.Application/Service/QC/DTO/PreviousSurgeryViewModel.cs new file mode 100644 index 00000000..11e5e60d --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/DTO/PreviousSurgeryViewModel.cs @@ -0,0 +1,73 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-22 11:27:53 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +namespace IRaCIS.Core.Application.Contracts +{ + /// PreviousSurgeryView 列表视图模型 + public class PreviousSurgeryView + { + public Guid Id { get; set; } + public DateTime CreateTime { get; set; } + public string CreateUser { get; set; } = string.Empty; + public DateTime? OperationTime { get; set; } + public Guid SubjectVisitId { get; set; } + public bool IsSubjectLevel { get; set; } + public string Path { get; set; } = string.Empty; + public string FileName { get; set; } = string.Empty; + public string OperationName { get; set; } = string.Empty; + //public Guid SubjectId { get; set; } + public Guid CreateUserId { get; set; } + + public string FullFilePath { get; set; } = string.Empty; + } + + ///PreviousSurgeryQuery 列表查询参数模型 + public class PreviousSurgeryQuery + { + public bool? IsSubjectLevel { get; set; } + public string OperationName { get; set; } = string.Empty; + + } + + public class PreviousSurgeryAddOrEdit + { + public Guid? Id { get; set; } + + public DateTime? OperationTime { get; set; } + public Guid? SubjectVisitId { get; set; } + public bool IsSubjectLevel { get; set; } = true; + public string Path { get; set; } = String.Empty; + public string FileName { get; set; } = String.Empty; + public string OperationName { get; set; } = string.Empty; + //public Guid SubjectId { get; set; } + } + + public class PreviousPDFView + { + public Guid Id { get; set; } + public DateTime CreateTime { get; set; } + public Guid SubjectVisitId { get; set; } + public string Path { get; set; } = string.Empty; + public string FileName { get; set; } = string.Empty; + public Guid CreateUserId { get; set; } + + public string FullFilePath { get; set; } = string.Empty; + } + + + + /// PreviousPDFAddOrEdit 列表查询参数模型 + public class PreviousPDFAddOrEdit + { + public Guid? Id { get; set; } + public Guid SubjectVisitId { get; set; } + public string Path { get; set; } = string.Empty; + public string FileName { get; set; } = string.Empty; + } + + +} + + diff --git a/IRaCIS.Core.Application/Service/QC/DTO/QARecordViewModel.cs b/IRaCIS.Core.Application/Service/QC/DTO/QARecordViewModel.cs new file mode 100644 index 00000000..a29ca59f --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/DTO/QARecordViewModel.cs @@ -0,0 +1,484 @@ +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Domain.Share; +using Newtonsoft.Json; + +namespace IRaCIS.Core.Application.Contracts.DTO +{ + + + public class VisitQACommand + { + public QCChallengeCommand QARecord { get; set; }=new QCChallengeCommand(); + + public List QATrialTemplateItemList { get; set; } = new List(); + } + + + + + + + + + + public class QARecordTrialTemplateItemCommand + { + public int ShowOrder { get; set; } + public string TemplateItemName { get; set; } = String.Empty; + public Guid QATrialTemplateItemId { get; set; } + public string Note { get; set; } = String.Empty; + public QAItemStatus Status { get; set; } = QAItemStatus.Undefined; + + } + + public class QARecordTrialTemplateItemDTO : QARecordTrialTemplateItemCommand + { + public Guid QARecordId { get; set; } + } + + + + public class QCQuestionAnswerCommand + { + public Guid? Id { get; set; } + public string Answer { get; set; } = String.Empty; + + //public string ChildAnswer { get; set; } = String.Empty; + + public Guid? TrialQCQuestionConfigureId { get; set; } + + } + + + public class QCQuestionAnswerItemDto + { + //public string ChildAnswer { get; set; } = String.Empty; + + [JsonIgnore] + public List QCQuestionAnswerList { get; set; } = new List(); + + + public TrialQCProcess? QCProcessEnum { get; set; } + + // 1代表第一个人QC数据 2 代表第二个人QC数据 + public CurrentQC? CurrentQCEnum { get; set; } + + public Guid? SubjectVisitId { get; set; } + + public Guid? Id { get; set; } + public string Answer { get; set; } = String.Empty; + + public Guid TrialQCQuestionConfigureId { get; set; } + public Guid TrialId { get; set; } + public string QuestionName { get; set; } = String.Empty; + public bool IsRequired { get; set; } + public bool IsEnable { get; set; } + public string Type { get; set; } = String.Empty; + public Guid? ParentId { get; set; } + public string TypeValue { get; set; } = String.Empty; + public string ParentTriggerValue { get; set; } = String.Empty; + public int ShowOrder { get; set; } + + public int? ParentShowOrder { get; set; } + + public bool IsChild => ParentId != null; + } + + + public class QATrialTemplateItemDto + { + public Guid QATrialTemplateId { get; set; } + public string TemplateItemName { get; set; } = String.Empty; + public Guid QATrialTemplateItemId { get; set; } + public int ShowOrder { get; set; } + } + + + public class QARecordItemQuery + { + public Guid QaTrialTemplateId { get; set; } + public Guid? QaRecordId { get; set; } + + } + + public class QATemplateQuery + { + public Guid TrialId { get; set; } + + public string Modalities { get; set; } = String.Empty; + } + + public class QCChallengeCommand + { + public Guid? Id { get; set; } + + public string ChallengeType { get; set; } = String.Empty; + + public Guid SubjectVisitId { get; set; } + + public string Content { get; set; } = string.Empty; + + public string ActionContent { get; set; } = string.Empty; + + public QCChanllengeReuploadEnum ReuploadEnum { get; set; } + + + public DateTime? DeadlineTime { get; set; } + + } + + public class QCChanllengeCreatorDto + { + public Guid CreateUserId { get; set; } + + public string Creator { get; set; } = String.Empty; + + public string CreatorRealName { get; set; } = String.Empty; + + //public string FirstName { get; set; } + //public string LastName { get; set; } + + } + + public class ParticipantDTO + { + public Guid HandleUserId { get; set; } + public string HandleUser { get; set; } = String.Empty; + + public string HandleUserRealName { get; set; } = String.Empty; + } + + public class QCChallengeWithUser : QCChallengeCommand + { + public Guid SubjectId { get; set; } + public UserTypeEnum UserTypeEnum { get; set; } + public Guid CreateUserId { get; set; } + public string ChallengeCode { get; set; } = String.Empty; + + public DateTime? LatestMsgTime { get; set; } + + public string LatestReplyUser { get; set; } = String.Empty; + + public DateTime? ClosedTime { get; set; } + + public bool IsClosed { get; set; } + public DateTime? ReUploadedTime { get; set; } + public string CreateUser { get; set; } = String.Empty; + public DateTime CreateTime { get; set; } + + public bool IsOverTime => IsClosed ? ClosedTime > DeadlineTime : DateTime.Now > DeadlineTime; + + public string ChallengeDuration + { + get + { + if (!ClosedTime.HasValue) + return ""; + else return string.Format("{0}天{1}小时{2}分钟", (ClosedTime - CreateTime)?.Days, (ClosedTime - CreateTime)?.Hours, (ClosedTime - CreateTime)?.Minutes); + } + } + + + public QCChanllengeDialogDTO[] DialogList { get; set; } = new QCChanllengeDialogDTO[0]; + + //public int ReplyCount { get; set; } + + } + + public class CRCChallengeAndDialog + { + public QCChallengeWithUser QCChallenge { get; set; } = new QCChallengeWithUser(); + + public List DialogList { get; set; } = new List(); + } + + + public class HistoryGroupDTO + { + public DateTime CreateTime { get; set; } + + public string IQA { get; set; } = string.Empty; + + public DateTime? IQADeadline { get; set; } + + public string IQANote { get; set; } = string.Empty; + + public DateTime? IQACreateTime { get; set; } + + + public List TemplateItems { get; set; } = new List(); + } + + public class TemplateItemProblem + { + [JsonIgnore] + public string IQA { get; set; } = string.Empty; + [JsonIgnore] + public DateTime? IQADeadline { get; set; } + [JsonIgnore] + public string IQANote { get; set; } = string.Empty; + [JsonIgnore] + public DateTime? IQACreateTime { get; set; } + + public Guid QARecordId { get; set; } + public int ShowOrder { get; set; } + public string TemplateItemName { get; set; } = String.Empty; + public string Note { get; set; } = String.Empty; + public QAItemStatus Status { get; set; } = QAItemStatus.Undefined; + public DateTime CreateTime { get; set; } = DateTime.Now; + } + + + public class TrialVisitQADTO + { + + public SubjectClinicalDataDto SubjectClinicalData { get; set; } = new SubjectClinicalDataDto(); + + public List NoneDicomStudyList { get; set; } = new List(); + + public List QCQuestionAnswerList { get; set; } = new List(); + + public List StudyList { get; set; } = new List(); + + public List SeriesList { get; set; } = new List(); + + public QARelationInfo RelationInfo { get; set; } = new QARelationInfo(); + + } + + + public class QAStudySeriesInfo + { + public List StudyList { get; set; } = new List(); + + public List SeriesList { get; set; } = new List(); + } + + + public class QADictionaryCommand + { + + public string Note { get; set; } = String.Empty; + public Guid DictionaryId { get; set; } + + } + + public class QADictionaryDTO : QADictionaryCommand + { + public string DictionaryValue { get; set; } = String.Empty; + } + + public class QADicView + { + public Guid QARecordId { get; set; } + public string DictionaryValue { get; set; } = String.Empty; + public string Note { get; set; } = String.Empty; + public Guid DictionaryId { get; set; } + } + + + + + + public class QARecordDTO + { + public QCChallengeWithUser QARecord { get; set; } = new QCChallengeWithUser(); + + public List QADictionaryList { get; set; } = new List(); + + } + + public class QARelationInfo + { + public int TrialClinicalInformationTransmissionEnum { get; set; } + public int TrialChangeDefalutDays { get; set; } + public TrialQCProcess TrialQCProcessEnum { get; set; } + public string TrialChallengeTypes { get; set; } = String.Empty; + public string TrialBodyPartTypes { get; set; } = String.Empty; + public string TrialImageQCSignText { get; set; } = String.Empty; + public string TrialModalitys { get; set; } = String.Empty; + + //public string FirstName { get; set; } + //public string LastName { get; set; } + //public string Modalities { get; set; } + + public Guid SiteId { get; set; } + public string SiteName { get; set; } = String.Empty; + public Guid SubjectId { get; set; } + public Guid SubjectVisitId { get; set; } + + public string SubjectCode { get; set; } = String.Empty; + public string SubjectName { get; set; } = String.Empty; + public int SubjectAge { get; set; } + public string SubjectSex { get; set; } = String.Empty; + + + public decimal VisitNum { get; set; } + public string VisitName { get; set; } = string.Empty; + //public DateTime? SVSTDTC { get; set; } + //public DateTime? SVENDTC { get; set; } + public bool InPlan { get; set; } + public int? VisitDay { get; set; } + public bool IsBaseLine { get; set; } = false; + public string AnonymousVisitName { get; set; } = string.Empty; + public int? VisitWindowLeft { get; set; } + public int? VisitWindowRight { get; set; } + + public DateTime? SubjectFirstGiveMedicineTime { get; set; } + + public bool IsHaveFirstGiveMedicineDate { get; set; } = true; + + + + + public int? TotalChallengeCount { get; set; } + + public int? NotClosedChallengeCount { get; set; } + + + + } + public class QAStudyInfoDTO + { + + public bool IsDeleted { get; set; } + public string StudyInstanceUid { get; set; } = string.Empty; + + public Guid TrialId { get; set; } + + public Guid StudyId { get; set; } + + public int StudyStatus { get; set; } + + public string StudyCode { get; set; } = string.Empty; + + public string Modalities { get; set; } = String.Empty; + + public int SeriesCount { get; set; } + + public int InstanceCount { get; set; } + + public string Uploader { get; set; } = string.Empty; + public DateTime? StudyTime { get; set; } + + public DateTime? UploadedTime { get; set; } + public string BodyPartExamined { get; set; } = String.Empty; + + public string BodyPartForEdit { get; set; } = String.Empty; + + + + //public string PatientName { get; set; } = string.Empty; + //public string PatientAge { get; set; } = string.Empty; + //public string PatientSex { get; set; } = string.Empty; + + //public string Comment { get; set; } + //public string QAComment { get; set; } + + //public string UploaderFirstName { get; set; } + //public string UploaderLastName { get; set; } + + + + } + + public class QASeriesInfoDto + { + public string StudyCode { get; set; } = string.Empty; + public Guid Id { get; set; } + public Guid StudyId { get; set; } + + public string BodyPartForEdit { get; set; } = String.Empty; + + + public string SeriesInstanceUid { get; set; } = String.Empty; + public int SeriesNumber { get; set; } + public DateTime SeriesTime { get; set; } + public string Modality { get; set; } = String.Empty; + public string Description { get; set; } = String.Empty; + public int InstanceCount { get; set; } + + public DateTime CreateTime { get; set; } + + public string BodyPartExamined { get; set; } = String.Empty; + + public bool IsReading { get; set; } + + public bool IsDeleted { get; set; } + + //public bool IsDicomData { get; set; } = true; + + public Guid[] InstanceList = new Guid[0]; + } + + + + public class QADialogCommand + { + public string TalkContent { get; set; } = String.Empty; + public Guid QCChallengeId { get; set; } + + public Guid SubjectVisitId { get; set; } + } + + + public class CheckChallengeDialogCommand + { + public string TalkContent { get; set; } = String.Empty; + + public Guid SubjectVisitId { get; set; } + } + + + public class CheckChanllengeDialogDTO + { + public Guid Id { get; set; } + public string TalkContent { get; set; } = String.Empty; + + public DateTime CreateTime { get; set; } + + public Guid CreateUserId { get; set; } + + public string CreateUser { get; set; } = String.Empty; + + public bool IsCurrentUser { get; set; } + + public UserTypeEnum UserTypeEnum { get; set; } + } + + public class QCChanllengeDialogDTO : CheckChanllengeDialogDTO + { + public Guid QCChallengeId { get; set; } + + } + + + public class DialogDTO + { + public Guid Id { get; set; } + public string TalkContent { get; set; } = String.Empty; + public string From { get; set; } = String.Empty; + public string To { get; set; } = string.Empty; + public Guid UserId { get; set; } + + public string PreviousContent { get; set; } = String.Empty; + + public bool HasReply { get; set; } + + public DateTime CreateTime { get; set; } + } + + public class DialogNode + { + public Guid Id { get; set; } + public Guid ParentId { get; set; } + public Guid UserId { get; set; } + public string UserName { get; set; } = String.Empty; + public bool HasReply { get; set; } + + public string TalkContent { get; set; } = String.Empty; + //public Guid QARecordId { get; set; } + public DateTime CreateTime { get; set; } + } + +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/QC/DTO/QCListViewModel.cs b/IRaCIS.Core.Application/Service/QC/DTO/QCListViewModel.cs new file mode 100644 index 00000000..dc871303 --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/DTO/QCListViewModel.cs @@ -0,0 +1,422 @@ +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infrastructure.Extention; +using System.ComponentModel.DataAnnotations; + +namespace IRaCIS.Core.Application.Contracts +{ + + public class CRCVisitSearchDTO : PageInput + { + + public AuditStateEnum[]? AuditStateArray { get; set; } + public SubmitStateEnum? SubmitState { get; set; } + public ChallengeStateEnum? ChallengeState { get; set; } + + [NotDefault] + public Guid TrialId { get; set; } + public Guid? SiteId { get; set; } + public Guid? SubjectId { get; set; } + + public bool? IsUrgent { get; set; } + public string SubjectInfo { get; set; } = string.Empty; + public string VisitPlanInfo { get; set; } = string.Empty; + + } + + public class QCVisitSearchDTO : PageInput + { + + //public AuditStateEnum? AuditState { get; set; } + + public AuditStateEnum[]? AuditStateArray { get; set; } + + public bool? IsUrgent { get; set; } + public Guid TrialId { get; set; } + public Guid? SiteId { get; set; } + public Guid? SubjectId { get; set; } + + public string SubjectInfo { get; set; } = String.Empty; + public string VisitPlanInfo { get; set; } = String.Empty; + + public Guid? HandleUserId { get; set; } + } + + + public class CRCRequestToQCCommand + { + [NotDefault] + public Guid TrialId { get; set; } + + public Guid? SignId { get; set; } + + public Guid[] SubjectVisitIds { get; set; }=new Guid[0]; + } + + + + public class CRCReuploadFinishedCommand + { + [NotDefault] + public Guid TrialId { get; set; } + + [NotDefault] + public Guid QCChallengeId { get; set; } + + public bool SetOrCancel { get; set; } + + public Guid? SignId { get; set; } + } + + public class UploadSubjectAndVisitCommand + { + public Guid SubjectVisitId { get; set; } + public Guid SubjectId { get; set; } + public PDStateEnum PDState { get; set; } + + //前端有可能不传递 不是基线的时候 + public bool? IsEnrollmentConfirm { get; set; } + public DateTime? SubjectFirstGiveMedicineTime { get; set; } + + } + + public class TrialSubjectAndSVConfig + { + public bool IsHaveFirstGiveMedicineDate { get; set; } + public bool IsSubjectSexView { get; set; } = false; + public bool IsSubjectExpeditedView { get; set; } = false; + + public bool IsEnrollementQualificationConfirm { get; set; } + public bool IsPDProgressView { get; set; } + + + public string OutEnrollmentVisitName { get; set; } = String.Empty; + + public string BodyPartTypes { get; set; } = String.Empty; + + public string Modalitys { get; set; } = String.Empty; + public string DocumentConfirmSignText { get; set; } = String.Empty; + + public string ImageQCSignText { get; set; } = String.Empty; + + public string PreliminaryAuditReuploadText { get; set; } = string.Empty; + + public string ReviewAuditReuploadText { get; set; } = string.Empty; + + public string CheckBackText { get; set; } = String.Empty; + + public string CheckPassText { get; set; } = String.Empty; + + public int ClinicalInformationTransmissionEnum { get; set; } + + public TrialQCProcess QCProcessEnum { get; set; } + + } + + + + public class QCCRCVisitViewModel : QCVisitBasicListViewModel + { + public Guid? OutPlanPreviousVisitId { get; set; } + public bool IsOutEnromentVisit { get; set; } + + public PDStateEnum PDState { get; set; } + public bool IsEnrollmentConfirm { get; set; } + public DateTime? SubjectFirstGiveMedicineTime { get; set; } + + public SubjectStatus SubjectStatus { get; set; } + public string SubjectCode { get; set; } = String.Empty; + + //public int? StudyCount { get; set; } + public String TrialSiteCode { get; set; } = String.Empty; + + public DateTime? SubmitTime { get; set; } + public AuditStateEnum AuditState { get; set; } + + public ChallengeStateEnum ChallengeState { get; set; } + + public int? DicomStudyCount { get; set; } + public int? NoneDicomStudyCount { get; set; } + + public bool IsHaveClinicalData { get; set; } + + } + + + public class ChallengeQuery : PageInput + { + [NotDefault] + public Guid TrialId { get; set; } + + public string VisitPlanInfo { get; set; } = String.Empty; + public string SubjectCode { get; set; } = String.Empty; + public QCChanllengeReuploadEnum? ReuploadEnum { get; set; } + public bool? IsUrgent { get; set; } + public Guid? SubjectId { get; set; } + public Guid? SiteId { get; set; } + public bool? IsClosed { get; set; } + public bool? IsOverTime { get; set; } + public Guid? CreateUserId { get; set; } + + } + + public class QCCRCChallengeViewModel + { + + public bool IsBaseLine { get; set; } + public bool IsUrgent { get; set; } + public Guid? ClinicalDataSignUserId { get; set; } + + //public string CreatorFirstName { get; set; } + //public string CreatorLastName { get; set; } + public string BlindName { get; set; } = String.Empty; + public string Creator { get; set; } = String.Empty; + + //public string ReplyerRealName { get; set; } + + public DateTime? ReUploadedTime { get; set; } + public Guid SubjectVisitId { get; set; } + public Guid SubjectId { get; set; } + public Guid Id { get; set; } + public String TrialSiteCode { get; set; } = String.Empty; + + public string SubjectCode { get; set; } = String.Empty; + + public decimal VisitNum { get; set; } + public string VisitName { get; set; } = string.Empty; + + public RequestBackStateEnum RequestBackState { get; set; } + + public bool IsClosed { get; set; } + + public DateTime? ClosedTime { get; set; } + + public string ClosedUser { get; set; } = String.Empty; + + public DateTime? CreateTime { get; set; } + + public Guid CreateUserId { get; set; } + + public string CreateUser { get; set; } = String.Empty; + + //public bool NeedReUpload { get; set; } + public QCChanllengeReuploadEnum ReuploadEnum { get; set; } + public DateTime? DeadlineTime { get; set; } + + public string ChallengeCode { get; set; } = String.Empty; + + public string ChallengeType { get; set; } = string.Empty; + + public string Note { get; set; } = string.Empty; + + + public bool IsOverTime => IsClosed ? ClosedTime > DeadlineTime : DateTime.Now > DeadlineTime; + + public Guid LatestReplyUserId { get; set; } + + + public string LatestReplyUser { get; set; } = String.Empty; + + public string Content { get; set; } = string.Empty; + + public UserTypeEnum UserTypeEnum { get; set; } + + + public DateTime? LatestMsgTime { get; set; } + + public string ChallengeDuration + { + get + { + if (!ClosedTime.HasValue) + return ""; + else return string.Format("{0}天{1}小时{2}分钟", (ClosedTime - CreateTime)?.Days, (ClosedTime - CreateTime)?.Hours, (ClosedTime - CreateTime)?.Minutes); + } + } + + + } + + public class CheckQuery : PageInput + { + public string SubjectInfo { get; set; } = String.Empty; + public string VisitPlanInfo { get; set; } = String.Empty; + + //核查状态 + public CheckStateEnum? CheckState { get; set; } + + public Guid TrialId { get; set; } + + public Guid? SiteId { get; set; } + + //public bool? IsClosed { get; set; } + } + + + public class ForwardQuery : PageInput + { + public Guid TrialId { get; set; } + + public Guid? SiteId { get; set; } + public string SubjectInfo { get; set; } = String.Empty; + public string VisitPlanInfo { get; set; } = String.Empty; + + public ForwardStateEnum? ForwardState { get; set; } + } + + public class ForWardViewModel + { + public Guid Id { get; set; } + public string BlindName { get; set; } = String.Empty; + public String TrialSiteCode { get; set; } = String.Empty; + public string SubjectCode { get; set; } = String.Empty; + + public decimal VisitNum { get; set; } + public string VisitName { get; set; } = string.Empty; + + public Guid? ForwardUserId { get; set; } + + public string ForwardUser { get; set; } = String.Empty; + + public DateTime? ForwardTime { get; set; } + + public ForwardStateEnum ForwardState { get; set; } + + public CheckStateEnum CheckState { get; set; } + + + } + + public class QCCheckViewModel + { + public string BlindName { get; set; } = String.Empty; + public bool IsUrgent { get; set; } + + public DateTime? CheckPassedTime { get; set; } + public Guid Id { get; set; } + + public Guid? CheckChallengeLatestUserId { get; set; } + + public AuditStateEnum AuditState { get; set; } + + public CheckStateEnum CheckState { get; set; } + public String TrialSiteCode { get; set; } = String.Empty; + public string SubjectCode { get; set; } = String.Empty; + + public decimal VisitNum { get; set; } + public string VisitName { get; set; } = string.Empty; + + //public string Modality { get; set; } + + public RequestBackStateEnum RequestBackState { get; set; } + + public DateTime? StudyTime { get; set; } + public DateTime? CheckTime { get; set; } + public CheckChanllengeTypeEnum CheckChallengeState { get; set; } + + public bool? IsCheckBack { get; set; } + + public string CheckResult { get; set; } = String.Empty; + } + + public class QCVisitViewModel : QCVisitBasicListViewModel + { + public int? DicomStudyCount { get; set; } + public int? NoneDicomStudyCount { get; set; } + public bool IsHaveClinicalData { get; set; } + + public SubjectStatus SubjectStatus { get; set; } + public int? StudyCount { get; set; } + + public string SubjectCode { get; set; } = String.Empty; + public String TrialSiteCode { get; set; } = String.Empty; + + public ChallengeStateEnum ChallengeState { get; set; } + + + public int? ChallengeCount { get; set; } + + + public AuditStateEnum AuditState { get; set; } + + public DateTime? SubmitTime { get; set; } + + public string CurrentActionUser { get; set; } = String.Empty; + public string PreliminaryAuditUser { get; set; } = String.Empty; + + + public string ReviewAuditUser { get; set; } = String.Empty; + public DateTime? ReviewAuditTime { get; set; } + public DateTime? PreliminaryAuditTime { get; set; } + + public bool IsEnrollmentConfirm { get; set; } = false; + public DateTime? SubjectFirstGiveMedicineTime { get; set; } + + public PDStateEnum PDState { get; set; } = PDStateEnum.None; + } + + //public class QCHistoryChallengeViewModel: QCCRCChallengeViewModel + //{ + + + + //} + + public class QCChallengeViewModel : QCCRCChallengeViewModel + { + + + } + + + + public class QCVisitBasicListViewModel + { + + public bool? IsConfirmedClinicalData { get; set; } + public bool IsQCConfirmedReupload { get; set; } + + public bool IsFinalVisit { get; set; } + public string BlindName { get; set; } = String.Empty; + public bool IsUrgent { get; set; } + //public bool IsReuploaded { get; set; } + //public bool NeedReUpload { get; set; } + public QCChanllengeReuploadEnum ReuploadEnum { get; set; } + public Guid? Id { get; set; } + public Guid TrialId { get; set; } + public Guid SubjectId { get; set; } + public Guid SiteId { get; set; } + public bool InPlan { get; set; } = true; + public int VisitExecuted { get; set; } + public decimal VisitNum { get; set; } + public string VisitName { get; set; } = string.Empty; + public int VisitDay { get; set; } + public string SVUPDES { get; set; } = string.Empty; + public DateTime? SVSTDTC { get; set; } + public DateTime? SVENDTC { get; set; } + + public TrialQCProcess QCProcessEnum { get; set; } + public DateTime? EarliestScanDate { get; set; } + public DateTime? LatestScanDate { get; set; } + + public bool IsBaseLine { get; set; } = false; + public string AnonymousVisitName { get; set; } = string.Empty; + public int VisitWindowLeft { get; set; } + public int VisitWindowRight { get; set; } + + public bool IsTake { get; set; } + public Guid? CurrentActionUserId { get; set; } + public Guid? PreliminaryAuditUserId { get; set; } + public DateTime? CurrentActionUserExpireTime { get; set; } + + public SubmitStateEnum SubmitState { get; set; } + + public RequestBackStateEnum RequestBackState { get; set; } + + public bool IsLostVisit { get; set; } + + //public Guid? ClinicalDataSignUserId { get; set; } + } + + + +} diff --git a/IRaCIS.Core.Application/Service/QC/DTO/QCQuestionConfigureViewModel.cs b/IRaCIS.Core.Application/Service/QC/DTO/QCQuestionConfigureViewModel.cs new file mode 100644 index 00000000..70bd2828 --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/DTO/QCQuestionConfigureViewModel.cs @@ -0,0 +1,63 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-11 11:48:52 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using System; +using System.Collections.Generic; +namespace IRaCIS.Core.Application.Contracts +{ + /// QCQuestionConfigureView 列表视图模型 + public class QCQuestionConfigureView + { + public Guid Id { get; set; } + public string QuestionName { get; set; } = String.Empty; + public bool IsRequired { get; set; } + public bool IsEnable { get; set; } + public string Type { get; set; } = String.Empty; + public string ParentTriggerValue { get; set; } + public Guid? ParentId { get; set; } + public string TypeValue { get; set; } = String.Empty; + public int ShowOrder { get; set; } + + public int? ParentShowOrder { get; set; } + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } + public DateTime UpdateTime { get; set; } + public Guid UpdateUserId { get; set; } + } + + ///QCQuestionQuery 列表查询参数模型 + public class QCQuestionQuery + { + /// QuestionName + public string QuestionName { get; set; } = String.Empty; + + /// TypeValue + public string Type { get; set; } = String.Empty; + + } + + /// QCQuestionAddOrEdit 列表查询参数模型 + public class QCQuestionAddOrEdit + { + public Guid? Id { get; set; } + public string QuestionName { get; set; } = String.Empty; + public bool IsRequired { get; set; } + public bool IsEnable { get; set; } + public string Type { get; set; } = String.Empty; + + public string ParentTriggerValue { get; set; } + public Guid? ParentId { get; set; } + + + public string TypeValue { get; set; } = String.Empty; + public string ChildInvalidValue { get; set; } = String.Empty; + public int ShowOrder { get; set; } + + } + + +} + + diff --git a/IRaCIS.Core.Application/Service/QC/DTO/TrialQCQuestionConfigureViewModel.cs b/IRaCIS.Core.Application/Service/QC/DTO/TrialQCQuestionConfigureViewModel.cs new file mode 100644 index 00000000..83fee8e9 --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/DTO/TrialQCQuestionConfigureViewModel.cs @@ -0,0 +1,93 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-11 11:48:52 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using System.ComponentModel.DataAnnotations; + +namespace IRaCIS.Core.Application.Contracts +{ + /// TrialQCQuestionConfigureView 列表视图模型 + public class TrialQCQuestionConfigureView + { + public Guid Id { get; set; } + public Guid TrialId { get; set; } + public string QuestionName { get; set; } = string.Empty; + public bool IsRequired { get; set; } + public bool IsEnable { get; set; } + public string Type { get; set; } = string.Empty; + public string TypeValue { get; set; } = string.Empty; + + public Guid? ParentId { get; set; } + public string ParentTriggerValue { get; set; } = string.Empty; + public int? ParentShowOrder { get; set; } + + public int ShowOrder { get; set; } + + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } + public DateTime UpdateTime { get; set; } + public Guid UpdateUserId { get; set; } + + } + + + public class TrialQCQuestionFilterSelect + { + [NotDefault] + public Guid TrialId { get; set; } + + + public Guid? Id { get; set; } + + public string[] TypeArray { get; set; }=new string[0]; + } + + public class TrialQCQuestionSelect + { + public Guid Id { get; set; } + public string QuestionName { get; set; } = string.Empty; + + public Guid? ParentId { get; set; } + public int ShowOrder { get; set; } + + public string TypeValue { get; set; } + } + + ///TrialQCQuestionQuery 列表查询参数模型 + public class TrialQCQuestionQuery + { + + public Guid TrialId { get; set; } + /// QuestionName + public string QuestionName { get; set; } = string.Empty; + + /// TypeValue + public string Type { get; set; }=String.Empty; + + + } + + /// TrialQCQuestionAddOrEdit 列表查询参数模型 + public class TrialQCQuestionAddOrEdit:TrialQCQuestionConfigureBatchAdd + { + public Guid? Id{ get; set; } + public Guid TrialId { get; set; } + + } + + public class TrialQCQuestionConfigureBatchAdd + { + + public string QuestionName { get; set; } = string.Empty; + public bool IsRequired { get; set; } + public bool IsEnable { get; set; } + public string Type { get; set; } = string.Empty; + public Guid? ParentId { get; set; } + public string TypeValue { get; set; } = string.Empty; + public string ParentTriggerValue { get; set; } = string.Empty; + public int ShowOrder { get; set; } + } +} + + diff --git a/IRaCIS.Core.Application/Service/QC/Interface/IClinicalDataService.cs b/IRaCIS.Core.Application/Service/QC/Interface/IClinicalDataService.cs new file mode 100644 index 00000000..ae4b81cc --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/Interface/IClinicalDataService.cs @@ -0,0 +1,30 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-22 11:29:44 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Core.Application.Contracts +{ + public interface IClinicalDataService + { + Task AddOrUpdatePreviousHistory(PreviousHistoryAddOrEdit addOrEditPreviousHistory); + Task AddOrUpdatePreviousOther(PreviousOtherAddOrEdit addOrEditPreviousOther); + Task AddOrUpdatePreviousPDF(PreviousPDFAddOrEdit addOrEditPreviousPDF); + Task AddOrUpdatePreviousSurgery(PreviousSurgeryAddOrEdit addOrEditPreviousSurgery); + Task DeletePreviousHistory(Guid previousHistoryId); + Task DeletePreviousOther(Guid previousOtherId); + Task DeletePreviousPDF(Guid previousPDFId); + Task DeletePreviousSurgery(Guid previousSurgeryId); + Task> GetPreviousHistoryList(PreviousHistoryQuery queryPreviousHistory); + Task> GetPreviousOtherList(PreviousOtherQuery queryPreviousOther); + Task> GetPreviousPDFList(Guid subjectVisitId); + Task> GetPreviousSurgeryList(PreviousSurgeryQuery queryPreviousSurgery); + Task GetSubjectVisitClinicalData(Guid subjectVisitId); + Task UploadVisitClinicalData(IFormCollection formCollection, Guid subjectVisitId, [FromServices] IWebHostEnvironment _hostEnvironment); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/QC/Interface/INoneDicomStudyService.cs b/IRaCIS.Core.Application/Service/QC/Interface/INoneDicomStudyService.cs new file mode 100644 index 00000000..7ceb3638 --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/Interface/INoneDicomStudyService.cs @@ -0,0 +1,23 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-06 10:54:55 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Core.Application.Contracts +{ + public interface INoneDicomStudyService + { + Task AddOrUpdateNoneDicomStudy(NoneDicomStudyAddOrEdit addOrEditNoneDicomStudy); + Task DeleteNoneDicomStudy(Guid noneDicomStudyId); + Task DeleteNoneDicomStudyFile(Guid noneDicomStudyFileId); + Task> GetNoneDicomStudyFileList(Guid noneDicomStudyId); + Task> GetNoneDicomStudyList(Guid subjectVisitId); + Task> GetVisitNoneDicomStudyFileList(Guid subjectVisitId); + Task UploadNoneDicomFile(IFormCollection formCollection, Guid subjectVisitId, Guid noneDicomStudyId, [FromServices] IWebHostEnvironment _hostEnvironment); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/QC/Interface/IQCListService.cs b/IRaCIS.Core.Application/Service/QC/Interface/IQCListService.cs new file mode 100644 index 00000000..d1a0dde5 --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/Interface/IQCListService.cs @@ -0,0 +1,30 @@ +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Application.Contracts.DTO; +using IRaCIS.Core.Domain.Share; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Core.Application.Image.QA +{ + public interface IQCListService + { + Task> GetCheckChallengeDialogList(Guid subjectVisitId); + Task, TrialSubjectAndSVConfig>> GetConsistencyVerificationList(CheckQuery checkQuery); + Task, TrialSubjectAndSVConfig>> GetCRCChallengeList(ChallengeQuery challengeQuery); + Task> GetCRCVisitChallengeAndDialog(Guid subjectVisitId, [FromRoute] TrialQCProcess trialQCProcess); + Task, TrialSubjectAndSVConfig>> GetCRCVisitList(CRCVisitSearchDTO visitSearchDTO); + Task>> GetForwardList(ForwardQuery forwardQuery); + Task> GetHistoryChallengeList(Guid subjectVisitId, [FromRoute] TrialQCProcess trialQCProcess, [FromRoute] CurrentQC currentQCType); + Task> GetQCChallengeCreatorList(Guid trialId); + Task> GetQCChallengeDialogList(Guid qaChallengeId); + Task, TrialSubjectAndSVConfig>> GetQCChallengeList(ChallengeQuery challengeQuery); + Task> GetQCParticipantList(Guid trialId); + Task> GetQCQuestionAnswerList(Guid subjectVisitId, Guid trialId, [FromRoute] TrialQCProcess trialQCProcess, [FromRoute] CurrentQC currentQCType); + Task, TrialSubjectAndSVConfig>> GetQCVisitList(QCVisitSearchDTO visitSearchDTO); + Task> GetSubjectVisitSelectList(Guid subjectId); + Task> GetSubjectVisitUploadedStudyList(Guid subjectVisitId); + Task GetUploadInitInfo(Guid subjectVisitId); + Task GetVisitQCInfo(Guid subjectVisitId, [FromRoute] TrialQCProcess trialQCProcess, [FromRoute] CurrentQC currentQCType); + Task GetVisitQCStudyAndSeriesList(Guid subjectVisitId); + Task GetVisitQCSubjectInfo(Guid subjectVisitId); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/QC/Interface/IQCOperationService.cs b/IRaCIS.Core.Application/Service/QC/Interface/IQCOperationService.cs new file mode 100644 index 00000000..c07b1399 --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/Interface/IQCOperationService.cs @@ -0,0 +1,41 @@ +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Application.Contracts.DTO; +using IRaCIS.Core.Domain.Share; +using MediatR; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Core.Application.Image.QA +{ + public interface IQCOperationService + { + Task CheckBack(Guid subjectVisitId, Guid signId); + Task SetNeedReupload(Guid trialId, Guid signId, Guid qcChallengeId); + Task QCPassedOrFailed(Guid trialId, Guid subjectVisitId, Guid signId, [FromRoute] AuditStateEnum auditState); + Task SetCheckPass(Guid subjectVisitId,Guid signId); + + + + + Task> AddCheckChallengeReply(CheckChallengeDialogCommand checkDialogCommand); + Task AddOrUpdateQCChallenge(QCChallengeCommand qaQuestionCommand, Guid trialId, [FromRoute] TrialQCProcess trialQCProcess, [FromRoute] CurrentQC currentQCType); + Task AddOrUpdateQCQuestionAnswerList(QCQuestionAnswerCommand[] qcQuestionAnswerCommands, Guid trialId, Guid subjectVisitId, [FromRoute] TrialQCProcess trialQCProcess, [FromRoute] CurrentQC currentQCType, [FromServices] IServiceProvider serviceProvider); + Task AddQCChallengeReply(QADialogCommand qaDialogCommand); + Task CloseCheckChallenge(Guid subjectVisitId); + Task CloseQCChallenge(Guid qcChallengeId, Guid subjectVisitId, [FromRoute] QCChallengeCloseEnum closeEnum, [FromRoute] string closeReason); + Task CRCRequestReUpload(Guid qcChallengeId); + Task CRCRequestToQC(CRCRequestToQCCommand cRCRequestToQCCommand); + Task CRCRequstCheckBack(Guid subjectVisitId); + Task DeleteQCChallenge(Guid qcChallengeId); + Task DeleteStudyList(Guid[] ids, Guid subjectVisitId, Guid trialId); + Task ObtainOrCancelQCTask(Guid trialId, Guid subjectVisitId, bool obtaionOrCancel); + Task SetReuploadFinished(CRCReuploadFinishedCommand cRCReuploadFinishedCommand); + Task SetSeriesState(Guid subjectVisitId, Guid studyId, Guid seriesId, int state); + Task SetVisitUrgent(Guid trialId, Guid subjectVisitId, bool setOrCancel); + Task UpdateModality(Guid id, int type, [FromQuery] string modality, [FromQuery] string bodyPart); + Task UpdateSubjectAndSVInfo(UploadSubjectAndVisitCommand command); + Task UploadVisitCheckExcel(IFormFile file, [FromServices] IMediator _mediator, Guid trialId, [FromServices] IWebHostEnvironment _hostEnvironment); + Task VerifyCanQCPassedOrFailed(Guid subjectVisitId); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/QC/Interface/IQCQuestionService.cs b/IRaCIS.Core.Application/Service/QC/Interface/IQCQuestionService.cs new file mode 100644 index 00000000..fda12a5c --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/Interface/IQCQuestionService.cs @@ -0,0 +1,16 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-11 11:04:54 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Core.Application.Contracts +{ + public interface IQCQuestionService + { + Task AddOrUpdateQCQuestionConfigure(QCQuestionAddOrEdit addOrEditQCQuestionConfigure); + Task DeleteQCQuestionConfigure(Guid qCQuestionConfigureId); + Task> GetQCQuestionConfigureList(QCQuestionQuery queryQCQuestionConfigure); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/QC/Interface/ITrialQCQuestionConfigureService.cs b/IRaCIS.Core.Application/Service/QC/Interface/ITrialQCQuestionConfigureService.cs new file mode 100644 index 00000000..dc6258bf --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/Interface/ITrialQCQuestionConfigureService.cs @@ -0,0 +1,17 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-11 11:04:54 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Core.Application.Contracts +{ + public interface ITrialQCQuestionConfigureService + { + Task AddOrUpdateTrialQCQuestionConfigure(TrialQCQuestionAddOrEdit addOrEditTrialQCQuestionConfigure); + Task BatchAddTrialQCQuestionConfigure(List batchList, Guid trialId); + Task DeleteTrialQCQuestionConfigure(Guid trialQCQuestionConfigureId); + //Task> GetTrialQCQuestionConfigureList(TrialQCQuestionQuery queryTrialQCQuestionConfigure); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/QC/NoneDicomStudyService.cs b/IRaCIS.Core.Application/Service/QC/NoneDicomStudyService.cs new file mode 100644 index 00000000..76c0dbf7 --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/NoneDicomStudyService.cs @@ -0,0 +1,262 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-06 10:54:55 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using IRaCIS.Core.Domain.Share; +using SharpCompress.Archives; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Infra.EFCore; + +namespace IRaCIS.Core.Application.Contracts +{ + /// + /// NoneDicomStudyService + /// + [ApiExplorerSettings(GroupName = "Image")] + public class NoneDicomStudyService : BaseService, INoneDicomStudyService + { + private readonly IRepository _noneDicomStudyRepository; + private readonly IRepository _noneDicomStudyFileRepository; + + public NoneDicomStudyService(IRepository noneDicomStudyRepository,IRepository noneDicomStudyFileRepository) + { + _noneDicomStudyRepository = noneDicomStudyRepository; + _noneDicomStudyFileRepository = noneDicomStudyFileRepository; + } + + + [HttpGet("{subjectVisitId:guid}")] + public async Task> GetNoneDicomStudyList(Guid subjectVisitId) + { + + var noneDicomStudyQueryable = _noneDicomStudyRepository.Where(t => t.SubjectVisitId == subjectVisitId) + .ProjectTo(_mapper.ConfigurationProvider, new { token = _userInfo.UserToken }); + + return await noneDicomStudyQueryable.ToListAsync(); + } + + [UnitOfWork] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task AddOrUpdateNoneDicomStudy(NoneDicomStudyAddOrEdit addOrEditNoneDicomStudy) + { + var entity = await _noneDicomStudyRepository.InsertOrUpdateAsync(addOrEditNoneDicomStudy, false); + + if (_repository.Entry(entity).State == Microsoft.EntityFrameworkCore.EntityState.Added) + { + entity.Code = _noneDicomStudyRepository.Where(t => t.TrialId == addOrEditNoneDicomStudy.TrialId).Select(t => t.Code).DefaultIfEmpty().Max() + 1; + } + + await _repository.SaveChangesAsync(); + + var svTime = _repository.Where(t => t.Id == addOrEditNoneDicomStudy.SubjectVisitId).Select(t => new + { + DicomStudyMinStudyTime = t.StudyList.Min(t => (DateTime?)t.StudyTime), + DicomStudyMaxStudyTime = t.StudyList.Max(t => (DateTime?)t.StudyTime), + NoneDicomStudyMinStudyTime = t.NoneDicomStudyList.Min(t => (DateTime?)t.ImageDate), + NoneDicomStudyMaxStudyTime = t.NoneDicomStudyList.Max(t => (DateTime?)t.ImageDate) + }).FirstOrDefault().IfNullThrowException(); + + var minArray = new DateTime?[] { svTime.DicomStudyMinStudyTime, svTime.NoneDicomStudyMinStudyTime, addOrEditNoneDicomStudy.ImageDate }; + var maxArray = new DateTime?[] { svTime.DicomStudyMaxStudyTime, svTime.NoneDicomStudyMaxStudyTime, addOrEditNoneDicomStudy.ImageDate }; + + await _repository.UpdateFromQueryAsync(t => t.Id == addOrEditNoneDicomStudy.SubjectVisitId, u => new SubjectVisit() + { + VisitExecuted = VisitExecutedEnum.Executed, + + EarliestScanDate = minArray.Min(), + + LatestScanDate = maxArray.Max() + }); + + await _repository.SaveChangesAsync(); + + + return ResponseOutput.Ok(entity.Id); + + } + + [TypeFilter(typeof(TrialResourceFilter))] + [HttpDelete("{noneDicomStudyId:guid}/{trialId:guid}")] + public async Task DeleteNoneDicomStudy(Guid noneDicomStudyId) + { + var noneDicomStudy = await _noneDicomStudyRepository.FirstOrDefaultAsync(t => t.Id == noneDicomStudyId); + + if (noneDicomStudy == null) return Null404NotFound(noneDicomStudy); + + await _noneDicomStudyRepository.DeleteFromQueryAsync(t => t.Id == noneDicomStudyId); + await _noneDicomStudyFileRepository.DeleteFromQueryAsync(t => t.NoneDicomStudyId == noneDicomStudyId); + + var svTime = await _repository.Where(t => t.Id == noneDicomStudy.SubjectVisitId).Select(t => new + { + DicomStudyMinStudyTime = t.StudyList.Min(t => (DateTime?)t.StudyTime), + DicomStudyMaxStudyTime = t.StudyList.Max(t => (DateTime?)t.StudyTime), + NoneDicomStudyMinStudyTime = t.NoneDicomStudyList.Min(t => (DateTime?)t.ImageDate), + NoneDicomStudyMaxStudyTime = t.NoneDicomStudyList.Max(t => (DateTime?)t.ImageDate) + }).FirstOrDefaultAsync().IfNullThrowException(); + + var minArray = new DateTime?[] { svTime.DicomStudyMinStudyTime, svTime.NoneDicomStudyMinStudyTime }; + var maxArray = new DateTime?[] { svTime.DicomStudyMaxStudyTime, svTime.NoneDicomStudyMaxStudyTime }; + + await _repository.UpdateFromQueryAsync(t => t.Id == noneDicomStudy.SubjectVisitId, u => new SubjectVisit() + { + EarliestScanDate = minArray.Min(), + + LatestScanDate = maxArray.Max() + }); + return ResponseOutput.Ok(); + } + + + [HttpDelete("{noneDicomStudyFileId:guid}")] + public async Task DeleteNoneDicomStudyFile(Guid noneDicomStudyFileId) + { + var subjectVisitId = await _noneDicomStudyFileRepository.Where(t=>t.Id== noneDicomStudyFileId).Select(t => t.NoneDicomStudy.SubjectVisitId).FirstOrDefaultAsync(); + + var success = await _noneDicomStudyFileRepository.DeleteFromQueryAsync(t => t.Id == noneDicomStudyFileId); + + //如果既没有 dicom数据 也没有非dicom 文件 那么提交状态变更回去 + if (await _repository.CountAsync(t => t.SubjectVisitId == subjectVisitId) == 0 && await _repository.CountAsync(t => t.NoneDicomStudy.SubjectVisitId == subjectVisitId) == 0) + { + await _repository.UpdateFromQueryAsync(t => t.Id == subjectVisitId && t.SubmitState == SubmitStateEnum.ToSubmit, u => new SubjectVisit() { VisitExecuted = 0, SubmitState = SubmitStateEnum.None }); + } + + return ResponseOutput.Ok(); + } + + /// + /// 非Dicom检查 文件列表 + /// + /// + /// + [HttpGet("{noneDicomStudyId:guid}")] + public async Task> GetNoneDicomStudyFileList(Guid noneDicomStudyId) + { + return await _noneDicomStudyFileRepository.Where(t => t.NoneDicomStudyId == noneDicomStudyId) + .ProjectTo(_mapper.ConfigurationProvider, new { token = _userInfo.UserToken }).ToListAsync(); + } + + [HttpGet("{subjectVisitId:guid}")] + public async Task> GetVisitNoneDicomStudyFileList(Guid subjectVisitId) + { + return await _repository.Where(t => t.NoneDicomStudy.SubjectVisitId == subjectVisitId).ProjectTo(_mapper.ConfigurationProvider, new { token = _userInfo.UserToken }).ToListAsync(); + } + + + /// + /// 上传非Dicom 文件 支持压缩包 + /// + /// + /// + /// + /// + /// + //[DisableRequestSizeLimit] + [RequestSizeLimit(1_073_741_824)] + [HttpPost("{noneDicomStudyId:guid}/{subjectVisitId:guid}")] + public async Task UploadNoneDicomFile(IFormCollection formCollection, Guid subjectVisitId, Guid noneDicomStudyId, [FromServices] IWebHostEnvironment _hostEnvironment) + { + var rootPath = Directory.GetParent(_hostEnvironment.ContentRootPath.TrimEnd('\\')).IfNullThrowException().FullName; + + //上传根路径 + var _fileStorePath = Path.Combine(rootPath, StaticData.TrialDataFolder); + + var sv = await _repository.Where(t => t.Id == subjectVisitId).Select(t => new { t.TrialId, t.SiteId, t.SubjectId }).FirstOrDefaultAsync().IfNullThrowConvertException(); + + string uploadFolderPath = Path.Combine(_fileStorePath, sv.TrialId.ToString(), sv.SiteId.ToString(), sv.SubjectId.ToString(), subjectVisitId.ToString(), StaticData.NoneDicomFolder); + + if (!Directory.Exists(uploadFolderPath)) + { + Directory.CreateDirectory(uploadFolderPath); + } + + var startTime = DateTime.Now; + + foreach (IFormFile file in formCollection.Files) + { + if (file.FileName.Contains(".Zip", StringComparison.OrdinalIgnoreCase) || file.FileName.Contains(".rar", StringComparison.OrdinalIgnoreCase)) + { + var archive = ArchiveFactory.Open(file.OpenReadStream()); + + foreach (var entry in archive.Entries) + { + if (!entry.IsDirectory) + { + DealCompressFile(entry, sv, subjectVisitId, noneDicomStudyId, uploadFolderPath); + } + } + } + else + { + var trustedFileNameForFileStorage = GetStoreFileName(file.FileName); + var relativePath = $"/{StaticData.TrialDataFolder}/{sv.TrialId}/{sv.SiteId}/{sv.SubjectId}/{subjectVisitId}/{StaticData.NoneDicomFolder}/{trustedFileNameForFileStorage}"; + + var filePath = Path.Combine(uploadFolderPath, trustedFileNameForFileStorage); + + using (FileStream fs = System.IO.File.Create(filePath)) + { + await file.CopyToAsync(fs); + await fs.FlushAsync(); + } + + await _noneDicomStudyFileRepository.AddAsync(new NoneDicomStudyFile() { FileName = file.FileName, Path = relativePath, NoneDicomStudyId = noneDicomStudyId }); + + } + } + + // 上传非Dicom 后 将状态改为待提交 分为普通上传 和QC后重传 普通上传时才改为待提交 + await _repository.UpdateFromQueryAsync(t => t.Id == subjectVisitId && t.SubmitState == SubmitStateEnum.None, u => new SubjectVisit() { SubmitState = SubmitStateEnum.ToSubmit }); + + await _repository.AddAsync(new StudyMonitor() + { + FileCount = formCollection.Files.Count, FileSize = formCollection.Files.Sum(t => t.Length), IsDicom = false, + IsDicomReUpload = false, StudyId = noneDicomStudyId, + UploadStartTime = startTime, UploadFinishedTime = DateTime.Now, IP = _userInfo.IP, + TrialId = sv.TrialId, + SiteId = sv.SiteId, + SubjectId = sv.SubjectId, + SubjectVisitId = subjectVisitId, + }); + + await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(/*list*/); + } + + private string GetStoreFileName(string fileName) + { + var realName = fileName; + + var fileNameEX = Path.GetExtension(realName); + + var trustedFileNameForFileStorage = Guid.NewGuid().ToString() + fileNameEX; + + return trustedFileNameForFileStorage; + } + + private async void DealCompressFile(IArchiveEntry entry, dynamic sv, Guid subjectVisitId, Guid noneDicomStudyId, string uploadFolderPath) + { + var trustedFileNameForFileStorage = GetStoreFileName(entry.Key); + + var relativePath = $"/{StaticData.TrialDataFolder}/{sv.TrialId}/{sv.SiteId}/{sv.SubjectId}/{subjectVisitId}/{StaticData.NoneDicomFolder}/{trustedFileNameForFileStorage}"; + + var filePath = Path.Combine(uploadFolderPath, trustedFileNameForFileStorage); + + entry.WriteToFile(filePath); + + var fileName = string.Empty; + + if (entry.Key.Contains("\\")) + { + fileName = entry.Key.Split("\\").Last(); + } + + + await _repository.AddAsync(new NoneDicomStudyFile() { FileName = fileName, Path = relativePath, NoneDicomStudyId = noneDicomStudyId }); + } + + } +} diff --git a/IRaCIS.Core.Application/Service/QC/QCListService.cs b/IRaCIS.Core.Application/Service/QC/QCListService.cs new file mode 100644 index 00000000..2e1e0d22 --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/QCListService.cs @@ -0,0 +1,756 @@ +using IRaCIS.Core.Application.Contracts; +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Infrastructure.ExpressionExtend; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Application.Contracts.DTO; +using IRaCIS.Core.Infra.EFCore; + +namespace IRaCIS.Core.Application.Image.QA +{ + [ApiExplorerSettings(GroupName = "Image")] + public class QCListService : BaseService, IQCListService + { + private readonly IRepository _subjectVisitRepository; + + + public QCListService(IRepository subjectVisitRepository) + { + _subjectVisitRepository = subjectVisitRepository; + } + + #region CRC上传、质疑页面 + /// + /// CRC 访视上传列表 + /// + /// + /// + [HttpPost] + public async Task, TrialSubjectAndSVConfig>> GetCRCVisitList(CRCVisitSearchDTO visitSearchDTO) + { + + var query = _repository.Where(x => x.TrialId == visitSearchDTO.TrialId) + .WhereIf(visitSearchDTO.SiteId != null, t => t.SiteId == visitSearchDTO.SiteId) + .WhereIf(visitSearchDTO.SubjectId != null, t => t.Subject.Id == visitSearchDTO.SubjectId) + .WhereIf(!string.IsNullOrEmpty(visitSearchDTO.SubjectInfo), t => t.Subject.Code.Contains(visitSearchDTO.SubjectInfo)) + .WhereIf(!string.IsNullOrEmpty(visitSearchDTO.VisitPlanInfo), visitSearchDTO.VisitPlanInfo.Contains('.') ? t => t.InPlan == false : t => t.VisitNum == decimal.Parse(visitSearchDTO.VisitPlanInfo)) + .WhereIf(visitSearchDTO.AuditStateArray != null && visitSearchDTO.AuditStateArray?.Length > 0, t => visitSearchDTO.AuditStateArray!.Contains(t.AuditState)) + .WhereIf(visitSearchDTO.SubmitState != null, t => t.SubmitState == visitSearchDTO.SubmitState) + .WhereIf(visitSearchDTO.ChallengeState != null, t => t.ChallengeState == visitSearchDTO.ChallengeState) + .WhereIf(visitSearchDTO.IsUrgent != null, t => t.IsUrgent == visitSearchDTO.IsUrgent) + .WhereIf(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ClinicalResearchCoordinator, t => t.TrialSite.CRCUserList.Any(t => t.UserId == _userInfo.Id)) + .ProjectTo(_mapper.ConfigurationProvider); + + + var pageList = await query.ToPagedListAsync(visitSearchDTO.PageIndex, visitSearchDTO.PageSize, visitSearchDTO.SortField, visitSearchDTO.Asc); + + var config = await _repository.Where(t => t.Id == visitSearchDTO.TrialId).ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync().IfNullThrowException(); + + return ResponseOutput.Ok(pageList, config!); + + } + + + + /// + /// CRC 质疑列表 + /// + /// + /// + [HttpPost] + public async Task, TrialSubjectAndSVConfig>> GetCRCChallengeList(ChallengeQuery challengeQuery) + { + + #region CRC 质疑列表 join连表方式 + //var query = from qcChanllenge in _qcChallengeRepository.Find(qcChallengeLambda) + // join subjectVisit in _subjectVisitRepository.Find() on qcChanllenge.SubjectVisitId equals subjectVisit.Id + // join trialSite in _trialSiteRepository.Find(t => t.TrialId == challengeQuery.TrialId) on subjectVisit.SiteId equals trialSite.SiteId + // join subject in _subjectRepository.Find() on subjectVisit.SubjectId equals subject.Id + // select new QCCRCChallengeViewModel() + // { + // Id = qcChanllenge.Id, + // SubjectVisitId = qcChanllenge.SubjectVisitId, + // ChallengeCode = qcChanllenge.ChallengeCode, + // ChallengeType = qcChanllenge.ChallengeType, + // Note = qcChanllenge.Note, + // Content = qcChanllenge.Content, + // UserTypeEnum = qcChanllenge.UserTypeEnum, + // CreateTime = qcChanllenge.CreateTime, + // CreateUser = qcChanllenge.CreateUser, + // CreateUserId = qcChanllenge.CreateUserId, + + // DeadlineTime = qcChanllenge.DeadlineTime, + // IsClosed = qcChanllenge.IsClosed, + // ClosedTime = qcChanllenge.ClosedTime, + // ClosedUser = qcChanllenge.ClosedUser, + // NeedReUpload = qcChanllenge.NeedReUpload, + + // VisitName = subjectVisit.VisitName, + // VisitNum = subjectVisit.VisitNum, + // SubjectCode = subject.Code, + // TrialSiteCode = trialSite.TrialSiteCode, + + // LatestMsgTime = qcChanllenge.LatestMsgTime, + // LatestReplyUser = qcChanllenge.LatestReplyUser, + // }; + #endregion + + + var query2 = _repository.Where(x => x.TrialId == challengeQuery.TrialId) + .WhereIf(challengeQuery.ReuploadEnum != null, t => t.ReuploadEnum == challengeQuery.ReuploadEnum) + .WhereIf(challengeQuery.IsClosed != null, t => t.IsClosed == challengeQuery.IsClosed) + .WhereIf(challengeQuery.SiteId != null, t => t.SubjectVisit.SiteId == challengeQuery.SiteId) + .WhereIf(challengeQuery.SubjectId != null, t => t.SubjectVisit.SubjectId == challengeQuery.SubjectId) + .WhereIf(challengeQuery.CreateUserId != null, t => t.CreateUserId == challengeQuery.CreateUserId) + .WhereIf(challengeQuery.SubjectCode != null, t => t.SubjectVisit.Subject.Code.Contains(challengeQuery.SubjectCode!)) + .WhereIf(!string.IsNullOrEmpty(challengeQuery.VisitPlanInfo), challengeQuery.VisitPlanInfo.Contains('.') ? t => t.SubjectVisit.VisitNum.ToString().Contains(".") : t => t.SubjectVisit.VisitNum == decimal.Parse(challengeQuery.VisitPlanInfo)) + .WhereIf(challengeQuery.IsOverTime != null, t => DateTime.Now > t.DeadlineTime) + .WhereIf(challengeQuery.IsUrgent != null, t => t.SubjectVisit.IsUrgent == challengeQuery.IsUrgent) + .WhereIf(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ClinicalResearchCoordinator, t => t.SubjectVisit.TrialSite.CRCUserList.Any(t => t.UserId == _userInfo.Id)) + .ProjectTo(_mapper.ConfigurationProvider); + + var pageList = await query2.ToPagedListAsync(challengeQuery.PageIndex, challengeQuery.PageSize, challengeQuery.SortField, challengeQuery.Asc,string.IsNullOrWhiteSpace(challengeQuery.SortField), new string[] { "IsUrgent desc", "IsClosed asc" }); + + var config = await _repository.Where(t => t.Id == challengeQuery.TrialId).ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync().IfNullThrowException(); + return ResponseOutput.Ok(pageList, config); + + + } + + #endregion + + + #region QC 质疑 质控页面 + + /// + /// QC 质疑列表 分页 + /// + /// + /// + [HttpPost] + public async Task, TrialSubjectAndSVConfig>> GetQCChallengeList(ChallengeQuery challengeQuery) + { + #region linq方式 + Expression 拼接 + + //Expression> qcChallengeLambda = x => x.TrialId == challengeQuery.TrialId; + + //if (challengeQuery.ReuploadEnum != null) + //{ + // qcChallengeLambda = qcChallengeLambda.And(t => t.ReuploadEnum == challengeQuery.ReuploadEnum); + //} + //if (challengeQuery.IsClosed != null) + //{ + // qcChallengeLambda = qcChallengeLambda.And(t => t.IsClosed == challengeQuery.IsClosed); + //} + //if (challengeQuery.SiteId != null) + //{ + // qcChallengeLambda = qcChallengeLambda.And(t => t.SubjectVisit.SiteId == challengeQuery.SiteId.Value); + //} + //if (challengeQuery.SubjectId != null) + //{ + // qcChallengeLambda = qcChallengeLambda.And(t => t.SubjectVisit.Subject.Id == challengeQuery.SubjectId.Value); + //} + + //var query = from qcChanllenge in _qcChallengeRepository.Find(qcChallengeLambda) + // join subjectVisit in _subjectVisitRepository.Find(subjectVisitLambda) on qcChanllenge.SubjectVisitId equals subjectVisit.Id + // join trialSite in _trialSiteRepository.Find(t => t.TrialId == challengeQuery.TrialId) on subjectVisit.SiteId equals trialSite.SiteId + // join subject in _subjectRepository.Find() on subjectVisit.SubjectId equals subject.Id + // select new QCChallengeViewModel() + // { + // Id = qcChanllenge.Id, + // SubjectVisitId = qcChanllenge.SubjectVisitId, + // ChallengeCode = qcChanllenge.ChallengeCode.ToString(), + // ChallengeType = qcChanllenge.ChallengeType, + // Note = qcChanllenge.Note, + // UserTypeEnum = qcChanllenge.UserTypeEnum, + // Content = qcChanllenge.Content, + // CreateTime = qcChanllenge.CreateTime, + // CreateUserId = qcChanllenge.CreateUserId, + // CreateUser = qcChanllenge.CreateUser, + // SubjectCode = subject.Code, + // TrialSiteCode = trialSite.TrialSiteCode, + // DeadlineTime = qcChanllenge.DeadlineTime, + // IsClosed = qcChanllenge.IsClosed, + // ClosedTime = qcChanllenge.ClosedTime, + // ClosedUser = qcChanllenge.ClosedUser, + // NeedReUpload = qcChanllenge.NeedReUpload, + // VisitName = subjectVisit.VisitName, + // VisitNum = subjectVisit.VisitNum, + // LatestMsgTime = qcChanllenge.LatestMsgTime, + // LatestReplyUser = qcChanllenge.LatestReplyUser, + + // }; + #endregion + + var query = _repository.Where(x => x.TrialId == challengeQuery.TrialId) + .WhereIf(challengeQuery.ReuploadEnum != null, t => t.ReuploadEnum == challengeQuery.ReuploadEnum) + .WhereIf(challengeQuery.IsClosed != null, t => t.IsClosed == challengeQuery.IsClosed) + .WhereIf(challengeQuery.SiteId != null, t => t.SubjectVisit.SiteId == challengeQuery.SiteId) + .WhereIf(challengeQuery.SubjectId != null, t => t.SubjectVisit.SubjectId == challengeQuery.SubjectId) + .WhereIf(challengeQuery.CreateUserId != null, t => t.CreateUserId == challengeQuery.CreateUserId) + .WhereIf(!string.IsNullOrEmpty(challengeQuery.SubjectCode), t => t.SubjectVisit.Subject.Code.Contains(challengeQuery.SubjectCode)) + .WhereIf(!string.IsNullOrEmpty(challengeQuery.VisitPlanInfo), challengeQuery.VisitPlanInfo.Contains('.') ? t => t.SubjectVisit.InPlan == false : t => t.SubjectVisit.VisitNum == decimal.Parse(challengeQuery.VisitPlanInfo)) + .WhereIf(challengeQuery.IsUrgent != null, t => t.SubjectVisit.IsUrgent == challengeQuery.IsUrgent) + .WhereIf(challengeQuery.IsOverTime != null, t => DateTime.Now > t.DeadlineTime) + .ProjectTo(_mapper.ConfigurationProvider); + + var pageList = await query.ToPagedListAsync(challengeQuery.PageIndex, challengeQuery.PageSize, challengeQuery.SortField, challengeQuery.Asc, string.IsNullOrWhiteSpace(challengeQuery.SortField), new string[] { "IsUrgent desc", "IsClosed asc" }); + + var config = await _repository.Where(t => t.Id == challengeQuery.TrialId).ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync().IfNullThrowException(); + return ResponseOutput.Ok(pageList, config); + } + + /// + /// QC 访视列表 + /// + /// + /// + [HttpPost] + public async Task, TrialSubjectAndSVConfig>> GetQCVisitList(QCVisitSearchDTO visitSearchDTO) + { + + #region 经典 linq + //Expression> subjectLambda = x => true; + + //Expression> subjectVisitLambda = x => x.TrialId == visitSearchDTO.TrialId; + //Expression> studyLambda = x => x.TrialId == visitSearchDTO.TrialId; + ////Expression> visitPlanLambda = x => x.TrialId == subjectVisitSearch.TrialId; + + //if (visitSearchDTO.SiteId != null && visitSearchDTO.SiteId != Guid.Empty) + //{ + // subjectVisitLambda = subjectVisitLambda.And(t => t.SiteId == visitSearchDTO.SiteId.Value); + // studyLambda = studyLambda.And(t => t.SiteId == visitSearchDTO.SiteId.Value); + //} + + //if (visitSearchDTO.SubjectId != null && visitSearchDTO.SubjectId != Guid.Empty) + //{ + // subjectLambda = subjectLambda.And(t => t.Id == visitSearchDTO.SubjectId.Value); + + // studyLambda = studyLambda.And(t => t.SubjectId == visitSearchDTO.SubjectId.Value); + //} + + //if (!string.IsNullOrEmpty(visitSearchDTO.SubjectInfo)) + //{ + // var search = visitSearchDTO.SubjectInfo.Trim(); + // subjectLambda = subjectLambda.And(t => t.FirstName.Contains(search) || t.LastName.Contains(search) || t.Code.Contains(search)); + //} + + //if (!string.IsNullOrWhiteSpace(visitSearchDTO.VisitPlanInfo)) + //{ + // var visitInfo = visitSearchDTO.VisitPlanInfo.Trim(); + + // if (visitInfo.Contains('.')) // 包含小数点的是计划外访视 + // { + // subjectVisitLambda = subjectVisitLambda.And(t => t.VisitNum.ToString().Contains(".")); + // } + // else + // { + // if (int.TryParse(visitInfo, out var visitNum)) + // { + // subjectVisitLambda = subjectVisitLambda.And(t => t.VisitNum == visitNum); + // } + // } + //} + + //var query = from subjectVisit in _subjectVisitRepository.Where(subjectVisitLambda) + // join subject in _subjectRepository.Where(subjectLambda) on subjectVisit.SubjectId equals subject.Id + // join trialSite in _trialSiteRepository.Where(t => t.TrialId == visitSearchDTO.TrialId) on subjectVisit.SiteId equals trialSite.SiteId + // join study in _studyRepository.Where(studyLambda).GroupBy(t => t.SubjectVisitId).Select(g => new { SubjectVisitId = g.Key, StudyCount = g.Count() }) on subjectVisit.Id equals study.SubjectVisitId into d + // from study in d.DefaultIfEmpty() + // join trial in _trialRepository.AsQueryable() on subjectVisit.TrialId equals trial.Id + + // select new QCVisitViewModel() + // { + // Id = subjectVisit.Id, + // //VisitPlanId = visitStage.Id, + // SubjectId = subjectVisit.SubjectId, + // TrialId = subjectVisit.TrialId, + // SiteId = subjectVisit.SiteId, + + // VisitDay = subjectVisit.VisitDay, + // VisitNum = subjectVisit.VisitNum, + // VisitName = subjectVisit.VisitName, + // SVENDTC = subjectVisit.SVENDTC, + // SVSTDTC = subjectVisit.SVSTDTC, + // SVUPDES = subjectVisit.SVUPDES, + // InPlan = subjectVisit.InPlan, + // VisitExecuted = subjectVisit.VisitExecuted, + + + // StudyCount = study.StudyCount, + // SubjectStatus = subject.Status, + + // SubjectCode = subject.Code, + + // IsUrgent = subjectVisit.IsUrgent, + + // TrialSiteCode = trialSite.TrialSiteCode, + + // EarliestScanDate = subjectVisit.EarliestScanDate, + // LatestScanDate = subjectVisit.LatestScanDate, + + // QCProcessEnum = trial.QCProcessEnum, + // CurrentActionUserId = subjectVisit.CurrentActionUserId, + // CurrentActionUser = subjectVisit.CurrentActionUser, + // CurrentActionUserExpireTime = subjectVisit.CurrentActionUserExpireTime, + // PreliminaryAuditTime = subjectVisit.PreliminaryAuditTime, + // PreliminaryAuitUser = subjectVisit.PreliminaryAuitUser, + // ReviewAuitTime = subjectVisit.ReviewAuitTime, + // ReviewAuitUser = subjectVisit.ReviewAuitUser, + + // AuditState = subjectVisit.AuditState, + // SubmitTime = subjectVisit.SubmitTime, + // IsTake = subjectVisit.IsTake, + + // }; + #endregion + + #region WhereIf + ling 统计 + //var query = from subjectVisit in _subjectVisitRepository.Where(x => x.TrialId == visitSearchDTO.TrialId) + // .WhereIf(visitSearchDTO.SiteId != null, t => t.SiteId == visitSearchDTO.SiteId) + // .WhereIf(visitSearchDTO.SubjectId != null, t => t.Subject.Id == visitSearchDTO.SubjectId) + // .WhereIf(!string.IsNullOrEmpty(subjectInfo), t => /*t.Subject.FirstName.Contains(subjectInfo) || t.Subject.LastName.Contains(subjectInfo) ||*/ t.Subject.Code.Contains(subjectInfo)) + // .WhereIf(!string.IsNullOrEmpty(visitInfo), visitInfo.Contains('.') ? t => t.VisitNum.ToString().Contains(".") : t => t.VisitNum.ToString() == visitInfo) + // .WhereIf(visitSearchDTO.AuditState != null, t => t.AuditState == visitSearchDTO.AuditState) + // .WhereIf(visitSearchDTO.HandleUser != null, t => t.PreliminaryAuitUser.Contains(visitSearchDTO.HandleUser) || t.ReviewAuitUser.Contains(visitSearchDTO.HandleUser)) + // .WhereIf(visitSearchDTO.IsUrgent != null, t => t.IsUrgent == visitSearchDTO.IsUrgent) + // //.WhereIf(visitSearchDTO.SubmitState != null, t => t.SubmitState == visitSearchDTO.SubmitState) + // //.WhereIf(visitSearchDTO.ChallengeState != null, t => t.ChallengeState == visitSearchDTO.ChallengeState) + // join subject in _subjectRepository.AsQueryable() on subjectVisit.SubjectId equals subject.Id + // join trialSite in _trialSiteRepository.Where(t => t.TrialId == visitSearchDTO.TrialId) on subjectVisit.SiteId equals trialSite.SiteId + // join study in _studyRepository.AsQueryable().GroupBy(t => t.SubjectVisitId).Select(g => new { SubjectVisitId = g.Key, StudyCount = g.Count() }) on subjectVisit.Id equals study.SubjectVisitId into d + // from study in d.DefaultIfEmpty() + // join trial in _trialRepository.AsQueryable() on subjectVisit.TrialId equals trial.Id + + // select new QCVisitViewModel() + // { + // Id = subjectVisit.Id, + // //VisitPlanId = visitStage.Id, + // SubjectId = subjectVisit.SubjectId, + // TrialId = subjectVisit.TrialId, + // SiteId = subjectVisit.SiteId, + + // VisitDay = subjectVisit.VisitDay, + // VisitNum = subjectVisit.VisitNum, + // VisitName = subjectVisit.VisitName, + // SVENDTC = subjectVisit.SVENDTC, + // SVSTDTC = subjectVisit.SVSTDTC, + // SVUPDES = subjectVisit.SVUPDES, + // InPlan = subjectVisit.InPlan, + // VisitExecuted = subjectVisit.VisitExecuted, + // IsUrgent = subjectVisit.IsUrgent, + // EarliestScanDate = subjectVisit.EarliestScanDate, + // LatestScanDate = subjectVisit.LatestScanDate, + + // CurrentActionUserId = subjectVisit.CurrentActionUserId, + // CurrentActionUser = subjectVisit.CurrentActionUser, + // CurrentActionUserExpireTime = subjectVisit.CurrentActionUserExpireTime, + // PreliminaryAuditTime = subjectVisit.PreliminaryAuditTime, + // PreliminaryAuitUser = subjectVisit.PreliminaryAuitUser, + // ReviewAuitTime = subjectVisit.ReviewAuitTime, + // ReviewAuitUser = subjectVisit.ReviewAuitUser, + + // AuditState = subjectVisit.AuditState, + // SubmitTime = subjectVisit.SubmitTime, + // IsTake = subjectVisit.IsTake, + + // QCProcessEnum = trial.QCProcessEnum, + + // TrialSiteCode = trialSite.TrialSiteCode, + + // SubjectStatus = subject.Status, + // SubjectCode = subject.Code, + + // StudyCount = study.StudyCount, + + // }; + + #endregion + + var query = _subjectVisitRepository.Where(x => x.TrialId == visitSearchDTO.TrialId) + .WhereIf(visitSearchDTO.SiteId != null, t => t.SiteId == visitSearchDTO.SiteId) + .WhereIf(visitSearchDTO.SubjectId != null, t => t.Subject.Id == visitSearchDTO.SubjectId) + .WhereIf(!string.IsNullOrEmpty(visitSearchDTO.SubjectInfo), t => /*t.Subject.FirstName.Contains(subjectInfo) || t.Subject.LastName.Contains(subjectInfo) ||*/ t.Subject.Code.Contains(visitSearchDTO.SubjectInfo)) + .WhereIf(!string.IsNullOrEmpty(visitSearchDTO.VisitPlanInfo), visitSearchDTO.VisitPlanInfo.Contains('.') ? t => t.VisitNum.ToString().Contains(".") : t => t.VisitNum == decimal.Parse(visitSearchDTO.VisitPlanInfo)) + //.WhereIf(visitSearchDTO.AuditState != null, t => t.AuditState == visitSearchDTO.AuditState) + .WhereIf(visitSearchDTO.AuditStateArray != null&& visitSearchDTO.AuditStateArray?.Length>0, t => visitSearchDTO.AuditStateArray!.Contains(t.AuditState)) + + .WhereIf(visitSearchDTO.HandleUserId != null, t => t.PreliminaryAuditUserId == visitSearchDTO.HandleUserId + || t.ReviewAuditUserId == visitSearchDTO.HandleUserId + || t.QCChallengeList.Any(t => t.CreateUserId == visitSearchDTO.HandleUserId) + || t.QCChallengeDialogList.Any(t => t.CreateUserId == visitSearchDTO.HandleUserId)) + .WhereIf(visitSearchDTO.IsUrgent != null, t => t.IsUrgent == visitSearchDTO.IsUrgent) + //.WhereIf(visitSearchDTO.SubmitState != null, t => t.SubmitState == visitSearchDTO.SubmitState) + //.WhereIf(visitSearchDTO.ChallengeState != null, t => t.ChallengeState == visitSearchDTO.ChallengeState) + .ProjectTo(_mapper.ConfigurationProvider); + + var pageList = query.ToPagedList(visitSearchDTO.PageIndex, visitSearchDTO.PageSize, visitSearchDTO.SortField, visitSearchDTO.Asc); + + var config = await _repository.Where(t => t.Id == visitSearchDTO.TrialId).ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync().IfNullThrowException(); + return ResponseOutput.Ok(pageList, config); + + } + + #endregion + + + #region 一致性核查 转发页面 + /// + /// 获取一致性核查列表 CRC/PM 公用 + /// + /// + /// + [HttpPost] + public async Task, TrialSubjectAndSVConfig>> GetConsistencyVerificationList(CheckQuery checkQuery) + { + #region linq 废弃 byzhouhang 2021 11 30 + //Expression> subjectLambda = x => true; + //Expression> subjectVisitLambda = x => x.TrialId == checkQuery.TrialId && x.AuditState == AuditStateEnum.QCPassed; + //Expression> studyLambda = x => x.TrialId == checkQuery.TrialId; + //if (checkQuery.SiteId != null && checkQuery.SiteId != Guid.Empty) + //{ + // subjectVisitLambda = subjectVisitLambda.And(t => t.SiteId == checkQuery.SiteId.Value); + //} + //if (checkQuery.SubjectId != null && checkQuery.SubjectId != Guid.Empty) + //{ + // subjectLambda = subjectLambda.And(t => t.Id == checkQuery.SubjectId.Value); + //} + + //if (_userInfo.UserTypeEnumInt == (int)UserType.ClinicalResearchCoordinator) + //{ + // var siteQuery = _userTrialSiteRepository.Where(t => t.UserId == _userInfo.Id && t.TrialId == checkQuery.TrialId).Select(t => t.SiteId); + + // subjectVisitLambda = subjectVisitLambda.And(t => siteQuery.Contains(t.SiteId)); + //} + + //var query = + // from subjectVisit in _subjectVisitRepository.Where(subjectVisitLambda) + // join trialSite in _trialSiteRepository.Where(t => t.TrialId == checkQuery.TrialId) on subjectVisit.SiteId equals trialSite.SiteId + // join subject in _subjectRepository.Where() on subjectVisit.SubjectId equals subject.Id + + // select new QCCheckViewModel() + // { + // Id = subjectVisit.Id, + // AuditState = subjectVisit.AuditState, + + // CheckTime = subjectVisit.CheckTime, + // CheckState = subjectVisit.CheckState, + + // CheckChallengeState = subjectVisit.CheckChallengeState, + + // CheckResult = subjectVisit.CheckResult, + + // VisitName = subjectVisit.VisitName, + // VisitNum = subjectVisit.VisitNum, + + // SubjectCode = subject.Code, + + // //Modality=study.Modalities, + // //StudyTime = subjectVisit.sv, + // TrialSiteCode = trialSite.TrialSiteCode, + // }; + #endregion + + + var query = _subjectVisitRepository.Where(x => x.TrialId == checkQuery.TrialId) + .Where(x => x.AuditState == AuditStateEnum.QCPassed) //一致性核查中的,或者还没一致性核查的 + .WhereIf(checkQuery.CheckState != null, t => t.CheckState == checkQuery.CheckState) + .WhereIf(checkQuery.SiteId != null, t => t.SiteId == checkQuery.SiteId) + .WhereIf(!string.IsNullOrEmpty(checkQuery.SubjectInfo), t => t.Subject.Code.Contains(checkQuery.SubjectInfo)) + .WhereIf(!string.IsNullOrEmpty(checkQuery.VisitPlanInfo), checkQuery.VisitPlanInfo.Contains('.') ? t => t.InPlan == false : t => t.VisitNum == decimal.Parse(checkQuery.VisitPlanInfo)) + .WhereIf(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ClinicalResearchCoordinator, t => t.TrialSite.CRCUserList.Any(t => t.UserId == _userInfo.Id))//CRC 过滤负责的site + .ProjectTo(_mapper.ConfigurationProvider); + + var pageList = await query.ToPagedListAsync(checkQuery.PageIndex, checkQuery.PageSize, checkQuery.SortField, checkQuery.Asc); + var config = await _repository.Where(t => t.Id == checkQuery.TrialId).ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync().IfNullThrowException(); + return ResponseOutput.Ok(pageList, config); + } + + /// + /// 一致性核查 聊天记录列表 + /// + /// + /// + /// + + [HttpGet("{subjectVisitId:guid}")] + public async Task> GetCheckChallengeDialogList(Guid subjectVisitId) + { + + var list = await _repository.Where(t => t.SubjectVisitId == subjectVisitId).OrderBy(t => t.CreateTime) + .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + + list.ForEach(t => t.IsCurrentUser = _userInfo.Id == t.CreateUserId); + + return list; + } + + /// + /// 转发列表 查询一致性核查通过的,针对不一致性核查的 要维护CV状态 + /// + /// + /// + [HttpPost] + public async Task>> GetForwardList(ForwardQuery forwardQuery) + { + //var trialConfig = _trialReposioty.Select(t => new { TrialId = t.Id, t.QCProcessEnum, t.IsImageConsistencyVerification }).FirstOrDefault(t => t.TrialId == forwardQuery.TrialId); + + var query = _subjectVisitRepository.Where(x => x.TrialId == forwardQuery.TrialId) + .Where(t => t.CheckState == CheckStateEnum.CVPassed) + .WhereIf(forwardQuery.ForwardState != null, t => t.ForwardState == forwardQuery.ForwardState) + .WhereIf(forwardQuery.SiteId != null, t => t.SiteId == forwardQuery.SiteId) + .WhereIf(!string.IsNullOrEmpty(forwardQuery.SubjectInfo), t => t.Subject.Code.Contains(forwardQuery.SubjectInfo)) + .WhereIf(!string.IsNullOrEmpty(forwardQuery.VisitPlanInfo), forwardQuery.VisitPlanInfo.Contains('.') ? t => t.InPlan == false : t => t.VisitNum == decimal.Parse(forwardQuery.VisitPlanInfo)) + //.WhereIf(_userInfo.UserTypeEnumInt == (int)UserType.ClinicalResearchCoordinator, t => t.TrialSite.CRCUserList.Any(t => t.UserId == _userInfo.Id))//CRC 过滤负责的site + .ProjectTo(_mapper.ConfigurationProvider); + + var pageList = await query.ToPagedListAsync(forwardQuery.PageIndex, forwardQuery.PageSize, forwardQuery.SortField, forwardQuery.Asc); + + return ResponseOutput.Ok(pageList); + } + + #endregion + + + + #region QC 具体质控页面 各种列表 + /// + /// 获取某次访视 QA界面所有信息 单独每一项都有接口(往下看),这里是一个大接口,方便第一次获取完整的所有的数据 + /// + /// + /// 项目配置的针对访视检查是那种审核,0 不审,1 单审,2双审 + /// 当前 QC进入的是那种审核 1 单审,2复审 + + [HttpGet("{subjectVisitId:guid}/{trialQCProcess:int}/{currentQCType:int}")] + public async Task GetVisitQCInfo(Guid subjectVisitId, [FromRoute] TrialQCProcess trialQCProcess, [FromRoute] CurrentQC currentQCType) + { + var sv = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == subjectVisitId).IfNullThrowException(); + + if (currentQCType == CurrentQC.First) + { + if (_userInfo.Id != sv.CurrentActionUserId) + { + throw new Exception("当前用户不是初审用户,不允许操作!"); + } + } + else + { + if (_userInfo.Id != sv.CurrentActionUserId) + { + throw new Exception("当前用户不是复审用户,不允许操作!"); + } + } + + var temp = await GetVisitQCStudyAndSeriesList(subjectVisitId); + + var qacheckList = await GetQCQuestionAnswerList(subjectVisitId, sv.TrialId, trialQCProcess, currentQCType); + + return new TrialVisitQADTO + { + QCQuestionAnswerList = qacheckList, + StudyList = temp.StudyList, + SeriesList = temp.SeriesList, + RelationInfo = await GetVisitQCSubjectInfo(subjectVisitId), + NoneDicomStudyList = await _repository.Where(t => t.SubjectVisitId == subjectVisitId).ProjectTo(_mapper.ConfigurationProvider, new { token = _userInfo.UserToken }).ToListAsync(), + SubjectClinicalData = await _subjectVisitRepository.Where(t => t.Id == subjectVisitId) + .ProjectTo(_mapper.ConfigurationProvider, new { subjectVisitId = subjectVisitId, token = _userInfo.UserToken }).FirstOrDefaultAsync() + }; + } + + /// + /// 获取某次访视 QC 问题核对答案 列表 初始化进去的时候是模板项,QC填写了就是对应的内容 + /// + /// + /// + /// 项目配置的针对访视检查是那种审核,0 不审,1 单审,2双审 + /// 当前 QC进入的是那种审核 1 单审,2复审 + + [HttpGet("{trialId:guid}/{subjectVisitId:guid}/{trialQCProcess:int}/{currentQCType:int}")] + public async Task> GetQCQuestionAnswerList(Guid subjectVisitId, Guid trialId, [FromRoute] TrialQCProcess trialQCProcess, [FromRoute] CurrentQC currentQCType) + { + + //https://github.com/dotnet/efcore/issues/26833 ef core 导航属性不支持右连接 + + // 直接用ProjectTo的方式 多做一次查询 代码简洁很多 特别是连表多,字段多的情况 + if (await _repository.AnyAsync(t => t.SubjectVisitId == subjectVisitId && t.QCProcessEnum == trialQCProcess && t.CurrentQCEnum == currentQCType)) + { + var list = await _repository.Where(t => t.SubjectVisitId == subjectVisitId && t.QCProcessEnum == trialQCProcess && t.CurrentQCEnum == currentQCType).OrderBy(t => t.TrialQCQuestionConfigure.ShowOrder) + .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + + return list; + } + else + { + return await _repository.Where(t => t.IsEnable == true && t.TrialId == trialId).OrderBy(t => t.ShowOrder).ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + } + + } + + + /// + /// 获次QC 历史质疑列表 不分页 + /// + /// + /// 项目配置的针对访视检查是那种审核,0 不审,1 单审,2双审 + /// 当前 QC进入的是那种审核 1 单审,2复审 + + [HttpGet("{subjectVisitId:guid}/{trialQCProcess:int}/{currentQCType:int}")] + public async Task> GetHistoryChallengeList(Guid subjectVisitId, [FromRoute] TrialQCProcess trialQCProcess, [FromRoute] CurrentQC currentQCType) + { + var qaChallengeQuery = _repository.Where(t => t.SubjectVisitId == subjectVisitId && t.QCProcessEnum == trialQCProcess) + .WhereIf(currentQCType == CurrentQC.First, t => t.CurrentQCEnum == currentQCType)//复审的时候可以看到初审的质疑 + .ProjectTo(_mapper.ConfigurationProvider); + var qaChallenges = await qaChallengeQuery.ToListAsync(); + + return qaChallenges; + } + + + /// + /// 获取访视下的受试者访视、受试者、site信息 + /// + /// + /// + public async Task GetVisitQCSubjectInfo(Guid subjectVisitId) + { + + // automapper Explicit expansion ProjectTo + return await _subjectVisitRepository.Where(t => t.Id == subjectVisitId).ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync().IfNullThrowException(); + + } + + + + /// + /// 访视下的Study 和Series列表 + /// + /// + /// + [HttpGet("{subjectVisitId:guid}")] + public async Task GetVisitQCStudyAndSeriesList(Guid subjectVisitId) + { + var studyList = await _repository.Where(s => s.SubjectVisitId == subjectVisitId).IgnoreQueryFilters().ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + var studyIds = studyList.Select(t => t.StudyId).ToList(); + + var seriesList = await _repository.Where(t => studyIds.Contains(t.StudyId)).IgnoreQueryFilters().ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + + return new QAStudySeriesInfo { StudyList = studyList, SeriesList = seriesList }; + + } + + /// + /// 访视下的检查列表 + /// + /// + /// + [HttpGet("{subjectVisitId:guid}")] + public async Task> GetSubjectVisitUploadedStudyList(Guid subjectVisitId) + { + return await _repository.Where(s => s.SubjectVisitId == subjectVisitId).IgnoreQueryFilters().ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + } + + + /// + /// 展开 某一QC质疑 下得 聊天记录 + /// + /// + /// + [HttpGet("{qaChallengeId:guid}")] + public async Task> GetQCChallengeDialogList(Guid qaChallengeId) + { + + var list = await _repository.Where(t => t.QCChallengeId == qaChallengeId).OrderBy(t => t.CreateTime) + .ProjectTo(_mapper.ConfigurationProvider, new { currentUserId = _userInfo.Id }).ToListAsync(); + + //利用automapper 运行时映射 + //list.ForEach(t => t.IsCurrentUser = _userInfo.Id == t.CreateUserId); + + return list; + } + + #endregion + + + /// + /// CRC/PM 看到某次访视下的所有质疑和聊天内容 包括初审和复审的 。 + /// + /// + /// + /// + [HttpGet("{subjectVisitId:guid}/{trialQCProcess:int}")] + public async Task> GetCRCVisitChallengeAndDialog(Guid subjectVisitId, [FromRoute] TrialQCProcess trialQCProcess) + { + + var qaChallengeQuery = _repository.Where(t => t.SubjectVisitId == subjectVisitId && t.QCProcessEnum == trialQCProcess) + .ProjectTo(_mapper.ConfigurationProvider, new { currentUserId = _userInfo.Id }); + + var list = await qaChallengeQuery.ToListAsync(); + + //list.ForEach(t => t.DialogList.ToList().ForEach(u => u.IsCurrentUser = _userInfo.Id == u.CreateUserId)); + + return list; + } + + /// + /// 质询发起人 + /// + /// + /// + [HttpGet("{trialId:guid}")] + public async Task> GetQCChallengeCreatorList(Guid trialId) + { + + return await _repository.Where(t => t.TrialId == trialId && t.User.UserTypeEnum == UserTypeEnum.IQC).Select(t => new QCChanllengeCreatorDto() + { + CreatorRealName = t.User.LastName + " / " + t.User.FirstName, + Creator = t.User.UserName, + CreateUserId = t.UserId + }).ToListAsync(); + + } + + /// + /// QC 质疑页面 处理用户 下拉框 + /// + /// + /// + [HttpGet("{trialId:guid}")] + public async Task> GetQCParticipantList(Guid trialId) + { + return await _repository.Where(t => t.TrialId == trialId && (t.User.UserTypeEnum == UserTypeEnum.IQC || t.User.UserTypeEnum == UserTypeEnum.ClinicalResearchCoordinator)).Select(t => new ParticipantDTO() + { + HandleUserRealName = t.User.LastName + " / " + t.User.FirstName, + HandleUser = t.User.UserName, + HandleUserId = t.UserId + }).ToListAsync(); + } + + + + /// + /// 添加计划外访视 下拉框 选择上一次访视 + /// + /// + /// + [HttpGet("{subjectId:guid}")] + public async Task> GetSubjectVisitSelectList(Guid subjectId) + { + return await _subjectVisitRepository.Where(t => t.SubjectId == subjectId).OrderBy(T => T.VisitNum).ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + } + + + + /// + /// 上传界面 受试者 访视、site 基本信息 + /// + /// + /// + [HttpGet("{subjectVisitId:guid}/{trialQCProcess:int}")] + [Obsolete] + public async Task GetUploadInitInfo(Guid subjectVisitId) + { + return await _subjectVisitRepository.Where(t => t.Id == subjectVisitId).ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync().IfNullThrowException(); + } + + + } +} diff --git a/IRaCIS.Core.Application/Service/QC/QCOperationService.cs b/IRaCIS.Core.Application/Service/QC/QCOperationService.cs new file mode 100644 index 00000000..851d9899 --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/QCOperationService.cs @@ -0,0 +1,1743 @@ +using Hangfire; +using IRaCIS.Core.Application.MediatR.CommandAndQueries; +using IRaCIS.Core.Application.BackGroundJob; +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Application.Contracts.DTO; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Domain.Share; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using System.Net.Http.Headers; +using Microsoft.AspNetCore.Http; +using MiniExcelLibs; +using ExcelDataReader; +using System.Text; +using System.Data; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Authorization; +using WinSCP; + +namespace IRaCIS.Core.Application.Image.QA +{ + [ApiExplorerSettings(GroupName = "Image")] + public class QCOperationService : BaseService, IQCOperationService + { + + private readonly DicomFileStoreHelper _dicomFileStoreHelper; + private readonly IRepository _subjectVisitRepository; + + private object _locker = new object(); + + public QCOperationService(DicomFileStoreHelper dicomFileStoreHelper, IRepository subjectVisitRepository) + { + _dicomFileStoreHelper = dicomFileStoreHelper; + _subjectVisitRepository = subjectVisitRepository; + } + + #region QC质疑 以及回复 关闭 + + [HttpPost("{trialId:guid}/{subjectVisitId:guid}/{currentQCType:int}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task VerifyQCCanAddChallenge(Guid subjectVisitId, [FromRoute] CurrentQC currentQCType) + { + if (!await _repository.AnyAsync(t => t.SubjectVisitId == subjectVisitId && t.CurrentQCEnum == currentQCType)) + { + return ResponseOutput.NotOk("必须填写审核问题,并保存,否则不允许添加质疑"); + } + return ResponseOutput.Ok(); + + } + + /// + /// 添加和更新质疑 + /// + /// + /// + /// + /// + /// + [HttpPost("{trialId:guid}/{trialQCProcess:int}/{currentQCType:int}")] + [TypeFilter(typeof(TrialResourceFilter))] + [Authorize(Policy = "ImageQCPolicy")] + public async Task AddOrUpdateQCChallenge(QCChallengeCommand qaQuestionCommand, Guid trialId, [FromRoute] TrialQCProcess trialQCProcess, [FromRoute] CurrentQC currentQCType) + { + + if (qaQuestionCommand.Id == null) + { + if (await _repository.AnyAsync(t => t.IsClosed == false && t.SubjectVisitId == qaQuestionCommand.SubjectVisitId && t.ReuploadEnum == QCChanllengeReuploadEnum.QCAgreeUpload)) + { + return ResponseOutput.NotOk("当前访视有未关闭的 同意CRC上传的质疑,不允许再次添加质疑"); + } + + QCChallenge? qcChallenge = null; + var success = false; + lock (_locker) + { + var trialConfig = _repository.Where(t => t.Id == trialId).Select(t => new { TrialId = t.Id, t.QCProcessEnum, t.IsImageConsistencyVerification }).FirstOrDefault(); + + if (trialConfig == null) return Null404NotFound(trialConfig); + + #region 处理访视状态变更 + //var dbSubjectVisit = _subjectVisitRepository.FirstOrDefault(t => t.Id == qaQuestionCommand.SubjectVisitId); + + //if (trialConfig.QCProcessEnum == TrialQCProcess.NotAudit) + //{ + // return ResponseOutput.NotOk("项目配置为不审,不允许添加QA质疑 "); + //} + //else if (trialConfig.QCProcessEnum == TrialQCProcess.SingleAudit) + //{ + // if ((int)dbSubjectVisit.AuditState == (int)SubjectVisitStateEnum.Submitted) + // { + // dbSubjectVisit.AuditState = AuditStateEnum.InPrimaryQC; + // } + // else + // { + // return ResponseOutput.NotOk($"项目配置为单审,当前审核状态不为{SubjectVisitStateEnum.Submitted},不允许添加QA质疑 "); + // } + //} + //else if (trialConfig.QCProcessEnum == TrialQCProcess.DoubleAudit) + //{ + // if ((int)dbSubjectVisit.AuditState == (int)SubjectVisitStateEnum.Submitted) + // { + // dbSubjectVisit.AuditState = AuditStateEnum.InPrimaryQC; + // } + // else if ((int)dbSubjectVisit.AuditState == (int)SubjectVisitStateEnum.PrimaryQCPassed) + // { + // dbSubjectVisit.AuditState = AuditStateEnum.InSecondaryQC; + // } + // else + // { + // return ResponseOutput.NotOk($"项目配置为双审,访视状态为 {SubjectVisitStateEnum.Submitted}或{SubjectVisitStateEnum.PrimaryQCPassed}才允许添加QA质疑 "); + // } + //} + + #endregion + + //获取编号 + var code = _repository.Where(t => t.TrialId == trialId).Select(t => t.ChallengeCode).DefaultIfEmpty().Max(); + + qcChallenge = _mapper.Map(qaQuestionCommand); + + qcChallenge.QCProcessEnum = trialConfig.QCProcessEnum; + qcChallenge.CurrentQCEnum = currentQCType; + qcChallenge.TrialId = trialId; + qcChallenge.CreateUser = _userInfo.RealName; + qcChallenge.ChallengeCode = code + 1; + qcChallenge.UserTypeEnum = (UserTypeEnum)_userInfo.UserTypeEnumInt; + _ = _repository.AddAsync(qcChallenge).Result; + + success = _repository.SaveChangesAsync().Result; + } + + + //分开两个事务 处理访视质疑状态 + await DealChallengeState(qcChallenge.SubjectVisitId); + + return ResponseOutput.Result(success, qcChallenge.Id); + + #region 添加的时候把记录给留存 + //var templateItems = _mapper.Map>(visitQaCommand.QATrialTemplateItemList); + //templateItems.ForEach(u => + //{ + // u.IQA = _userInfo.RealName; + // u.IQACreateTime = DateTime.Now; + // u.IQANote = visitQaCommand.QARecord.Note; + // u.IQADeadline = visitQaCommand.QARecord.DeadlineTime; + // u.QARecordId = qaRecord.Id; + // qaRecord.QARecordTemplateItemDetailList.Add(u); + //}); + + ////添加了QA记录 引用了QA模板后就不允许删除和编辑,需要维护项目模板状态 传统做法 + //var qaTrailTemplate = _qaTrailTemplateRepository.FirstOrDefault(t => t.Id == visitQaCommand.QARecord.QATrialTemplateId); + //qaTrailTemplate.Status = QATrialTemplateStatus.HasQuote; + #endregion + + } + else + { + + var qcChallenge = await _repository.FirstOrDefaultAsync(t => t.Id == qaQuestionCommand.Id); + + if (qcChallenge == null) return Null404NotFound(qcChallenge); + + _mapper.Map(qaQuestionCommand, qcChallenge); + + var success = await _repository.SaveChangesAsync(); + + await DealChallengeState(qcChallenge.SubjectVisitId); + + + return ResponseOutput.Result(success); + + } + + } + + + /// + /// 关闭质疑,什么情况下允许? + /// + /// + /// + /// + /// + /// + [HttpPost("{trialId:guid}/{qcChallengeId:guid}/{subjectVisitId:guid}/{closeEnum}/{closeReason}")] + [TypeFilter(typeof(TrialResourceFilter))] + //[Authorize(Policy = "ImageQCPolicy")] + public async Task CloseQCChallenge(Guid qcChallengeId, Guid subjectVisitId, [FromRoute] QCChallengeCloseEnum closeEnum, [FromRoute] string closeReason) + { + + var dbQCChallenge = await _repository.FirstOrDefaultAsync(t => t.Id == qcChallengeId); + + if (dbQCChallenge == null) return Null404NotFound(dbQCChallenge); + + if (dbQCChallenge.ReuploadEnum == QCChanllengeReuploadEnum.CRCRequestReupload || dbQCChallenge.ReuploadEnum == QCChanllengeReuploadEnum.QCAgreeUpload) + { + return ResponseOutput.NotOk("CRC申请重传的/QC同意重传的质疑,在CRC设置重传完成前不允许关闭质疑"); + } + + dbQCChallenge.CloseResonEnum = closeEnum; + + dbQCChallenge.IsClosed = true; + + dbQCChallenge.ClosedTime = DateTime.Now; + + + dbQCChallenge.DialogList.Add(new QCChallengeDialog() + { + SubjectVisitId = dbQCChallenge.SubjectVisitId, + UserTypeEnum = (UserTypeEnum)_userInfo.UserTypeEnumInt, + QCChallengeId = dbQCChallenge.Id, + TalkContent = "关闭原因: " + closeReason + }); + + var success = await _repository.SaveChangesAsync(); + + await DealChallengeState(subjectVisitId); + + return ResponseOutput.Result(success); + } + + + + /// + /// 访视级别统计 质疑最新的状态 + /// + /// + private async Task DealChallengeState(Guid subjectVisitId) + { + var sv = await _repository.FirstOrDefaultAsync(t => t.Id == subjectVisitId); + + + var closedStateList = await _repository.Where(t => t.SubjectVisitId == subjectVisitId).Select(t => t.IsClosed).ToListAsync(); + + if (closedStateList.Count == 0) + { + sv.ChallengeState = ChallengeStateEnum.No; + } + else if (closedStateList.All(t => t is true)) + { + sv.ChallengeState = ChallengeStateEnum.HaveAndAllClosed; + } + else + { + sv.ChallengeState = ChallengeStateEnum.HaveAndHaveNotClosed; + } + await _repository.SaveChangesAsync(); + } + + + /// + /// 删除QC质疑记录 + /// + /// + [HttpDelete("{qcChallengeId:guid}/{trialId:guid}")] + [TypeFilter(typeof(TrialResourceFilter))] + [Authorize(Policy = "ImageQCPolicy")] + public async Task DeleteQCChallenge(Guid qcChallengeId) + { + + if (await _repository.AnyAsync(t => t.QCChallengeId == qcChallengeId)) + { + ResponseOutput.NotOk("this QC Challenge Has been replied "); + } + + var qaRecord = await _repository.FirstOrDefaultAsync(t => t.Id == qcChallengeId); + + if (qaRecord == null) return Null404NotFound(qaRecord); + + await _repository.DeleteAsync(qaRecord); + + + var success1 = await _repository.SaveChangesAsync(); + + return ResponseOutput.Result(success1 /*|| success2 || success3*/); + } + + /// + /// 针对 某条QC质疑 添加回复 + /// + /// + /// + [HttpPost("{trialId:guid}")] + + [TypeFilter(typeof(TrialResourceFilter))] + [Authorize(Policy = "ImageQCPolicy")] + public async Task AddQCChallengeReply(QADialogCommand qaDialogCommand) + { + var qaReply = _mapper.Map(qaDialogCommand); + + await _repository.AddAsync(qaReply); + + qaReply.CreateUser = _userInfo.RealName; + qaReply.UserTypeEnum = (UserTypeEnum)_userInfo.UserTypeEnumInt; + + var dbQCChallenge = await _repository.FirstOrDefaultAsync(t => t.Id == qaDialogCommand.QCChallengeId); + + + if (dbQCChallenge == null) return Null404NotFound(dbQCChallenge); + + dbQCChallenge.LatestMsgTime = DateTime.Now; + + + dbQCChallenge.LatestReplyUserId = _userInfo.Id; + + + var success = await _repository.SaveChangesAsync(); + + + return ResponseOutput.Result(success, qaReply); + } + + #endregion + + + #region 一致性核查 + + /// + /// 一致性核查 质疑的添加/回复 + /// + /// + /// + [HttpPost("{trialId:guid}")] + [TypeFilter(typeof(TrialResourceFilter))] + [Authorize(Policy = "ImageCheckPolicy")] + public async Task> AddCheckChallengeReply(CheckChallengeDialogCommand checkDialogCommand) + { + var qaReply = _mapper.Map(checkDialogCommand); + + qaReply.CreateUser = _userInfo.RealName; + qaReply.UserTypeEnum = (UserTypeEnum)_userInfo.UserTypeEnumInt; + + await _repository.AddAsync(qaReply); + + //修改一致性核查 质疑状态 + var sv = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == checkDialogCommand.SubjectVisitId); + + + if (_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ClinicalResearchCoordinator) + { + sv.CheckChallengeState = CheckChanllengeTypeEnum.CRCWaitPMReply; + + } + else if (_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ProjectManager) + { + sv.CheckChallengeState = CheckChanllengeTypeEnum.PMWaitCRCReply; + } + else + { + throw new Exception("一致性核查对话操作用户 只允许 CRC/PM"); + } + + var success = await _repository.SaveChangesAsync(); + + return ResponseOutput.Result(success, qaReply); + } + + /// + /// 关闭 一致性核查质疑 + /// + /// + [HttpPut("{trialId:guid}/{subjectVisitId:guid}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task CloseCheckChallenge(Guid subjectVisitId) + { + + var sv = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == subjectVisitId); + + if (sv == null) return Null404NotFound(sv); + + if (sv.RequestBackState == RequestBackStateEnum.PM_AgressBack) + { + ResponseOutput.NotOk("执行一致性核查的访视 不允许关闭质疑!"); + } + + sv.CheckChallengeState = CheckChanllengeTypeEnum.Closed; + + await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(sv); + } + + /// + /// 手动设置一致性核查通过 + /// + /// + /// + /// + [HttpPut("{trialId:guid}/{signId:guid}/{subjectVisitId:guid}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task SetCheckPass(Guid subjectVisitId, Guid signId) + { + if (_userInfo.UserTypeEnumInt != (int)UserTypeEnum.ProjectManager) + { + ResponseOutput.NotOk("只允许PM 手动设置一致性核查通过"); + } + + var sv = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == subjectVisitId); + + if (sv == null) return Null404NotFound(sv); + + if (sv.RequestBackState == RequestBackStateEnum.PM_AgressBack) + { + ResponseOutput.NotOk("当前是PM同意回退,不允许设置一致性核查通过"); + } + + if (sv.CheckChallengeState != CheckChanllengeTypeEnum.Closed && sv.AuditState == AuditStateEnum.QCPassed) + { + ResponseOutput.NotOk("一致性核查质疑未关闭/审核状态不是通过,不允许设置一致性核查通过"); + } + + sv.CheckState = CheckStateEnum.CVPassed; + + sv.ForwardState = ForwardStateEnum.ToForward; + + sv.CheckPassedTime = DateTime.Now; + + await _repository.SaveChangesAsync(); + + var signSuccess = await _repository.UpdateFromQueryAsync(t => t.Id == signId, u => new TrialSign() { IsCompleted = true }); + + return ResponseOutput.Result(signSuccess); + + } + + /// + /// CRC 请求回退 + /// + /// + /// + [HttpPut("{trialId:guid}/{subjectVisitId:guid}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task CRCRequstCheckBack(Guid subjectVisitId) + { + var sv = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == subjectVisitId); + + + if (sv == null) return Null404NotFound(sv); + + if (sv.CheckState == CheckStateEnum.CVPassed) + { + return ResponseOutput.NotOk("核查通过的数据不允许申请回退"); + } + await _subjectVisitRepository.UpdateFromQueryAsync(t => t.Id == subjectVisitId, u => new SubjectVisit() { RequestBackState = RequestBackStateEnum.CRC_RequestBack }); + + return ResponseOutput.Ok(); + + } + + /// + /// 一致性核查 回退 对话记录不清除 只允许PM回退 + /// + /// + [HttpPut("{trialId:guid}/{signId:guid}/{subjectVisitId:guid}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task CheckBack(Guid subjectVisitId, Guid signId) + { + if (_userInfo.UserTypeEnumInt != (int)UserTypeEnum.ProjectManager) + { + return ResponseOutput.NotOk(" 只允许PM 回退!"); + } + + var sv = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == subjectVisitId); + + if (sv == null) return Null404NotFound(sv); + + if (sv.CheckState == CheckStateEnum.CVPassed || sv.CheckState == CheckStateEnum.ToCheck) + { + return ResponseOutput.NotOk("待核查/核查通过的数据不允许回退"); + } + + //sv.CheckChallengeState = (int)CheckChanllengeTypeEnum.None; + //sv.CheckState = CheckStateEnum.None; + //sv.ChallengeState = (int)ChallengeStateEnum.No; + + sv.AuditState = AuditStateEnum.None; + sv.SubmitState = SubmitStateEnum.ToSubmit; + + //回退后,回退状态恢复 + sv.RequestBackState = RequestBackStateEnum.NotRequest; + sv.IsCheckBack = true; + sv.CheckState = CheckStateEnum.None; + sv.CheckChallengeState = CheckChanllengeTypeEnum.None; + + sv.SVENDTC = null; + sv.SVSTDTC = null; + + sv.PreliminaryAuditTime = null; + sv.SubmitTime = null; + sv.ReviewAuditTime = null; + sv.CurrentActionUserExpireTime = null; + + + sv.IsTake = false; + sv.CurrentActionUserId = null; + sv.PreliminaryAuditUserId = null; + sv.ReviewAuditUserId = null; + + //var success1 = _studyRepository.Delete(t => t.SubjectVisitId == subjectVisitId); + //var succeess2 = _instanceRepository.Delete(t => t.SubjectVisitId == subjectVisitId); + //var success3 = _seriesRepository.Delete(t => t.SubjectVisitId == subjectVisitId); + + //_qcChallengeRepository.Delete(t => t.SubjectVisitId == subjectVisitId); + //_qcChallengeDialogRepository.Delete(t => t.SubjectVisitId == subjectVisitId); + //_checkChallengeDialogRepository.Delete(t => t.SubjectVisitId == subjectVisitId); + await _repository.AddAsync(new CheckChallengeDialog() { SubjectVisitId = subjectVisitId, TalkContent = "PM执行了一致性核查回退", UserTypeEnum = (UserTypeEnum)_userInfo.UserTypeEnumInt }); + + await _repository.DeleteFromQueryAsync(t => t.SubjectVisitId == subjectVisitId); + + var success = await _repository.SaveChangesAsync(); + + var signSuccess = await _repository.UpdateFromQueryAsync(t => t.Id == signId, u => new TrialSign() { IsCompleted = true }); + + return ResponseOutput.Result(signSuccess && success); + + } + + + /// + /// 一致性核查 excel上传 支持三种格式 + /// + /// + /// + /// + /// + /// + [HttpPost("{trialId:guid}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task UploadVisitCheckExcel(IFormFile file, [FromServices] IMediator _mediator, Guid trialId, [FromServices] IWebHostEnvironment _hostEnvironment) + { + if (_userInfo.UserTypeEnumInt != (int)UserTypeEnum.ProjectManager) + { + return ResponseOutput.NotOk("只允许PM 操作"); + } + + var rootPath = Directory.GetParent(_hostEnvironment.ContentRootPath.TrimEnd('\\')).IfNullThrowException().FullName; + + var fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.ToString().Trim('"').ToLower(); + if (!fileName.EndsWith(".xlsx") && !fileName.EndsWith(".csv") && !fileName.EndsWith(".xls")) + { + return ResponseOutput.NotOk("只允许Excel、 CSV!"); + } + + //上传根路径 + string uploadFolderPath = Path.Combine(rootPath, "UploadFile", "Check"); + + if (!Directory.Exists(uploadFolderPath)) + { + Directory.CreateDirectory(uploadFolderPath); + } + + //存放核对表 + var filePath = Path.Combine(uploadFolderPath, DateTime.Now.ToString("yyyy-MM-dd-hh-mm-ss") + fileName); + using (FileStream fs = System.IO.File.Create(filePath)) + { + await file.CopyToAsync(fs); + await fs.FlushAsync(); + } + + //获取Excel表内容 + var config = new MiniExcelLibs.Csv.CsvConfiguration() + { + StreamReaderFunc = (stream) => new StreamReader(stream, Encoding.GetEncoding("gb2312")) + }; + + var etcCheckList = new List(); + + if (fileName.EndsWith(".csv")) + { + //因为csv 需要加配置文件 不然都是null + etcCheckList = MiniExcel.Query(filePath, null, configuration: config).ToList(); + } + else if (fileName.EndsWith(".xlsx")) + { + etcCheckList = MiniExcel.Query(filePath).ToList(); + } + else + { + //为了支持 xls 引入新的组件库 + using (var stream = System.IO.File.Open(filePath, FileMode.Open, FileAccess.Read)) + { + // Auto-detect format, supports: + // - Binary Excel files (2.0-2003 format; *.xls) + // - OpenXml Excel files (2007 format; *.xlsx, *.xlsb) + using (var reader = ExcelReaderFactory.CreateReader(stream)) + { + + // 2. Use the AsDataSet extension method + var dateset = reader.AsDataSet(); + + foreach (DataRow col in dateset.Tables[0].Rows) + { + + etcCheckList.Add(new CheckViewModel() + { + SiteCode = col[0].ToString(), + SubjectCode = col[1].ToString(), + VisitName = col[2].ToString(), + StudyDate = col[3].ToString(), + Modality = col[4].ToString(), + }); + } + + etcCheckList.Remove(etcCheckList[0]); + + // The result of each spreadsheet is in result.Tables + } + } + } + + + //etcCheckList = etcCheckList.Where(t=>) + + if (etcCheckList == null || etcCheckList.Count == 0) + { + return ResponseOutput.NotOk("请上传规定格式的Excel文件,保证有有效数据!"); + } + else + { + //处理Excel 有时只是清除某些行的数据 读取也会读到数据,只是数据是null 后面处理的时候转为字符串为报错 + etcCheckList.ForEach(t => + { + t.Modality = t.Modality ?? string.Empty; + t.SiteCode = t.SiteCode ?? string.Empty; + t.SubjectCode = t.SubjectCode ?? string.Empty; + t.VisitName = t.VisitName ?? string.Empty; + t.StudyDate = t.StudyDate ?? string.Empty; + }); + + var dt = DateTime.Now; + + etcCheckList = etcCheckList.Where(t => !(t.Modality == string.Empty && t.SiteCode == string.Empty && t.SubjectCode == string.Empty && t.VisitName == string.Empty && t.StudyDate == string.Empty && DateTime.TryParse(t.StudyDate, out dt))).ToList(); + + if (etcCheckList.Count == 0) + { + return ResponseOutput.NotOk("请上传规定格式的Excel文件,保证有有效数据!"); + } + + } + + await _mediator.Send(new ConsistencyVerificationRequest() { ETCList = etcCheckList, TrialId = trialId }); + + return ResponseOutput.Ok(); + + } + + #endregion + + + #region QC 核对问题 操作检查各种操作 + + /// + /// 添加或者更新 QC核对问题列表 两个人不能同时操作,就算意外进去了,提交数据,也不会覆盖前一个人数据, 后台已经做好判断 + /// + [HttpPost("{trialId:guid}/{subjectVisitId:guid}/{trialQCProcess:int}/{currentQCType:int}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task AddOrUpdateQCQuestionAnswerList(QCQuestionAnswerCommand[] qcQuestionAnswerCommands, Guid trialId, Guid subjectVisitId, [FromRoute] TrialQCProcess trialQCProcess, [FromRoute] CurrentQC currentQCType, [FromServices] IServiceProvider serviceProvider) + { + //验证是否能操作 + var verifyResult = await VerifyQCCanOpt(subjectVisitId); + + if (!verifyResult.IsSuccess) + { + return verifyResult; + } + + //更新 + if (qcQuestionAnswerCommands.Any(t => t.Id != null)) + { + + #region 先删除再添加 + + //await _repository.DeleteFromQueryAsync(t => t.SubjectVisitId == subjectVisitId && t.QCProcessEnum == trialQCProcess && t.CurrentQCEnum == currentQCType); + + //var addlist = _mapper.Map>(qcQuestionAnswerCommands); + + //addlist.ForEach(t => { t.TrialId = trialId; t.SubjectVisitId = subjectVisitId; t.CurrentQCEnum = currentQCType; t.QCProcessEnum = trialQCProcess; }); + + //await _repository.AddRangeAsync(addlist); + + #endregion + + #region 先查询再更新 + + var questionAnswerList = await _repository.Where(t => t.SubjectVisitId == subjectVisitId && t.QCProcessEnum == trialQCProcess && t.CurrentQCEnum == currentQCType, true).ToListAsync(); + + qcQuestionAnswerCommands.ToList().ForEach(t => + { + var temp = questionAnswerList.FirstOrDefault(u => u.Id == t.Id); + + if (temp != null) + { + temp.Answer = t.Answer; + //temp.ChildAnswer = t.ChildAnswer; + } + + }); + + //Automapper 映射有问题 automapper 会把Guid? 类型的null 值转为 guid.Empty 导致映射错误 + //_mapper.Map(qcQuestionAnswerCommands, questionAnswerList); + #endregion + + + + return ResponseOutput.Ok(await _repository.SaveChangesAsync()); + } + else + { + var addlist = _mapper.Map>(qcQuestionAnswerCommands); + + addlist.ForEach(t => { t.TrialId = trialId; t.SubjectVisitId = subjectVisitId; t.CurrentQCEnum = currentQCType; t.QCProcessEnum = trialQCProcess; }); + + await _repository.AddRangeAsync(addlist); + + return ResponseOutput.Result(await _repository.SaveChangesAsync()); + } + + + } + + + /// + /// 1、设置为不读片,2 设置为读片(取消 先前设置为不读片) 4 设置为删除(数据库记录软删除) 5 恢复为未删除 + /// + /// + /// + /// + /// + /// + [HttpPut("{trialId:guid}/{subjectVisitId:guid}/{studyId:guid}/{seriesId:guid}/{state:int}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task SetSeriesState(Guid subjectVisitId, Guid studyId, Guid seriesId, int state) + { + //验证是否能操作 + var verifyResult = await VerifyQCCanOpt(subjectVisitId); + + if (!verifyResult.IsSuccess) + { + return verifyResult; + } + + var series = await _repository.Where(t => t.Id == seriesId, true).IgnoreQueryFilters().FirstOrDefaultAsync(); + + if (series == null) return Null404NotFound(series); + + if (state == 1) + { + series.IsReading = false; + } + else if (state == 2) + { + series.IsReading = true; + } + else if (state == 4) + { + series.IsDeleted = true; + + var study = await _repository.Where(t => t.Id == studyId, true).IgnoreQueryFilters().FirstOrDefaultAsync(); + if (study == null) return Null404NotFound(study); + + var instanceIdList = await _repository.Where(t => t.SeriesId == seriesId).Select(t => t.Id).ToListAsync(); + + //instanceIdList.ForEach(t => + //{ + // var path = _dicomFileStoreHelper.GetInstanceFilePath(study, seriesId, t.ToString()); + + // if (System.IO.File.Exists(path)) + // { + // File.Delete(path); + // } + + //}); + + study.InstanceCount = study.InstanceCount - instanceIdList.Count; + study.SeriesCount = study.SeriesCount - 1; + + study.IsDeleted = study.SeriesCount == 0; + + } + else if (state == 5) + { + series.IsDeleted = false; + + var study = await _repository.Where(t => t.Id == studyId, true).IgnoreQueryFilters().FirstOrDefaultAsync(); + + if (study == null) return Null404NotFound(study); + + var instanceIdList = await _repository.Where(t => t.SeriesId == seriesId).Select(t => t.Id).ToListAsync(); + + study.InstanceCount = study.InstanceCount + instanceIdList.Count; + + study.SeriesCount = study.SeriesCount + 1; + + study.IsDeleted = study.SeriesCount == 0; + } + + return ResponseOutput.Ok(await _repository.SaveChangesAsync()); + } + + + /// + ///type 1 :study 2: series 3:非dicom QC修改检查部位和 拍片类型 + /// + /// + /// + /// + /// + /// + [HttpPut("{trialId:guid}/{id:guid}/{type:int}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task UpdateModality(Guid id, int type, [FromQuery] string modality, [FromQuery] string bodyPart) + { + if (type == 1) + { + var study = await _repository.FirstOrDefaultAsync(t => t.Id == id); + if (study == null) return Null404NotFound(study); + + study.BodyPartForEdit = bodyPart; + study.Modalities = modality; + await _repository.UpdateFromQueryAsync(t => t.StudyId == id, r => new DicomSeries() { BodyPartForEdit = bodyPart, Modality = modality }); + + } + else if (type == 2) + { + var series = await _repository.FirstOrDefaultAsync(t => t.Id == id); + if (series == null) return Null404NotFound(series); + series.BodyPartForEdit = bodyPart; + } + else if (type == 3) + { + + } + await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(); + } + + /// + /// 验证是否质疑都关闭了 可以审核通过和不通过 + /// + /// + /// + [HttpGet("{subjectVisitId:guid}")] + public async Task VerifyCanQCPassedOrFailed(Guid subjectVisitId) + { + + if (await _repository.AnyAsync(t => t.SubjectVisitId == subjectVisitId && t.IsClosed == false)) + { + return ResponseOutput.NotOk("有质疑未关闭,不允许该操作"); + } + return ResponseOutput.Ok(); + } + + + + /// + /// 删除检查列表 + /// + /// + /// + /// + /// + [HttpPost, Route("{trialId:guid}/{subjectVisitId:guid}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task DeleteStudyList(Guid[] ids, Guid subjectVisitId, Guid trialId) + { + //提交了 但是IQC同意的时候 是可以删除的 + if (await _repository.AnyAsync(t => t.Id == subjectVisitId && t.SubmitState == SubmitStateEnum.Submitted && + (!t.QCChallengeList.Any(u => u.ReuploadEnum == QCChanllengeReuploadEnum.QCAgreeUpload)))) + { + return ResponseOutput.NotOk("CRC Has Submited Image,can not delete"); + } + + + #region will calls error wried + //ids.ToList().ForEach(async id => + //{ + // var success1 = await _repository.DeleteFromQueryAsync(t => t.Id == id); + // var succeess2 = await _repository.DeleteFromQueryAsync(t => t.StudyId == id); + // var success3 = await _repository.DeleteFromQueryAsync(t => t.StudyId == id); + //}); + #endregion + + foreach (var id in ids) + { + var success1 = await _repository.DeleteFromQueryAsync(t => t.Id == id); + var succeess2 = await _repository.DeleteFromQueryAsync(t => t.StudyId == id); + var success3 = await _repository.DeleteFromQueryAsync(t => t.StudyId == id); + + //删除 物理文件 + + var instanceIdList = await _repository.Where(t => t.StudyId == id) + .Select(t => new { InstanceId = t.Id, t.SeriesId, t.StudyId, t.SubjectId, t.SiteId }).ToListAsync(); + + instanceIdList.ForEach(t => + { + var path = _dicomFileStoreHelper.GetInstanceFilePath(new DicomStudy() { Id = t.StudyId, SubjectId = t.SubjectId, TrialId = trialId, SiteId = t.SiteId, SubjectVisitId = subjectVisitId }, t.SeriesId, t.InstanceId.ToString()); + + if (System.IO.File.Exists(path)) + { + File.Delete(path); + } + + }); + } + + + + //一个访视下面有多个检查,所以需要检测 没有的时候才清空 非dicom 是检查文件 不是表记录 + if (await _repository.CountAsync(t => t.SubjectVisitId == subjectVisitId) == 0 && await _repository.CountAsync(t => t.NoneDicomStudy.SubjectVisitId == subjectVisitId) == 0) + { + await _repository.UpdateFromQueryAsync(t => t.Id == subjectVisitId && t.SubmitState == SubmitStateEnum.ToSubmit, + u => new SubjectVisit() { VisitExecuted = 0, SVENDTC = null, SVSTDTC = null, SubmitState = SubmitStateEnum.None }); + + //_qaNoticeRepository.Delete(t => t.SubjectVisitId == subjectVisitId); + //_qaNoticeUserRepository.Delete(t => t.SubjectVisitId == subjectVisitId); + //_qcChallengeRepository.Delete(t => t.SubjectVisitId == subjectVisitId); + //_qcChallengeDialogRepository.Delete(t => t.SubjectVisitId == subjectVisitId); + //_trialQCQuestionAnswerRepository.Delete(t => t.SubjectVisitId == subjectVisitId); + //_checkChallengeDialogRepository.Delete(t => t.SubjectVisitId == subjectVisitId); + } + + + var svTime = await _repository.Where(t => t.Id == subjectVisitId).Select(t => new + { + DicomStudyMinStudyTime = t.StudyList.Min(t => (DateTime?)t.StudyTime), + DicomStudyMaxStudyTime = t.StudyList.Max(t => (DateTime?)t.StudyTime), + NoneDicomStudyMinStudyTime = t.NoneDicomStudyList.Min(t => (DateTime?)t.ImageDate), + NoneDicomStudyMaxStudyTime = t.NoneDicomStudyList.Max(t => (DateTime?)t.ImageDate) + }).FirstOrDefaultAsync().IfNullThrowException(); + + var minArray = new DateTime?[] { svTime.DicomStudyMinStudyTime, svTime.NoneDicomStudyMinStudyTime }; + var maxArray = new DateTime?[] { svTime.DicomStudyMaxStudyTime, svTime.NoneDicomStudyMaxStudyTime }; + + await _repository.UpdateFromQueryAsync(t => t.Id == subjectVisitId, u => new SubjectVisit() + { + EarliestScanDate = minArray.Min(), + + LatestScanDate = maxArray.Max() + }); + + return ResponseOutput.Ok(); + } + + #endregion + + + + #region 临床数据签名 领取、 设置紧急、RequestToQC QC通过、不通过 + //[HttpPut("{trialId:guid}/{subjectVisitId:guid}")] + //public async Task SignClinicalData(Guid trialId, Guid subjectVisitId) + //{ + // await _repository.UpdateFromQueryAsync(t => t.Id == subjectVisitId, u => new SubjectVisit() { ClinicalDataSignTime = DateTime.Now, ClinicalDataSignUserId = _userInfo.Id }); + + // return ResponseOutput.Ok(); + //} + + + /// + /// 手动领取 或者取消 QC任务 + /// + /// + /// + /// true 获取 false是取消领取 + /// + [HttpPost("{trialId:guid}/{subjectVisitId:guid}/{obtaionOrCancel:bool}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task ObtainOrCancelQCTask(Guid trialId, Guid subjectVisitId, bool obtaionOrCancel) + { + + var dbSubjectVisit = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == subjectVisitId); + + if (dbSubjectVisit == null) return Null404NotFound(dbSubjectVisit); + + if (obtaionOrCancel) + { + + if (await _subjectVisitRepository.AnyAsync(t => t.Trial.QCQuestionConfirmedUserId == null && t.Id == subjectVisitId)) + { + return ResponseOutput.NotOk("QC问题未确认,不允许领取任务"); + } + + + if (await _subjectVisitRepository.AnyAsync(t => t.IsTake && t.SubjectId != dbSubjectVisit.SubjectId && t.CurrentActionUserId == _userInfo.Id && t.TrialId == dbSubjectVisit.TrialId)) + { + return ResponseOutput.NotOk("您已经领取了其他受试者,完成后才允许领取新的受试者"); + } + + #region 处理验证 + + var trialConfig = await _repository.GetQueryable() + .Select(t => new { TrialId = t.Id, t.QCProcessEnum, t.IsImageConsistencyVerification }) + .FirstOrDefaultAsync(t => t.TrialId == trialId) + .IfNullThrowException(); + + if (dbSubjectVisit.IsTake) + { + return ResponseOutput.NotOk("当前已被领取,不允许领取"); + } + + if (trialConfig.QCProcessEnum == TrialQCProcess.NotAudit) + { + return ResponseOutput.NotOk("项目配置为不审,没有领取或者取消QC Task"); + } + else if (trialConfig.QCProcessEnum == TrialQCProcess.SingleAudit) + { + if (dbSubjectVisit.PreliminaryAuditUserId == _userInfo.Id) + { + return ResponseOutput.NotOk("初审已通过,不能继续领取"); + } + + if (dbSubjectVisit.SubmitState == SubmitStateEnum.Submitted && dbSubjectVisit.AuditState == AuditStateEnum.ToAudit) + { + dbSubjectVisit.AuditState = AuditStateEnum.InPrimaryQC; + } + if (dbSubjectVisit.SubmitState == SubmitStateEnum.Submitted && dbSubjectVisit.AuditState == AuditStateEnum.InPrimaryQC) + { + //单审QC 释放后 也是可以领取的 + } + else + { + return ResponseOutput.NotOk("项目配置为单审,不满足SubmmitState:已提交 或者 AuditState:待审核/审核中, 不允许领取,请刷新界面"); + } + } + else if (trialConfig.QCProcessEnum == TrialQCProcess.DoubleAudit) + { + if (dbSubjectVisit.PreliminaryAuditUserId == _userInfo.Id) + { + return ResponseOutput.NotOk("复审不能和初审是同一个人"); + } + + //提交 并且初审通过 那么领取后进入 复审中 + if (dbSubjectVisit.SubmitState == SubmitStateEnum.Submitted && dbSubjectVisit.AuditState == AuditStateEnum.PrimaryQCPassed) + { + dbSubjectVisit.AuditState = AuditStateEnum.InSecondaryQC; + + } + else if (dbSubjectVisit.SubmitState == SubmitStateEnum.Submitted && dbSubjectVisit.AuditState == AuditStateEnum.ToAudit) + { + dbSubjectVisit.AuditState = AuditStateEnum.InPrimaryQC; + } + else if (dbSubjectVisit.SubmitState == SubmitStateEnum.Submitted && (dbSubjectVisit.AuditState == AuditStateEnum.InPrimaryQC || dbSubjectVisit.AuditState == AuditStateEnum.InSecondaryQC)) + { + //初审中 复审中 领取也是ok的 其他人接着做 + } + else + { + return ResponseOutput.NotOk("项目配置为复审,不满足提交状态:已提交 或者 审核状态:待审核/QC中, 不允许领取,请刷新界面"); + } + } + + #endregion + + dbSubjectVisit.IsTake = true; + + dbSubjectVisit.CurrentActionUserId = _userInfo.Id; + + + dbSubjectVisit.CurrentActionUserExpireTime = DateTime.Now.AddHours(1); + + + //启动定时任务 1h后处理 + //BackgroundJob.Schedule(t => t.CancelQCObtaion(subjectVisitId, DateTime.Now), TimeSpan.FromHours(1)); + } + else + { + dbSubjectVisit.IsTake = false; + + dbSubjectVisit.CurrentActionUserId = null; + + + dbSubjectVisit.CurrentActionUserExpireTime = null; + + } + var success = await _repository.SaveChangesAsync(); + return ResponseOutput.Result(success); + } + + + /// + /// CRC RequestToQC 批量提交 + /// + /// + [HttpPost] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task CRCRequestToQC(CRCRequestToQCCommand cRCRequestToQCCommand) + { + var trialConfig = await _repository.GetQueryable() + .Select(t => new { TrialId = t.Id, t.QCProcessEnum, t.IsImageConsistencyVerification, t.IsUrgent, t.IsHaveFirstGiveMedicineDate, t.ClinicalInformationTransmissionEnum }) + .FirstOrDefaultAsync(t => t.TrialId == cRCRequestToQCCommand.TrialId); + + if (trialConfig == null) return Null404NotFound(trialConfig); + + var dbSubjectVisitList = await _subjectVisitRepository.Where(t => cRCRequestToQCCommand.SubjectVisitIds.Contains(t.Id), true).Include(t => t.Subject).ToListAsync(); + + + if (dbSubjectVisitList.Any(t => t.SubmitState == SubmitStateEnum.None)) + { + return ResponseOutput.NotOk("有访视未上传任何Dicom/非Dicom数据 不允许提交"); + } + + foreach (var dbSubjectVisit in dbSubjectVisitList) + { + + //基线不验证 + if (trialConfig.IsHaveFirstGiveMedicineDate && !dbSubjectVisit.IsBaseLine && dbSubjectVisit.Subject.FirstGiveMedicineTime == null) + { + return ResponseOutput.NotOk("项目配置了需要填写首次给药日期 但是受试者没有填写首次给药日期,不允许提交"); + } + + //基线 且配置了临床数据 + if (trialConfig.ClinicalInformationTransmissionEnum != 0 && dbSubjectVisit.IsBaseLine/*&&dbSubjectVisit.ClinicalDataSignUserId==null*/) + { + //已确认临床数据完整性 + dbSubjectVisit.IsConfirmedClinicalData = true; + + var signSuccess = await _repository.UpdateFromQueryAsync(t => t.Id == cRCRequestToQCCommand.SignId, u => new TrialSign() { IsCompleted = true }); + + + ////现在修改为 提交时 设置签名信息 + //dbSubjectVisit.ClinicalDataSignUserId = _userInfo.Id; + //dbSubjectVisit.ClinicalDataSignTime = DateTime.Now; + + //那么没有录入 不允许提交 + //if (!await _repository.AnyAsync(t => t.PreviousHistoryList.Any() || t.PreviousOtherList.Any() || t.PreviousSurgeryList.Any())) + //{ + // return ResponseOutput.NotOk("没有临床数据,不允许提交"); + //} + + //return ResponseOutput.NotOk("没有签名临床数据,不允许提交"); + } + + + var maxVisit = await _subjectVisitRepository.Where(t => t.SubjectId == dbSubjectVisit.SubjectId && t.SubmitState == SubmitStateEnum.Submitted) + .OrderByDescending(t => t.VisitNum).Select(t => new { t.Id, t.VisitNum }).FirstOrDefaultAsync(); + + //修改受试者最新访视 + dbSubjectVisit.Subject.LatestSubjectVisitId = maxVisit == null ? dbSubjectVisit.Id : maxVisit.VisitNum < dbSubjectVisit.VisitNum ? dbSubjectVisit.Id : maxVisit.Id; + + //var maxVisitNum = maxVisit == null ? dbSubjectVisit.VisitNum : maxVisit.VisitNum < dbSubjectVisit.VisitNum ? dbSubjectVisit.VisitNum : maxVisit.VisitNum; + + ////判断是否有缺失影像 + //dbSubjectVisit.Subject.IsMissingImages = await _subjectVisitRepository.AnyAsync(t => (t.VisitNum < maxVisitNum && t.SubmitState != SubmitStateEnum.Submitted && t.IsLostVisit == false)); + + //项目或者Subject IsUrgent 提交时 访视也设置为紧急 + if (trialConfig.IsUrgent || dbSubjectVisit.Subject.IsUrgent || (dbSubjectVisit.PDState == PDStateEnum.PDProgress && !dbSubjectVisit.IsBaseLine) || (dbSubjectVisit.IsEnrollmentConfirm && dbSubjectVisit.IsBaseLine)) + { + dbSubjectVisit.IsUrgent = true; + } + + + if (dbSubjectVisit.SubmitState == SubmitStateEnum.ToSubmit || dbSubjectVisit.SubmitState == SubmitStateEnum.Submitted) + { + dbSubjectVisit.SubmitState = SubmitStateEnum.Submitted; + dbSubjectVisit.SubmitTime = DateTime.Now; + + } + + //不审 直接QC通过 可能一致性核查 也可能不一致性核查 + if (trialConfig.QCProcessEnum == TrialQCProcess.NotAudit) + { + dbSubjectVisit.AuditState = AuditStateEnum.QCPassed; + + // 不一致性核查 就CVPassed ToForward 否则就是待核查 + dbSubjectVisit.CheckState = trialConfig.IsImageConsistencyVerification ? CheckStateEnum.ToCheck : CheckStateEnum.CVPassed; + + dbSubjectVisit.ForwardState = trialConfig.IsImageConsistencyVerification ? ForwardStateEnum.None : ForwardStateEnum.ToForward; + + } + else if (trialConfig.QCProcessEnum == TrialQCProcess.SingleAudit) + { + dbSubjectVisit.AuditState = AuditStateEnum.ToAudit; + } + else if (trialConfig.QCProcessEnum == TrialQCProcess.DoubleAudit) + { + dbSubjectVisit.AuditState = AuditStateEnum.ToAudit; + + + } + + + } + + + var success = await _repository.SaveChangesAsync(); + + + return ResponseOutput.Result(success); + + + + } + + /// + /// 设置QC 通过或者不通过 7:QC failed 8:QC passed + /// + /// + /// + /// + /// + /// + [HttpPost("{trialId:guid}/{subjectVisitId:guid}/{signId:guid}/{auditState:int}")] + + [TypeFilter(typeof(TrialResourceFilter))] + public async Task QCPassedOrFailed(Guid trialId, Guid subjectVisitId, Guid signId, [FromRoute] AuditStateEnum auditState) + { + + if (!await _repository.AnyAsync(t => t.TrialId == trialId && t.UserId == _userInfo.Id)) + { + return ResponseOutput.NotOk("您已经被移出项目,不允许该操作"); + } + + //判断质疑是否都关闭了 + if (await _repository.AnyAsync(t => t.SubjectVisitId == subjectVisitId && t.IsClosed == false)) + { + return ResponseOutput.NotOk("有质疑未关闭,不允许该操作"); + } + + + var trialConfig = await _repository.GetQueryable() + .Select(t => new { TrialId = t.Id, t.QCProcessEnum, t.IsImageConsistencyVerification }) + .FirstOrDefaultAsync(t => t.TrialId == trialId) + .IfNullThrowException(); + + var dbSubjectVisit = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == subjectVisitId); + + if (dbSubjectVisit == null) return Null404NotFound(dbSubjectVisit); + + //有人QC Passed + if (auditState == AuditStateEnum.QCPassed) + { + //判断 QC流程 不审 单审 双审 + + if (trialConfig.QCProcessEnum == TrialQCProcess.NotAudit) + { + return ResponseOutput.NotOk("不审状态下,不允许设置为QC Passed"); + + } + else if (trialConfig.QCProcessEnum == TrialQCProcess.SingleAudit) + { + if (dbSubjectVisit.AuditState == AuditStateEnum.InPrimaryQC) + { + if (!await _repository.AnyAsync(t => t.SubjectVisitId == subjectVisitId && t.CurrentQCEnum == CurrentQC.First)) + { + return ResponseOutput.NotOk("必须填写审核问题,并保存,否则不允许提交"); + } + + // 单审 + dbSubjectVisit.AuditState = AuditStateEnum.QCPassed; + dbSubjectVisit.CheckState = trialConfig.IsImageConsistencyVerification ? CheckStateEnum.ToCheck : CheckStateEnum.CVPassed; + dbSubjectVisit.ForwardState = trialConfig.IsImageConsistencyVerification ? ForwardStateEnum.None : ForwardStateEnum.ToForward; + dbSubjectVisit.PreliminaryAuditUserId = _userInfo.Id; + dbSubjectVisit.PreliminaryAuditTime = DateTime.Now; + + dbSubjectVisit.IsTake = false; + dbSubjectVisit.CurrentActionUserExpireTime = null; + + + } + else + { + return ResponseOutput.NotOk("项目配置为单审 当前审核状态不为 InPrimaryQC,不能变更到 QCPassed"); + } + + } + else if (trialConfig.QCProcessEnum == TrialQCProcess.DoubleAudit) + { + + // 双审 如果当前db 状态是 InPrimaryQC 当前操作为 QCPassed 那么设置为 PrimaryQCPassed + if (dbSubjectVisit.AuditState == AuditStateEnum.InPrimaryQC) + { + + if (!await _repository.AnyAsync(t => t.SubjectVisitId == subjectVisitId && t.CurrentQCEnum == CurrentQC.First)) + { + return ResponseOutput.NotOk("必须填写审核问题,并保存,否则不允许提交"); + } + + dbSubjectVisit.AuditState = AuditStateEnum.PrimaryQCPassed; + dbSubjectVisit.PreliminaryAuditUserId = _userInfo.Id; + dbSubjectVisit.PreliminaryAuditTime = DateTime.Now; + + dbSubjectVisit.IsTake = false; + dbSubjectVisit.CurrentActionUserExpireTime = null; + } + else if (dbSubjectVisit.AuditState == AuditStateEnum.InSecondaryQC) + { + + if (!await _repository.AnyAsync(t => t.SubjectVisitId == subjectVisitId && t.CurrentQCEnum == CurrentQC.Second)) + { + return ResponseOutput.NotOk("必须填写审核问题,并保存,否则不允许提交"); + } + + dbSubjectVisit.AuditState = AuditStateEnum.QCPassed; + + dbSubjectVisit.CheckState = trialConfig.IsImageConsistencyVerification ? CheckStateEnum.ToCheck : CheckStateEnum.CVPassed; + + dbSubjectVisit.ForwardState = trialConfig.IsImageConsistencyVerification ? ForwardStateEnum.None : ForwardStateEnum.ToForward; + + dbSubjectVisit.ReviewAuditUserId = _userInfo.Id; + + dbSubjectVisit.ReviewAuditTime = DateTime.Now; + + dbSubjectVisit.IsTake = false; + dbSubjectVisit.CurrentActionUserExpireTime = null; + + } + else + { + return ResponseOutput.NotOk($"项目配置为双审 当前审核状态为 {dbSubjectVisit.AuditState},不能变更到 QCPassed"); + } + } + } + + else if (auditState == AuditStateEnum.QCFailed) + { + //判断 QC流程 不审 单审 双审 + + if (trialConfig.QCProcessEnum == TrialQCProcess.NotAudit) + { + return ResponseOutput.NotOk("不审状态下,不允许设置为QC Failed"); + } + else if (trialConfig.QCProcessEnum == TrialQCProcess.SingleAudit) + { + + if (dbSubjectVisit.AuditState == AuditStateEnum.InPrimaryQC) + { + // 单审 + dbSubjectVisit.AuditState = AuditStateEnum.QCFailed; + } + else + { + return ResponseOutput.NotOk("项目配置为单审 当前审核状态不为 InPrimaryQC,不能变更到 QCFailed"); + } + } + else if (trialConfig.QCProcessEnum == TrialQCProcess.DoubleAudit) + { + // 双审 + if (dbSubjectVisit.AuditState == AuditStateEnum.InPrimaryQC || dbSubjectVisit.AuditState == AuditStateEnum.InSecondaryQC) + { + dbSubjectVisit.AuditState = AuditStateEnum.QCFailed; + } + else + { + return ResponseOutput.NotOk($"项目配置为双审 当前审核状态为 {dbSubjectVisit.AuditState},不能变更到 QCFailed"); + } + + + } + } + + //删除 软删除的物理文件 + + var instanceIdList = await _repository.Where(t => t.DicomSerie.IsDeleted && t.SubjectVisitId == subjectVisitId) + .Select(t => new { InstanceId = t.Id, t.SeriesId, t.StudyId, t.SubjectId, t.SiteId }).ToListAsync(); + + instanceIdList.ForEach(t => + { + var path = _dicomFileStoreHelper.GetInstanceFilePath(new DicomStudy() { Id = t.StudyId, SubjectId = t.SubjectId, TrialId = trialId, SiteId = t.SiteId, SubjectVisitId = subjectVisitId }, t.SeriesId, t.InstanceId.ToString()); + + if (System.IO.File.Exists(path)) + { + File.Delete(path); + } + + }); + + await _repository.SaveChangesAsync(); + + var success = await _repository.UpdateFromQueryAsync(t => t.Id == signId, u => new TrialSign() { IsCompleted = true }); + + return ResponseOutput.Result(success); + + + } + + + /// + /// 设置、取消 访视紧急 + /// + /// + /// + /// + /// + [HttpPut("{trialId:guid}/{subjectVisitId:guid}/{setOrCancel:bool}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task SetVisitUrgent(Guid trialId, Guid subjectVisitId, bool setOrCancel) + { + var sv = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == subjectVisitId); + if (sv == null) return Null404NotFound(sv); + + sv.IsUrgent = setOrCancel; + var success = await _repository.SaveChangesAsync(); + return ResponseOutput.Ok(); + } + + #endregion + + + #region 重传、重传完设置 + + /// + /// QA设置 需要重传 + /// + /// + /// + /// + /// + [HttpPut("{trialId:guid}/{qcChallengeId:guid}/{signId:guid}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task SetNeedReupload(Guid trialId, Guid signId, Guid qcChallengeId) + { + if (_userInfo.UserTypeEnumInt != (int)UserTypeEnum.IQC) + { + return ResponseOutput.NotOk("重传 只允许QA 设置!"); + } + + //获取项目配置 + var trialConfig = await _repository.Where(t => t.Id == trialId).Select(t => new { TrialId = t.Id, t.QCProcessEnum, t.IsImageConsistencyVerification }) + .FirstOrDefaultAsync().IfNullThrowException(); + + if (trialConfig.QCProcessEnum == TrialQCProcess.NotAudit) + { + return ResponseOutput.NotOk("不审操作,不会有需要重传的操作!"); + } + + var qcChallenge = await _repository.FirstOrDefaultAsync(t => t.Id == qcChallengeId); + if (qcChallenge == null) return Null404NotFound(qcChallenge); + + if (await _repository.CountAsync(t => t.ReuploadEnum == QCChanllengeReuploadEnum.QCAgreeUpload && t.SubjectVisitId == qcChallenge.SubjectVisitId && t.IsClosed == false) >= 1) + { + return ResponseOutput.NotOk("当前访视,有一个未关闭的质疑 QC设置了需要重传,CRC还未完成上传,当前不允许再次设置"); + } + + qcChallenge.ReuploadEnum = QCChanllengeReuploadEnum.QCAgreeUpload; + qcChallenge.LatestMsgTime = DateTime.Now; + qcChallenge.LatestReplyUserId = _userInfo.Id; + + await _subjectVisitRepository.UpdateFromQueryAsync(t => t.Id == qcChallenge.SubjectVisitId, c => new SubjectVisit() { IsQCConfirmedReupload = true }); + + qcChallenge.DialogList.Add(new QCChallengeDialog() + { + SubjectVisitId = qcChallenge.SubjectVisitId, + UserTypeEnum = (UserTypeEnum)_userInfo.UserTypeEnumInt, + QCChallengeId = qcChallenge.Id, + TalkContent = "QC同意重传" + }); + + //双审 并且是2QC 那么需要回退到1QC 讲1QC数据清除 + if (trialConfig.QCProcessEnum == TrialQCProcess.DoubleAudit && await _repository.AnyAsync(t => t.Id == qcChallengeId && t.SubjectVisit.AuditState == AuditStateEnum.InSecondaryQC)) + { + + var sv = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == qcChallenge.SubjectVisitId); + + if (sv == null) return Null404NotFound(sv); + + // //一致性核查质疑状态 + // sv.CheckChallengeState = CheckChanllengeTypeEnum.None; + //// 一致性核查状态 + // sv.CheckState = CheckStateEnum.None; + // 审核状态 + sv.AuditState = AuditStateEnum.InPrimaryQC; + + sv.CurrentActionUserExpireTime = DateTime.Now.AddHours(1); + sv.CurrentActionUserId = _userInfo.Id; + BackgroundJob.Schedule(t => t.CancelQCObtaion(qcChallenge.SubjectVisitId, DateTime.Now), TimeSpan.FromHours(1)); + sv.IsTake = true; + + sv.PreliminaryAuditUserId = null; + sv.ReviewAuditUserId = null; + + + //删除1QC 填写的问题答案 + + await _repository.DeleteFromQueryAsync(t => t.SubjectVisitId == qcChallenge.SubjectVisitId && t.CurrentQCEnum == CurrentQC.First); + + //2QC 数据变为1QC + await _repository.UpdateFromQueryAsync(t => t.SubjectVisitId == qcChallenge.SubjectVisitId && t.CurrentQCEnum == CurrentQC.Second, k => new QCChallenge() { CurrentQCEnum = CurrentQC.First }); + await _repository.UpdateFromQueryAsync(t => t.SubjectVisitId == qcChallenge.SubjectVisitId && t.CurrentQCEnum == CurrentQC.Second, k => new TrialQCQuestionAnswer() { CurrentQCEnum = CurrentQC.First }); + + } + + + var success = await _repository.SaveChangesAsync(); + + var signSuccess = await _repository.UpdateFromQueryAsync(t => t.Id == signId, u => new TrialSign() { IsCompleted = true }); + + return ResponseOutput.Result(success && signSuccess); + + } + + + + + /// + /// CRC 设置已经重传完成 + /// + + /// + [HttpPost] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task SetReuploadFinished(CRCReuploadFinishedCommand cRCReuploadFinishedCommand) + { + if (_userInfo.UserTypeEnumInt != (int)UserTypeEnum.ClinicalResearchCoordinator) + { + return ResponseOutput.NotOk("重传完成 只允许CRC 设置!"); + } + + var trialConfig = await _repository.GetQueryable() + .Select(t => new { TrialId = t.Id, t.QCProcessEnum, t.IsImageConsistencyVerification, t.IsUrgent, t.IsHaveFirstGiveMedicineDate, t.ClinicalInformationTransmissionEnum }) + .FirstOrDefaultAsync(t => t.TrialId == cRCReuploadFinishedCommand.TrialId); + + + var qcChallenge = await _repository.FirstOrDefaultAsync(t => t.Id == cRCReuploadFinishedCommand.QCChallengeId); + + if (qcChallenge == null) return Null404NotFound(qcChallenge); + + qcChallenge.ReuploadEnum = QCChanllengeReuploadEnum.CRCReuploaded; + + qcChallenge.ReUploadedTime = DateTime.Now; + + qcChallenge.ReUploader = _userInfo.RealName; + + qcChallenge.LatestMsgTime = DateTime.Now; + + qcChallenge.LatestReplyUserId = _userInfo.Id; + + var dbSubjectVisit = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == qcChallenge.SubjectVisitId).IfNullThrowException(); + + + await _subjectVisitRepository.UpdateFromQueryAsync(t => t.Id == qcChallenge.SubjectVisitId, c => new SubjectVisit() { IsQCConfirmedReupload = false }); + + qcChallenge.DialogList.Add(new QCChallengeDialog() + { + SubjectVisitId = qcChallenge.SubjectVisitId, + + UserTypeEnum = (UserTypeEnum)_userInfo.UserTypeEnumInt, + + QCChallengeId = qcChallenge.Id, + + TalkContent = "CRC已重传完成" + }); + + + //基线 且配置了临床数据 + if (trialConfig.ClinicalInformationTransmissionEnum != 0 && dbSubjectVisit.IsBaseLine) + { + //已确认临床数据完整性 + dbSubjectVisit.IsConfirmedClinicalData = true; + + var signSuccess = await _repository.UpdateFromQueryAsync(t => t.Id == cRCReuploadFinishedCommand.SignId, u => new TrialSign() { IsCompleted = true }); + + + } + + var success = await _repository.SaveChangesAsync(); + + + return ResponseOutput.Ok(success); + + } + + + + [HttpPut("{trialId:guid}/{qcChallengeId:guid}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task CRCRequestReUpload(Guid qcChallengeId) + { + var qcChallenge = await _repository.FirstOrDefaultAsync(t => t.Id == qcChallengeId); + + if (qcChallenge == null) return Null404NotFound(qcChallenge); + + if (qcChallenge.ReuploadEnum == QCChanllengeReuploadEnum.CRCReuploaded) + { + qcChallenge.ReUploadedTime = null; + } + + qcChallenge.LatestMsgTime = DateTime.Now; + qcChallenge.LatestReplyUserId = _userInfo.Id; + qcChallenge.ReuploadEnum = QCChanllengeReuploadEnum.CRCRequestReupload; + + qcChallenge.DialogList.Add(new QCChallengeDialog() + { + SubjectVisitId = qcChallenge.SubjectVisitId, + UserTypeEnum = (UserTypeEnum)_userInfo.UserTypeEnumInt, + QCChallengeId = qcChallenge.Id, + TalkContent = "CRC申请重传/上传影像" + }); + + await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(); + } + + #endregion + + + + /// + /// 上传界面 更新受试者首次给药日期 是否入组确认,以及访视 是否PD进展 + /// + /// + /// + [HttpPut("{trialId:guid}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task UpdateSubjectAndSVInfo(UploadSubjectAndVisitCommand command) + { + if (command.IsEnrollmentConfirm != null) + { + if (await _subjectVisitRepository.Where(t => t.Id == command.SubjectVisitId) + .AnyAsync(t => t.SubmitState == SubmitStateEnum.Submitted && t.IsEnrollmentConfirm != command.IsEnrollmentConfirm)) + { + return ResponseOutput.NotOk("CRC已提交了 不能修改入组确认状态"); + } + + if (await _subjectVisitRepository.Where(t => t.Id == command.SubjectVisitId) + .AnyAsync(t => t.IsEnrollmentConfirm != command.IsEnrollmentConfirm && t.RequestBackState == RequestBackStateEnum.PM_AgressBack)) + { + return ResponseOutput.NotOk("回退的访视,不允许修改PD确认状态"); + } + + await _repository.UpdateFromQueryAsync(t => t.Id == command.SubjectVisitId, u => new SubjectVisit() + { + IsEnrollmentConfirm = command.IsEnrollmentConfirm.Value, + }); + } + + if (command.SubjectFirstGiveMedicineTime != null) + { + await _repository.UpdateFromQueryAsync(t => t.Id == command.SubjectId, u => new Subject() + { + FirstGiveMedicineTime = command.SubjectFirstGiveMedicineTime, + + }); + } + + ////受试者基线 入组确认 或者访视PD 进展 默认加急 + //await _repository.UpdateFromQueryAsync(t => t.Id == command.SubjectVisitId, u => new SubjectVisit() + //{ + // PDState = command.PDState, + // IsUrgent = (command.IsEnrollmentConfirm == true) || (command.PDState == PDStateEnum.PDProgress) + + //}); + + return ResponseOutput.Ok(); + } + + + + /// + /// 验证QC是否可以操作 数据库查询判断当前QC执行人和登陆的用户是否一致 + /// + /// + /// + private async Task VerifyQCCanOpt(Guid subjectVisitId) + { + var optUser = await _subjectVisitRepository.Where(t => t.Id == subjectVisitId).FirstOrDefaultAsync(); + + if (optUser == null) return Null404NotFound(optUser); + + if (optUser.CurrentActionUserId != _userInfo.Id) + { + return ResponseOutput.NotOk("其他QC正在进行审核,当前操作不允许"); + } + + return ResponseOutput.Ok(); + } + + + + [HttpGet("{trialId:guid}/{subjectVisitId:guid}")] + public async Task ForwardSVDicomImage(Guid subjectVisitId, [FromServices] DicomFileStoreHelper _dicomFileStoreHelper) + { + + + var info = await _subjectVisitRepository.Where(t => t.Id == subjectVisitId).ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync(); + + var targetPath = Path.Combine("/IMPORT-IMAGES", info.TrialCode + "_" + info.SubjectCode + "_" + info.VisitName); + + var path = _dicomFileStoreHelper.GetSubjectVisitPath(info.TrialId, info.SiteId, info.SubjectId, info.SubjectVisitId); + + try + { + // 主机及端口信息后面可以改到 配置文件 + SessionOptions sessionOptions = new SessionOptions + { + Protocol = Protocol.Sftp, + PortNumber = 8022, + HostName = "CS-690-sftp.mint-imaging.com", + UserName = "zdong", + Password = "Everest@2021", + //GiveUpSecurityAndAcceptAnySshHostKey = true, + SshHostKeyFingerprint = @"ecdsa-sha2-nistp384 384 59gkjJ5lMwv3jsB8Wz2B35tBAIor5pSd8PcJYtoamPo=" + }; + + using (Session session = new Session()) + { + session.Open(sessionOptions); + if (!session.FileExists(targetPath)) + { + session.CreateDirectory(targetPath); + } + + var files = (new DirectoryInfo(path)).GetFiles(); + + foreach (var file in files) + { + if (file.Extension.Contains("dcm")) + { + string remoteFilePath = + RemotePath.TranslateLocalPathToRemote(file.FullName, path, targetPath); + + var result = session.PutFiles(file.FullName, remoteFilePath, false); + + if (!result.IsSuccess) + { + await _subjectVisitRepository.UpdateFromQueryAsync(t => t.Id == subjectVisitId, + u => new SubjectVisit() { ForwardState = ForwardStateEnum.ForwardFailed }); + + return ResponseOutput.NotOk("Forward Failed"); + } + } + } + + + } + + } + catch (Exception e) + { + await _subjectVisitRepository.UpdateFromQueryAsync(t => t.Id == subjectVisitId, + u => new SubjectVisit() { ForwardState = ForwardStateEnum.ForwardFailed }); + + } + + await _subjectVisitRepository.UpdateFromQueryAsync(t => t.Id == subjectVisitId, + u => new SubjectVisit() { ForwardState = ForwardStateEnum.Forwarded }); + + return ResponseOutput.Ok(); + } + + } +} diff --git a/IRaCIS.Core.Application/Service/QC/QCQuestionService.cs b/IRaCIS.Core.Application/Service/QC/QCQuestionService.cs new file mode 100644 index 00000000..fe0d94b3 --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/QCQuestionService.cs @@ -0,0 +1,55 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-11 11:04:54 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Core.Application.Contracts +{ + /// + /// 系统QC 问题管理 + /// + [ApiExplorerSettings(GroupName = "Image")] + public class QCQuestionConfigureService : BaseService, IQCQuestionService + { + private readonly IRepository _qcQuestionRepository; + + public QCQuestionConfigureService(IRepository qcQuestionRepository) + { + _qcQuestionRepository = qcQuestionRepository; + } + + + [HttpPost] + public async Task> GetQCQuestionConfigureList(QCQuestionQuery queryQCQuestionConfigure) + { + + var QCQuestionQueryable = _qcQuestionRepository + .WhereIf(!string.IsNullOrWhiteSpace(queryQCQuestionConfigure.QuestionName), t => t.QuestionName.Contains(queryQCQuestionConfigure.QuestionName)) + .WhereIf(!string.IsNullOrWhiteSpace(queryQCQuestionConfigure.Type), t => t.Type.Contains(queryQCQuestionConfigure.Type)) + .ProjectTo(_mapper.ConfigurationProvider); + + return await QCQuestionQueryable.ToListAsync(); + } + + public async Task AddOrUpdateQCQuestionConfigure(QCQuestionAddOrEdit addOrEditQCQuestionConfigure) + { + + var entity = await _qcQuestionRepository.InsertOrUpdateAsync(addOrEditQCQuestionConfigure, true); + return ResponseOutput.Ok(entity.Id.ToString()); + } + + + [HttpDelete("{qCQuestionConfigureId:guid}")] + public async Task DeleteQCQuestionConfigure(Guid qCQuestionConfigureId) + { + var success = await _qcQuestionRepository.DeleteFromQueryAsync(t => t.Id == qCQuestionConfigureId); + return ResponseOutput.Result(success); + } + + + } +} diff --git a/IRaCIS.Core.Application/Service/QC/TrialQCQuestionService.cs b/IRaCIS.Core.Application/Service/QC/TrialQCQuestionService.cs new file mode 100644 index 00000000..2f4b9175 --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/TrialQCQuestionService.cs @@ -0,0 +1,162 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-11 11:04:54 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Core.Application.Contracts +{ + /// + /// 项目QC 问题 管理 + /// + [ApiExplorerSettings(GroupName = "Image")] + public class TrialQCQuestionConfigureService : BaseService, ITrialQCQuestionConfigureService + { + private readonly IRepository _trialQcQuestionRepository; + + public TrialQCQuestionConfigureService(IRepository trialQcQuestionRepository) + { + _trialQcQuestionRepository = trialQcQuestionRepository; + } + + + [HttpPost] + public async Task<(List, object)> GetTrialQCQuestionConfigureList(TrialQCQuestionQuery queryTrialQCQuestionConfigure) + { + + + var trialQCQuestionQueryable = _trialQcQuestionRepository.Where(t => t.TrialId == queryTrialQCQuestionConfigure.TrialId) + .WhereIf(!string.IsNullOrWhiteSpace(queryTrialQCQuestionConfigure.QuestionName), t => t.QuestionName.Contains(queryTrialQCQuestionConfigure.QuestionName)) + .WhereIf(!string.IsNullOrWhiteSpace(queryTrialQCQuestionConfigure.Type), t => t.Type.Contains(queryTrialQCQuestionConfigure.Type)) + .ProjectTo(_mapper.ConfigurationProvider); + + var list = await trialQCQuestionQueryable.OrderBy(t => t.ShowOrder).ToListAsync(); + + var signInfo = await _repository.Where(t => t.Id == queryTrialQCQuestionConfigure.TrialId) + .Select(trial => new { trial.QCQuestionConfirmedTime, trial.QCQuestionConfirmedUserId, RealName = trial.QCQuestionConfirmedUser.LastName + " / " + trial.QCQuestionConfirmedUser.FirstName, trial.QCQuestionConfirmedUser.UserName }).FirstOrDefaultAsync(); + + return (list, signInfo!); + } + + + /// + /// 父问题 下拉框选项 需要排除自己 、把自己设置为父亲 (互为父亲) 、是自己孙辈的(明明是自己子孙,却设置为自己父亲) + /// + /// + /// + [HttpPost] + public async Task> GetTrialQCQuestionSelectList(TrialQCQuestionFilterSelect trialQCQuestionFilterSelect) + { + + //设置父亲的时候,不允许设置为自己的孙子 这种会形成环 + + var initList = await _trialQcQuestionRepository.Where(t => t.TrialId == trialQCQuestionFilterSelect.TrialId) + .WhereIf(trialQCQuestionFilterSelect.TypeArray.Count() > 0, t => trialQCQuestionFilterSelect.TypeArray.Contains(t.Type)) + //.WhereIf(trialQCQuestionFilterSelect.Id != null, t => t.Id != trialQCQuestionFilterSelect.Id /*&& t.ParentId != trialQCQuestionFilterSelect.Id*/) + .OrderBy(t => t.ShowOrder).ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + + //父亲的序号肯定要比自己小 + if (trialQCQuestionFilterSelect.Id != null) + { + var selectItem = initList.FirstOrDefault(t => t.Id == trialQCQuestionFilterSelect.Id)!; + + initList = initList.Where(t => t.Id != selectItem.Id && t.ShowOrder < selectItem.ShowOrder).ToList(); + } + + + var exceptList = GetChildId(trialQCQuestionFilterSelect.Id ?? Guid.Empty, initList); + + + return initList.Where(t => !exceptList.Contains(t.Id)).ToList(); + } + + + private List GetChildId(Guid parentId, List list) + { + + var ids = new List(); + + var childIds = list.Where(t => t.ParentId == parentId).Select(t => t.Id).ToList(); + + foreach (var childId in childIds) + { + ids.AddRange(childId); + + var childs = GetChildId(childId, list); + + ids.AddRange(childs); + + } + + return ids; + } + + /// + /// 批量添加 QC 问题 + /// + /// + /// + /// + [HttpPost("{trialId:guid}")] + public async Task BatchAddTrialQCQuestionConfigure(List batchList, Guid trialId) + { + if (!await _repository.AnyAsync(t => t.Id == trialId && t.QCQuestionConfirmedUserId == null)) + { + return ResponseOutput.NotOk("QC已确认,不允许操作"); + } + + var batchConfigList = _mapper.Map>(batchList); + batchConfigList.ForEach(t => t.TrialId = trialId); + + await _trialQcQuestionRepository.AddRangeAsync(batchConfigList); + + var success = await _repository.SaveChangesAsync(); + + return ResponseOutput.Result(success); + } + + + public async Task AddOrUpdateTrialQCQuestionConfigure(TrialQCQuestionAddOrEdit addOrEditTrialQCQuestionConfigure) + { + if (!await _repository.AnyAsync(t => t.Id == addOrEditTrialQCQuestionConfigure.TrialId && t.QCQuestionConfirmedUserId == null)) + { + return ResponseOutput.NotOk("QC已确认,不允许操作"); + } + + var entity = await _trialQcQuestionRepository.InsertOrUpdateAsync(addOrEditTrialQCQuestionConfigure, true); + + return ResponseOutput.Ok(entity.Id.ToString()); + } + + + [HttpDelete("{trialQCQuestionConfigureId:guid}")] + public async Task DeleteTrialQCQuestionConfigure(Guid trialQCQuestionConfigureId) + { + + if (await _trialQcQuestionRepository.AnyAsync(t => t.ParentId == trialQCQuestionConfigureId)) + { + return ResponseOutput.NotOk("清除子问题 才允许删除父问题"); + } + + if (!await _trialQcQuestionRepository.AnyAsync(t => t.Id == trialQCQuestionConfigureId && t.Trial.QCQuestionConfirmedUserId == null)) + { + return ResponseOutput.NotOk("QC已确认,不允许操作"); + } + + if (await _repository.AnyAsync(t => t.TrialQCQuestionConfigureId == trialQCQuestionConfigureId)) + { + return ResponseOutput.NotOk("已有QC审核记录,不允许删除问题项"); + } + + var success = await _trialQcQuestionRepository.DeleteFromQueryAsync(t => t.Id == trialQCQuestionConfigureId); + return ResponseOutput.Result(success); + } + + + + + } +} diff --git a/IRaCIS.Core.Application/Service/QC/_MapConfig.cs b/IRaCIS.Core.Application/Service/QC/_MapConfig.cs new file mode 100644 index 00000000..928fa337 --- /dev/null +++ b/IRaCIS.Core.Application/Service/QC/_MapConfig.cs @@ -0,0 +1,243 @@ +using AutoMapper; +using AutoMapper.EquivalencyExpression; +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Application.Contracts.DTO; +using IRaCIS.Core.Application.MediatR.CommandAndQueries; + +namespace IRaCIS.Core.Application.Service +{ + public class QCConfig : Profile + { + public QCConfig() + { + string token = string.Empty; + //一致性核查 + CreateMap(); + + CreateMap(); + CreateMap(); + + + #region QA 废弃 + //CreateMap(); + + //CreateMap(); + + + //CreateMap(); + + + //CreateMap(); + + + //CreateMap(); + + //CreateMap(); + //CreateMap(); + + + //CreateMap(); + + //CreateMap(); + + //CreateMap(); + //CreateMap(); + + //CreateMap(); + + + + CreateMap(); + + + + #endregion + CreateMap(); + + CreateMap(); + + CreateMap(); + + CreateMap(); + + CreateMap(); + + + CreateMap() + .ForMember(d => d.ParentShowOrder, u => u.MapFrom(s => s.ParentQuestion.ShowOrder)); ; + + CreateMap(); + + CreateMap(); + + CreateMap() + .ForMember(d => d.ParentShowOrder, u => u.MapFrom(s => s.ParentQCQuestion.ShowOrder)); + + CreateMap(); + + CreateMap(); + + CreateMap().ReverseMap(); + //受试者临床数据 添加编辑 + CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); + + // 受试者临床数据 视图映射 + Guid subjectVisitId = Guid.Empty; + CreateMap() + .ForMember(d => d.SubjectCode, u => u.MapFrom(s => s.Subject.Code)) + .ForMember(d => d.SubjectVisitId, u => u.MapFrom(s => s.Id)) + .ForMember(d => d.TrialSiteCode, u => u.MapFrom(s => s.TrialSite.TrialSiteCode)) + .ForMember(d => d.PreviousHistoryList, u => u.MapFrom(s => s.PreviousHistoryList.Where(t => t.SubjectVisitId == subjectVisitId || t.IsSubjectLevel))) + .ForMember(d => d.PreviousOtherList, u => u.MapFrom(s => s.PreviousOtherList.Where(t => t.SubjectVisitId == subjectVisitId || t.IsSubjectLevel))) + .ForMember(d => d.PreviousSurgeryList, u => u.MapFrom(s => s.PreviousSurgeryList.Where(t => t.SubjectVisitId == subjectVisitId || t.IsSubjectLevel))); + + CreateMap(); + CreateMap(); + + CreateMap() + .ForMember(d => d.FullFilePath, u => u.MapFrom(s => s.Path + "?access_token=" + token)); + + + //影像质控 + CreateMap() + .ForMember(d => d.ChallengeCount, u => u.MapFrom(s => s.QCChallengeList.Count())) + + //.ForMember(d => d.SubjectCode, u => u.MapFrom(s => s.Subject.Code)) + //.ForMember(d => d.IsEnrollmentConfirm, u => u.MapFrom(s => s.Subject.IsEnrollmentConfirm)) + //.ForMember(d => d.FistGiveMedicineTime, u => u.MapFrom(s => s.Subject.FistGiveMedicineTime)) + //.ForMember(d => d.SubjectVisitId, u => u.MapFrom(s => s.Id)) + .ForMember(d => d.TrialSiteCode, u => u.MapFrom(s => s.TrialSite.TrialSiteCode)) + .ForMember(d => d.QCProcessEnum, u => u.MapFrom(s => s.Trial.QCProcessEnum)) + .ForMember(d => d.SubjectStatus, u => u.MapFrom(s => s.Subject.Status)) + .ForMember(d => d.StudyCount, u => u.MapFrom(s => s.StudyList.Count())) + .ForMember(d => d.CurrentActionUser, u => u.MapFrom(s => s.CurrentActionUser.LastName + " / " + s.CurrentActionUser.FirstName)) + .ForMember(d => d.PreliminaryAuditUser, u => u.MapFrom(s => s.PreliminaryAuditUser.LastName + " / " + s.PreliminaryAuditUser.FirstName)) + .ForMember(d => d.ReviewAuditUser, u => u.MapFrom(s => s.ReviewAuditUser.LastName + " / " + s.ReviewAuditUser.FirstName)) + .ForMember(d => d.IsHaveClinicalData, u => u.MapFrom(t => t.IsBaseLine ? t.PreviousHistoryList.Any() || t.PreviousOtherList.Any() || t.PreviousPDFList.Any() || t.PreviousSurgeryList.Any() : false)) + .ForMember(d => d.DicomStudyCount, u => u.MapFrom(t => t.StudyList.Count())) + .ForMember(d => d.NoneDicomStudyCount, u => u.MapFrom(t => t.NoneDicomStudyList.Count(t => t.NoneDicomFileList.Any()))); + + //CRC 上传列表 + CreateMap()/*.IncludeMembers(t=>t.Subject)*/ + //.ForMember(d => d.SubjectStatus, u => u.MapFrom(s => s.Subject.Status)) + //.ForMember(d => d.SubjectCode, u => u.MapFrom(s => s.Subject.Code)) + // .ForMember(d => d.SubejctIsEnrollmentConfirm, u => u.MapFrom(t => t.Subject.IsEnrollmentConfirm)) + // .ForMember(d => d.SubejctFistGiveMedicineTime, u => u.MapFrom(t => t.Subject.FistGiveMedicineTime)) + .ForMember(d => d.TrialSiteCode, u => u.MapFrom(s => s.TrialSite.TrialSiteCode)) + .ForMember(d => d.QCProcessEnum, u => u.MapFrom(s => s.Trial.QCProcessEnum)) + .ForMember(d => d.SubjectId, u => u.MapFrom(t => t.Subject.Id)) + + .ForMember(d => d.IsHaveClinicalData, u => u.MapFrom(t=> t.IsBaseLine? t.PreviousHistoryList.Any() || t.PreviousOtherList.Any() || t.PreviousPDFList.Any() || t.PreviousSurgeryList.Any() :false)) + + //.ForMember(d => d.VisitName, u => u.MapFrom(t =>t.InPlan? t.VisitStage.VisitName : t.VisitName)) + //.ForMember(d => d.VisitNum, u => u.MapFrom(t => t.InPlan ? t.VisitStage.VisitNum : t.VisitNum)) + //.ForMember(d => d.VisitDay, u => u.MapFrom(t => t.InPlan ? t.VisitStage.VisitDay : t.VisitDay)) + .ForMember(d => d.DicomStudyCount, u => u.MapFrom(t => t.StudyList.Count())) + .ForMember(d => d.NoneDicomStudyCount, u => u.MapFrom(t => t.NoneDicomStudyList.Count(t=>t.NoneDicomFileList.Any()))); + //.ForMember(d => d.StudyCount, u => u.MapFrom(s => s.StudyList.Count())); + CreateMap(); + + //一致性核查 + CreateMap() + .ForMember(d => d.SubjectCode, u => u.MapFrom(s => s.Subject.Code)) + .ForMember(d => d.TrialSiteCode, u => u.MapFrom(s => s.TrialSite.TrialSiteCode)); + + CreateMap() + .ForMember(d => d.SubjectCode, u => u.MapFrom(s => s.Subject.Code)) + .ForMember(d => d.TrialSiteCode, u => u.MapFrom(s => s.TrialSite.TrialSiteCode)); + + //QC 界面 受试者 site 基本信息 展平的属性 比如 SubjectAge => Subject.Age + CreateMap().IncludeMembers(t => t.Subject) + .ForMember(d => d.SubjectVisitId, u => u.MapFrom(s => s.Id)) + .ForMember(d => d.SubjectName, u => u.MapFrom(s => s.Subject.ShortName)) + .ForMember(d => d.IsHaveFirstGiveMedicineDate, u => u.MapFrom(s => s.Trial.IsHaveFirstGiveMedicineDate)) + //.ForMember(d => d.ChangeDefalutDays, u => u.MapFrom(s => s.Trial.ChangeDefalutDays)) + .ForMember(d => d.SubjectFirstGiveMedicineTime, u => u.MapFrom(s => s.Subject.FirstGiveMedicineTime)) + .ForMember(d => d.SiteName, u => u.MapFrom(s => s.Site.SiteName)) + .ForMember(d => d.TotalChallengeCount, u => u.MapFrom(s => s.QCChallengeList.Count())) + .ForMember(d => d.NotClosedChallengeCount, u => u.MapFrom(s => s.QCChallengeList.Count(c=>c.IsClosed==false))); + + + CreateMap(MemberList.None); + + + + // 临床数据上传 路径拼接返回 + + CreateMap() + .ForMember(d => d.FullFilePath, u => u.MapFrom(s => s.Path + "?access_token=" + token)); + CreateMap() + .ForMember(d => d.FullFilePath, u => u.MapFrom(s => s.Path + "?access_token=" + token)); + CreateMap() + .ForMember(d => d.FullFilePath, u => u.MapFrom(s => s.Path + "?access_token=" + token)); + + //一致性核查 质疑对话 + CreateMap(); + + //QC 质疑对话 + var currentUserId = Guid.Empty; + CreateMap() + .ForMember(d => d.IsCurrentUser, u => u.MapFrom(s => s.CreateUserId == currentUserId)); + //质疑编号 + CreateMap() + .ForMember(d => d.LatestReplyUser, u => u.MapFrom(t => t.LatestReplyUser.LastName + t.Creator.FirstName)) + .ForMember(d => d.SubjectId, u => u.MapFrom(t => t.SubjectVisit.SubjectId )) + .ForMember(d => d.ChallengeCode, u => u.MapFrom(s => "Q" + s.ChallengeCode.ToString("D5"))); + + //CRC 质疑列表 + CreateMap() + .ForMember(d => d.IsUrgent, u => u.MapFrom(s => s.SubjectVisit.IsUrgent)) + .ForMember(d => d.IsBaseLine, u => u.MapFrom(s => s.SubjectVisit.IsBaseLine)) + .ForMember(d => d.SubjectCode, u => u.MapFrom(s => s.SubjectVisit.Subject.Code)) + .ForMember(d => d.SubjectId, u => u.MapFrom(t => t.SubjectVisit.SubjectId)) + .ForMember(d => d.TrialSiteCode, u => u.MapFrom(s => s.SubjectVisit.TrialSite.TrialSiteCode)) + .ForMember(d => d.VisitName, u => u.MapFrom(s => s.SubjectVisit.VisitName)) + .ForMember(d => d.RequestBackState, u => u.MapFrom(s => s.SubjectVisit.RequestBackState)) + .ForMember(d => d.VisitNum, u => u.MapFrom(s => s.SubjectVisit.VisitNum)) + .ForMember(d => d.BlindName, u => u.MapFrom(s => s.SubjectVisit.BlindName)) + .ForMember(d => d.Creator, u => u.MapFrom(s => s.Creator.UserName)) + .ForMember(d => d.CreateUser, u => u.MapFrom(s => s.Creator.LastName + s.Creator.FirstName)) + .ForMember(d => d.LatestReplyUser, u => u.MapFrom(t => t.LatestReplyUser.LastName + t.Creator.FirstName)) + .ForMember(d => d.ChallengeCode, u => u.MapFrom(s => "Q" + s.ChallengeCode.ToString("D5"))); //排序的时候有坑 把这个带到sql 中去了 + //.AfterMap((src, dest) => dest.ChallengeCode = "Q" + src.ChallengeCode.ToString("D5"));//实测没有效果 + + + + + //质疑问题答案 + CreateMap().EqualityComparison((odto, o) => odto.Id == o.Id) + .ForAllMembers(opt => opt.Condition((src, dest, srcMember) => srcMember != null)); + //.ForMember(d => d.TrialQCQuestionConfigureId, opt => opt.Ignore())//前端更新的时候不会传递这个参数,但是添加的时候会传递 + //.ForMember(d => d.SubjectVisitId, opt => opt.Ignore()); + //更新的时候,因为前端没有传递TrialQCQuestionConfigureId 导致映射后的数据变为 Guid.Empty,明明配置了 如果source 为null 就不映射 但是没生效 临时解决 + //.BeforeMap((src, dest) => src.TrialQCQuestionConfigureId = dest.TrialQCQuestionConfigureId == System.Guid.Empty ? src.TrialQCQuestionConfigureId : dest.TrialQCQuestionConfigureId); + + + CreateMap().IncludeMembers(t => t.TrialQCQuestionConfigure) + .ForMember(d => d.ParentShowOrder, u => u.MapFrom(s => s.TrialQCQuestionConfigure.ParentQCQuestion.ShowOrder)); + + CreateMap() + .ForMember(d => d.Id, u => u.Ignore()) + .ForMember(d => d.ParentShowOrder, u => u.MapFrom(s => s.ParentQCQuestion.ShowOrder)) + .ForMember(d => d.TrialQCQuestionConfigureId, u => u.MapFrom(s => s.Id)); + + + CreateMap().ReverseMap(); + + + CreateMap() + .ForMember(d => d.FullFilePath, u => u.MapFrom(s => s.Path + "?access_token=" + token)); + + CreateMap() + .ForMember(d => d.FileCount, u => u.MapFrom(s => s.NoneDicomFileList.Count)) + .ForMember(d => d.NoneDicomStudyFileList, u => u.MapFrom(s => s.NoneDicomFileList)); + + + } + } + +} diff --git a/IRaCIS.Core.Application/Service/ReadingAndReport/DTO/GlobalReportDTO.cs b/IRaCIS.Core.Application/Service/ReadingAndReport/DTO/GlobalReportDTO.cs new file mode 100644 index 00000000..02bb2b3e --- /dev/null +++ b/IRaCIS.Core.Application/Service/ReadingAndReport/DTO/GlobalReportDTO.cs @@ -0,0 +1,91 @@ +using IRaCIS.Core.Domain.Models; + +namespace IRaCIS.Core.Application.Contracts +{ + public class HistoryVisitRSDTO + { + + public Guid StudyId { get; set; } + public string StudyCode { get; set; } = String.Empty; + + public decimal VisitNum { get; set; } + + public string VisitName { get; set; } = String.Empty; + + public string OverallResponse { get; set; } = String.Empty; + + public string TpCode { get; set; } = String.Empty; + + public GlobalRSSelectView GlobalRSSelect { get; set; } = new GlobalRSSelectView(); + + } + + public class GlobalRSSelectView + { + public bool? Agree { get; set; } + public string NewRS { get; set; } = String.Empty; + public string Note { get; set; } = String.Empty; + } + + public class HistoryGlobalRsDTO + { + + public decimal VisitNum { get; set; } + + public string VisitName { get; set; } = String.Empty; + + public string OverallResponse { get; set; } = String.Empty; + } + + public class PreviousGlobalReadsView + { + public string SubjectNote { get; set; } = String.Empty; + + public List PreviousGlobalReadsList { get; set; } = new List(); + } + + + public class GlobalTaskReportCommand + { + public Guid GlobalId { get; set; } + public string SubjectNote { get; set; } = String.Empty; + public string SubjectCode { get; set; } = String.Empty; + public Guid SubjectId { get; set; } + + public List GlobalRSReportList { get; set; }=new List(); + } + + public class GlobalReportCommand + { + public Guid GlobalId { get; set; } + public string TpCode { get; set; } = String.Empty; + public int VisitNum { get; set; } + public bool? Agree { get; set; } + public string NewRS { get; set; } = String.Empty; + public string Note { get; set; } = String.Empty; + + } + + + public class AdReportDTO + { + public WorkloadAD ADInfo { get; set; } = new WorkloadAD(); + public PreviousGlobalReadsView Global1 { get; set; } = new PreviousGlobalReadsView(); + public PreviousGlobalReadsView Global2 { get; set; } = new PreviousGlobalReadsView(); + + public IEnumerable Global1VisitRS { get; set; }=new List(); + public IEnumerable Global2VisitRS { get; set; } = new List(); + + } + + + public class ADReportCommand + { + + public Guid AdId { get; set; } + public Guid SelectGlobalId { get; set; } + public string ADNote { get; set; } = String.Empty; + } + + +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/ReadingAndReport/DTO/ReportDTO.cs b/IRaCIS.Core.Application/Service/ReadingAndReport/DTO/ReportDTO.cs new file mode 100644 index 00000000..81250081 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ReadingAndReport/DTO/ReportDTO.cs @@ -0,0 +1,280 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace IRaCIS.Core.Application.Contracts +{ + public class LesionInformation + { + public Guid TUId { get; set; } + public int LesionType { get; set; } //病灶类型 + public string STUDYID { get; set; } = string.Empty; // 项目ID + public string USUBJID { get; set; } = string.Empty;// 受试者ID + public int TUSEQ { get; set; } // 病灶序号 + + // 病灶分组,主要是用于将分裂或者结合在一起的已经被标识的肿瘤分类 + public string TUGRPID { get; set; } = string.Empty; + + //内部或外部的肿瘤/病灶标识。 例如:医学影像 ID + public string TUREFID { get; set; } = string.Empty; + + public string TUSPID { get; set; } = string.Empty;// 申办方标识 + + // 关联测量结果,及TUGRPID一起,标识分裂及合并 + public string TULNKID { get; set; } = string.Empty; + + /// + /// 检查项目 + /// TUMIDENT (Tumor Identification ) + /// TUSPLIT (Tumor Split ) + /// TUMERGE (Tumor Merged) + /// + public string TUTESTCD { get; set; } = string.Empty; + public string TUTEST { get; set; } = string.Empty; + + public string TUORRES { get; set; } = string.Empty;//TARGET, NON-TARGET, NEW + + public string TUSTRESC { get; set; } = string.Empty;//包含从 TUORRES拷贝的所有发现的结果 + public string TUNAM { get; set; } = string.Empty;//完成肿瘤标识的供应商名称或标识 + + public string LocDescription { get; set; } = string.Empty;// 部位描述 + public string TULOC { get; set; } = string.Empty; // 解剖学部位 + public string TULAT { get; set; } = string.Empty;//解剖学部位或者样本更详细的偏侧性修饰语,比如,左侧, 右侧,双侧。 + public string TUDIR { get; set; } = string.Empty;//解剖学部位或者样本更详细的方向性修饰语,比如,上边的 ,里面的。 + public string TUPORTOT { get; set; } = string.Empty;//解剖学部位或者样本更详细的分布,安排,分配的修饰语, 比如,全部的,单一的,部分的,多数的。 + + public string TUMETHOD { get; set; } = string.Empty; //用来标识肿瘤的办法。 例如,核磁共振,CT扫描。 + + public string TUEVAL { get; set; } = string.Empty; //评估者的角色。 例如:研究者,独立评估者。 + public string TUEVALID { get; set; } = string.Empty;//这个特定的评估者变量是与 TUEVAL一起使用来提供更详 细的信息 + public string TUACPTFL { get; set; } = string.Empty; + + // 访视信息 + public decimal VISITNUM { get; set; } = 0; + public string VISIT { get; set; } = string.Empty; + public int VISITDY { get; set; } + public string EPOCH { get; set; } = string.Empty; + public string TUDTC { get; set; } = string.Empty; + public int TUDY { get; set; } = 0; + + //TR + + public string TRTESTCD { get; set; } = string.Empty; + public string TRTEST { get; set; } = string.Empty; + public string TRORRES { get; set; } = string.Empty;//原始收到或采集的肿瘤测量/评估结 + public string TRORRESU { get; set; } = string.Empty;//原始单位 + public string TRSTRESC { get; set; } = string.Empty;//标准化结 果 + public double TRSTRESN { get; set; }//标准化结 果(N) + public string TRSTRESU { get; set; } = string.Empty;// 标准化单位 + public string TRSTAT { get; set; } = string.Empty; // 未做状态 + public string TRREASND { get; set; } = string.Empty; // 未做原因 + public string Note { get; set; } = string.Empty; + public string TRGRPID { get; set; } = string.Empty; + public string TRDTC { get; set; } = string.Empty; + public int TRDY { get; set; } = 0; + ////RS + //public string RSCAT { get; set; } // 类别,用来识别对反应评估中使用的标准,以及适当时提供版本号 + //public string RSORRES { get; set; } //原始接收、采集或者计算的肿瘤反应评估的结果。 + //public string RSSTRESC { get; set; } //标准化结果 + + public bool CoveredLesion { get; set; } + } + public class VisitLesion : LesionInformation + { + public LesionInformation CurrentLesion { get; set; } = new LesionInformation(); + } + + public class EfficacyAssessment + { + public string TargetLesion { get; set; } = string.Empty; + public string Non_targetLesion { get; set; } = string.Empty; + public string OverallAssessment { get; set; } = string.Empty; + + public string TargetLesionNote { get; set; } = string.Empty; + public string Non_targetLesionNote { get; set; } = string.Empty; + public string OverallAssessmentNote { get; set; } = string.Empty; + } + + public class PreviousOverallAssessment + { + public string VisitName { get; set; } = string.Empty; + public string OverallAssessment { get; set; } = string.Empty; + } + public class VisitLesionInfo + { + //基线病灶信息及本次访视测量信息 + public IList BLLesionList { get; set; } = new List(); + // public IList BLVisitLesionList { get; set; } = new List(); + //public IList CurrentVisitLesionList { get; set; } = new List(); + + // 以往病灶信息 + public IList PreviousNewLesionList { get; set; } = new List(); + //本次疑似新病灶 + public IList EquivocalNewLesionList { get; set; } = new List(); + public IList UnequivocalNewLesionList { get; set; } = new List(); + public ReportDTO ReportResult { get; set; } = new ReportDTO(); + public EfficacyAssessment Efficacy { get; set; } = new EfficacyAssessment(); + public List PreviousOverallAssessment { get; set; } = new List(); + + public MinVisit MinVisitInfo { get; set; } = new MinVisit(); + } + + public class MinVisit + { + public double MinSum { get; set; } = 0.0; + public string MinVistName { get; set; } = string.Empty; + } + public class ReportCommand + { + public List LesionInformationList { get; set; } = new List(); + + //RS + public string RSCAT { get; set; } = string.Empty;// 类别,用来识别对反应评估中使用的标准,以及适当时提供版本号 + public string RSORRES { get; set; } = string.Empty; //原始接收、采集或者计算的肿瘤反应评估的结果。 + public string RSSTRESC { get; set; } = string.Empty; //标准化结果 + } + public class TUDTO + { + public int LesionType { get; set; } + public string STUDYID { get; set; } = string.Empty; + public string USUBJID { get; set; } = string.Empty; + public int TUSEQ { get; set; } + public string TUGRPID { get; set; } = string.Empty; + public string TUREFID { get; set; } = string.Empty;//内部或外部的肿瘤/病灶标识。 例如:医学影像 ID + public string TUSPID { get; set; } = string.Empty; + public string TULNKID { get; set; } = string.Empty; + public string TUTESTCD { get; set; } = string.Empty; + public string TUTEST { get; set; } = string.Empty; + + public double TUORRES { get; set; } = 0; + public string TUSTRESC { get; set; } = string.Empty; + public string TUNAM { get; set; } = string.Empty; + public string TULOC { get; set; } = string.Empty; + public string TULAT { get; set; } = string.Empty; + public string TUDIR { get; set; } = string.Empty; + public string TUPORTOT { get; set; } = string.Empty; + public string TUMETHOD { get; set; } = string.Empty; + public string TUEVAL { get; set; } = string.Empty; + public string TUEVALID { get; set; } = string.Empty; + public string TUACPTFL { get; set; } = string.Empty; + public double VISITNUM { get; set; } = 0; + public string VISIT { get; set; } = string.Empty; + public string VISITDY { get; set; } = string.Empty; + public string EPOCH { get; set; } = string.Empty; + public string TUDTC { get; set; } = string.Empty; + public int TUDY { get; set; } = 0; + } + public class TRDTO + { + public string STUDYID { get; set; } = string.Empty; + public string DOMAIN { get; set; } = string.Empty; + public string USUBJID { get; set; } = string.Empty; + public int TRSEQ { get; set; } + public string TRGRPID { get; set; } = string.Empty; + public string TRREFID { get; set; } = string.Empty; + public string TRSPID { get; set; } = string.Empty; + public string TRLNKID { get; set; } = string.Empty; + public string TRLNKGRP { get; set; } = string.Empty; + public string TRTESTCD { get; set; } = string.Empty; + public string TRTEST { get; set; } = string.Empty; + public string TRORRES { get; set; } = string.Empty; + public string TRORRESU { get; set; } = string.Empty; + public string TRSTRESC { get; set; } = string.Empty; + public double TRSTRESN { get; set; } = 0; + public string TRSTRESU { get; set; } = string.Empty; + public string TRSTAT { get; set; } = string.Empty; + public string TRREASND { get; set; } = string.Empty; + public string TRNAM { get; set; } = string.Empty; + public string TRMETHOD { get; set; } = string.Empty; + public string TREVAL { get; set; } = string.Empty; + public string TREVALID { get; set; } = string.Empty; + public string TRACPTFL { get; set; } = string.Empty; + public decimal VISITNUM { get; set; } = 0; + public string VISIT { get; set; } = string.Empty; + public int VISITDY { get; set; } = 0; + public string EPOCH { get; set; } = string.Empty; + public string TRDTC { get; set; } = string.Empty; + public int TRDY { get; set; } = 0; + public string Note { get; set; } = string.Empty; + public bool CoveredLesion { get; set; } = true; + } + + public class RSDTO + { + public string STUDYID { get; set; } = string.Empty; + public string DOMAIN { get; set; } = string.Empty; + public string USUBJID { get; set; } = string.Empty; + public int RSSEQ { get; set; } + public string RSGRPID { get; set; } = string.Empty; + public string RSREFID { get; set; } = string.Empty; + public string RSSPID { get; set; } = string.Empty; + public string RSLNKID { get; set; } = string.Empty; + public string RSLNKGRP { get; set; } = string.Empty; + public string RSTESTCD { get; set; } = string.Empty; + public string RSTEST { get; set; } = string.Empty; + public string RSCAT { get; set; } = string.Empty; + public string RSORRES { get; set; } = string.Empty; + public string RSSTRESC { get; set; } = string.Empty; + public string RSSTAT { get; set; } = string.Empty; + public string RSREASND { get; set; } = string.Empty; + public string RSNAM { get; set; } = string.Empty; + + public string RSEVAL { get; set; } = string.Empty; + public string RSEVALID { get; set; } = string.Empty; + public string RSACPTFL { get; set; } = string.Empty; + public decimal VISITNUM { get; set; } = 0; + public string VISIT { get; set; } = string.Empty; + public int VISITDY { get; set; } = 0; + public string EPOCH { get; set; } = string.Empty; + public string RSDTC { get; set; } = string.Empty; + public int RSDY { get; set; } = 0; + public Guid StudyGuid { get; set; } = Guid.Empty; + public Guid TrialGuid { get; set; } = Guid.Empty; + public Guid SubjectGuid { get; set; } = Guid.Empty; + + public string Note { get; set; } = string.Empty; + } + public class ReportDTO + { + public bool Qualified { get; set; } = true; + public string DiseaseProgression { get; set; } = string.Empty; + public string NotEvaluable { get; set; } = string.Empty; + public string Timepoint { get; set; } = string.Empty; + public string TrialCode { get; set; } = string.Empty; + public string SubjectCode { get; set; } = string.Empty; + public decimal? VisitNum { get; set; } + public string VisitName { get; set; } = string.Empty; + public int? DiseaseSituation { get; set; } + public string Comment { get; set; } = String.Empty; + public Guid? TPId { get; set; } + public Guid? DicomStudyId { get; set; } + public string TpCode { get; set; } = string.Empty; + + public bool AffectRead { get; set; }//是否影响读片 + public string AffectReadNote { get; set; } = String.Empty;//备注说明 + } + + + public class VisitReportCommand + { + public List LesionInformation { get; set; } = new List(); + public List TRList { get; set; } = new List(); + public List RSList { get; set; } = new List(); + public ReportDTO ReportResult { get; set; } = new ReportDTO(); + } + + public class BaseLineReportCommand + { + //public Guid TimepointId { get; set; } + public string SumOfDiameter { get; set; } = String.Empty; + public string SumDiameterOfNonLymphNode { get; set; } = String.Empty; + public IList LesionInformation { get; set; } = new List(); + public ReportDTO ReportResult { get; set; } = new ReportDTO(); + } + + public class BaseLineReportDTO + { + public IList LesionInformation { get; set; } = new List(); + public ReportDTO ReportResult { get; set; } = new ReportDTO(); + } +} diff --git a/IRaCIS.Core.Application/Service/ReadingAndReport/DTO/WorkloadReadingDTO.cs b/IRaCIS.Core.Application/Service/ReadingAndReport/DTO/WorkloadReadingDTO.cs new file mode 100644 index 00000000..965927af --- /dev/null +++ b/IRaCIS.Core.Application/Service/ReadingAndReport/DTO/WorkloadReadingDTO.cs @@ -0,0 +1,39 @@ +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Contracts +{ + public class WorkloadReadingDTO + { + public Guid Id { get; set; } + + public Guid StudyId { get; set; } + public Guid TrialId { get; set; } + public string TrialCode { get; set; } = string.Empty; + public string TrialIndication { get; set; } = string.Empty; + public string Sponsor { get; set; } = string.Empty; + public int Expedited { get; set; } + + public Guid SubjectId { get; set; } + public string SubjectCode { get; set; } = string.Empty; + + public string VisitName { get; set; } = string.Empty; + public decimal VisitNum { get; set; } + + + public Guid WorkloadId { get; set; } + public string WorkloadCode { get; set; } = string.Empty; + public int WorkloadType { get; set; } + public int Status { get; set; } + + public DateTime UpdateTime { get; set; } + } + + public class WorkloadQueryParam : PageInput + { + public Guid? TrialId { get; set; } = Guid.Empty; + public string SubjectCode { get; set; } = string.Empty; + public int? Status { get; set; } + public int WorkloadType { get; set; } + public int? Expedited { get; set; } + } +} diff --git a/IRaCIS.Core.Application/Service/ReadingAndReport/GlobalReportService.cs b/IRaCIS.Core.Application/Service/ReadingAndReport/GlobalReportService.cs new file mode 100644 index 00000000..e63303cb --- /dev/null +++ b/IRaCIS.Core.Application/Service/ReadingAndReport/GlobalReportService.cs @@ -0,0 +1,197 @@ +//using IRaCIS.Core.Application.Contracts; +//using System.Linq.Dynamic.Core; +//using AutoMapper; +//using IRaCIS.Application.Contracts; +//using IRaCIS.Core.Infra.EFCore; +//using IRaCIS.Core.Domain.Models; +//using IRaCIS.Core.Infrastructure.Extention; +//using Microsoft.AspNetCore.Mvc; + +//namespace IRaCIS.Core.Application.Services +//{ +// [ ApiExplorerSettings(GroupName = "Reading")] +// public class GlobalReportService : BaseService, IGlobalService +// { +// private readonly IRepository _globalRsRepository; +// private readonly IRepository _globalResultRepository; +// private readonly IRSRepository _rsRepository; +// private readonly IRepository _visitStageRepository; + +// private readonly IRepository _subjectVisitRepository; +// private readonly IWorkloadADRepository _workloadAdRepository; +// private readonly IWorkloadGlobalRepository _workloadGlobalRepository; +// private readonly IRepository _trialRepository; +// private readonly IRepository _subjectRepository; + +// public GlobalReportService(IRepository globalRsRepository, IRepository globalResultRepository, +// IRSRepository rsRepository, IRepository visitStageRepository, IRepository subjectVisitRepository, IWorkloadADRepository workloadAdRepository, +// IWorkloadGlobalRepository workloadGlobalRepository, IRepository trialRepository, IRepository subjectRepository) +// { +// _globalRsRepository = globalRsRepository; +// _globalResultRepository = globalResultRepository; +// _rsRepository = rsRepository; +// _visitStageRepository = visitStageRepository; + +// _subjectVisitRepository = subjectVisitRepository; +// _workloadAdRepository = workloadAdRepository; +// _workloadGlobalRepository = workloadGlobalRepository; +// _trialRepository = trialRepository; +// _subjectRepository = subjectRepository; +// } + +// [HttpGet("{trialId:guid}/{subjectId:guid}/{visitNum:decimal}/{globalId:guid}/{globalCode}")] +// public IEnumerable GetHistoryVisitRsList(Guid trialId, Guid subjectId, decimal visitNum, Guid globalId, string globalCode) +// { + +// var tpGroup = 'T' + globalCode.Trim().Substring(globalCode.Trim().Length - 2); + + +// var query = from rs in _rsRepository.AsQueryable() +// .Where(t => t.SubjectGuid == subjectId && t.TrialGuid == trialId && t.RSTESTCD == "OVRLRESP" && t.VISITNUM <= visitNum && t.TpCode.Contains(tpGroup)) + +// join globalRs in _globalRsRepository.Where(t => t.GlobalId == globalId) on new { rs.TpCode, VisitNum = rs.VISITNUM } equals new { globalRs.TpCode, globalRs.VisitNum } into cc +// from globalRs in cc.DefaultIfEmpty() + +// join visitStage in _visitStageRepository.Where(t => t.TrialId == trialId) on rs.VISITNUM equals visitStage.VisitNum +// select new HistoryVisitRSDTO +// { +// StudyId = rs.StudyGuid, +// StudyCode = rs.STUDYID, +// VisitNum = visitStage.VisitNum, +// VisitName = visitStage.VisitName, +// OverallResponse = rs.RSORRES, +// TpCode = rs.TpCode, +// GlobalRSSelect = new GlobalRSSelectView() +// { +// Agree = globalRs.Agree, +// NewRS = globalRs.NewRS, +// Note = globalRs.Note +// } +// }; + +// var visitRsList = query.OrderBy(t => t.VisitNum).ToList(); + +// return visitRsList; + +// } +// #pragma warning disable +// [HttpGet("{trialId:guid}/{subjectId:guid}/{visitNum:decimal}/{globalId:guid}")] +// public PreviousGlobalReadsView GetHistoryGlobalRsList(Guid trialId, Guid subjectId, decimal visitNum, +// Guid globalId) +// { +// //subjectCode = subjectCode.Trim(); + +// var query = from globalResult in _globalResultRepository.AsQueryable() +// .Where(t => t.SubjectId == subjectId && t.VisitNum < visitNum) +// join visitStage in _visitStageRepository.Where(t => t.TrialId == trialId) on globalResult.VisitNum equals visitStage.VisitNum +// select new HistoryGlobalRsDTO +// { +// VisitNum = visitStage.VisitNum, +// VisitName = visitStage.VisitName, +// OverallResponse = globalResult.Result +// }; + +// var subjectNote = _globalResultRepository.FirstOrDefault(t => t.GlobalId == globalId)?.SubjectNote; + +// return new PreviousGlobalReadsView() +// { +// PreviousGlobalReadsList = query.ToList(), +// SubjectNote = subjectNote +// }; + +// } + +// public IResponseOutput AddGlobalReport(GlobalTaskReportCommand globalTaskReportCommand) +// { +// //删除上一次保存得记录 +// _globalResultRepository.Delete(t => t.GlobalId == globalTaskReportCommand.GlobalId); +// _globalRsRepository.Delete(t => t.GlobalId == globalTaskReportCommand.GlobalId); + + +// var globalRsList = _mapper.Map>(globalTaskReportCommand.GlobalRSReportList); + +// _globalRsRepository.AddRange(globalRsList); + +// var first = globalRsList.OrderByDescending(t => t.VisitNum).First(); + +// var globalResult = new GlobalResult() +// { +// VisitNum = first.VisitNum, +// SubjectCode = globalTaskReportCommand.SubjectCode, +// SubjectNote = globalTaskReportCommand.SubjectNote, +// SubjectId = globalTaskReportCommand.SubjectId, + +// GlobalId = first.GlobalId, +// Result = first.NewRS +// }; + +// _globalResultRepository.Add(globalResult); + +// return ResponseOutput.Result(_globalResultRepository.SaveChanges()) ; + +// } + +// //public string GetCommentsForSubject(Guid globalId) +// //{ +// // return _globalResultRepository.FirstOrDefault(t => t.GlobalId == globalId)?.SubjectNote; +// //} +// [HttpGet("{adId}")] +// public AdReportDTO GetAdReport(Guid adId) +// { +// var adReport = new AdReportDTO(); +// var ad = _workloadAdRepository.FirstOrDefault(t => t.Id == adId); +// var globalId1 = ad.Global1Id; +// var globalId2 = ad.Global2Id; + +// var query = from global in _workloadGlobalRepository.Where(t => t.Id == globalId1 || t.Id == globalId2) +// join trial in _trialRepository.Where(t => t.Id == ad.TrialId) on global.TrialId equals trial.Id +// join subject in _subjectRepository.AsQueryable() on global.SubjectId equals subject.Id +// select new WorkloadReadingDTO +// { +// Id = global.Id, +// WorkloadId = global.Id, +// WorkloadType = 1, +// Status = global.Status, +// UpdateTime = global.UpdateTime, +// TrialId = global.TrialId, +// SubjectId = global.SubjectId, +// SubjectCode = subject.Code, +// //VisitNum = visit.VisitNum, +// //VisitName = visit.VisitName, +// VisitNum = global.VisitNum, +// VisitName = global.VisitName, + +// Expedited = trial.Expedited, +// TrialCode = trial.TrialCode, +// TrialIndication = trial.Indication, +// WorkloadCode = global.GlobalCode +// }; + +// var globalList = query.ToList(); + +// var global1 = globalList.First(t => t.Id == globalId1); +// var global2 = globalList.First(t => t.Id == globalId2); + +// adReport.Global1VisitRS = GetHistoryVisitRsList(global1.TrialId, global1.SubjectId, +// global1.VisitNum, global1.Id, global1.WorkloadCode); + +// adReport.Global2VisitRS = GetHistoryVisitRsList(global2.TrialId, global2.SubjectId +// ,global2.VisitNum, global2.Id, global2.WorkloadCode); + +// adReport.Global1 = GetHistoryGlobalRsList(global1.TrialId, global1.SubjectId, global1.VisitNum, global1.Id); +// adReport.Global2 = GetHistoryGlobalRsList(global2.TrialId, global2.SubjectId, global2.VisitNum, global2.Id); + +// adReport.ADInfo = ad; + +// return adReport; + +// } + +// public IResponseOutput AddAdjudicationReport(ADReportCommand adReportCommand) +// { +// return ResponseOutput.Result(_workloadAdRepository.Update(t => t.Id == adReportCommand.AdId, +// u => new WorkloadAD() +// { AdNote = adReportCommand.ADNote, SelectGlobalId = adReportCommand.SelectGlobalId })) ; +// } +// } +//} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/ReadingAndReport/Interface/IGlobalService.cs b/IRaCIS.Core.Application/Service/ReadingAndReport/Interface/IGlobalService.cs new file mode 100644 index 00000000..9211036c --- /dev/null +++ b/IRaCIS.Core.Application/Service/ReadingAndReport/Interface/IGlobalService.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Core.Application.Contracts +{ + public interface IGlobalService + { + //string GetCommentsForSubject(Guid globalId); + IEnumerable GetHistoryVisitRsList(Guid trialId, Guid subjectId, decimal visitNum, Guid globalId,string globalCode); + + PreviousGlobalReadsView GetHistoryGlobalRsList(Guid trialId, Guid subjectId, decimal visitNum, Guid globalId); + + IResponseOutput AddGlobalReport(GlobalTaskReportCommand globalTaskReportCommand); + + + + AdReportDTO GetAdReport(Guid adId); + IResponseOutput AddAdjudicationReport(ADReportCommand adReportCommand); + } + + + +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/ReadingAndReport/Interface/IReportService.cs b/IRaCIS.Core.Application/Service/ReadingAndReport/Interface/IReportService.cs new file mode 100644 index 00000000..42569650 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ReadingAndReport/Interface/IReportService.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace IRaCIS.Core.Application.Contracts +{ + public interface IReportService + { + //bool SaveReport(ReportCommand reportCommand); + + /// 添加基线期病灶标识及测量信息 + bool AddLesion(List lesionInformation, ReportDTO report); + ///// 获取基线期信息 + //IList GetBLLesion(string trialCode, string subjectCode); + + /// + /// 获取访视病灶信息 + /// + VisitLesionInfo GetVisitLineLesion(string trialCode, string subjectCode, decimal visitNum,string tpCode); + + bool SaveVisitReport(VisitReportCommand visitReportCommand); + bool AddBaseLineLesion(BaseLineReportCommand baseLineReportCommand); + BaseLineReportDTO getBLLineLesion(string trialCode, string subjectCode, string tpCode); + + bool SubmiteReport(Guid tpId); + } +} diff --git a/IRaCIS.Core.Application/Service/ReadingAndReport/Interface/IReviewerReadingService.cs b/IRaCIS.Core.Application/Service/ReadingAndReport/Interface/IReviewerReadingService.cs new file mode 100644 index 00000000..3c0531a2 --- /dev/null +++ b/IRaCIS.Core.Application/Service/ReadingAndReport/Interface/IReviewerReadingService.cs @@ -0,0 +1,10 @@ +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface IReviewerReadingService + { + PageOutput GetWorkloadList(WorkloadQueryParam param); + } +} diff --git a/IRaCIS.Core.Application/Service/ReadingAndReport/ReadingService.cs b/IRaCIS.Core.Application/Service/ReadingAndReport/ReadingService.cs new file mode 100644 index 00000000..f4e5e1aa --- /dev/null +++ b/IRaCIS.Core.Application/Service/ReadingAndReport/ReadingService.cs @@ -0,0 +1,184 @@ +//using IRaCIS.Application.Interfaces; +//using IRaCIS.Application.Contracts; +//using IRaCIS.Core.Infra.EFCore; + +//using IRaCIS.Core.Domain.Models; +//using System.Linq.Expressions; +//using IRaCIS.Core.Infrastructure.ExpressionExtend; +//using IRaCIS.Core.Infrastructure.Extention; +//using Microsoft.AspNetCore.Mvc; + +//namespace IRaCIS.Core.Application.Services +//{ +// [ApiExplorerSettings(GroupName = "Reading")] +// public class ReadingService : BaseService, IReviewerReadingService +// { +// private readonly IWorkloadTPRepository _workloadTPRepository; +// private readonly IWorkloadGlobalRepository _workloadGlobalRepository; +// private readonly IWorkloadADRepository _workloadADRepository; +// private readonly IRepository _trialRepository; + +// private readonly IRepository _subjectRepository; +// private readonly IRepository _subjectVisitRepository; + +// public ReadingService(IWorkloadTPRepository workloadTPRepository, +// IWorkloadGlobalRepository workloadGlobalRepository, +// IWorkloadADRepository workloadADRepository, +// IRepository trialRepository, +// IRepository subjectRepository, +// IRepository subjectVisitRepository, +// IUserInfo userInfo) +// { +// _workloadTPRepository = workloadTPRepository; +// _workloadGlobalRepository = workloadGlobalRepository; +// _workloadADRepository = workloadADRepository; +// _trialRepository = trialRepository; + +// _subjectRepository = subjectRepository; +// _subjectVisitRepository = subjectVisitRepository; + +// } +// /// +// /// 获取医生个人任务列表 +// /// WorkloadType 1-tp,2-global,3-ad +// /// +// /// +// /// +// [HttpPost] +// public PageOutput GetWorkloadList(WorkloadQueryParam param) +// { +// #pragma warning disable +// IQueryable query = null; +// Expression> subjectLambda = x => true; +// if (param.SubjectCode != string.Empty) +// { +// subjectLambda = subjectLambda.And(o => o.Code.Contains(param.SubjectCode)); +// } +// Expression> trialLambda = x => true; +// if (param.Expedited != null) +// { +// trialLambda = trialLambda.And(o => o.Expedited == param.Expedited); +// } +// if (param.TrialId != null && param.TrialId != Guid.Empty) +// { +// trialLambda = trialLambda.And(o => o.Id == param.TrialId); +// } + + +// if (param.WorkloadType == 1)// TP +// { +// Expression> workloadLambda = x => true; +// workloadLambda = workloadLambda.And(u => u.ReviewerId == _userInfo.Id); +// if (param.Status != null) +// { +// if (param.Status == 30)//30 的时候,待读和正在读(未提交的) +// { +// workloadLambda = workloadLambda.And(o => o.Status == 30 || o.Status == 40); +// } +// else +// workloadLambda = workloadLambda.And(o => o.Status == param.Status); +// } +// query = from tp in _workloadTPRepository.Where(workloadLambda) +// join trial in _trialRepository.Where(trialLambda) on tp.TrialId equals trial.Id +// join subject in _subjectRepository.Where(subjectLambda) on tp.SubjectId equals subject.Id +// join visit in _subjectVisitRepository.AsQueryable() on tp.SubjectVisitId equals visit.Id +// select new WorkloadReadingDTO +// { +// Id = tp.Id, +// StudyId = tp.StudyId, +// WorkloadId = tp.Id, +// WorkloadType = 1, +// Status = tp.Status, +// UpdateTime = tp.UpdateTime, +// TrialId = tp.TrialId, +// SubjectId = tp.SubjectId, +// SubjectCode = subject.Code, +// VisitNum = visit.VisitNum, +// VisitName = visit.VisitName, +// Expedited = trial.Expedited, +// TrialCode = trial.TrialCode, +// TrialIndication = trial.Indication, +// WorkloadCode = tp.TimepointCode +// }; +// } +// else if (param.WorkloadType == 2)//Global +// { +// Expression> workloadLambda = x => true; +// workloadLambda = workloadLambda.And(u => u.ReviewerId == _userInfo.Id); +// if (param.Status != null) +// { +// if (param.Status == 30) +// { +// workloadLambda = workloadLambda.And(o => o.Status == 30 || o.Status == 40); +// } +// else +// workloadLambda = workloadLambda.And(o => o.Status == param.Status); +// } +// query = from global in _workloadGlobalRepository.Where(workloadLambda) +// join trial in _trialRepository.Where(trialLambda) on global.TrialId equals trial.Id +// join subject in _subjectRepository.Where(subjectLambda) on global.SubjectId equals subject.Id +// //join visit in _subjectVisitRepository.GetAll() on global.VisitId equals visit.Id +// select new WorkloadReadingDTO +// { +// Id = global.Id, +// WorkloadId = global.Id, +// WorkloadType = 1, +// Status = global.Status, +// UpdateTime = global.UpdateTime, +// TrialId = global.TrialId, +// SubjectId = global.SubjectId, +// SubjectCode = subject.Code, +// //VisitNum = visit.VisitNum, +// //VisitName = visit.VisitName, +// VisitNum = global.VisitNum, +// VisitName = global.VisitName, + +// Expedited = trial.Expedited, +// TrialCode = trial.TrialCode, +// TrialIndication = trial.Indication, +// WorkloadCode = global.GlobalCode +// }; +// } +// else if (param.WorkloadType == 3)//AD +// { +// Expression> workloadLambda = x => true; +// workloadLambda = workloadLambda.And(u => u.ReviewerId == _userInfo.Id); +// if (param.Status != null) +// { +// if (param.Status == 30)//30 的时候,待读和正在读(未提交的) +// { +// workloadLambda = workloadLambda.And(o => o.Status == 30 || o.Status == 40); +// } +// else +// workloadLambda = workloadLambda.And(o => o.Status == param.Status); +// } +// query = from ad in _workloadADRepository.Where(workloadLambda) +// join trial in _trialRepository.Where(trialLambda) on ad.TrialId equals trial.Id +// join subject in _subjectRepository.Where(subjectLambda) on ad.SubjectId equals subject.Id +// //join visit in _subjectVisitRepository.GetAll() on ad.visi equals visit.Id +// select new WorkloadReadingDTO +// { +// Id = ad.Id, +// WorkloadId = ad.Id, +// WorkloadType = 1, +// Status = ad.Status, +// UpdateTime = ad.UpdateTime, +// TrialId = ad.TrialId, +// SubjectId = ad.SubjectId, +// SubjectCode = subject.Code, +// //VisitNum = visit.VisitNum, +// //VisitName = visit.VisitName, +// Expedited = trial.Expedited, +// TrialCode = trial.TrialCode, +// TrialIndication = trial.Indication, +// WorkloadCode = ad.ADCode +// }; +// } +// query.OrderByDescending(u => u.UpdateTime); +// var count = query.Count(); +// query = query.Skip((param.PageIndex - 1) * param.PageSize).Take(param.PageSize); +// return new PageOutput(param.PageIndex, +// param.PageSize, count, query.ToList()); +// } +// } +//} diff --git a/IRaCIS.Core.Application/Service/ReadingAndReport/ReportService.cs b/IRaCIS.Core.Application/Service/ReadingAndReport/ReportService.cs new file mode 100644 index 00000000..396cadbb --- /dev/null +++ b/IRaCIS.Core.Application/Service/ReadingAndReport/ReportService.cs @@ -0,0 +1,635 @@ +//using AutoMapper; +//using IRaCIS.Core.Infrastructure.ExpressionExtend; +//using IRaCIS.Core.Application.Contracts; +//using IRaCIS.Core.Infra.EFCore; +//using IRaCIS.Core.Domain.Models; + +//using IRaCIS.Core.Domain.Share; +//using Microsoft.AspNetCore.Authorization; +//using Microsoft.AspNetCore.Mvc; + +//namespace IRaCIS.Core.Application.Services +//{ +// [ ApiExplorerSettings(GroupName = "Reading")] +// public class ReportService : BaseService, IReportService +// { +// private readonly ITURepository _tURepository; +// private readonly ITRRepository _tRRepository; +// private readonly IRSRepository _rSRepository; +// private readonly IRepository _reportRepository; +// private readonly IRepository _visitStageRepository; +// private readonly IRepository _subjectVisitRepository; +// private readonly IWorkloadTPRepository _workloadTPRepository; +// private readonly IWorkloadGlobalRepository _workloadGlobalRepository; + + +// #pragma warning disable +// private string linkGroupId; +// public ReportService(ITURepository tURepository, ITRRepository tRRepository, +// IRSRepository rSRepository, IRepository reportRepository, +// IWorkloadTPRepository workloadTPRepository, +// IRepository visitStageRepository, +// IRepository subjectVisitRepository, +// IWorkloadGlobalRepository workloadGlobalRepository, +// IMapper mapper) +// { +// _tURepository = tURepository; +// _tRRepository = tRRepository; +// _rSRepository = rSRepository; +// _reportRepository = reportRepository; +// _workloadTPRepository = workloadTPRepository; +// _visitStageRepository = visitStageRepository; +// _subjectVisitRepository = subjectVisitRepository; +// _workloadGlobalRepository = workloadGlobalRepository; + + +// } + +// public bool AddLesion(List lesionList, ReportDTO report) +// { +// TU lastTu = _tURepository.AsQueryable().OrderByDescending("TUSEQ").FirstOrDefault(); +// int tuSeq = 101; +// if (lastTu != null) { tuSeq = lastTu.TUSEQ + 1; } +// TR lastTr = _tRRepository.AsQueryable().OrderByDescending("TRSEQ").FirstOrDefault(); +// int trSeq = 1; +// if (lastTr != null) { trSeq = lastTr.TRSEQ + 1; } + +// foreach (var lesion in lesionList) +// { +// var linkId = lesion.TULNKID; +// if (string.IsNullOrWhiteSpace(lesion.TULNKID)) +// { +// linkId = (lesion.TUORRES == "TARGET") ? "T" + tuSeq.ToString() : "NT" + tuSeq.ToString(); +// } +// _tURepository.Add(new TU +// { +// STUDYID = lesion.STUDYID, +// LesionType = lesion.LesionType, +// USUBJID = lesion.USUBJID, +// TUSEQ = tuSeq, +// TULNKID = linkId, + +// TUTESTCD = lesion.TUTESTCD, +// TUTEST = lesion.TUTEST, +// TUORRES = lesion.TUORRES, +// TUSTRESC = lesion.TUSTRESC, + +// TULOC = lesion.TULOC, +// LocDescription = lesion.LocDescription, +// TULAT = lesion.TULAT, +// TUDIR = lesion.TUDIR, +// TUPORTOT = lesion.TUPORTOT, +// TUMETHOD = lesion.TUMETHOD, +// TUEVAL = lesion.TUEVAL, + +// VISIT = lesion.VISIT, +// VISITNUM = lesion.VISITNUM, +// VISITDY = lesion.VISITDY, + +// TUREFID = lesion.TUREFID, +// TUACPTFL = lesion.TUACPTFL, +// TUEVALID = _userInfo.ReviewerCode,//lesion.TUEVALID, +// TUGRPID = string.Empty, +// TUDY = lesion.TUDY,// 检查日 +// EPOCH = lesion.EPOCH, +// TUDTC = lesion.TUDTC, +// TUNAM = lesion.TUNAM, +// TUSPID = lesion.TUSPID, +// TpCode = report.TpCode +// }); +// _tRRepository.Add(new TR +// { +// STUDYID = lesion.STUDYID, + +// USUBJID = lesion.USUBJID, +// TRSEQ = trSeq, +// TRLNKID = linkId, +// TRGRPID = lesion.TRGRPID, +// TRLNKGRP = linkGroupId, + +// TRTESTCD = lesion.TRTESTCD, +// TRTEST = lesion.TRTEST, +// TRORRES = lesion.TRORRES, +// TRORRESU = lesion.TRORRESU, + +// TRNAM = lesion.TUNAM, +// TRMETHOD = lesion.TUMETHOD, +// TREVAL = lesion.TUEVAL, +// TREVALID = _userInfo.ReviewerCode,//lesion.TUEVALID, +// TRACPTFL = lesion.TUACPTFL, +// CoveredLesion = true, +// Note = lesion.Note, + +// VISITNUM = lesion.VISITNUM, +// VISIT = lesion.VISIT, +// VISITDY = lesion.VISITDY, +// EPOCH = lesion.EPOCH, +// TRDTC = lesion.TRDTC, +// TRDY = lesion.TRDY, +// TpCode = report.TpCode +// }); +// tuSeq++; +// trSeq++; +// } + +// return _rSRepository.SaveChanges(); +// } + +// private IList GetBLLesion(string trialCode, string SubjectCode, string tpcodeGroup) +// { +// int[] t = { 1, 2, 3 }; +// var query = from tu in _tURepository.Where(u => t.Contains(u.LesionType) +// && u.STUDYID == trialCode && u.USUBJID == SubjectCode && u.TpCode.Contains(tpcodeGroup) /*u.TUEVALID==_userInfo.ReviewerCode*/) +// join tr in _tRRepository.Where(u => u.VISITNUM == 1 && u.STUDYID == trialCode +// && u.USUBJID == SubjectCode && u.TREVALID == _userInfo.ReviewerCode) +// on tu.TULNKID equals tr.TRLNKID into temp +// from tr in temp.DefaultIfEmpty() +// select new LesionInformation +// { +// TUId = tu.Id, +// LesionType = tu.LesionType, +// TULNKID = tu.TULNKID, +// TUSEQ = tu.TUSEQ, +// TULOC = tu.TULOC, +// LocDescription = tu.LocDescription, +// TULAT = tu.TULAT, +// TUDIR = tu.TUDIR, +// TUPORTOT = tu.TUPORTOT, +// TRORRES = tr.TRORRES, +// VISITNUM = tu.VISITNUM, +// Note = tr.Note +// }; +// return query.ToList(); +// } + +// /// 获取基线期病灶信息及其他信息 +// [HttpGet, Route("getBLLineLesion/{trialCode}/{subjectCode}/{tpCode}")] +// public BaseLineReportDTO getBLLineLesion(string trialCode, string subjectCode, string tpCode) +// { +// var tpcodeGroup = tpCode.Substring(tpCode.Length - 3, 3); +// var report = _reportRepository.FirstOrDefault(u => u.TrialCode == trialCode && u.SubjectCode == subjectCode && u.VisitNum == 1); ; + +// BaseLineReportDTO result = new BaseLineReportDTO +// { +// LesionInformation = GetBLLesion(trialCode, subjectCode, tpcodeGroup).Where(u => u.VISITNUM == 1).ToList(), +// ReportResult = _mapper.Map(report) +// }; +// return result; +// } + +// /// 获取其他访视病灶信息 +// [AllowAnonymous] +// [HttpGet, Route("getVisitLineLesion/{trialCode}/{subjectCode}/{visitNum:decimal}/{tpCode}")] +// public VisitLesionInfo GetVisitLineLesion(string trialCode, string subjectCode, decimal visitNum, string tpCode) +// { +// var tpcodeGroup = tpCode.Substring(tpCode.Length - 3, 3); +// VisitLesionInfo visitLesionInfo = new VisitLesionInfo(); +// var blLesionList = GetBLLesion(trialCode, subjectCode, tpcodeGroup); +// int[] t = { 1, 2, 3 }; +// var query = from tu in _tURepository.Where(u => u.STUDYID == trialCode && u.USUBJID == subjectCode && u.TpCode.Contains(tpcodeGroup) /*u.TUEVALID == _userInfo.ReviewerCode*/) +// join tr in _tRRepository.Where(u => u.VISITNUM == visitNum && u.STUDYID == trialCode && u.USUBJID == subjectCode && u.TpCode.Contains(tpcodeGroup)) +// on tu.TULNKID equals tr.TRLNKID into temp +// from tr in temp.DefaultIfEmpty() +// select new LesionInformation +// { +// TUId = tu.Id, +// TUSEQ = tu.TUSEQ, +// TULOC = tu.TULOC, +// LesionType = tu.LesionType, +// TULNKID = tu.TULNKID, +// LocDescription = tu.LocDescription, +// TULAT = tu.TULAT, +// TUDIR = tu.TUDIR, +// TUPORTOT = tu.TUPORTOT, +// TRORRESU = tr.TRORRESU, +// TRORRES = tr.TRORRES, +// Note = tr.Note, +// CoveredLesion = tr.CoveredLesion, +// VISITNUM = tr.VISITNUM +// }; +// var list = query.ToList(); +// var currentVisitLesion = list.Where(u => t.Contains(u.LesionType) && u.VISITNUM == visitNum).ToList(); +// foreach (var item in blLesionList) +// { +// var temp = _mapper.Map(item); +// temp.CurrentLesion = currentVisitLesion.Where(u => u.STUDYID == item.STUDYID +// && u.USUBJID == item.USUBJID && u.TUEVAL == item.TUEVAL +// && u.TULNKID == item.TULNKID).FirstOrDefault() ?? new LesionInformation(); +// visitLesionInfo.BLLesionList.Add(temp); +// } + +// List blIdList = blLesionList.Select(u => u.TUId).ToList(); +// List visitIdList = currentVisitLesion.Where(u => t.Contains(u.LesionType)).Select(u => u.TUId).ToList(); +// var visitLesion = visitIdList.Except(blIdList); + +// foreach (var item in visitLesion) +// { +// VisitLesion tempLesion = new VisitLesion(); +// tempLesion.CurrentLesion = currentVisitLesion.Where(u => u.TUId == item).FirstOrDefault() ?? new LesionInformation(); +// tempLesion.LesionType = tempLesion.CurrentLesion.LesionType; +// tempLesion.TUSEQ = tempLesion.CurrentLesion.TUSEQ; +// visitLesionInfo.BLLesionList.Add(tempLesion); +// } +// // 以往新病灶,病灶类型为5,且访视编号小于当前访视,且不是基线 +// var previousLesion = list.Where(u => u.VISITNUM < visitNum && u.VISITNUM != 1 && u.LesionType == 5).ToList(); +// foreach (var item in previousLesion) +// { +// var temp = _mapper.Map(item); +// temp.CurrentLesion = currentVisitLesion.Where(u => u.TULNKID==item.TULNKID && u.STUDYID == item.STUDYID && u.USUBJID == item.USUBJID && u.TUEVAL == item.TUEVAL).FirstOrDefault() ?? new LesionInformation(); +// visitLesionInfo.PreviousNewLesionList.Add(temp); +// } + +// visitLesionInfo.EquivocalNewLesionList = list.Where(u => u.VISITNUM <= visitNum && u.LesionType == 4).ToList(); +// visitLesionInfo.UnequivocalNewLesionList = list.Where(u => u.VISITNUM == visitNum && u.LesionType == 5).ToList(); + +// var report = _reportRepository.FirstOrDefault(u => u.TrialCode == trialCode && u.SubjectCode == subjectCode && u.VisitNum == visitNum); ; +// visitLesionInfo.ReportResult = _mapper.Map(report); + +// var rs = _rSRepository.Where(u => u.STUDYID == trialCode && u.USUBJID == subjectCode +// && u.RSEVALID == _userInfo.ReviewerCode && u.VISITNUM == visitNum); + +// var target = rs.FirstOrDefault(u => u.RSTESTCD == "TRGRESP"); +// var non_target = rs.FirstOrDefault(u => u.RSTESTCD == "NTRGRESP"); +// var overall = rs.FirstOrDefault(u => u.RSTESTCD == "OVRLRESP"); + +// visitLesionInfo.Efficacy = new EfficacyAssessment +// { + +// TargetLesion = target?.RSORRES, +// TargetLesionNote = target?.Note, +// Non_targetLesion = non_target?.RSORRES, +// Non_targetLesionNote = non_target?.Note, +// OverallAssessment = overall?.RSORRES, +// OverallAssessmentNote = overall?.Note +// }; + +// var preRS = _rSRepository.Where(u => u.STUDYID == trialCode && u.USUBJID == subjectCode +// && u.VISITNUM < visitNum && u.RSTESTCD == "OVRLRESP" +// && u.RSEVAL == _userInfo.ReviewerCode /*&& u.VISITNUM == visitNum*/) +// .Select(u => new PreviousOverallAssessment +// { +// VisitName = u.VISIT, +// OverallAssessment = u.RSORRES +// }).ToList(); +// visitLesionInfo.PreviousOverallAssessment = preRS; + +// var targetList = _tRRepository.Where(u => (!string.IsNullOrWhiteSpace(u.TRLNKID)) && (!u.TRLNKID.Contains("N")) && u.STUDYID == trialCode && +// u.USUBJID == subjectCode && u.TpCode.Contains(tpcodeGroup) && u.VISITNUM < visitNum).ToList(); + +// var q = from ttt in targetList +// group ttt by ttt.VISIT into s +// select new MinVisit +// { +// MinSum = s.Sum(p => p.TRORRES_Double), +// MinVistName = s.Key +// }; +// var tempList = q.ToList(); +// MinVisit min = new MinVisit(); +// if (tempList.Count>0) +// { +// min = tempList.FirstOrDefault(); +// } +// foreach (var item in tempList) +// { +// if (min.MinSum > item.MinSum) +// { +// min = item; +// } +// } +// visitLesionInfo.MinVisitInfo = min; + +// return visitLesionInfo; +// } + +// /// +// /// 提交报告 +// /// +// [AllowAnonymous] +// [HttpPost("{tpId:guid}")] +// public bool SubmiteReport(Guid tpId) +// { +// // 提交报告 同时查询VisitStage(项目访视计划,看是否需要添加全局) + +// var query = from workloadTp in _workloadTPRepository.Where(u => u.Id == tpId) +// join subjectVisit in _subjectVisitRepository.AsQueryable() +// on workloadTp.SubjectVisitId equals subjectVisit.Id +// join visitStage in _visitStageRepository.AsQueryable() +// on new { workloadTp.TrialId, subjectVisit.VisitNum } equals new { visitStage.TrialId, visitStage.VisitNum } +// select new +// { +// WorkloadTp = workloadTp, +// VisitNum = subjectVisit.VisitNum, +// VisitName = subjectVisit.VisitName, +// NeedGlobal = visitStage.NeedGlobal +// }; +// var workload = query.ToList().FirstOrDefault(); +// if (workload != null) +// { +// if (workload.NeedGlobal)// 需要产生全局 +// { +// var group = workload.WorkloadTp.TimepointCode.Substring(workload.WorkloadTp.TimepointCode.Length - 2, 2); +// _workloadGlobalRepository.Add(new WorkloadGlobal +// { +// GlobalCode = workload.WorkloadTp.TimepointCode + "G" + group, +// ReviewerId = Guid.Empty, +// SiteId = workload.WorkloadTp.SiteId, +// Status = -1, +// SubjectId = workload.WorkloadTp.SubjectId, +// TrialId = workload.WorkloadTp.TrialId, +// VisitId = workload.WorkloadTp.SubjectVisitId, +// VisitNum = workload.VisitNum, +// VisitName = workload.VisitName, +// UpdateTime = DateTime.Now +// }); +// } +// } +// return _workloadTPRepository.Update(u => u.Id == tpId, s => new WorkloadTP +// { +// Status = (int)WorkloadStatus.ReviewFinish +// }); +// } + + +// /// +// /// 保存 基线期病灶及测量信息及其他信息,不会改变状态 +// /// +// [AllowAnonymous] +// [HttpPost] +// public bool AddBaseLineLesion(BaseLineReportCommand baseLineReportCommand) +// { +// if (baseLineReportCommand.LesionInformation.Count < 1) return false; + +// var addedLesion = baseLineReportCommand.LesionInformation[0]; + +// //删除已经有的数据 +// _tURepository.Delete(u => u.VISITNUM == addedLesion.VISITNUM && u.USUBJID == addedLesion.USUBJID && u.TUEVALID == _userInfo.ReviewerCode); +// _tRRepository.Delete(u => u.VISITNUM == addedLesion.VISITNUM && u.USUBJID == addedLesion.USUBJID && u.TREVALID == _userInfo.ReviewerCode); +// _reportRepository.Delete(u => u.TPId == baseLineReportCommand.ReportResult.TPId); + +// _workloadTPRepository.Update(u => u.Id == baseLineReportCommand.ReportResult.TPId, s => new WorkloadTP +// { +// Status = (int)WorkloadStatus.Reading +// }); + +// //序号按照每个项目、每个受试者、每个评估者,一条记录 +// TU lastTu = _tURepository.Where(u => u.STUDYID == addedLesion.STUDYID && u.USUBJID == addedLesion.USUBJID +// && u.TUEVAL == addedLesion.TUEVAL).OrderByDescending("TUSEQ").FirstOrDefault(); +// int tuSeq = 101; +// if (lastTu != null) { tuSeq = lastTu.TUSEQ; } +// TR lastTr = _tRRepository.AsQueryable().OrderByDescending("TRSEQ").FirstOrDefault(); +// int trSeq = 1; +// if (lastTr != null) { trSeq = lastTr.TRSEQ; } + +// linkGroupId = Guid.NewGuid().ToString(); +// foreach (var lesion in baseLineReportCommand.LesionInformation) +// { +// var linkId = lesion.TULNKID; +// if (string.IsNullOrWhiteSpace(lesion.TULNKID)) +// { +// linkId = (lesion.TUORRES == "TARGET") ? "T" + tuSeq.ToString() : "NT" + tuSeq.ToString(); +// } +// _tURepository.Add(new TU +// { +// STUDYID = lesion.STUDYID, +// LesionType = lesion.LesionType, +// USUBJID = lesion.USUBJID, +// TUSEQ = tuSeq, +// TULNKID = linkId, + +// TUTESTCD = lesion.TUTESTCD, +// TUTEST = lesion.TUTEST, +// TUORRES = lesion.TUORRES, +// TUSTRESC = lesion.TUSTRESC, + +// TULOC = lesion.TULOC, +// LocDescription = lesion.LocDescription, +// TULAT = lesion.TULAT, +// TUDIR = lesion.TUDIR, +// TUPORTOT = lesion.TUPORTOT, +// TUMETHOD = lesion.TUMETHOD, +// TUEVAL = lesion.TUEVAL, +// VISIT = lesion.VISIT, +// VISITNUM = lesion.VISITNUM, +// VISITDY = lesion.VISITDY, + +// TUREFID = lesion.TUREFID, +// TUACPTFL = lesion.TUACPTFL, +// TUEVALID = _userInfo.ReviewerCode,//lesion.TUEVALID, +// TUGRPID = string.Empty, +// TUDY = lesion.TUDY,// 检查日 +// EPOCH = lesion.EPOCH, +// TUDTC = lesion.TUDTC, +// TUNAM = lesion.TUNAM, +// TUSPID = lesion.TUSPID, + +// TpCode = baseLineReportCommand.ReportResult.TpCode +// }); +// _tRRepository.Add(new TR +// { +// STUDYID = lesion.STUDYID, + +// USUBJID = lesion.USUBJID, +// TRSEQ = trSeq, +// TRLNKID = linkId, +// TRGRPID = lesion.TRGRPID, +// TRLNKGRP = linkGroupId, + +// TRTESTCD = lesion.TRTESTCD, +// TRTEST = lesion.TRTEST, +// TRORRES = lesion.TRORRES, +// TRORRESU = lesion.TRORRESU, + +// TRNAM = lesion.TUNAM, +// TRMETHOD = lesion.TUMETHOD, +// TREVAL = lesion.TUEVAL, +// TREVALID = _userInfo.ReviewerCode,//lesion.TUEVALID, +// TRACPTFL = lesion.TUACPTFL, + +// Note = lesion.Note, + +// VISITNUM = lesion.VISITNUM, +// VISIT = lesion.VISIT, +// VISITDY = lesion.VISITDY, +// EPOCH = lesion.EPOCH, +// TRDTC = lesion.TRDTC, +// TRDY = lesion.TRDY, +// TpCode = baseLineReportCommand.ReportResult.TpCode +// }); +// tuSeq++; +// trSeq++; +// } + +// _tRRepository.Add(new TR +// { +// STUDYID = addedLesion.STUDYID, + +// USUBJID = addedLesion.USUBJID, +// TRSEQ = trSeq, +// TRLNKID = string.Empty, +// TRGRPID = addedLesion.TRGRPID, +// TRLNKGRP = linkGroupId, + +// TRTESTCD = addedLesion.TRTESTCD, +// TRTEST = "Sum of Diameter", +// TRORRES = baseLineReportCommand.SumOfDiameter, +// TRORRESU = "mm",//addedLesion.TRORRESU, + +// TRNAM = addedLesion.TUNAM, +// TRMETHOD = addedLesion.TUMETHOD, +// TREVAL = addedLesion.TUEVAL, +// TREVALID = _userInfo.ReviewerCode,//addedLesion.TUEVALID, +// TRACPTFL = addedLesion.TUACPTFL, + +// Note = string.Empty, + +// VISITNUM = addedLesion.VISITNUM, +// VISIT = addedLesion.VISIT, +// VISITDY = addedLesion.VISITDY, +// EPOCH = addedLesion.EPOCH, +// TRDTC = addedLesion.TRDTC, +// TRDY = addedLesion.TRDY, +// TpCode = baseLineReportCommand.ReportResult.TpCode +// }); +// trSeq++; +// _tRRepository.Add(new TR +// { +// STUDYID = addedLesion.STUDYID, + +// USUBJID = addedLesion.USUBJID, +// TRSEQ = trSeq, +// TRLNKID = string.Empty, +// TRGRPID = addedLesion.TRGRPID, +// TRLNKGRP = linkGroupId, + +// TRTESTCD = addedLesion.TRTESTCD, +// TRTEST = "Sum Diameters of Non Lymph Node Tumors", +// TRORRES = baseLineReportCommand.SumDiameterOfNonLymphNode, +// TRORRESU = "mm",//addedLesion.TRORRESU, + +// TRNAM = addedLesion.TUNAM, +// TRMETHOD = addedLesion.TUMETHOD, +// TREVAL = addedLesion.TUEVAL, +// TREVALID = _userInfo.ReviewerCode,//addedLesion.TUEVALID, +// TRACPTFL = addedLesion.TUACPTFL, + +// Note = string.Empty, + +// VISITNUM = addedLesion.VISITNUM, +// VISIT = addedLesion.VISIT, +// VISITDY = addedLesion.VISITDY, +// EPOCH = addedLesion.EPOCH, +// TRDTC = addedLesion.TRDTC, +// TRDY = addedLesion.TRDY, +// TpCode = baseLineReportCommand.ReportResult.TpCode +// }); +// trSeq++; +// _reportRepository.Add(_mapper.Map(baseLineReportCommand.ReportResult)); + +// return _reportRepository.SaveChanges(); +// } + +// /// +// /// 添加访视报告信息 +// /// LesionInformation 为新病灶及测量信息(包括分裂及合并产生的) +// /// TRList 已经存在的病灶的测量信息 +// /// RSList 疗效信息 +// /// +// [AllowAnonymous] +// [HttpPost, Route("saveVisitReport")] +// public bool SaveVisitReport(VisitReportCommand visitReportCommand) +// { +// linkGroupId = Guid.NewGuid().ToString(); +// var visitNum = visitReportCommand.ReportResult.VisitNum; +// var subjectId = visitReportCommand.ReportResult.SubjectCode; + +// //删除已经有的数据 +// _tURepository.Delete(u => u.VISITNUM == visitNum && u.USUBJID == subjectId && u.TUEVALID == _userInfo.ReviewerCode); +// _tRRepository.Delete(u => u.VISITNUM == visitNum && u.USUBJID == subjectId && u.TREVALID == _userInfo.ReviewerCode); +// _reportRepository.Delete(u => u.TPId == visitReportCommand.ReportResult.TPId); +// _rSRepository.Delete(u => u.VISITNUM == visitNum && u.RSEVALID == _userInfo.ReviewerCode); + +// var report = visitReportCommand.ReportResult; + +// _workloadTPRepository.Update(u => u.Id == report.TPId, s => new WorkloadTP +// { +// Status = (int)WorkloadStatus.Reading +// }); +// if (visitReportCommand.LesionInformation.Count > 0) +// { +// AddLesion(visitReportCommand.LesionInformation, report); +// } + +// TR lastTr = _tRRepository.AsQueryable().OrderByDescending("TRSEQ").FirstOrDefault(); +// int trSeq = 1; +// if (lastTr != null) { trSeq = lastTr.TRSEQ + 1; } + +// foreach (var lesion in visitReportCommand.TRList) +// { +// _tRRepository.Add(new TR +// { +// STUDYID = lesion.STUDYID, + +// USUBJID = lesion.USUBJID, +// TRSEQ = trSeq, +// TRLNKID = lesion.TRLNKID, +// TRGRPID = lesion.TRGRPID, +// TRLNKGRP = linkGroupId, + +// TRTESTCD = lesion.TRTESTCD, +// TRTEST = lesion.TRTEST, +// TRORRES = lesion.TRORRES, +// TRORRESU = lesion.TRORRESU, + +// TRNAM = lesion.TRNAM, +// TRMETHOD = lesion.TRMETHOD, +// TREVAL = lesion.TREVAL, +// TREVALID = _userInfo.ReviewerCode,// lesion.TREVALID, +// TRACPTFL = lesion.TRACPTFL, + +// Note = lesion.Note, +// CoveredLesion = lesion.CoveredLesion, +// VISITNUM = lesion.VISITNUM, +// VISIT = lesion.VISIT, +// VISITDY = lesion.VISITDY, +// EPOCH = lesion.EPOCH, +// TRDTC = lesion.TRDTC, +// TRDY = lesion.TRDY, +// TpCode = visitReportCommand.ReportResult.TpCode +// }); +// trSeq++; +// } +// foreach (var rs in visitReportCommand.RSList) +// { +// _rSRepository.Add(new RS +// { +// STUDYID = rs.STUDYID, +// USUBJID = rs.USUBJID, +// RSSEQ = 0,// +// RSLNKGRP = linkGroupId, +// RSTESTCD = rs.RSTESTCD, +// RSTEST = rs.RSTEST, +// RSCAT = rs.RSCAT, +// RSORRES = rs.RSORRES, +// RSSTRESC = rs.RSSTRESC, +// RSSTAT = rs.RSSTAT, +// RSREASND = rs.RSCAT, +// RSEVAL = rs.RSEVAL, +// RSEVALID = _userInfo.ReviewerCode,// +// VISITNUM = rs.VISITNUM, +// VISIT = rs.VISIT, +// RSDTC = rs.RSDTC, +// RSDY = rs.RSDY, +// TpCode = visitReportCommand.ReportResult.TpCode, +// StudyGuid = rs.StudyGuid, +// TrialGuid = rs.TrialGuid, +// SubjectGuid = rs.SubjectGuid, +// Note = rs.Note +// }); +// } + +// _reportRepository.Add(_mapper.Map(visitReportCommand.ReportResult)); + +// return _rSRepository.SaveChanges(); +// } +// } +//} diff --git a/IRaCIS.Core.Application/Service/ReadingAndReport/_MapConfig.cs b/IRaCIS.Core.Application/Service/ReadingAndReport/_MapConfig.cs new file mode 100644 index 00000000..a6da341b --- /dev/null +++ b/IRaCIS.Core.Application/Service/ReadingAndReport/_MapConfig.cs @@ -0,0 +1,23 @@ +using AutoMapper; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Application.Contracts.Dicom.DTO; +using IRaCIS.Core.Domain.Models; + +namespace IRaCIS.Core.Application.Service +{ + public class ReadingAndReportConfig : Profile + { + public ReadingAndReportConfig() + { + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap(); + + } + } + +} diff --git a/IRaCIS.Core.Application/Service/SiteSurvey/DTO/TrialSiteEquipmentSurveyViewModel.cs b/IRaCIS.Core.Application/Service/SiteSurvey/DTO/TrialSiteEquipmentSurveyViewModel.cs new file mode 100644 index 00000000..e503d80a --- /dev/null +++ b/IRaCIS.Core.Application/Service/SiteSurvey/DTO/TrialSiteEquipmentSurveyViewModel.cs @@ -0,0 +1,64 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-23 13:21:04 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +namespace IRaCIS.Core.Application.Contracts +{ + /// TrialSiteEquipmentSurveyView 列表视图模型 + public class TrialSiteEquipmentSurveyView + { + public Guid Id { get; set; } + public Guid TrialSiteSurveyId { get; set; } + public string EquipmentType { get; set; } = string.Empty; + public Guid? EquipmentTypeId { get; set; } + + public string Parameters { get; set; } = string.Empty; + public string ManufacturerName { get; set; } = string.Empty; + public string ScannerType { get; set; } = string.Empty; + public string Note { get; set; } = string.Empty; + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid UpdateUserId { get; set; } + } + + ///TrialSiteEquipmentSurveyQuery 列表查询参数模型 + public class TrialSiteEquipmentSurveyQuery + { + + public Guid TrialSiteSurveyId { get; set; } + + public string ScannerType { get; set; } = string.Empty; + + ///// Parameters + //public string Parameters { get; set; } + + ///// ManufacturerName + //public string ManufacturerName { get; set; } + + ///// ScannerType + //public string ScannerType { get; set; } + + ///// Note + //public string Note { get; set; } + + } + + /// TrialSiteEquipmentSurveyAddOrEdit 列表查询参数模型 + public class TrialSiteEquipmentSurveyAddOrEdit + { + public Guid? Id { get; set; } + public Guid TrialSiteSurveyId { get; set; } + + public Guid? EquipmentTypeId { get; set; } + public string Parameters { get; set; } = string.Empty; + public string ManufacturerName { get; set; } = string.Empty; + public string ScannerType { get; set; } = string.Empty; + public string Note { get; set; } = string.Empty; + } + + +} + + diff --git a/IRaCIS.Core.Application/Service/SiteSurvey/DTO/TrialSiteSurveyViewModel.cs b/IRaCIS.Core.Application/Service/SiteSurvey/DTO/TrialSiteSurveyViewModel.cs new file mode 100644 index 00000000..c7e3706a --- /dev/null +++ b/IRaCIS.Core.Application/Service/SiteSurvey/DTO/TrialSiteSurveyViewModel.cs @@ -0,0 +1,193 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-23 13:21:04 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using System.ComponentModel.DataAnnotations; +using IRaCIS.Core.Domain.Share; +namespace IRaCIS.Core.Application.Contracts +{ + + public class TrialSurveyInitInfo + { + public Guid TrialId { get; set; } + public string Sponsor { get; set; } = string.Empty; + + //研究方案号 + public string ResearchProgramNo { get; set; } = string.Empty; + + //实验名称 + public string ExperimentName { get; set; } = string.Empty; + + public string TrialCode { get; set; } = string.Empty; + + public Guid IndicationTypeId { get; set; } + + public string IndicationType { get; set; } = string.Empty; + + public string TrialSiteSurveyUserRoles { get; set; } = string.Empty; + + public string TrialSiteSurveyEquipmentType { get; set; } = string.Empty; + + public List TrialSiteSelectList { get; set; }=new List(); + + } + + public class TrialSiteForSelect + { + public Guid Id { get; set; } + public Guid TrialId { get; set; } + + public Guid SiteId { get; set; } + + public string TrialSiteCode { get; set; } = string.Empty; + + public string TrialSiteAliasName { get; set; } = string.Empty; + } + + public class LoginReturnDTO + { + public TrialSurveyInitInfo TrialInfo { get; set; } = new TrialSurveyInitInfo(); + + public TrialSiteSurveyView TrialSiteSurvey { get; set; } = new TrialSiteSurveyView(); + + public List TrialSiteEquipmentSurveyList { get; set; } = new List(); + + public List TrialSiteUserSurveyList { get; set; } = new List(); + } + + + /// TrialSiteSurveyView 列表视图模型 + public class TrialSiteSurveyView + { + + public string TrialSiteCode { get; set; } = String.Empty; + public string TrialSiteAliasName { get; set; } = String.Empty; + + public string SiteName { get; set; } = string.Empty; + public bool IsAbandon { get; set; } + public Guid Id { get; set; } + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid UpdateUserId { get; set; } + public string UserName { get; set; } = string.Empty; + public string Phone { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public int AverageEngravingCycle { get; set; } + public bool IsConfirmImagingTechnologist { get; set; } + public string NotConfirmReson { get; set; } = string.Empty; + public int EfficacyEvaluatorType { get; set; } + public bool IsFollowStudyParameters { get; set; } + public string NotFollowReson { get; set; } = string.Empty; + //public bool IsLocked { get; set; } = false; + public TrialSiteSurveyEnum State { get; set; } + } + + ///TrialSiteSurveyQuery 列表查询参数模型 + public class TrialSiteSurveyQuery + { + public Guid TrialId { get; set; } + + public Guid SiteId { get; set; } + + ///// UserName + //public string UserName { get; set; } + + ///// Phone + //public string Phone { get; set; } + + ///// Email + //public string Email { get; set; } + + ///// NotConfirmReson + //public string NotConfirmReson { get; set; } + + ///// NotFollowReson + //public string NotFollowReson { get; set; } + + } + + + + public class SiteSurveySendVerifyCode + { + public VerifyType verificationType { get; set; } + public string EmailOrPhone { get; set; } = string.Empty; + + } + + public class LoginDto + { + public Guid TrialId { get; set; } + + public Guid SiteId { get; set; } + + + public bool IsUpdate { get; set; } + + + public string ReplaceUserEmailOrPhone { get; set; } = string.Empty; + + public VerifyType verificationType { get; set; } + public string EmailOrPhone { get; set; } = string.Empty; + + public string verificationCode { get; set; } = string.Empty; + } + + + /// TrialSiteSurveyAddOrEdit 列表查询参数模型 + public class TrialSiteSurveyAddOrEdit + { + public Guid? Id { get; set; } + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public string UserName { get; set; } = string.Empty; + public string Phone { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public int AverageEngravingCycle { get; set; } + public bool IsConfirmImagingTechnologist { get; set; } + public string NotConfirmReson { get; set; } = string.Empty; + public int EfficacyEvaluatorType { get; set; } + public bool IsFollowStudyParameters { get; set; } + public string NotFollowReson { get; set; } = string.Empty; + + + } + + public class TrialSiteSurvyeSubmitDTO + { + [NotDefault] + public Guid TrialId { get; set; } + [NotDefault] + public Guid TrialSiteSurveyId { get; set; } + + public string? LoginUrl { get; set; } + + public string? RouteUrl { get; set; } + } + + public class TrialSiteSurveyQueryDTO + { + //public Guid? SiteId { get; set; } + + public string SiteName { get; set; } = String.Empty; + public string TrialSiteCode { get; set; } = String.Empty; + public string TrialSiteAliasName { get; set; } = String.Empty; + } + + public class CopyTrialSiteSurveyDTO + { + public Guid TrialSiteSurveyId { get; set; } + public string UserName { get; set; } = string.Empty; + public string Phone { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + } + + +} + + diff --git a/IRaCIS.Core.Application/Service/SiteSurvey/DTO/TrialSiteUserSurveyViewModel.cs b/IRaCIS.Core.Application/Service/SiteSurvey/DTO/TrialSiteUserSurveyViewModel.cs new file mode 100644 index 00000000..515cbbf7 --- /dev/null +++ b/IRaCIS.Core.Application/Service/SiteSurvey/DTO/TrialSiteUserSurveyViewModel.cs @@ -0,0 +1,70 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-23 13:21:04 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using IRaCIS.Core.Domain.Share; +namespace IRaCIS.Core.Application.Contracts +{ + /// TrialSiteUserSurveyView 列表视图模型 + public class TrialSiteUserSurveyView: TrialSiteUserSurveyAddOrEdit + { + public bool IsGenerateSuccess { get; set; } + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid UpdateUserId { get; set; } + + public string UserType { get; set; } = string.Empty; + public TrialSiteSurveyEnum State { get; set; } + + public string TrialRoleName { get; set; } + + public Guid? SystemUserId { get; set; } + } + + ///TrialSiteUserSurveyQuery 列表查询参数模型 + public class TrialSiteUserSurveyQuery + { + + public Guid TrialSiteSurveyId { get; set; } + + ///// UserName + //public string Name { get; set; } + + ///// Phone + //public string Phone { get; set; } + + ///// Email + //public string Email { get; set; } + + } + + /// TrialSiteUserSurveyAddOrEdit 列表查询参数模型 + public class TrialSiteUserSurveyAddOrEdit + { + public Guid? Id { get; set; } + public Guid TrialRoleNameId { get; set; } + public Guid TrialSiteSurveyId { get; set; } + public Guid? UserTypeId { get; set; } + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public string Phone { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public bool IsCorrect { get; set; } + public bool IsGenerateAccount { get; set; } + + public string OrganizationName { get; set; } = string.Empty; + } + + + public class TrialSiteUserSurveyVerfyResult + { + public Guid TrialSiteUserSurveyId { get; set; } + + public List ErroMsgList { get; set; } = new List(); + } + +} + + diff --git a/IRaCIS.Core.Application/Service/SiteSurvey/Interface/ITrialSiteEquipmentSurveyService.cs b/IRaCIS.Core.Application/Service/SiteSurvey/Interface/ITrialSiteEquipmentSurveyService.cs new file mode 100644 index 00000000..66780456 --- /dev/null +++ b/IRaCIS.Core.Application/Service/SiteSurvey/Interface/ITrialSiteEquipmentSurveyService.cs @@ -0,0 +1,16 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-23 13:20:59 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + + +namespace IRaCIS.Core.Application.Contracts +{ + public interface ITrialSiteEquipmentSurveyService + { + Task AddOrUpdateTrialSiteEquipmentSurvey(TrialSiteEquipmentSurveyAddOrEdit addOrEditTrialSiteEquipmentSurvey); + Task DeleteTrialSiteEquipmentSurvey(Guid trialSiteEquipmentSurveyId); + Task> GetTrialSiteEquipmentSurveyList(Guid trialSiteSurveyId, string? scannerType); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/SiteSurvey/Interface/ITrialSiteSurveyService.cs b/IRaCIS.Core.Application/Service/SiteSurvey/Interface/ITrialSiteSurveyService.cs new file mode 100644 index 00000000..5d007c94 --- /dev/null +++ b/IRaCIS.Core.Application/Service/SiteSurvey/Interface/ITrialSiteSurveyService.cs @@ -0,0 +1,24 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-23 13:20:59 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using IRaCIS.Application.Services; +using IRaCIS.Core.Application.Auth; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Core.Application.Contracts +{ + public interface ITrialSiteSurveyService + { + Task AddOrUpdateTrialSiteSurvey(TrialSiteSurveyAddOrEdit addOrEditTrialSiteSurvey); + Task DeleteTrialSiteSurvey(Guid trialSiteSurveyId); + Task GetSiteSurveyInfo(Guid trialSiteSurveyId, Guid trialId); + Task> GetTrialSiteSurveyList(Guid trialId, TrialSiteSurveyQueryDTO trialSiteSurveyQueryDTO); + Task GetTrialSurveyInitInfo(Guid trialId); + Task SendVerifyCode(SiteSurveySendVerifyCode userInfo, [FromServices] IMailVerificationService _mailVerificationService); + //Task TrialSurveyLock(Guid trialSiteSurveyId, bool isLock); + //IResponseOutput TrialSurveySubmmit(Guid trialId, Guid trialSiteSurveyId); + Task VerifySendCode(LoginDto userInfo, [FromServices] ITokenService _tokenService); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/SiteSurvey/Interface/ITrialSiteUserSurveyService.cs b/IRaCIS.Core.Application/Service/SiteSurvey/Interface/ITrialSiteUserSurveyService.cs new file mode 100644 index 00000000..32942f1a --- /dev/null +++ b/IRaCIS.Core.Application/Service/SiteSurvey/Interface/ITrialSiteUserSurveyService.cs @@ -0,0 +1,15 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-23 13:20:59 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +namespace IRaCIS.Core.Application.Contracts +{ + public interface ITrialSiteUserSurveyService + { + Task AddOrUpdateTrialSiteUserSurvey(TrialSiteUserSurveyAddOrEdit addOrEditTrialSiteUserSurvey); + Task DeleteTrialSiteUserSurvey(Guid trialSiteUserSurveyId); + Task> GetTrialSiteUserSurveyList(Guid trialSiteSurveyId); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/SiteSurvey/Interface/JsonPatchUserRequestExample.cs b/IRaCIS.Core.Application/Service/SiteSurvey/Interface/JsonPatchUserRequestExample.cs new file mode 100644 index 00000000..0fa6ff83 --- /dev/null +++ b/IRaCIS.Core.Application/Service/SiteSurvey/Interface/JsonPatchUserRequestExample.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.JsonPatch.Operations; +using Swashbuckle.AspNetCore.Filters; + +namespace IRaCIS.Core.Application.Contracts +{ + /// + /// 实测 标注在服务方法上 没用 + /// + public class JsonPatchUserRequestExample : IExamplesProvider + { + public Operation[] GetExamples() + { + return new[] + { + new Operation + { + op = "replace", + path = "/name", + value = "Gordon" + }, + new Operation + { + op = "replace", + path = "/surname", + value = "Freeman" + } + }; + } + + object IExamplesProvider.GetExamples() + { + return new[] + { + new Operation + { + op = "replace", + path = "/name", + value = "Gordon" + }, + new Operation + { + op = "replace", + path = "/surname", + value = "Freeman" + } + }; + } + } + + +} diff --git a/IRaCIS.Core.Application/Service/SiteSurvey/TrialSiteEquipmentSurveyService.cs b/IRaCIS.Core.Application/Service/SiteSurvey/TrialSiteEquipmentSurveyService.cs new file mode 100644 index 00000000..bad6e77b --- /dev/null +++ b/IRaCIS.Core.Application/Service/SiteSurvey/TrialSiteEquipmentSurveyService.cs @@ -0,0 +1,82 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-23 13:20:59 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infra.EFCore; + +namespace IRaCIS.Core.Application.Contracts +{ + /// + /// TrialSiteEquipmentSurveyService + /// + [ApiExplorerSettings(GroupName = "Trial")] + public class TrialSiteEquipmentSurveyService : BaseService, ITrialSiteEquipmentSurveyService + { + private readonly IRepository _trialSiteEquipmentSurveyRepository; + + public TrialSiteEquipmentSurveyService(IRepository trialSiteEquipmentSurveyRepository) + { + _trialSiteEquipmentSurveyRepository = trialSiteEquipmentSurveyRepository; + } + + + [HttpGet("{trialSiteSurveyId:guid}")] + public async Task> GetTrialSiteEquipmentSurveyList(Guid trialSiteSurveyId, string? scannerType) + { + var trialSiteEquipmentSurveyQueryable = _trialSiteEquipmentSurveyRepository.Where(t => t.TrialSiteSurveyId == trialSiteSurveyId) + .WhereIf(!string.IsNullOrEmpty(scannerType), t => t.ScannerType.Contains(scannerType!)) + .ProjectTo(_mapper.ConfigurationProvider); + + return await trialSiteEquipmentSurveyQueryable.ToListAsync(); + } + + [TypeFilter(typeof(TrialResourceFilter))] + [HttpPost("{trialId:guid}")] + public async Task AddOrUpdateTrialSiteEquipmentSurvey(TrialSiteEquipmentSurveyAddOrEdit addOrEditTrialSiteEquipmentSurvey) + { + if (_userInfo.UserTypeEnumInt == (int)UserTypeEnum.CPM || _userInfo.UserTypeEnumInt == (int)UserTypeEnum.APM) + { + return ResponseOutput.NotOk("CPM/APM 不允许操作"); + } + + if (addOrEditTrialSiteEquipmentSurvey.Id != null) + { + if (await _trialSiteEquipmentSurveyRepository.Where(t => t.Id == addOrEditTrialSiteEquipmentSurvey.Id).AnyAsync(t => t.TrialSiteSurvey.State==TrialSiteSurveyEnum.PMCreatedAndLock)) + { + return ResponseOutput.NotOk("已锁定,不允许操作"); + } + } + + + var entity = await _trialSiteEquipmentSurveyRepository.InsertOrUpdateAsync(addOrEditTrialSiteEquipmentSurvey, true); + + return ResponseOutput.Ok(entity.Id.ToString()); + + } + + [TypeFilter(typeof(TrialResourceFilter))] + [HttpDelete("{trialSiteEquipmentSurveyId:guid}/{trialId:guid}")] + public async Task DeleteTrialSiteEquipmentSurvey(Guid trialSiteEquipmentSurveyId) + { + if (_userInfo.UserTypeEnumInt == (int)UserTypeEnum.CPM || _userInfo.UserTypeEnumInt == (int)UserTypeEnum.APM) + { + return ResponseOutput.NotOk("CPM/APM 不允许操作"); + } + + if (await _trialSiteEquipmentSurveyRepository.Where(t => t.Id == trialSiteEquipmentSurveyId).AnyAsync(t => t.TrialSiteSurvey.State==TrialSiteSurveyEnum.PMCreatedAndLock)) + { + return ResponseOutput.NotOk("已锁定,不允许操作"); + } + var success = await _trialSiteEquipmentSurveyRepository.DeleteFromQueryAsync(t => t.Id == trialSiteEquipmentSurveyId); + + return ResponseOutput.Result(success); + } + + + } +} diff --git a/IRaCIS.Core.Application/Service/SiteSurvey/TrialSiteSurveyService.cs b/IRaCIS.Core.Application/Service/SiteSurvey/TrialSiteSurveyService.cs new file mode 100644 index 00000000..5a69cba9 --- /dev/null +++ b/IRaCIS.Core.Application/Service/SiteSurvey/TrialSiteSurveyService.cs @@ -0,0 +1,691 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-23 13:20:59 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Domain.Share; +using System.Text.RegularExpressions; +using IRaCIS.Core.Infrastructure; +using IRaCIS.Application.Services; +using IRaCIS.Core.Application.Auth; +using IRaCIS.Application.Contracts; +using Microsoft.AspNetCore.Authorization; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Infra.EFCore; +using MailKit.Security; +using MimeKit; + +namespace IRaCIS.Core.Application.Contracts +{ + /// + /// TrialSiteSurveyService + /// + [ApiExplorerSettings(GroupName = "Trial")] + public class TrialSiteSurveyService : BaseService, ITrialSiteSurveyService + { + private readonly IRepository _trialSiteSurveyRepository; + private readonly IRepository _trialSiteUserSurveyRepository; + private readonly IRepository _userRepository; + + public TrialSiteSurveyService(IRepository trialSiteSurveyRepository,IRepository trialSiteUserSurveyRepository,IRepository userRepository) + { + _trialSiteSurveyRepository = trialSiteSurveyRepository; + _trialSiteUserSurveyRepository = trialSiteUserSurveyRepository; + _userRepository = userRepository; + } + + private object lockObj { get; set; } = new object(); + + /// + /// 发送验证码 + /// + /// + /// + /// + [AllowAnonymous] + public async Task SendVerifyCode(SiteSurveySendVerifyCode userInfo, [FromServices] IMailVerificationService _mailVerificationService) + { + var verificationType = userInfo.verificationType; + //检查手机或者邮箱是否有效 + if (!Regex.IsMatch(userInfo.EmailOrPhone, @"/^1[34578]\d{9}$/") && !Regex.IsMatch(userInfo.EmailOrPhone, @"^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$")) + { + + throw new BusinessValidationFailedException(verificationType == VerifyType.Email + ? "Please input a legal email" + : "Please input a legal phone"); + + } + + //邮箱 + if (verificationType == VerifyType.Email) + { + //验证码 6位 + int verificationCode = new Random().Next(100000, 1000000); + + await _mailVerificationService.AnolymousSendEmail(userInfo.EmailOrPhone, verificationCode); + } + //手机短信 + else + { + + } + + return ResponseOutput.Ok(); + + } + + /// + /// 验证后 如果数据库该项目不存在该邮箱 那么就插入记录 存在 + /// + /// + /// + /// + /// + [HttpPost] + [AllowAnonymous] + public async Task VerifySendCode(LoginDto userInfo, [FromServices] ITokenService _tokenService) + { + + var isReplaceUser = !string.IsNullOrEmpty(userInfo.ReplaceUserEmailOrPhone); + + + if (userInfo.IsUpdate && isReplaceUser && !await _trialSiteSurveyRepository.AnyAsync(t => (t.Email == userInfo.ReplaceUserEmailOrPhone || t.Phone == userInfo.ReplaceUserEmailOrPhone) && t.SiteId == userInfo.SiteId && t.TrialId == userInfo.TrialId)) + { + return ResponseOutput.NotOk("该项目Site不存在该交接人的调研记录,不允许选择更新"); + } + + + if (userInfo.IsUpdate && await _trialSiteSurveyRepository.AnyAsync(t => (t.Email == userInfo.EmailOrPhone || t.Phone == userInfo.EmailOrPhone) && t.SiteId == userInfo.SiteId && t.TrialId == userInfo.TrialId && t.State != TrialSiteSurveyEnum.PMCreatedAndLock)) + { + return ResponseOutput.NotOk("您的记录未锁定,不允许选择更新,若已经提交,可被驳回后进行操作"); + } + + + //自己的记录锁定了 只能更新自己的,不能更新别人的(但是别人能更新自己锁定的) + if (userInfo.IsUpdate && userInfo.ReplaceUserEmailOrPhone != userInfo.EmailOrPhone && await _trialSiteSurveyRepository.AnyAsync(t => (t.Email == userInfo.EmailOrPhone || t.Phone == userInfo.EmailOrPhone) && t.SiteId == userInfo.SiteId && t.TrialId == userInfo.TrialId && t.State == TrialSiteSurveyEnum.PMCreatedAndLock)) + { + //自己的锁了 想更新别人的 + return ResponseOutput.NotOk("当前Site 您的调研记录已锁定,不允许更新其他人邮箱调研记录"); + } + + //自己的锁定了 如果有其他未锁定的,也不能更新自己的 + if (userInfo.IsUpdate && userInfo.ReplaceUserEmailOrPhone == userInfo.EmailOrPhone && + await _trialSiteSurveyRepository.AnyAsync(t => (t.Email == userInfo.EmailOrPhone || t.Phone == userInfo.EmailOrPhone) && t.SiteId == userInfo.SiteId && t.TrialId == userInfo.TrialId && t.State == TrialSiteSurveyEnum.PMCreatedAndLock) + && await _trialSiteSurveyRepository.AnyAsync(t => (t.Email != userInfo.EmailOrPhone && t.Phone != userInfo.EmailOrPhone) && t.SiteId == userInfo.SiteId && t.TrialId == userInfo.TrialId && t.State != TrialSiteSurveyEnum.PMCreatedAndLock)) + { + + return ResponseOutput.NotOk("当前Site 您的调研记录已锁定,也存在其他未锁定的记录,不允许更新自己的调研记录"); + } + + + ////存在未锁定的记录,却去更新已锁定的 + if (userInfo.IsUpdate && userInfo.ReplaceUserEmailOrPhone != userInfo.EmailOrPhone && await _trialSiteSurveyRepository.AnyAsync(t => t.SiteId == userInfo.SiteId && t.TrialId == userInfo.TrialId && t.State != TrialSiteSurveyEnum.PMCreatedAndLock) + && await _trialSiteSurveyRepository.AnyAsync(t => (t.Email == userInfo.ReplaceUserEmailOrPhone || t.Phone == userInfo.ReplaceUserEmailOrPhone) && t.SiteId == userInfo.SiteId && t.TrialId == userInfo.TrialId && t.State == TrialSiteSurveyEnum.PMCreatedAndLock) + && !await _trialSiteSurveyRepository.AnyAsync(t => (t.Email == userInfo.ReplaceUserEmailOrPhone || t.Phone == userInfo.ReplaceUserEmailOrPhone) && t.SiteId == userInfo.SiteId && t.TrialId == userInfo.TrialId && t.State != TrialSiteSurveyEnum.PMCreatedAndLock) + ) + { + return ResponseOutput.NotOk("当前Site 存在未锁定的调研记录,不允许更新已锁定邮箱的调研记录"); + } + + + + //var verificationRecord = await _repository + // .FirstOrDefaultAsync(t => (t.EmailOrPhone == userInfo.EmailOrPhone) && t.Code == userInfo.verificationCode && t.CodeType == userInfo.verificationType); + + ////检查数据库是否存在该验证码 + //if (verificationRecord == null) + //{ + // return ResponseOutput.NotOk("Verification code error"); + //} + //else + //{ + // //检查验证码是否失效 + // if (verificationRecord.ExpirationTime < DateTime.Now) + // { + // return ResponseOutput.NotOk("The verification code has expired"); + // } + // else //验证码正确 并且 没有超时 + { + TrialSiteSurvey dbEntity = null; + + + //替换交接人 + if (isReplaceUser) + { + //该交接人的记录 是否有未锁定的 有就用未锁定的,没有就用 锁定的最后一条 + + var noLockedLastSurvey = await _trialSiteSurveyRepository.Where(t => (t.Email == userInfo.ReplaceUserEmailOrPhone || t.Phone == userInfo.ReplaceUserEmailOrPhone) && t.SiteId == userInfo.SiteId && t.TrialId == userInfo.TrialId && t.State == TrialSiteSurveyEnum.PMCreatedAndLock == false, true) + .Include(u => u.TrialSiteEquipmentSurveyList).Include(u => u.TrialSiteUserSurveyList).OrderByDescending(t => t.CreateTime).FirstOrDefaultAsync(); + + //都是锁定的 + if (noLockedLastSurvey == null) + { + + var latestLock = await _trialSiteSurveyRepository.Where(t => t.SiteId == userInfo.SiteId && t.TrialId == userInfo.TrialId).OrderByDescending(t => t.CreateTime).FirstOrDefaultAsync(); + + if (latestLock!.Email != userInfo.ReplaceUserEmailOrPhone) + { + return ResponseOutput.NotOk($"该邮箱{userInfo.ReplaceUserEmailOrPhone }对应的调查表不是最新锁定的记录,不允许更新!"); + } + + var lockedLastSurvey = await _trialSiteSurveyRepository.Where(t => (t.Email == userInfo.ReplaceUserEmailOrPhone || t.Phone == userInfo.ReplaceUserEmailOrPhone) && t.SiteId == userInfo.SiteId && t.TrialId == userInfo.TrialId && t.State == TrialSiteSurveyEnum.PMCreatedAndLock == true) + .Include(u => u.TrialSiteEquipmentSurveyList).Include(u => u.TrialSiteUserSurveyList).OrderByDescending(t => t.CreateTime).FirstOrDefaultAsync().IfNullThrowConvertException(); + + //Copy 一份 更换邮箱 + + var copy = lockedLastSurvey.Clone(); + + copy.State = TrialSiteSurveyEnum.ToSubmit; + + copy.Email = userInfo.EmailOrPhone; + + if (userInfo.ReplaceUserEmailOrPhone != userInfo.EmailOrPhone) + { + copy.UserName = String.Empty; + copy.Phone = String.Empty; + } + + + copy.Id = Guid.Empty; + copy.TrialSiteEquipmentSurveyList.ForEach(t => t.Id = Guid.Empty); + copy.TrialSiteUserSurveyList.ForEach(t => t.Id = Guid.Empty); + + dbEntity = await _repository.AddAsync(copy); + + } + else + { + //有未锁定的 更新下邮箱 + noLockedLastSurvey.Email = userInfo.EmailOrPhone; + noLockedLastSurvey.UserName = String.Empty; + noLockedLastSurvey.Phone = String.Empty; + + dbEntity = noLockedLastSurvey; + } + + + + ////邮箱相同的话 就是同一个人进来 copy一份 + //if (userInfo.EmailOrPhone == userInfo.ReplaceUserEmailOrPhone) + //{ + // dbEntity = await _repository.Where(t => (t.Email == userInfo.EmailOrPhone || t.Phone == userInfo.EmailOrPhone) && t.SiteId == userInfo.SiteId && t.TrialId == userInfo.TrialId).Include(u => u.TrialSiteEquipmentSurveyList).Include(u => u.TrialSiteUserSurveyList).FirstOrDefaultAsync().IfNullThrowConvertException(); + + // var clone = dbEntity.Clone(); + // clone.Id = Guid.Empty; + // clone.TrialSiteEquipmentSurveyList.ForEach(t => t.Id = Guid.Empty); + // clone.TrialSiteUserSurveyList.ForEach(t => t.Id = Guid.Empty); + // clone.State = TrialSiteSurveyEnum.ToSubmit; + + // dbEntity = await _repository.AddAsync(clone); + + //} + + } + else + { + + + var dbEntityList = await _trialSiteSurveyRepository.Where(t => t.TrialId == userInfo.TrialId && t.SiteId == userInfo.SiteId).ToListAsync(); + + + //没有记录 new一份 + if (dbEntityList.Count == 0) + { + + dbEntity = await _repository.AddAsync(_mapper.Map(userInfo)); + + } + else + { + + + + //该site 下不存在该邮箱的记录 + if (!dbEntityList.Any(t => t.Email == userInfo.EmailOrPhone || t.Phone == userInfo.EmailOrPhone)) + { + return ResponseOutput.NotOk("该Site下已经有其他用户已填写的调研表,您不被允许继续填写"); + } + + + //有没有该邮箱 未锁定的 + var nolockEntity = dbEntityList.Where(t => t.Email == userInfo.EmailOrPhone || t.Phone == userInfo.EmailOrPhone).FirstOrDefault(t => t.State != TrialSiteSurveyEnum.PMCreatedAndLock); + + // 未锁定的 为空 + if (nolockEntity == null) + { + //查看最新锁定的 + dbEntity = dbEntityList.Where(t => t.Email == userInfo.EmailOrPhone || t.Phone == userInfo.EmailOrPhone).OrderByDescending(t => t.CreateTime).FirstOrDefault(t => t.State == TrialSiteSurveyEnum.PMCreatedAndLock).IfNullThrowException(); + + } + else //有未锁定的 直接用未锁定的 + { + dbEntity = nolockEntity; + } + + } + //_mapper.Map(userInfo, dbEntity); + + + + + } + + + //删除验证码历史记录 + await _repository.DeleteFromQueryAsync(t => t.EmailOrPhone == userInfo.EmailOrPhone && t.Code == userInfo.verificationCode && t.CodeType == userInfo.verificationType); + + await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(new + { + TrialSiteSurveyId = dbEntity!.Id, + Token = _tokenService.GetToken(IRaCISClaims.Create(new UserBasicInfo() + { + Id = Guid.Empty, + IsReviewer = false, + IsAdmin = false, + RealName = "SiteSurvey", + UserName = "SiteSurvey", + Sex = 0, + //UserType = "ShareType", + UserTypeEnum = UserTypeEnum.Undefined, + Code = "SiteSurvey", + })) + }); + + } + //} + + } + + + /// + /// 直接查询相关所有数据 + /// + /// + [HttpGet("{trialId:guid}/{trialSiteSurveyId:guid}")] + public async Task GetSiteSurveyInfo(Guid trialSiteSurveyId, Guid trialId) + { + var result = await _trialSiteSurveyRepository.Where(t => t.Id == trialSiteSurveyId && t.TrialId == trialId) + .ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync().IfNullThrowException(); + + return result; + } + + + /// + /// 实际这里只会是更新 添加在login的时候做了 + /// + /// + /// + public async Task AddOrUpdateTrialSiteSurvey(TrialSiteSurveyAddOrEdit addOrEditTrialSiteSurvey) + { + + if (addOrEditTrialSiteSurvey.Id != null) + { + if (await _trialSiteSurveyRepository.AnyAsync(t => t.Id == addOrEditTrialSiteSurvey.Id && t.State == TrialSiteSurveyEnum.PMCreatedAndLock)) + { + return ResponseOutput.NotOk("已锁定,不允许操作"); + } + } + var entity = await _trialSiteSurveyRepository.InsertOrUpdateAsync(addOrEditTrialSiteSurvey, true); + + return ResponseOutput.Ok(entity.Id.ToString()); + } + + + /// + /// 删除调研表 + /// + /// + /// + [HttpDelete("{trialSiteSurveyId:guid}/{trialId:guid}")] + public async Task DeleteTrialSiteSurvey(Guid trialSiteSurveyId) + { + + + if (await _trialSiteSurveyRepository.AnyAsync(t => t.Id == trialSiteSurveyId && t.State == TrialSiteSurveyEnum.PMCreatedAndLock)) + { + return ResponseOutput.NotOk("已锁定,不允许操作"); + } + + var success = await _trialSiteSurveyRepository.DeleteFromQueryAsync(t => t.Id == trialSiteSurveyId); + + return ResponseOutput.Result(success); + } + + + + /// + /// 获取 项目 某个site的调研记录 + /// + /// + [HttpPost("{trialId:guid}")] + public async Task> GetTrialSiteSurveyList(Guid trialId, TrialSiteSurveyQueryDTO trialSiteSurveyQueryDTO) + { + var trialSiteSurveyQueryable = _trialSiteSurveyRepository.Where(t => t.TrialId == trialId).IgnoreQueryFilters() + //.WhereIf(trialSiteSurveyQueryDTO.si != null, t => t.SiteId == siteId) + .WhereIf(!string.IsNullOrWhiteSpace(trialSiteSurveyQueryDTO.SiteName), t => t.Site.SiteName.Contains(trialSiteSurveyQueryDTO.SiteName)) + .WhereIf(!string.IsNullOrWhiteSpace(trialSiteSurveyQueryDTO.TrialSiteAliasName), t => t.TrialSite.TrialSiteAliasName.Contains(trialSiteSurveyQueryDTO.TrialSiteAliasName)) + .WhereIf(!string.IsNullOrWhiteSpace(trialSiteSurveyQueryDTO.TrialSiteCode), t => t.TrialSite.TrialSiteAliasName.Contains(trialSiteSurveyQueryDTO.TrialSiteCode)) + //.WhereIf(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.SPM || _userInfo.UserTypeEnumInt == (int)UserTypeEnum.CPM, t => t.State >= TrialSiteSurveyEnum.ToSubmit) + //.WhereIf(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ProjectManager || _userInfo.UserTypeEnumInt == (int)UserTypeEnum.APM || _userInfo.UserTypeEnumInt == (int)UserTypeEnum.CPM, t => t.State >= TrialSiteSurveyEnum.SPMApproved) + .ProjectTo(_mapper.ConfigurationProvider); + + return await trialSiteSurveyQueryable.ToListAsync(); + } + + //[HttpPut("{trialId:guid}/{isReplaceUser:bool}")] + //public async Task CopyTrialSurveyInitInfo(CopyTrialSiteSurveyDTO copyDto, bool isReplaceUser) + //{ + // var survey = await _repository.Where(t => t.Id == copyDto.TrialSiteSurveyId).FirstOrDefaultAsync(); + + // if (survey == null) return Null404NotFound(survey); + + // survey.Id = Guid.Empty; + // if (isReplaceUser) + // { + // survey.UserName = copyDto.UserName; + // survey.Email = copyDto.Email; + // survey.Phone = copyDto.Phone; + // } + // else + // { + + // } + + // return ResponseOutput.Ok(); + //} + + + + /// + /// 初始登陆界面 项目基本信息+下拉框数据 + /// + /// + /// + [AllowAnonymous] + [HttpGet("{trialId:guid}")] + public async Task GetTrialSurveyInitInfo(Guid trialId) + { + var info = await _repository.Where(t => t.Id == trialId).ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync().IfNullThrowException(); + + return info; + } + + + + /// + /// 驳回 + /// + /// + /// + /// + [HttpPut("{trialId:guid}/{trialSiteSurveyId:guid}")] + public async Task SubmissionRejection(Guid trialId, Guid trialSiteSurveyId) + { + if (await _repository.AnyAsync(t => t.State == TrialSiteSurveyEnum.PMCreatedAndLock && t.Id == trialSiteSurveyId)) + { + return ResponseOutput.NotOk("已锁定,不允许操作"); + } + + if (_userInfo.UserTypeEnumInt == (int)UserTypeEnum.SPM || _userInfo.UserTypeEnumInt == (int)UserTypeEnum.CPM) + { + await _repository.UpdateFromQueryAsync(t => t.Id == trialSiteSurveyId, u => new TrialSiteSurvey() { State = TrialSiteSurveyEnum.ToSubmit }); + } + else if (_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ProjectManager || _userInfo.UserTypeEnumInt == (int)UserTypeEnum.APM) + { + var hasSPMOrCPM = await _trialSiteSurveyRepository.AnyAsync(t => t.TrialId == trialId && t.Trial.TrialUserList.Any(u => u.User.UserTypeEnum == UserTypeEnum.SPM || u.User.UserTypeEnum == UserTypeEnum.CPM)); + + if (hasSPMOrCPM) + { + await _trialSiteSurveyRepository.UpdateFromQueryAsync(t => t.Id == trialSiteSurveyId, u => new TrialSiteSurvey() { State = TrialSiteSurveyEnum.CRCSubmitted }); + + } + else + { + await _trialSiteSurveyRepository.UpdateFromQueryAsync(t => t.Id == trialSiteSurveyId, u => new TrialSiteSurvey() { State = TrialSiteSurveyEnum.ToSubmit }); + } + } + return ResponseOutput.Ok(); + + } + + + [HttpPut("{trialId:guid}/{trialSiteSurveyId:guid}")] + public async Task AbandonSiteSurvey(Guid trialSiteSurveyId) + { + var survey = (await _trialSiteSurveyRepository.FirstOrDefaultAsync(t => t.Id == trialSiteSurveyId)).IfNullThrowConvertException(); + + if (survey.State != TrialSiteSurveyEnum.ToSubmit) + { + return ResponseOutput.NotOk("只允许废除未提交的记录"); + } + + survey.IsAbandon = true; + + await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(); + + } + + + /// + /// 提交 后台自动识别是谁提交 + /// + /// + /// + //[TypeFilter(typeof(TrialResourceFilter))] + [HttpPost] + public async Task TrialSurveySubmit(TrialSiteSurvyeSubmitDTO siteSurvyeSubmit) + { + + var trialId= siteSurvyeSubmit.TrialId; + var trialSiteSurveyId = siteSurvyeSubmit.TrialSiteSurveyId; + + if (_userInfo.IsAdmin) + { + return ResponseOutput.NotOk("不允许Admin操作"); + } + + + if (_userInfo.UserTypeEnumInt == (int)UserTypeEnum.Undefined) + { + var hasSPMOrCPM = await _trialSiteSurveyRepository.AnyAsync(t => t.TrialId == trialId && t.Trial.TrialUserList.Any(u => u.User.UserTypeEnum == UserTypeEnum.SPM || u.User.UserTypeEnum == UserTypeEnum.CPM)); + + if (hasSPMOrCPM) + { + await _trialSiteSurveyRepository.UpdateFromQueryAsync(t => t.Id == trialSiteSurveyId && t.State == TrialSiteSurveyEnum.ToSubmit, u => new TrialSiteSurvey() { State = TrialSiteSurveyEnum.CRCSubmitted }); + + } + else + { + await _trialSiteSurveyRepository.UpdateFromQueryAsync(t => t.Id == trialSiteSurveyId && t.State == TrialSiteSurveyEnum.ToSubmit, u => new TrialSiteSurvey() { State = TrialSiteSurveyEnum.SPMApproved }); + + } + + } + else if (_userInfo.UserTypeEnumInt == (int)UserTypeEnum.SPM || _userInfo.UserTypeEnumInt == (int)UserTypeEnum.CPM) + { + if (_repository.Where(t => t.TrialSiteSurveyId == trialSiteSurveyId && t.IsCorrect == false).Any()) + { + return ResponseOutput.NotOk("人员信息有不正确项,不允许提交"); + } + + await _trialSiteSurveyRepository.UpdateFromQueryAsync(t => t.Id == trialSiteSurveyId && t.State == TrialSiteSurveyEnum.CRCSubmitted, u => new TrialSiteSurvey() { State = TrialSiteSurveyEnum.SPMApproved }); + + } + else if (_userInfo.UserTypeEnumInt == (int)UserTypeEnum.APM || _userInfo.UserTypeEnumInt == (int)UserTypeEnum.ProjectManager) + { + + var trialSiteSurvey = _trialSiteSurveyRepository.Where(t => t.Id == trialSiteSurveyId).FirstOrDefault(); + + if (trialSiteSurvey == null) return Null404NotFound(trialSiteSurvey); + + if (_trialSiteUserSurveyRepository.Where(t => t.TrialSiteSurveyId == trialSiteSurveyId && t.IsCorrect == false).Any()) + { + return ResponseOutput.NotOk("人员信息有不正确项,不允许提交"); + } + + //已生成的不管 管的只需要是 生成失败的并且需要生成账号的 + var needGenerateList = _trialSiteUserSurveyRepository.Where(t => t.TrialSiteSurveyId == trialSiteSurveyId && t.IsCorrect && t.IsGenerateAccount && t.IsGenerateSuccess == false).ToList(); + + + var trialInfo = await _repository.FirstOrDefaultAsync(t => t.Id == trialId); + + foreach (var item in needGenerateList) + { + + var messageToSend = new MimeMessage(); + //发件地址 + messageToSend.From.Add(new MailboxAddress("GRR", "iracis_grr@163.com")); + //收件地址 + messageToSend.To.Add(new MailboxAddress(String.Empty, item.Email)); + //主题 + messageToSend.Subject = "GRR Site survey (Trial Notice)"; + + var builder = new BodyBuilder(); + + //找下系统中是否存在该用户类型的 并且邮箱 或者手机的账户 + var sysUserInfo = await _userRepository.Where(t => t.UserTypeId == item.UserTypeId && t.EMail == item.Email ).Include(t=>t.UserTypeRole).FirstOrDefaultAsync(); + + int verificationCode = new Random().Next(100000, 1000000); + + //var baseApiUrl = baseUrl.Remove(baseUrl.IndexOf("#")) + "api"; + + + if (sysUserInfo==null) + { + + lock (lockObj) + { + var saveItem = _mapper.Map(item); + + saveItem.Code = _userRepository.Select(t => t.Code).DefaultIfEmpty().Max() + 1; + + saveItem.UserCode = AppSettings.UserCodePrefix + saveItem.Code.ToString("D4"); + + saveItem.UserName = saveItem.UserCode; + + saveItem.UserTypeEnum = _repository.Where(t => t.Id == saveItem.UserTypeId).Select(t => t.UserTypeEnum).First(); + + saveItem.Password = MD5Helper.Md5(verificationCode.ToString()); + + _ = _repository.AddAsync(saveItem).Result; + + _ = _repository.SaveChangesAsync().Result; + + + sysUserInfo = saveItem; + } + + } + + if (sysUserInfo.IsFirstAdd) + { + await _userRepository.UpdateFromQueryAsync(t => t.Id == sysUserInfo.Id, + u => new User() { Password = MD5Helper.Md5(verificationCode.ToString()) }); + } + + + builder.HtmlBody = @$" +
+
+
+ {sysUserInfo.LastName + "/" + sysUserInfo.FirstName}: +
+
+ 您参与的临床试验项目 {trialInfo.ExperimentName} ,独立影像评估相关工作将在网上进行。项目及账号信息为: +
+
+
+ 项目编号: {trialInfo.TrialCode} +
+
+ 试验方案号: {trialInfo.ResearchProgramNo} +
+
+ 试验名称: {trialInfo.ExperimentName} +
+
+ 用户名: {sysUserInfo.UserName} +
+
+ 密码: {(sysUserInfo.IsFirstAdd ? verificationCode.ToString() + "(请在登录后进行修改)" : "***(您已有账号, 若忘记密码, 请通过邮箱找回)")} +
+
+ 角色: {sysUserInfo.UserTypeRole.UserTypeShortName} +
+
+ 系统登录地址: {siteSurvyeSubmit.LoginUrl} (请确认加入后再登陆) +
+
+ + 查看并确认 + +
+
+ "; + + + messageToSend.Body = builder.ToMessageBody(); + + using (var smtp = new MailKit.Net.Smtp.SmtpClient()) + { + + smtp.ServerCertificateValidationCallback = (s, c, h, e) => true; + + smtp.MessageSent += (sender, args) => + { + + _= _trialSiteUserSurveyRepository.UpdateFromQueryAsync(t => t.Id == item.Id, u => new TrialSiteUserSurvey() { IsGenerateSuccess = true, SystemUserId = sysUserInfo.Id , ExpireTime = DateTime.Now.AddDays(7) }).Result; + + }; + + + await smtp.ConnectAsync("smtp.163.com", 25, SecureSocketOptions.StartTls); + + + await smtp.AuthenticateAsync("iracis_grr@163.com", "XLWVQKZAEKLDWOAH"); + + + await smtp.SendAsync(messageToSend); + + + await smtp.DisconnectAsync(true); + } + + } + + await _trialSiteSurveyRepository.UpdateFromQueryAsync(t => t.Id == trialSiteSurveyId && t.State == TrialSiteSurveyEnum.SPMApproved, u => new TrialSiteSurvey() { State = TrialSiteSurveyEnum.PMCreatedAndLock }); + + } + + return ResponseOutput.Ok(); + } + + + + ///// + ///// 锁定 + ///// + ///// + ///// + ///// + //[TypeFilter(typeof(TrialResourceFilter))] + //[HttpPost("{trialSiteSurveyId:guid}/{trialId:guid}/{isLock:bool}")] + //public async Task TrialSurveyLock(Guid trialSiteSurveyId, bool isLock) + //{ + // if (await _repository.Where(t => t.Id == trialSiteSurveyId).AnyAsync(t => t.TrialSiteUserSurveyList.Any(k => k.IsGenerateAccount && k.IsGenerateSuccess == false))) + // { + // ResponseOutput.NotOk("有用户账户没生成,不允许锁定"); + // } + + // await _repository.UpdateFromQueryAsync(t => t.Id == trialSiteSurveyId, k => new TrialSiteSurvey() { State==TrialSiteSurveyEnum.PMCreatedAndLock }); + + // return ResponseOutput.Ok(); + //} + } +} diff --git a/IRaCIS.Core.Application/Service/SiteSurvey/TrialSiteUserSurveyService.cs b/IRaCIS.Core.Application/Service/SiteSurvey/TrialSiteUserSurveyService.cs new file mode 100644 index 00000000..726f35bf --- /dev/null +++ b/IRaCIS.Core.Application/Service/SiteSurvey/TrialSiteUserSurveyService.cs @@ -0,0 +1,95 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-23 13:20:59 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Infrastructure; + +namespace IRaCIS.Core.Application.Contracts +{ + /// + /// TrialSiteUserSurveyService + /// + [ApiExplorerSettings(GroupName = "Trial")] + public class TrialSiteUserSurveyService : BaseService, ITrialSiteUserSurveyService + { + private readonly IRepository _trialSiteUserSurveyRepository; + + public TrialSiteUserSurveyService(IRepository trialSiteUserSurveyRepository) + { + _trialSiteUserSurveyRepository = trialSiteUserSurveyRepository; + } + + [HttpGet("{trialSiteSurveyId:guid}")] + public async Task> GetTrialSiteUserSurveyList(Guid trialSiteSurveyId) + { + + var trialSiteUserSurveyQueryable = _trialSiteUserSurveyRepository.Where(t => t.TrialSiteSurveyId == trialSiteSurveyId) + //.WhereIf(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.SPM, t => t.TrialSiteSurvey.State >= TrialSiteSurveyEnum.CRCSubmitted) + //.WhereIf(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ProjectManager || _userInfo.UserTypeEnumInt == (int)UserTypeEnum.APM|| _userInfo.UserTypeEnumInt == (int)UserTypeEnum.CPM, t => t.TrialSiteSurvey.State >= TrialSiteSurveyEnum.SPMApproved) + .ProjectTo(_mapper.ConfigurationProvider); + + return await trialSiteUserSurveyQueryable.ToListAsync(); + } + + [TypeFilter(typeof(TrialResourceFilter))] + [HttpPost("{trialId:guid}")] + public async Task AddOrUpdateTrialSiteUserSurvey(TrialSiteUserSurveyAddOrEdit addOrEditTrialSiteUserSurvey) + { + + + if (await _trialSiteUserSurveyRepository.Where(t => t.Id == addOrEditTrialSiteUserSurvey.Id).AnyAsync(t => t.TrialSiteSurvey.State == TrialSiteSurveyEnum.PMCreatedAndLock)) + { + return ResponseOutput.NotOk("已锁定,不允许操作"); + } + + if (addOrEditTrialSiteUserSurvey.UserTypeId != null && ( _userInfo.UserTypeEnumInt == (int)UserTypeEnum.ProjectManager || _userInfo.UserTypeEnumInt == (int)UserTypeEnum.APM)) + { + var existSysUser = await _repository.FirstOrDefaultAsync(t => t.EMail == addOrEditTrialSiteUserSurvey.Email && t.UserTypeId == addOrEditTrialSiteUserSurvey.UserTypeId); + + + if (existSysUser != null) + { + if (existSysUser.LastName != addOrEditTrialSiteUserSurvey.LastName || existSysUser.FirstName != addOrEditTrialSiteUserSurvey.FirstName) + { + return ResponseOutput.NotOk($"该用户在系统中账户名为:{existSysUser.LastName + " / " + existSysUser.FirstName} ,与填写信息存在不一致项, 现将界面信息修改为与系统一致,可进行保存", + new { existSysUser.LastName, existSysUser.FirstName, existSysUser.Phone }, ApiResponseCodeEnum.NeedTips); + } + + } + + } + + var entity = await _trialSiteUserSurveyRepository.InsertOrUpdateAsync(addOrEditTrialSiteUserSurvey, true); + + return ResponseOutput.Ok(entity.Id.ToString()); + + } + + [TypeFilter(typeof(TrialResourceFilter))] + [HttpDelete("{trialSiteUserSurveyId:guid}/{trialId:guid}")] + public async Task DeleteTrialSiteUserSurvey(Guid trialSiteUserSurveyId) + { + + + if (await _trialSiteUserSurveyRepository.Where(t => t.Id == trialSiteUserSurveyId).AnyAsync(t => t.TrialSiteSurvey.State == TrialSiteSurveyEnum.PMCreatedAndLock)) + { + return ResponseOutput.NotOk("已锁定,不允许操作"); + } + + var success = await _trialSiteUserSurveyRepository.DeleteFromQueryAsync(t => t.Id == trialSiteUserSurveyId); + + return ResponseOutput.Result(success); + } + + + + + + + } +} diff --git a/IRaCIS.Core.Application/Service/SiteSurvey/_MapConfig.cs b/IRaCIS.Core.Application/Service/SiteSurvey/_MapConfig.cs new file mode 100644 index 00000000..0c1ea76f --- /dev/null +++ b/IRaCIS.Core.Application/Service/SiteSurvey/_MapConfig.cs @@ -0,0 +1,61 @@ +using AutoMapper; +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Domain.Share; + +namespace IRaCIS.Core.Application.AutoMapper +{ + /// + /// 映射配置 + /// + public partial class SiteSurveyConfig : Profile + { + public SiteSurveyConfig() + { + //编辑 + CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); + + CreateMap().ForMember(d => d.Email, t => t.MapFrom(t => t.EmailOrPhone)); + + + //列表 + CreateMap() + .ForMember(t=>t.EquipmentType,u=>u.MapFrom(d=>d.EquipmentType.Value)); + + + + CreateMap() + .ForMember(d => d.TrialSiteAliasName, u => u.MapFrom(s => s.TrialSite.TrialSiteAliasName)) + .ForMember(d => d.SiteName, u => u.MapFrom(s => s.Site.SiteName)) + .ForMember(d => d.TrialSiteCode, u => u.MapFrom(s => s.TrialSite.TrialSiteCode)); + + CreateMap() + .ForMember(t => t.TrialRoleName, u => u.MapFrom(d => d.TrialRoleName.Value)) + .ForMember(d => d.UserType, u => u.MapFrom(s => s.UserTypeRole.UserTypeShortName)); + + + CreateMap() + .ForMember(d => d.Sponsor, u => u.MapFrom(s => s.Sponsor.SponsorName)) + .ForMember(d => d.IndicationType, u => u.MapFrom(s => s.IndicationType.Value)) + .ForMember(d => d.TrialSiteSelectList, u => u.MapFrom(s => s.TrialSiteList)) + .ForMember(d => d.TrialId, u => u.MapFrom(s => s.Id)); + + CreateMap(); + + CreateMap() + .ForMember(d => d.TrialSiteSurvey, u => u.MapFrom(s => s)) + .ForMember(d => d.TrialInfo, u => u.MapFrom(s => s.Trial)) + .ForMember(d => d.TrialSiteUserSurveyList, u => u.MapFrom(s => s.TrialSiteUserSurveyList)); + + + + + CreateMap(); + + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Stat/DTO/StatisticsViewModel.cs b/IRaCIS.Core.Application/Service/Stat/DTO/StatisticsViewModel.cs new file mode 100644 index 00000000..8ad98e37 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Stat/DTO/StatisticsViewModel.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Contracts +{ + + public class EnrollStatByReviewerDTO + { + public Guid Id { get; set; } + public string Hospital { get; set; } = string.Empty; + public string ChineseName { get; set; } = string.Empty; + + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public string ReviewerCode { get; set; } = string.Empty; + + public int Pending { get; set; } + public int Approved { get; set; } + public int Reading { get; set; } + public int Finished { get; set; } + public int Total { get; set; } + public double EntryRate + { + get + { + if (Total == 0) + { + return 0; + } + return Math.Round(((Reading + Finished) / (double)Total) * 100, 1, MidpointRounding.AwayFromZero); + ; + } + } + + } + + + public class EnrollStatByTrialDTO + { + public string TrialCode { get; set; } = string.Empty; + public Guid TrialId { get; set; } + public string Indication { get; set; } = string.Empty; + //public Guid CroId { get; set; } + public string Cro { get; set; } = string.Empty; + + public DateTime CreateTime { get; set; } + + public int Expedited { get; set; } + + public int EnrollCount { get; set; } + + public List ReviewerNameCNList=new List(); + + public List ReviewerNameList=new List(); + } + + + #region 参与项目统计 + + + public class ParticipateQueryDto:PageInput + { + public string UserInfo { get; set; } = string.Empty; + public string OrganizationName { get; set; } = string.Empty; + + } + + public class UserParticipateTrialStat + { + public Guid UserId { get; set; } + public string OrganizationName { get; set; } = string.Empty; + + public string UserCode { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + + public string UserName { get; set; } = string.Empty; + + public string Phone { get; set; } = string.Empty; + + public string Email { get; set; } = string.Empty; + + public int TrialCount { get; set; } + + } + + public class UserParticipateTrialDetail + { + public Guid TrialId { get; set; } + public string Code { get; set; } = string.Empty; + public string Indication { get; set; } = string.Empty; + + public int Expedited { get; set; } + + public string CROName { get; set; } = string.Empty; + public string UserType { get; set; } = string.Empty; + } + + #endregion + + + + #region 统计 20200413 + public class WorkloadByTrialAndReviewerDTO + { + public Guid Id { get; set; } + public string TrialCode { get; set; } = string.Empty; + public Guid TrialId { get; set; } + public string Indication { get; set; } = string.Empty; + public Guid? CroId { get; set; } + public string Cro { get; set; } = string.Empty; + public string ChineseName { get; set; } = string.Empty; + + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public string ReviewerCode { get; set; } = string.Empty; + + public int Training { get; set; } + public int Downtime { get; set; } + public int Timepoint { get; set; } + public int TimepointIn24H { get; set; } + public int TimepointIn48H { get; set; } + public int Adjudication { get; set; } + public int AdjudicationIn24H { get; set; } + public int AdjudicationIn48H { get; set; } + public int Global { get; set; } + + public int RefresherTraining { get; set; } + + public int PersonalTotal { get; set; } + } + + + public class StatisticsQueryDTO : PageInput + { + public Guid? CroId { get; set; } = Guid.Empty; + public string TrialCode { get; set; } = string.Empty; + public string Reviewer { get; set; } = string.Empty; + public DateTime BeginDate { get; set; } = DateTime.Now; + public DateTime EndDate { get; set; } = DateTime.Now; + public int StatType { get; set; } + + //医生付费类型 CN US + public int? Nation { get; set; } + public int? AttendedReviewerType { get; set; } + } + + public class StatisticsWorkloadQueryParam : PageInput + { + public Guid? CroId { get; set; } + public string TrialCode { get; set; } = string.Empty; + public string Reviewer { get; set; } = string.Empty; + public DateTime BeginDate { get; set; } = DateTime.Now; + public DateTime EndDate { get; set; } = DateTime.Now; + public int StatType { get; set; } + + public Guid? HospitalId { get; set; } + } + + public class RevenuesStatQueryDTO : PageInput + { + public Guid CroId { get; set; } = Guid.Empty; + + public string Reviewer { get; set; } = string.Empty; + public string TrialCode { get; set; } = string.Empty; + public DateTime BeginDate { get; set; } = DateTime.Now; + public DateTime EndDate { get; set; } = DateTime.Now; + + public int StatType { get; set; } + } + + + public class EnrollStatByReviewerQueryDTO : PageInput + { + public Guid? HospitalId { get; set; } = Guid.Empty; + public string Reviewer { get; set; } = string.Empty; + public DateTime BeginDate { get; set; } = DateTime.Now; + public DateTime EndDate { get; set; } = DateTime.Now; + } + + + public class EnrollStatByTrialQueryDTO : PageInput + { + public string TrialCode { get; set; } = string.Empty; + + public string Indication { get; set; } = string.Empty; + public DateTime? BeginDate { get; set; } = DateTime.Now; + public DateTime? EndDate { get; set; } = DateTime.Now; + + public Guid? CROId { get; set; } + + public int? Expedited { get; set; } + } + #endregion + + #region dashbord + + /// + /// 读片数量分类统计 + /// + public class ReadingDataDTO + { + public int Timepoint { get; set; } + public int Adjudication { get; set; } + public int Global { get; set; } + } + + public class ReadingDataMonthDTO : ReadingDataDTO + { + public string Month { get; set; } = String.Empty; + } + + public class ReadingDataRankDTO : ReadingDataDTO + { + public int TotalReadingCount { get; set; } + public Guid ReviewerId { get; set; } + public string ReviewerCode { get; set; } = String.Empty; + public string FirstName { get; set; } = String.Empty; + public string LastName { get; set; } = String.Empty; + public string ChineseName { get; set; } = String.Empty; + } + + public class RankReviewersDTO + { + public Guid? RankId { get; set; } + public string RankName { get; set; } = String.Empty; + public string RankNameAbbreviation + { + get + { + var arr = RankName.Split(' '); + return arr[0]; + } + } + public int ReviewerCount { get; set; } + } + + public class EnrollDataDTO + { + public int Year { get; set; } + public int Month { get; set; } + public string QuarterStr => Year + "-Q" + (Month / 3 + (Month % 3 == 0 ? 0 : 1)); + public string YearMonth => new DateTime(Year, Month, 1).ToString("yyyy-MM"); + public int EnrollCount { get; set; } + } + + + public class EnrollQuartDataDTO + { + //public int Year { get; set; } + + public string ViewStr { get; set; } = String.Empty; + + public int EnrollCount { get; set; } + } + + public class LatestWorkLoadDTO : ReadingDataDTO + { + public int TotalReadingCount => Timepoint + Adjudication + Global; + public Guid ReviewerId { get; set; } + public string ReviewerCode { get; set; } = String.Empty; + public string FirstName { get; set; } = String.Empty; + public string LastName { get; set; } = String.Empty; + public string ChineseName { get; set; } = String.Empty; + public string TrialCode { get; set; } = String.Empty; + } + + + + + public class TrialDataRankDTO + { + public Guid ReviewerId { get; set; } + public string ReviewerCode { get; set; } = String.Empty; + public string FirstName { get; set; } = String.Empty; + public string LastName { get; set; } = String.Empty; + public string ChineseName { get; set; } = String.Empty; + + public int TrialCount { get; set; } + } + + #endregion + + + + +} diff --git a/IRaCIS.Core.Application/Service/Stat/IStatisticsService.cs b/IRaCIS.Core.Application/Service/Stat/IStatisticsService.cs new file mode 100644 index 00000000..b5a80c5e --- /dev/null +++ b/IRaCIS.Core.Application/Service/Stat/IStatisticsService.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface IStatisticsService + { + PageOutput GetWorkloadByTrialAndReviewer(StatisticsWorkloadQueryParam param); + + PageOutput GetEnrollStatByTrial(EnrollStatByTrialQueryDTO param); + + PageOutput GetParticipateTrialStat(ParticipateQueryDto param); + + List GetParticipateTrialList(Guid userId); + + #region Dashboard 数据统计 + /// 按类型统计读片数量 + ReadingDataDTO GetReadingDataByType(); + + /// 按月份统计读片数量 + List GetReadingDataByMonth(int monthCount); + + /// 读片数量排行 + List GetReadingDataRank(int topCount); + + /// 按Position统计 Reviewers 数量 + List GetReviewersByRank(); + + /// 每月入组人次 + List GetEnrollDataByQuarter(int quarterCount, int monthCount); + + /// 参与项目数排行 + List GetTrialCountRank(int topCount); + + /// 最新工作量 (已确定的) + List GetLatestWorkLoadList( int searchCount); + #endregion + + PageOutput GetEnrollStatByReviewer(EnrollStatByReviewerQueryDTO enrollTrialStatisticsQueryParam); + } +} diff --git a/IRaCIS.Core.Application/Service/Stat/StatisticsService.cs b/IRaCIS.Core.Application/Service/Stat/StatisticsService.cs new file mode 100644 index 00000000..21b97e3e --- /dev/null +++ b/IRaCIS.Core.Application/Service/Stat/StatisticsService.cs @@ -0,0 +1,853 @@ +using IRaCIS.Core.Infrastructure.ExpressionExtend; +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Domain.Share; +using System.Linq.Expressions; + +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Application.Services +{ /// + /// Dashboard统计、全局工作量统计、入组两个维度统计(按照项目、按照人) + /// + + [ ApiExplorerSettings(GroupName = "Dashboard&Statistics")] + public class StatisticsService : BaseService, IStatisticsService + { + private readonly IRepository _doctorRepository; + private readonly IRepository _trialRepository; + private readonly IRepository _enrollRepository; + private readonly IRepository _workloadRepository; + private readonly IRepository _croCompanyRepository; + private readonly IRepository _dictionaryRepository; + private readonly IRepository _hospitalRepository; + private readonly IRepository _enrollDetailRepository; + private readonly IRepository _userRepository; + private readonly IRepository _userTrialRepository; + + + public StatisticsService(IRepository doctorRepository, IRepository trialRepository, + IRepository intoGroupRepository, IRepository workloadRepository, + IRepository croCompanyRepository, + IRepository dictionaryRepository, + IRepository hospitalRepository, + IRepository enrollDetailRepository, IRepository userRepository, + IRepository userTrialRepository) + { + _doctorRepository = doctorRepository; + _trialRepository = trialRepository; + _enrollRepository = intoGroupRepository; + _workloadRepository = workloadRepository; + _croCompanyRepository = croCompanyRepository; + _dictionaryRepository = dictionaryRepository; + _hospitalRepository = hospitalRepository; + _enrollDetailRepository = enrollDetailRepository; + _userRepository = userRepository; + _userTrialRepository = userTrialRepository; + + } + + /// 根据项目和医生,分页获取工作量统计[New] + [HttpPost] + public PageOutput GetWorkloadByTrialAndReviewer( + StatisticsWorkloadQueryParam param) + { + + + + var bDate = new DateTime(param.BeginDate.Year, param.BeginDate.Month, param.BeginDate.Day); + var eDate = new DateTime(param.EndDate.Year, param.EndDate.Month, param.EndDate.Day); + eDate = eDate.AddDays(1); + Expression> workloadLambda = x => x.DataFrom == (int)WorkLoadFromStatus.FinalConfirm; + workloadLambda = workloadLambda.And(x => x.WorkTime >= bDate && x.WorkTime < eDate); + + Expression> trialLambda = x => true; + if (_userInfo.UserTypeEnumInt ==(int) UserTypeEnum.SuperAdmin) //超级管理员按照条件查询所有 + { + if ( param.CroId != null) + { + trialLambda = trialLambda.And(u => u.CROId == param.CroId); + } + } + + else //不管是精鼎的pm还是我们的pm 还是运维人员 只能看到自己参与项目的统计 + { + List trialIdList = _userTrialRepository.Where(u => u.UserId == _userInfo.Id).Select(u => u.TrialId).ToList(); + trialLambda = trialLambda.And(u => trialIdList.Contains(u.Id)); + } + if (!string.IsNullOrWhiteSpace(param.TrialCode)) + { + trialLambda = trialLambda.And(u => u.TrialCode.Contains(param.TrialCode)); + } + + Expression> doctorLambda = x => true; + if (!string.IsNullOrWhiteSpace(param.Reviewer)) + { + + var reviewer = param.Reviewer.Trim(); + doctorLambda = doctorLambda.And(u => u.ChineseName.Contains(reviewer) + || u.FirstName.Contains(reviewer) + || u.LastName.Contains(reviewer) + || u.ReviewerCode.Contains(reviewer) + ); + + } + if (Guid.Empty != param.HospitalId && param.HospitalId != null) + { + doctorLambda = doctorLambda.And(u => u.HospitalId == param.HospitalId); + } + + var workloadQuery = from workLoad in _workloadRepository.Where(workloadLambda) + join trial in _trialRepository.Where(trialLambda) on workLoad.TrialId equals trial.Id + join doctor in _doctorRepository.Where(doctorLambda) on workLoad.DoctorId equals doctor.Id + group workLoad by new { workLoad.DoctorId, workLoad.TrialId } into gWorkLoad + + select new + { + DoctorId = gWorkLoad.Key.DoctorId, + TrialId = gWorkLoad.Key.TrialId, + Downtime = gWorkLoad.Sum(t => t.Downtime), + Training = gWorkLoad.Sum(t => t.Training), + Timepoint = gWorkLoad.Sum(t => t.Timepoint), + TimepointIn24H = gWorkLoad.Sum(t => t.TimepointIn24H), + TimepointIn48H = gWorkLoad.Sum(t => t.TimepointIn48H), + Global = gWorkLoad.Sum(t => t.Global), + Adjudication = gWorkLoad.Sum(t => t.Adjudication), + AdjudicationIn24H = gWorkLoad.Sum(t => t.AdjudicationIn24H), + AdjudicationIn48H = gWorkLoad.Sum(t => t.AdjudicationIn48H), + RefresherTraining = gWorkLoad.Sum(t => t.RefresherTraining), + + }; + var query = from w in workloadQuery + join trial in _trialRepository.Where(trialLambda) on w.TrialId equals trial.Id into t + from trialItem in t.DefaultIfEmpty() + join doctor in _doctorRepository.Where(doctorLambda) on w.DoctorId equals doctor.Id into tt + from doctorItem in tt.DefaultIfEmpty() + join cro in _croCompanyRepository.AsQueryable() on trialItem.CROId equals cro.Id into ttt + from croItem in ttt.DefaultIfEmpty() + select new WorkloadByTrialAndReviewerDTO + { + Id = Guid.NewGuid(), + TrialId = trialItem.Id, + Indication = trialItem.Indication, + TrialCode = trialItem.TrialCode, + CroId = trialItem.CROId, + Cro = croItem.CROName, + ReviewerCode = doctorItem.ReviewerCode, + ChineseName = doctorItem.ChineseName, + FirstName = doctorItem.FirstName, + LastName = doctorItem.LastName, + Training = w.Training, + Timepoint = w.Timepoint, + TimepointIn24H = w.TimepointIn24H, + TimepointIn48H = w.TimepointIn48H, + Adjudication = w.Adjudication, + AdjudicationIn24H = w.AdjudicationIn24H, + AdjudicationIn48H = w.AdjudicationIn48H, + Global = w.Global, + Downtime = w.Downtime, + RefresherTraining = w.RefresherTraining, + + PersonalTotal = w.Timepoint + w.TimepointIn24H + w.TimepointIn48H + w.Adjudication + w.AdjudicationIn24H + + w.AdjudicationIn48H + w.Global + }; + + var propName = param.SortField == "" ? "ReviewerCode" : param.SortField; + query = param.Asc + ? query.OrderBy(propName).ThenBy(t => t.TrialCode).ThenBy(u => u.ReviewerCode) + : query.OrderByDescending(propName).ThenBy(t => t.TrialCode).ThenBy(u => u.ReviewerCode); + if (propName == "FirstName" || propName == "LastName") + { + query = param.Asc + ? query.OrderBy(t => t.LastName).ThenBy(t => t.FirstName) + : query.OrderByDescending(t => t.LastName).ThenBy(t => t.FirstName); + } + var count = query.Count(); + + query = query + .Skip((param.PageIndex - 1) * param.PageSize) + .Take(param.PageSize); + var doctorViewList = query.ToList(); + + return new PageOutput(param.PageIndex, param.PageSize, count, doctorViewList); + } + + + + /// 项目入组 医生维度统计[New] + [HttpPost] + public PageOutput GetEnrollStatByReviewer(EnrollStatByReviewerQueryDTO param) + { + var bDate = new DateTime(param.BeginDate.Year, param.BeginDate.Month, 1); + var eDate = new DateTime(param.EndDate.Year, param.EndDate.Month, 1); + eDate = eDate.AddMonths(1); + + Expression> enrollLambda = x => true; + enrollLambda = enrollLambda.And(x => x.EnrollTime >= bDate && x.EnrollTime < eDate); + + Expression> hospitalLambda = x => true; + if (Guid.Empty != param.HospitalId && param.HospitalId != null) + { + hospitalLambda = hospitalLambda.And(u => u.Id == param.HospitalId); + } + + Expression> doctorLambda = x => true; + if (!string.IsNullOrWhiteSpace(param.Reviewer)) + { + + var reviewer = param.Reviewer.Trim(); + doctorLambda = doctorLambda.And(u => u.ChineseName.Contains(reviewer) + || u.FirstName.Contains(reviewer) + || u.LastName.Contains(reviewer) + || u.ReviewerCode.Contains(reviewer) + ); + } + + var enrollQueryable = + from enroll in _enrollRepository.Where(enrollLambda) + group enroll by new { enroll.DoctorId } + into g + select new + { + DoctorId = g.Key.DoctorId, + Submitted = g.Sum(t => + t.EnrollStatus == (int)EnrollStatus.HasCommittedToCRO ? 1 : 0), + Approved = g.Sum(t => + t.EnrollStatus == (int)EnrollStatus.InviteIntoGroup ? 1 : 0), + Reading = g.Sum(t => + t.EnrollStatus == (int)EnrollStatus.DoctorReading ? 1 : 0), + Finished = g.Sum(t => + t.EnrollStatus >= (int)EnrollStatus.Finished ? 1 : 0), + }; + + + var query = from w in enrollQueryable + + join doctor in _doctorRepository.Where(doctorLambda) on w.DoctorId equals doctor.Id + join hospital in _hospitalRepository.Where(hospitalLambda) on doctor.HospitalId equals hospital.Id + select new EnrollStatByReviewerDTO + { + Id = Guid.NewGuid(), + Hospital = hospital.HospitalName, + ReviewerCode = doctor.ReviewerCode, + ChineseName = doctor.ChineseName, + FirstName = doctor.FirstName, + LastName = doctor.LastName, + Pending = w.Submitted, + Approved = w.Approved, + Reading = w.Reading, + Finished = w.Finished, + + Total = w.Submitted + w.Approved + w.Reading + w.Finished + }; + + if (param.SortField != "EntryRate") + { + var propName = param.SortField == "" ? "ReviewerCode" : param.SortField; + query = param.Asc + ? query.OrderBy(propName).ThenBy(u => u.ReviewerCode) + : query.OrderByDescending(propName).ThenBy(u => u.ReviewerCode); + if (propName == "FirstName" || propName == "LastName") + { + query = param.Asc + ? query.OrderBy(t => t.LastName).ThenBy(t => t.FirstName) + : query.OrderByDescending(t => t.LastName).ThenBy(t => t.FirstName); + } + } + + + var count = query.Count(); + query = query + .Skip((param.PageIndex - 1) * param.PageSize) + .Take(param.PageSize); + var list = query.ToList(); + + if (param.SortField == "EntryRate") + { + list = param.Asc + ? list.OrderBy(t => t.EntryRate).ToList() + : list.OrderByDescending(t => t.EntryRate).ToList(); + } + + return new PageOutput(param.PageIndex, param.PageSize, count, list); + } + + [HttpPost] + public PageOutput GetEnrollStatByTrial(EnrollStatByTrialQueryDTO param) + { + + #region 筛选条件 + + Expression> trialLambda = x => true; + + if (param.Expedited != null) + { + trialLambda = trialLambda.And(o => o.Expedited == param.Expedited); + } + + if (!string.IsNullOrEmpty(param.TrialCode)) + { + var code = param.TrialCode.Trim(); + trialLambda = trialLambda.And(o => o.TrialCode.Contains(code)); + } + + if (!string.IsNullOrWhiteSpace(param.Indication)) + { + var indication = param.Indication.Trim(); + trialLambda = trialLambda.And(o => o.Indication.Contains(indication)); + } + + if ( param.CROId != null) + { + trialLambda = trialLambda.And(o => o.CROId == param.CROId); + } + + + if (param.BeginDate != null && param.EndDate != null) + { + var bDate = new DateTime(param.BeginDate.Value.Year, param.BeginDate.Value.Month, param.BeginDate.Value.Day); + var eDate = new DateTime(param.EndDate.Value.Year, param.EndDate.Value.Month, param.BeginDate.Value.Day); + eDate = eDate.AddMonths(1); + + trialLambda = trialLambda.And(o => o.CreateTime >= bDate && o.CreateTime < eDate); + + } + + #endregion + + var trialEnrollStatQuery = _enrollRepository.AsQueryable() + .Where(t => t.EnrollStatus >= (int)EnrollStatus.ConfirmIntoGroup).GroupBy(t => t.TrialId).Select(g => new + { + TrialId = g.Key, + EnrollCount = g.Count() + }); + + var trialQuery = from trial in _trialRepository.Where(trialLambda) + join cro in _croCompanyRepository.AsQueryable() on trial.CROId equals cro.Id into t + from cro in t.DefaultIfEmpty() + + join trialEnroll in trialEnrollStatQuery on trial.Id equals trialEnroll.TrialId + select new EnrollStatByTrialDTO + { + TrialId = trial.Id, + Cro = cro != null ? cro.CROName : "", + Expedited = trial.Expedited, + Indication = trial.Indication, + TrialCode = trial.TrialCode, + EnrollCount = trialEnroll.EnrollCount, + CreateTime = trial.CreateTime + }; + + var propName = param.SortField == "" ? "TrialCode" : param.SortField; + trialQuery = param.Asc + ? trialQuery.OrderBy(propName) + : trialQuery.OrderByDescending(propName); + + var count = trialQuery.Count(); + + trialQuery = trialQuery + .Skip((param.PageIndex - 1) * param.PageSize) + .Take(param.PageSize); + + var trialList = trialQuery.ToList(); + + var trialIds = trialList.Select(t => t.TrialId).ToList(); + + var enrollReviewerQuery = from enroll in _enrollRepository.AsQueryable() + .Where(t => t.EnrollStatus >= (int)EnrollStatus.ConfirmIntoGroup && trialIds.Contains(t.TrialId)) + join reviewer in _doctorRepository.AsQueryable() on enroll.DoctorId equals reviewer.Id + + select new + { + TrialId = enroll.TrialId, + NameCN = reviewer.ChineseName, + Name = reviewer.LastName + " / " + reviewer.FirstName + }; + var enrollReviewerByTrialList = enrollReviewerQuery.ToList().GroupBy(t => t.TrialId).Select(g => new + { + TrialId = g.Key, + ReviewerNameCNList = g.Select(t => t.NameCN).ToList(), + ReviewerNameList = g.Select(t => t.Name).ToList() + }).ToList(); + + + trialList.ForEach(t => + { + var trialDoctors = enrollReviewerByTrialList.FirstOrDefault(u => u.TrialId == t.TrialId); + + if (trialDoctors != null) + { + t.ReviewerNameCNList = trialDoctors.ReviewerNameCNList; + t.ReviewerNameList = trialDoctors.ReviewerNameList; + } + }); + + + return new PageOutput(param.PageIndex, param.PageSize, count, trialList); + + } + + /// 用户参与项目 统计[New] + [HttpPost] + public PageOutput GetParticipateTrialStat(ParticipateQueryDto param) + { + + Expression> userLambda = x => true; + Expression> userTrialLambda = x =>true; + if (!string.IsNullOrEmpty(param.UserInfo)) + { + var userInfo = param.UserInfo.Trim(); + userLambda = userLambda.And(o => o.UserCode.Contains(userInfo) || (o.LastName + ' ' + o.FirstName).Contains(userInfo) || o.UserName.Contains(userInfo)); + } + + if (!string.IsNullOrWhiteSpace(param.OrganizationName)) + { + var organizationName = param.OrganizationName.Trim(); + userLambda = userLambda.And(o => o.OrganizationName.Contains(organizationName)); + } + + var userTypeEnumStr = _userInfo.UserTypeEnumStr; + var userId = _userInfo.Id; + + + //PM 进来只能看到他负责的项目下的参与人员列表 + if (userTypeEnumStr == UserTypeEnum.ProjectManager.ToString()) + { + //参与到的项目 + var trialFilter = _userTrialRepository.Where(t => t.UserId == userId) + .Select(u => u.TrialId); + userTrialLambda = userTrialLambda.And(t => trialFilter.Contains(t.TrialId)); + } + //.Select(u => new { u.TrialId, u.UserId }).Distinct() + var trialStatQuery = _userTrialRepository.Where(userTrialLambda) + .GroupBy(t => t.UserId).Select( + g => new + { + UserId = g.Key, + TrialCount = g.Count() + }); + + var userQuery = from trialStat in trialStatQuery + join user in _userRepository.Where(userLambda) on trialStat.UserId equals user.Id + select new UserParticipateTrialStat + { + Email = user.EMail, + Name = user.LastName + ' ' + user.FirstName, + UserName = user.UserName, + OrganizationName = user.OrganizationName, + Phone = user.Phone, + TrialCount = trialStat.TrialCount, + UserCode = user.UserCode, + UserId = user.Id + }; + + var propName = param.SortField == "" ? "OrganizationName" : param.SortField; + userQuery = param.Asc + ? userQuery.OrderBy(propName) + : userQuery.OrderByDescending(propName); + + var count = userQuery.Count(); + + userQuery = userQuery + .Skip((param.PageIndex - 1) * param.PageSize) + .Take(param.PageSize); + var list = userQuery.ToList(); + + return new PageOutput(param.PageIndex, param.PageSize, count, list); + } + /// 用户参与项目 列表[New] + [HttpGet("{userId:guid}")] + public List GetParticipateTrialList(Guid userId) + { + + Expression> userTrialLambda = x => x.UserId== userId; + + var userTypeEnum = _userInfo.UserTypeEnumStr; + var loginUserId = _userInfo.Id; + + + //PM 进来只能看到他负责的项目下的参与人员列表 + if (userTypeEnum == UserTypeEnum.ProjectManager.ToString()) + { + //参与到的项目 + var trialFilter = _userTrialRepository.Where(t => t.UserId == loginUserId) + .Select(u => u.TrialId); + userTrialLambda = userTrialLambda.And(t => trialFilter.Contains(t.TrialId)); + } + + var query = from userTrial in _userTrialRepository.Where(userTrialLambda) + join trial in _trialRepository.AsQueryable() on userTrial.TrialId equals trial.Id + join cro in _croCompanyRepository.AsQueryable() on trial.CROId equals cro.Id into cc + from cro in cc.DefaultIfEmpty() + select new UserParticipateTrialDetail() + { + Code = trial.TrialCode, + CROName = cro.CROName, + Expedited = trial.Expedited, + Indication = trial.Indication, + UserType = userTrial.User.UserTypeRole.UserTypeShortName, + TrialId = trial.Id + + }; + + return query.ToList(); + } + + + #region Dashboard 数据统计 + /// 读片数分类统计[New] + public ReadingDataDTO GetReadingDataByType() + { + int tp = 0; int ad = 0; int g = 0; + var finalConfirmWorkload = _workloadRepository.Where(u => u.DataFrom == (int)WorkLoadFromStatus.FinalConfirm).ToList(); + foreach (var item in finalConfirmWorkload) + { + tp += (item.Timepoint + item.TimepointIn24H + item.TimepointIn48H); + ad += (item.Adjudication + item.AdjudicationIn24H + item.AdjudicationIn48H); + g += item.Global; + } + return new ReadingDataDTO + { + Timepoint = tp, + Adjudication = ad, + Global = g + }; + } + /// 获取最近几个月份的数据[New] + [HttpGet, Route("{monthCount:int}")] + public List GetReadingDataByMonth(int monthCount) + { + DateTime now = DateTime.Now.AddMonths(-1); + DateTime eTime = new DateTime(now.Year, now.Month, 20); + + DateTime now6 = now.AddMonths(-1 * (monthCount - 1)); + DateTime bTime = new DateTime(now.AddMonths(-(monthCount - 1)).Year, + now.AddMonths(-(monthCount - 1)).Month, 1, 0, 0, 0, 0); + + var query = from workload in _workloadRepository + .Where(u => u.DataFrom == (int)WorkLoadFromStatus.FinalConfirm && + u.WorkTime >= bTime && u.WorkTime < eTime) + group workload by workload.YearMonth + into gWorkLoad + select new ReadingDataMonthDTO + { + Month = gWorkLoad.Key, + Timepoint = gWorkLoad.Sum(t => t.Timepoint) + gWorkLoad.Sum(t => t.TimepointIn24H) + + gWorkLoad.Sum(t => t.TimepointIn48H), + Adjudication = gWorkLoad.Sum(t => t.Adjudication) + gWorkLoad.Sum(t => t.AdjudicationIn24H) + + gWorkLoad.Sum(t => t.AdjudicationIn48H), + Global = gWorkLoad.Sum(t => t.Global) + }; + var workloadList = query.OrderByDescending(u => u.Month).ToList(); + List result = new List(); + + for (int i = 0; i < monthCount; i++) + { + var tempTime = now.AddMonths(-1 * i); + var existedItem = workloadList.Find(u => u.Month == + (tempTime.Year + "-" + tempTime.Month.ToString().PadLeft(2, '0'))); + if (existedItem != null) + { + result.Add(new ReadingDataMonthDTO + { + Month = existedItem.Month, + Timepoint = existedItem.Timepoint, + Adjudication = existedItem.Adjudication, + Global = existedItem.Global + }); + } + else + { + result.Add(new ReadingDataMonthDTO + { + Month = tempTime.Year + "-" + tempTime.Month.ToString().PadLeft(2, '0'), + Timepoint = 0, + Adjudication = 0, + Global = 0 + }); + } + } + return result; + } + /// 读片数量排行前几的数据[New] + [HttpGet("{topCount:int}")] + public List GetReadingDataRank(int topCount) + { + var query = from workload in _workloadRepository + .Where(u => u.DataFrom == (int)WorkLoadFromStatus.FinalConfirm) + group workload by workload.DoctorId + into gWorkLoad + + select new ReadingDataRankDTO + { + TotalReadingCount = gWorkLoad.Sum(t => t.Timepoint) + gWorkLoad.Sum(t => t.TimepointIn24H) + + gWorkLoad.Sum(t => t.TimepointIn48H) + gWorkLoad.Sum(t => t.Adjudication) + gWorkLoad.Sum(t => t.AdjudicationIn24H) + + gWorkLoad.Sum(t => t.AdjudicationIn48H) + gWorkLoad.Sum(t => t.Global), + ReviewerId = gWorkLoad.Key, + Timepoint = gWorkLoad.Sum(t => t.Timepoint) + gWorkLoad.Sum(t => t.TimepointIn24H) + + gWorkLoad.Sum(t => t.TimepointIn48H), + Adjudication = gWorkLoad.Sum(t => t.Adjudication) + gWorkLoad.Sum(t => t.AdjudicationIn24H) + + gWorkLoad.Sum(t => t.AdjudicationIn48H), + Global = gWorkLoad.Sum(t => t.Global) + }; + var workloadList = query.OrderByDescending(u => u.TotalReadingCount).Take(topCount).ToList(); + var reviewerList = from w in workloadList + join doctor in _doctorRepository.Where() on w.ReviewerId equals doctor.Id into tt + from doctorItem in tt.DefaultIfEmpty() + select new ReadingDataRankDTO + { + TotalReadingCount = w.TotalReadingCount, + ReviewerId = w.ReviewerId, + Timepoint = w.Timepoint, + Adjudication = w.Adjudication, + Global = w.Global, + ReviewerCode = doctorItem.ReviewerCode, + FirstName = doctorItem.FirstName, + LastName = doctorItem.LastName, + ChineseName = doctorItem.ChineseName + }; + return reviewerList.ToList(); + } + + /// 按Rank统计Reviewer 数量[New] + public List GetReviewersByRank() + { + var query = from q in (from reviewer in _doctorRepository.AsQueryable() + //.Find(u => u.ReviewStatus == 1 && u.CooperateStatus == 1) + group reviewer by reviewer.RankId + into gReviewer + select new + { + RankId = gReviewer.Key, + ReviewerCount = gReviewer.Count() + }) + + join dic in _dictionaryRepository.Where() on q.RankId equals dic.Id into tt + from dicItem in tt.DefaultIfEmpty() + select new RankReviewersDTO + { + RankId = q.RankId, + ReviewerCount = q.ReviewerCount, + RankName = dicItem == null ? "Other" : dicItem.Value + }; + + var rankList = query.ToList(); + var staffList = rankList.Where(u => u.RankName == "Staff" || u.RankName == "Other"); + int staffCount = 0; + foreach (var item in staffList) + { + staffCount += item.ReviewerCount; + } + rankList.RemoveAll(u => u.RankName == "Staff" || u.RankName == "Other"); + + rankList.Add(new RankReviewersDTO { RankId = Guid.NewGuid(), RankName = "Staff", ReviewerCount = staffCount }); + + return rankList; + } + /// 最近几个季度入组人次[New] type==0 按照月份 + [HttpGet("{type:int}/{count:int}")] + public List GetEnrollDataByQuarter(int type, int count) + { + //等于0按照月份 否则按照季度 + if (type != 0) + { + var quarterCount = count; + var year = DateTime.Now.AddMonths(-(quarterCount - 1) * 3 - DateTime.Now.Month % 3 + 1).Year; + var month = DateTime.Now.AddMonths(-(quarterCount - 1) * 3 - DateTime.Now.Month % 3 + 1).Month; + + var beginMonth = new DateTime(year, month, 1); + + var endMonth = DateTime.Now; + var querySql = from enrollDetail in _enrollDetailRepository.Where(t => + t.CreateTime > beginMonth && t.CreateTime < endMonth && + t.EnrollStatus == (int)EnrollStatus.ConfirmIntoGroup) + select new + { + + OptTime = enrollDetail.CreateTime, + Year = enrollDetail.CreateTime.Year, + Month = enrollDetail.CreateTime.Month, + }; + + var result = querySql.GroupBy(t => new { t.Year, t.Month }).Select(s => new EnrollDataDTO + { + Year = s.Key.Year, + Month = s.Key.Month, + EnrollCount = s.Count() + }).ToList(); + + + #region 填充数据 + + List returnList = new List(); + for (int i = 0; i < DateTime.Now.Month % 3 + (quarterCount - 1) * 3; i++) + { + var tempTime = DateTime.Now.AddMonths(-1 * i); + var existedItem = result.FirstOrDefault(u => u.YearMonth == tempTime.ToString("yyyy-MM")); + if (existedItem != null) + { + returnList.Add(new EnrollDataDTO + { + Month = existedItem.Month, + Year = existedItem.Year, + EnrollCount = existedItem.EnrollCount + }); + } + else + { + returnList.Add(new EnrollDataDTO + { + Month = tempTime.Month, + Year = tempTime.Year, + EnrollCount = 0 + }); + } + } + + #endregion + + var returnResult = returnList.GroupBy(t => t.QuarterStr).Select(u => new EnrollQuartDataDTO() + { + ViewStr = u.Key, + EnrollCount = u.Sum(t => t.EnrollCount) + }).ToList(); + + return returnResult; + } + else + { + var year = DateTime.Now.AddMonths(-(count - 1)).Year; + var month = DateTime.Now.AddMonths(-(count - 1)).Month; + + var beginMonth = new DateTime(year, month, 1); + + var endMonth = DateTime.Now; + var querySql = from enrollDetail in _enrollDetailRepository.Where(t => + t.CreateTime > beginMonth && t.CreateTime < endMonth && + t.EnrollStatus == (int)EnrollStatus.ConfirmIntoGroup) + select new + { + + OptTime = enrollDetail.CreateTime, + Year = enrollDetail.CreateTime.Year, + Month = enrollDetail.CreateTime.Month, + }; + + var result = querySql.GroupBy(t => new { t.Year, t.Month }).Select(s => new EnrollDataDTO + { + Year = s.Key.Year, + Month = s.Key.Month, + EnrollCount = s.Count() + }).ToList(); + + #region 填充数据 + + List returnList = new List(); + for (int i = 0; i < count; i++) + { + var tempTime = DateTime.Now.AddMonths(-1 * i); + var existedItem = result.FirstOrDefault(u => u.YearMonth == tempTime.ToString("yyyy-MM")); + if (existedItem != null) + { + returnList.Add(new EnrollDataDTO + { + Month = existedItem.Month, + Year = existedItem.Year, + EnrollCount = existedItem.EnrollCount + }); + } + else + { + returnList.Add(new EnrollDataDTO + { + Month = tempTime.Month, + Year = tempTime.Year, + EnrollCount = 0 + }); + } + } + + #endregion + + var returnResult = returnList.GroupBy(t => t.YearMonth).Select(u => new EnrollQuartDataDTO() + { + ViewStr = u.Key, + EnrollCount = u.Sum(t => t.EnrollCount) + }).ToList(); + + return returnResult; + } + + } + /// 参与项目数排行 [New] + [HttpGet("{topCount:int}")] + public List GetTrialCountRank(int topCount) + { + var queryList = (from enrollDetail in _enrollDetailRepository.Where(t => + t.EnrollStatus == (int)EnrollStatus.ConfirmIntoGroup) + group enrollDetail by enrollDetail.DoctorId + into g + select new + { + ReviewerId = g.Key, + TrialCount = g.Count() + }).OrderByDescending(u => u.TrialCount).Take(topCount).ToList(); + + var trialDataRank = from stat in queryList + join doctor in _doctorRepository.AsQueryable() on stat.ReviewerId equals doctor.Id + select new TrialDataRankDTO() + { + TrialCount = stat.TrialCount, + ChineseName = doctor.ChineseName, + FirstName = doctor.FirstName, + LastName = doctor.LastName, + ReviewerId = doctor.Id, + ReviewerCode = doctor.ReviewerCode + }; + return trialDataRank.ToList(); + + } + /// 最新工作量 (已确定的)[New] + [HttpGet("{searchCount:int}")] + public List GetLatestWorkLoadList(int searchCount) + { + var workloadList = _workloadRepository.AsQueryable() + .Where(t => t.DataFrom == (int)WorkLoadFromStatus.FinalConfirm && + (t.Adjudication + t.AdjudicationIn24H + t.AdjudicationIn48H) > 0 && + (t.Timepoint + t.TimepointIn24H + t.TimepointIn48H) > 0) + .OrderByDescending(t => t.CreateTime).Take(searchCount).ToList(); + + + var reviewerList = from w in workloadList + join doctor in _doctorRepository.Where() on w.DoctorId equals doctor.Id into tt + from doctorItem in tt.DefaultIfEmpty() + join trial in _trialRepository.AsQueryable() on w.TrialId equals trial.Id into cc + from trialItem in cc.DefaultIfEmpty() + select new LatestWorkLoadDTO + { + + ReviewerId = w.DoctorId, + Timepoint = w.Timepoint + w.TimepointIn48H + w.TimepointIn24H, + Adjudication = w.Adjudication + w.AdjudicationIn48H + w.AdjudicationIn24H, + Global = w.Global, + ReviewerCode = doctorItem.ReviewerCode, + FirstName = doctorItem.FirstName, + LastName = doctorItem.LastName, + ChineseName = doctorItem.ChineseName, + + TrialCode = trialItem.TrialCode + }; + return reviewerList.ToList(); + + + } + + + + #endregion + + } +} diff --git a/IRaCIS.Core.Application/Service/Stat/_MapConfig.cs b/IRaCIS.Core.Application/Service/Stat/_MapConfig.cs new file mode 100644 index 00000000..38ef2f0f --- /dev/null +++ b/IRaCIS.Core.Application/Service/Stat/_MapConfig.cs @@ -0,0 +1,15 @@ +using AutoMapper; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Domain.Models; + +namespace IRaCIS.Core.Application.Service +{ + public class StatConfig : Profile + { + public StatConfig() + { + + } + } + +} diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/InspectionViewModel.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/InspectionViewModel.cs new file mode 100644 index 00000000..7f48a5b5 --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/InspectionViewModel.cs @@ -0,0 +1,75 @@ +using System.ComponentModel.DataAnnotations; + +namespace IRaCIS.Core.Application.Contracts; + +public class SignDTO +{ + public string UserName { get; set; } = String.Empty; + + public string PassWord { get; set; } = String.Empty; + + public string SignText { get; set; } = String.Empty; + + + + [NotDefault] + public Guid TrialId { get; set; } + + [NotDefault] + public Guid SignCodeId { get; set; } + + [NotDefault] + public string SignCode { get; set; } = String.Empty; + + public Guid? SubjectVisitId { get; set; } + +} + + +public class DataInspectionAddDTO +{ + + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public Guid SubjectId { get; set; } + public Guid SubjectVisitId { get; set; } + public int ModuleEnum { get; set; } + public int OptEnum { get; set; } + + + + public string BlindName { get; set; } = string.Empty; + public string Reason { get; set; } = string.Empty; + public bool IsSign { get; set; } + public string JsonDetail { get; set; } = string.Empty; + + + + + ////需要单独处理 + //public string IP { get; set; } + + //public Guid? SignId { get; set; } + +} + + +public interface IInspectionDTO +{ + public DataInspectionAddDTO AuditInfo { get; set; } +} + +public interface ISignDTO +{ + public SignDTO SignInfo { get; set; } + +} + +public class TrialDocumentConfirmDTO : IInspectionDTO, ISignDTO +{ + public UserConfirmCommand OptCommand { get; set; } + + public DataInspectionAddDTO AuditInfo { get; set; } + + public SignDTO SignInfo { get; set; } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/QANoticeViewModel.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/QANoticeViewModel.cs new file mode 100644 index 00000000..52db5cc7 --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/QANoticeViewModel.cs @@ -0,0 +1,35 @@ +using System; +using IRaCIS.Core.Domain.Share; + +namespace IRaCIS.Core.Application.Contracts +{ + + public class QANoticeDTO + { + public Guid Id { get; set; } + public Guid TrialId { get; set; } + + public Guid StudyId { get; set; } + + public string StudyStatusStr { get; set; } = string.Empty; + + public string Message { get; set; } = string.Empty; + + public DateTime SendTime { get; set; } + + public DateTime? DealTime { get; set; } + + + public string FromUser { get; set; } = string.Empty; + + public string FromUserType { get; set; } = string.Empty; + + public NoticeType NoticeTypeEnum { get; set; } + + public bool NeedDeal { get; set; } + + public bool IsMessageReceiver { get; set; } = false; + } + + +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialConfigDTO.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialConfigDTO.cs new file mode 100644 index 00000000..1bcbf53f --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialConfigDTO.cs @@ -0,0 +1,261 @@ +using IRaCIS.Core.Domain.Share; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text; + +namespace IRaCIS.Core.Application.Contracts +{ + + public class BasicTrialConfig + { + public Guid TrialId { get; set; } + + /// + /// 受试者编号具体规则 + /// + public string SubjectCodeRule { get; set; } = string.Empty; + + public bool IsSubjectSecondCodeView { get; set; } + + /// + /// 是否 验证拍片日期 + /// + public bool IsVerifyVisitImageDate { get; set; } = false; + + /// + /// 是否 提醒受试者编号规则 + /// + public bool IsNoticeSubjectCodeRule { get; set; } = true; + + /// + /// 是否 有基准时间(首次给药时间) + /// + public bool IsHaveFirstGiveMedicineDate { get; set; } = true; + + /// + /// 是否有 受试者年龄 + /// + public bool IsHaveSubjectAge { get; set; } = false; + + + /// + /// 出组后计划外访视名称 + /// + public string OutEnrollmentVisitName { get; set; } = "EOT"; + + /// + /// 临床信息传输 1:系统录入2:系统录入+PDF 0:无 + /// + public int ClinicalInformationTransmissionEnum { get; set; } = 1; + + public bool IsSubjectSexView { get; set; } = false; + + + public int ChangeDefalutDays { get; set; } = 5; + + + /// + /// 跨项目复制 + /// + public bool IsImageReplicationAcrossTrial { get; set; } = false; + + + public string BodyPartTypes { get; set; } = string.Empty; + + + public string Modalitys { get; set; } = string.Empty; + } + + public class TrialProcessConfig + { + + public List CriterionIds { get; set; } = new List(); + + public Guid TrialId { get; set; } + + + /// + /// QC流程 0 不审,1 单审,2双审 + /// + public TrialQCProcess QCProcessEnum { get; set; } + + /// + /// 影像一致性核查 + /// + public bool IsImageConsistencyVerification { get; set; } = false; + + /// + /// 1 Mint、2 PACS + /// + + public int ImagePlatform { get; set; } = 1; + + //阅片方式 + public int ReadingMode { get; set; } + + //阅片类型 + public int ReadingType { get; set; } + + + public bool IsGlobalReading { get; set; } = true; + + public bool? IsArbitrationReading { get; set; } + + public bool? IsClinicalReading { get; set; } + + + public int ArbitrationRule { get; set; } + + public int? DigitPlaces { get; set; } + + } + + public class TrialUrgentConfig + { + public Guid TrialId { get; set; } + + public bool IsSubjectExpeditedView { get; set; } = false; + + /// + /// 是否有 入组评估确认 + /// + public bool IsEnrollementQualificationConfirm { get; set; } = false; + + public bool IsUrgent { get; set; } + + public bool IsPDProgressView { get; set; } + + + } + + public class TrialStateChangeDTO + { + public Guid Id { get; set; } + public Guid TrialId { get; set; } + + + public string OriginState { get; set; } = String.Empty; + + + public string NowState { get; set; } = String.Empty; + + + public string Reason { get; set; } = String.Empty; + + + public DateTime CreateTime { get; set; } + + + public Guid CreateUserId { get; set; } + + public string UserName { get; set; } = String.Empty; + + public string UserRealName { get; set; } = String.Empty; + + + } + + + + public class SignConfirmDTO + { + [NotDefault] + public Guid TrialId { get; set; } + + [NotDefault] + public Guid SignId { get; set; } + + + [NotDefault] + public string SignCode { get; set; } + } + + public class TrialConfigDTO: BasicTrialConfig + { + + public List CriterionIds { get; set; } = new List(); + + public bool IsTrialProcessConfirmed { get; set; } + + public bool IsTrialBasicLogicConfirmed { get; set; } + + public bool IsTrialUrgentConfirmed { get; set; } + + + public int DigitPlaces { get; set; } + + + + /// + /// QC流程 0 不审,1 单审,2双审 + /// + public TrialQCProcess QCProcessEnum { get; set; } + + /// + /// 影像一致性核查 + /// + public bool IsImageConsistencyVerification { get; set; } = false; + + /// + /// 1 Mint、2 PACS + /// + + public int ImagePlatform { get; set; } = 1; + + //阅片方式 + public int ReadingMode { get; set; } + + //阅片类型 + public int ReadingType { get; set; } + + + public bool IsGlobalReading { get; set; } = true; + + public bool? IsArbitrationReading { get; set; } + + public bool? IsClinicalReading { get; set; } + + + public int ArbitrationRule { get; set; } + + + + + + public bool IsSubjectExpeditedView { get; set; } = false; + + /// + /// 是否有 入组评估确认 + /// + public bool IsEnrollementQualificationConfirm { get; set; } = false; + + public bool IsUrgent { get; set; } + + public bool IsPDProgressView { get; set; } + + + + //public bool IsTrialStart { get; set; } = false; + + public bool IsQCQuestionConfirmed { get; set; } + + public string TrialStatusStr { get; set; } + + + + + //public string DocumentConfirmSignText { get; set; } = string.Empty; + //public string ImageQCSignText { get; set; } = string.Empty; + //public string PreliminaryAuditReuploadText { get; set; } = string.Empty; + //public string ReviewAuditReuploadText { get; set; } = string.Empty; + //public string CheckBackText { get; set; } = string.Empty; + //public string CheckCloseText { get; set; } = string.Empty; + //public string ChallengeTypes { get; set; } = string.Empty; + //public bool IsImageExport { get; set; } = false; + //public bool IsCRAAuditClinicalInformation { get; set; } = false; + //public string TrialSiteSurveyUserRoles { get; set; } = string.Empty; + //public string TrialSiteSurveyEquipmentType { get; set; } = string.Empty; + + } +} diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialExternalUserViewModel.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialExternalUserViewModel.cs new file mode 100644 index 00000000..c1259367 --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialExternalUserViewModel.cs @@ -0,0 +1,149 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-03-04 13:33:45 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using IRaCIS.Core.Domain.Share; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using IRaCIS.Core.Application.Contracts.DTO; + +namespace IRaCIS.Core.Application.ViewModel +{ + /// TrialExternalUserView 列表视图模型 + public class TrialExternalUserView: TrialExternalUserAddOrEdit + { + + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid UpdateUserId { get; set; } + + public bool IsSystemUser { get; set; } + + + public Guid SystemUserId { get; set; } + + public TrialExternalUserStateEnum InviteState { get; set; } + + + public DateTime? ExpireTime { get; set; } + + public bool? IsJoin { get; set; } + + public DateTime? ConfirmTime { get; set; } + + public string RejectReason { get; set; } + } + + ///TrialExternalUserQuery 列表查询参数模型 + public class TrialExternalUserQuery + { + public Guid TrialId { get; set; } + + public string Phone { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + + + } + + /// TrialExternalUserAddOrEdit 列表查询参数模型 + public class TrialExternalUserAddOrEdit: VerifyUserAdd + { + public Guid? Id { get; set; } + + [NotDefault] + public Guid TrialId { get; set; } + + public string OrganizationName { get; set; } = String.Empty; + + + + } + public class TrialExternalUserConfirm + { + + + [NotDefault] + public Guid Id { get; set; } + + public bool? IsJoin { get; set; } + + public DateTime? ConfirmTime { get; set; } + + public string RejectReason { get; set; } + + } + + public class TrialSiteUserSurveyUserConfirm: TrialExternalUserConfirm + { + //[NotDefault] + //public Guid TrialId { get; set; } + + //[NotDefault] + //public Guid SiteId { get; set; } + } + + + public class TrialInfoWithPreparationInfo: TrialExternalUserConfirm + { + + public string ExperimentName { get; set; } = string.Empty; + + public string TrialCode { get; set; } = string.Empty; + + public string Indication { get; set; } = string.Empty; + + public string ResearchProgramNo { get; set; } = string.Empty; + + public Guid UserId { get; set; } + + public Guid TrialId { get; set; } + } + + public class TrialExternalUserSendEmail + { + [NotDefault] + public Guid TrialId { get; set; } + + public string BaseUrl { get; set; } = string.Empty; + + public string RouteUrl { get; set; } = string.Empty; + + public List SendUsers { get; set; } = new List(); + } + + public class UserEmail + { + public Guid Id { get; set; } + + public string Email { get; set; } = string.Empty; + + public bool IsSystemUser { get; set; } + + [NotDefault] + public Guid SystemUserId { get; set; } + + } + + + public class VerifyUserAdd + { + [NotDefault] + public Guid UserTypeId { get; set; } + + public string Email { get; set; } = string.Empty; + + + public string Phone { get; set; } = string.Empty; + + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + } + +} + + diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialResearchCenter.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialResearchCenter.cs new file mode 100644 index 00000000..65d28aaa --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialResearchCenter.cs @@ -0,0 +1,28 @@ +using System; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Contracts +{ + public class TrialResearchCenterDTO + { + public Guid Id { get; set; } + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public string MainCenter { get; set; } = string.Empty; + public DateTime OptTime { get; set; } + public int State { get; set; } + } + + public class TrialResearchCenterDetailDTO : TrialResearchCenterDTO + { + public string TrialCode { get; set; } = string.Empty; + public string TrialName { get; set; } = string.Empty; + public string ResearchCenterName { get; set; } = string.Empty; + } + + public class TrialCenterQueryDTO : PageInput + { + public Guid TrialId { get; set; } + public Guid CenterId { get; set; } + } +} diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialSiteViewModel.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialSiteViewModel.cs new file mode 100644 index 00000000..66ae1613 --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialSiteViewModel.cs @@ -0,0 +1,76 @@ +using System; +using IRaCIS.Core.Infrastructure.Extention; +using IRaCIS.Core.Domain.Share; + +namespace IRaCIS.Core.Application.Contracts.DTO +{ + + + public class SiteCrcQueryDTO : PageInput + { + public Guid TrialId { get; set; } = Guid.Empty; + public string UserRealName { get; set; } = String.Empty; + public string SiteName { get; set; } = String.Empty; + public string TrialSiteAliasName { get; set; } = String.Empty; + + } + + public class TrialSiteCommand + { + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + + public string SiteName { get; set; } + +} + +public class AssginSiteCRCCommand + { + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public Guid UserId { get; set; } + + public string UserRealName { get; set; } = String.Empty; + + } + + public class AssginSiteCRCListDTO:AssginSiteCRCCommand + { + public Guid UserTypeId { get; set; } + public string UserType { get; set; } = String.Empty; + public UserTypeEnum UserTypeEnum { get; set; } + public string OrganizationName { get; set; } = String.Empty; + //public string UserRealName { get; set; } + public string UserName { get; set; } = String.Empty; + public DateTime UpdateTime { get; set; } = DateTime.Now; + + public bool IsSelect { get; set; } + } + + + public class TrialSiteQuery : PageInput + { + public Guid TrialId { get; set; } + + public string SiteName { get; set; } = String.Empty; + + } + + public class TrialSiteScreeningDTO + { + public Guid Id { get; set; } + public string SiteName { get; set; } = String.Empty; + public string SiteCode { get; set; } = String.Empty; + public string City { get; set; } = String.Empty; + public string Country { get; set; } = String.Empty; + public Guid HospitalId { get; set; } = Guid.Empty; + + + public string HospitalName { get; set; } = String.Empty; + + public bool IsSelect { get; set; } + } + + + +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialUserPreparationViewModel.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialUserPreparationViewModel.cs new file mode 100644 index 00000000..503e64c0 --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialUserPreparationViewModel.cs @@ -0,0 +1,70 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-03-24 13:22:20 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using System; +using IRaCIS.Core.Domain.Share; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace IRaCIS.Core.Application.ViewModel +{ + /// TrialUserPreparation View 列表视图模型 + public class TrialUserPreparationView + { + public Guid UserId { get; set; } + public Guid TrialId { get; set; } + public DateTime UpdateTime { get; set; } + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public Guid UpdateUserId { get; set; } + public DateTime ExpireTime { get; set; } + + + + public string UserName { get; set; } + + public string UserRealName { get; set; } + + public string UserTypeShortName { get; set; } + + } + + ///TrialUserPreparation Query 列表查询参数模型 + public class TrialUserPreparationQuery + { + public Guid? UserTypeId { get; set; } + } + + /// TrialUserPreparation AddOrEdit 列表查询参数模型 + + + + + + + public class JoinCommand + { + public UserJoinTrialCommand TrialUserInfo { get; set; } + + //public TrialUserPreparationEdit JoinInfo { get; set; } + } + + + public class UserJoinTrialCommand + { + [NotDefault] + public Guid TrialId { get; set; } + + [NotDefault] + public Guid UserId { get; set; } + + public Guid? SiteId { get; set; } + + public bool IsExternal { get; set; } + } + +} + + diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialViewModel.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialViewModel.cs new file mode 100644 index 00000000..ec962a8b --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialViewModel.cs @@ -0,0 +1,213 @@ +using IRaCIS.Core.Infrastructure.Extention; +using IRaCIS.Core.Domain.Share; +using Newtonsoft.Json; + +namespace IRaCIS.Application.Contracts +{ + + + public class TrialBaseModel + { + public int? PlanSiteCount { get; set; } + public int? PlanVisitCount { get; set; } + public int TrialType { get; set; } + public Guid? Id { get; set; } + public string TrialCode { get; set; } = string.Empty; + public string Indication { get; set; } = string.Empty; + + //public Guid? ReviewTypeId { get; set; } = Guid.Empty; + + //public Guid? CriterionId { get; set; } = Guid.Empty; + public Guid? CROId { get; set; } = Guid.Empty; + public Guid? SponsorId { get; set; } = Guid.Empty; + public Guid? ReviewModeId { get; set; } = Guid.Empty; + public string Note { get; set; } = string.Empty; + public string TurnaroundTime { get; set; } = string.Empty; + public int ExpectedPatients { get; set; } + public decimal TimePointsPerPatient { get; set; } + public int? GRRReviewers { get; set; } + public int TotalReviewers { get; set; } + public int? Expedited { get; set; } + public int? AttendedReviewerType { get; set; } + + //研究方案号 + public string ResearchProgramNo { get; set; } = string.Empty; + + //实验名称 + public string ExperimentName { get; set; } = string.Empty; + + //主研单位 + public string MainResearchUnit { get; set; } = string.Empty; + + // 负责人PI + public string HeadPI { get; set; } = string.Empty; + + public Guid DeclarationTypeId { get; set; } + + public Guid IndicationTypeId { get; set; } + public Guid PhaseId { get; set; } + + + + } + + + public class QuestionTypeDto + { + public Guid DictionaryId { get; set; } + + public string Value { get; set; } = string.Empty; + + public string ValueCN { get; set; } = string.Empty; + + public bool IsSelect { get; set; } + } + + public class TrialSelectDTO + { + public Guid Id { get; set; } + + public string ExperimentName { get; set; } = string.Empty; + + public string TrialCode { get; set; } = string.Empty; + + public string Indication { get; set; } = string.Empty; + + public string ResearchProgramNo { get; set; } = string.Empty; + + } + + public class TrialModalitySelectDto + { + public Guid ModalityId { get; set; } + public string Modality { get; set; } = string.Empty; + } + + public class TrialDetailDTO : TrialBaseModel + { + + [JsonIgnore] + public List DictionaryList { get; set; } = new List(); + + public List ModalityList => DictionaryList.Where(t => t.ParentCode == StaticData.Modality).OrderBy(t => t.ShowOrder).Select(t => t.Value).ToList(); + public List ModalityIds => DictionaryList.Where(t => t.ParentCode == StaticData.Modality).OrderBy(t => t.ShowOrder).Select(t => t.Id).ToList(); + + public List CriterionList => DictionaryList.Where(t => t.ParentCode == StaticData.ReadingStandard).OrderBy(t => t.ShowOrder).Select(t => t.Value).ToList(); + public List CriterionIds => DictionaryList.Where(t => t.ParentCode == StaticData.ReadingStandard).OrderBy(t => t.ShowOrder).Select(t => t.Id).ToList(); + + public List ReviewTypeIds => DictionaryList.Where(t => t.ParentCode == StaticData.ReviewType).OrderBy(t => t.ShowOrder).Select(t => t.Id).ToList(); + + public List ReviewTypeList => DictionaryList.Where(t => t.ParentCode == StaticData.ReviewType).OrderBy(t => t.ShowOrder).Select(t => t.Value).ToList(); + + //public string ReviewType { get; set; } = string.Empty; + + + + //统计字段 + public string Phase { get; set; } = string.Empty; + + public string DeclarationType { get; set; } = string.Empty; + + public string IndicationType { get; set; } = string.Empty; + + + public bool IsLocked { get; set; } + public int EnrollStatus { get; set; } + + public int? SubjectCount { get; set; } + + public int? StudyCount { get; set; } = 0; + public int? SiteCount { get; set; } = 0; + + public string ReviewMode { get; set; } = string.Empty; + + + + + public string CRO { get; set; } = string.Empty; + public string Sponsor { get; set; } = string.Empty; + public int TrialEnrollStatus { get; set; } + public string TrialStatusStr { get; set; } = string.Empty; + + public DateTime? CreateTime { get; set; } + public bool IsDeleted { get; set; } + + } + + public class TrialAndTrialStateVieModel + { + public TrialDetailDTO TrialView { get; set; } = new TrialDetailDTO(); + + public int TrialMaxState { get; set; } + + + } + + + + public class TrialCommand : TrialBaseModel + { + + public List ModalityIds { get; set; } = new List(); + + public List CriterionIds { get; set; } = new List(); + + public List ReviewTypeIds { get; set; } = new List(); + + } + + + + public class TrialQueryDTO : PageInput + { + public Guid? DeclarationTypeId { get; set; } + + public Guid? IndicationTypeId { get; set; } + public Guid? SponsorId { get; set; } + public Guid? CROId { get; set; } + + + + public string ResearchProgramNo { get; set; } = String.Empty; + + public string ExperimentName { get; set; } = String.Empty; + public List ModalityIds { get; set; }=new List(); + public List CriterionIds { get; set; } = new List(); + + public List ReviewTypeIds { get; set; } = new List(); + //public Guid? ReviewTypeId { get; set; } + + public string Code { get; set; } = String.Empty; + + public string Indication { get; set; } = String.Empty; + public Guid? PhaseId { get; set; } + public string TrialStatusStr { get; set; } = String.Empty; + + public DateTime? BeginDate { get; set; } + public DateTime? EndDate { get; set; } + public int? Expedited { get; set; } + public int? AttendedReviewerType { get; set; } + } + + public class ReviewerTrialQueryDTO : PageInput + { + public string Code { get; set; } = string.Empty; + public string Indication { get; set; } = string.Empty; + public int? EnrollStatus { get; set; } + public int? Expedited { get; set; } + } + + + public class TrialByStatusQueryDTO : PageInput + { + public Guid DoctorId { get; set; } + public int Status { get; set; } + } + + + + + + + +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/UserTrialViewModel.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/UserTrialViewModel.cs new file mode 100644 index 00000000..c21ab978 --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/UserTrialViewModel.cs @@ -0,0 +1,230 @@ +using System.ComponentModel.DataAnnotations; +using IRaCIS.Core.Infrastructure.Extention; +using IRaCIS.Core.Domain.Share; + +namespace IRaCIS.Application.Contracts +{ + + + + + public class UserTrialDTO : UserTrialCommand + { + + public bool IsDeleted { get; set; } + + public DateTime? RemoveTime { get; set; } + public Guid? SiteId { get; set; } + public string Phone { get; set; } = String.Empty; + public DateTime? UpdateTime { get; set; } + public string UserType { get; set; } = String.Empty; + public string UserRealName { get; set; } = String.Empty; + + } + + + + public class TrialMaintenanceDTO : UserTrialCommand + { + public bool IsDeleted { get; set; } + public DateTime? RemoveTime { get; set; } + + public DateTime? JoinTime { get; set; } + + + + public DateTime UpdateTime { get; set; } = DateTime.Now; + + public Guid UserTypeId { get; set; } + public string UserType { get; set; } = String.Empty; + + public UserTypeEnum UserTypeEnum { get; set; } + + + public string OrganizationName { get; set; } = String.Empty; + + public string UserRealName { get; set; } = String.Empty; + public string UserName { get; set; } = String.Empty; + + + + + } + + public class SiteCRCCommand : UserTrialCommand + { + public Guid SiteId { get; set; } + + public Guid UserTypeId { get; set; } + public string UserType { get; set; } = String.Empty; + public string UserRealName { get; set; } = String.Empty; + + + public UserTypeEnum UserTypeEnum { get; set; } + + + public string OrganizationName { get; set; } = String.Empty; + + public string UserName { get; set; } = String.Empty; + } + + + + public class UserTrialCommand + { + public Guid? Id { get; set; } + public Guid UserId { get; set; } + public Guid TrialId { get; set; } + } + + + + + + public class SiteStatDTO : SiteStatSimpleDTO + { + + + //public int PlanVisitCount { get; set; } + + //public int? StudyCount { get; set; } + + public int? VisitCount { get; set; } + public int? SubjectCount { get; set; } + + + } + + public class SiteStatSimpleDTO + { + public Guid Id { get; set; } + + public Guid SiteId { get; set; } + + public int? UserCount { get; set; } + + public string TrialSiteCode { get; set; } = String.Empty; + public string TrialSiteAliasName { get; set; } = String.Empty; + public DateTime UpdateTime { get; set; } + + public string Site { get; set; } = String.Empty; + + public string SiteCode { get; set; } = String.Empty; + + + public string Hospital { get; set; } = String.Empty; + public string City { get; set; } = String.Empty; + public string Country { get; set; } = String.Empty; + + public string DirectorName { get; set; } = String.Empty; + public string DirectorPhone { get; set; } = String.Empty; + public string ContactName { get; set; } = String.Empty; + public string ContactPhone { get; set; } = String.Empty; + + public string Address { get; set; } = String.Empty; + + + public List UserNameList { get; set; } = new List(); + } + + + + public class UserSelectionModel + { + public Guid Id { get; set; } + public string RealName { get; set; } = string.Empty; + + public string UserName { get; set; } = string.Empty; + } + + + public class TrialMaintenanceQuery : PageInput + { + public Guid TrialId { get; set; } = Guid.Empty; + public string UserRealName { get; set; } = string.Empty; + public string UserType { get; set; } = string.Empty; + + public string UserName { get; set; } = string.Empty; + + public string OrganizationName { get; set; } = String.Empty; + + //public DateTime? RemoveTime { get; set; } + + //public DateTime? JoinTime { get; set; } + + + } + + public class SiteCRCQuery : TrialMaintenanceQuery + { + + public Guid SiteId { get; set; } + + } + + + + public class TrialUserQuery : PageInput + { + public Guid TrialId { get; set; } + public string UserName { get; set; } = string.Empty; + + public string UserRealName { get; set; } = string.Empty; + + public UserTypeEnum? UserTypeEnum { get; set; } + } + + public class TrialUserScreeningDTO : TrialUserAddCommand + { + + public int Sex { get; set; } // 1-男 2-女 + + public string Phone { get; set; } = string.Empty; + public string EMail { get; set; } = string.Empty; + + public string DepartmentName { get; set; } = String.Empty; + public string PositionName { get; set; } = String.Empty; + + public string UserName { get; set; } = String.Empty; + + public bool IsSelect { get; set; } + + + + public Guid UserTypeId { get; set; } + public string UserType { get; set; } = string.Empty; + public UserTypeEnum UserTypeEnum { get; set; } + + public string OrganizationName { get; set; } = string.Empty; + public string UserRealName { get; set; } = string.Empty; + + + } + + + public class TrialUserAddCommand + { + public Guid UserId { get; set; } + + public Guid TrialId { get; set; } + + + + } + + + public class UpdateTrialUserCommand + { + [NotDefault] + public Guid Id { get; set; } + + public Guid TrialId { get; set; } + + public bool IsDeleted { get; set; } + + public DateTime? RemoveTime { get; set; } + + public DateTime? JoinTime { get; set; } + } + +} diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialConfigService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialConfigService.cs new file mode 100644 index 00000000..0b236746 --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialConfigService.cs @@ -0,0 +1,14 @@ +using IRaCIS.Core.Application.Contracts; +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Infra.EFCore; + +namespace IRaCIS.Application.Interfaces +{ + public interface ITrialConfigService + { + + Task GetTrialConfigInfo(Guid trialId); + + + } +} diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialExternalUserService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialExternalUserService.cs new file mode 100644 index 00000000..7a44cc4a --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialExternalUserService.cs @@ -0,0 +1,27 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-03-04 13:33:52 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using IRaCIS.Core.Application.ViewModel; +namespace IRaCIS.Core.Application.Interfaces +{ + /// + /// ITrialExternalUserService + /// + public interface ITrialExternalUserService + { + + + Task> GetTrialExternalUserList(TrialExternalUserQuery queryTrialExternalUser); + + Task AddOrUpdateTrialExternalUser(TrialExternalUserAddOrEdit addOrEditTrialExternalUser); + + Task DeleteTrialExternalUser(Guid trialExternalUserId, bool isSystemUser, + Guid systemUserId); + + Task UserConfirmJoinTrial(Guid trialId, Guid trialExternalUserId); + + } +} diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialMaintenanceService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialMaintenanceService.cs new file mode 100644 index 00000000..99668f05 --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialMaintenanceService.cs @@ -0,0 +1,14 @@ +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Application.Contracts.DTO; + +namespace IRaCIS.Application.Interfaces +{ + public interface ITrialMaintenanceService + { + Task AddTrialUsers(TrialUserAddCommand[] userTrialCommands); + Task DeleteMaintenanceUser(Guid id, bool isDelete); + Task> GetMaintenanceUserList(TrialMaintenanceQuery param); + Task> GetSiteCRCScreeningList(SiteCRCQuery param); + Task> GetTrialUserScreeningList(TrialUserQuery trialUserQuery); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialService.cs new file mode 100644 index 00000000..a8037649 --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialService.cs @@ -0,0 +1,25 @@ +using IRaCIS.Application.Contracts; + +namespace IRaCIS.Application.Interfaces +{ + + public interface ITrialService + { + bool TrialExpeditedChange { get; set; } + + Task AddOrUpdateTrial(TrialCommand trialAddModel); + Task UpdateTrialStatus(Guid trialId, string statusStr); + + Task ConfirmTrialVisitPlan(Guid trialId, bool confirmOrCancel = true); + Task DeleteTrial(Guid trialId); + Task> GetReviewerTrialListByEnrollmentStatus(TrialByStatusQueryDTO param); + Task> GetTrialEnrollmentReviewerIds(Guid trialId); + Task GetTrialExpeditedState(Guid trialId); + Task GetTrialInfoAndLockState(Guid projectId); + Task GetTrialInfoAndMaxTrialState(Guid trialId); + Task> GetTrialList(TrialQueryDTO searchParam); + Task> GetTrialListByReviewer(ReviewerTrialQueryDTO searchModel); + Task GetTrialMaxState(Guid trialId); + Task UpdateEnrollStatus(Guid trialId, int status); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialSiteService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialSiteService.cs new file mode 100644 index 00000000..748ed3c8 --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialSiteService.cs @@ -0,0 +1,19 @@ +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Application.Contracts.DTO; + +namespace IRaCIS.Core.Application.Interfaces +{ + public interface ITrialSiteService + { + Task AddTrialSites(List trialSites); + Task AssignSiteCRC(List trialSiteCRCList); + Task DeleteSiteCRC(Guid id, bool isDelete); + Task DeleteTrialSite(Guid id); + Task EditTrialSite(Guid id, Guid trialId, string trialSiteCode, string? trialSiteAliasName); + Task> GetSiteCRCList(SiteCrcQueryDTO param); + Task> GetSiteCRCSimpleList(SiteCrcQueryDTO param); + Task> GetTrialSiteScreeningList(TrialSiteQuery trialSiteQuery); + Task> GetTrialSiteSelect(Guid trialId); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialUserPreparationService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialUserPreparationService.cs new file mode 100644 index 00000000..4a5b83bc --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialUserPreparationService.cs @@ -0,0 +1,27 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-03-24 13:22:50 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using IRaCIS.Core.Application.ViewModel; +namespace IRaCIS.Core.Application.Interfaces +{ + /// + /// ITrialUserPreparation Service + /// + public interface ITrialUserPreparationService + { + + + Task> GetTrialUserPreparationList(TrialUserPreparationQuery queryTrialUserPreparation ); + + + + //Task AddOrUpdateTrialUserPreparation (TrialUserPreparationAddOrEdit addOrEditTrialUserPreparation ); + + //Task DeleteTrialUserPreparation (Guid trialUserPreparationId); + + + } +} diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/TrialConfigService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialConfigService.cs new file mode 100644 index 00000000..19665f27 --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialConfigService.cs @@ -0,0 +1,377 @@ +using IRaCIS.Application.Interfaces; +using IRaCIS.Core.Application.Contracts; +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using IRaCIS.Core.Domain.Share; +using EasyCaching.Core; +using IRaCIS.Core.Application.Filter; + +namespace IRaCIS.Core.Application +{ + [ApiExplorerSettings(GroupName = "Trial")] + public class TrialConfigService : BaseService, ITrialConfigService + { + private readonly IRepository _trialRepository; + + public TrialConfigService(IRepository trialRepository) + { + _trialRepository = trialRepository; + } + + /// + /// 获取签名文本 + /// + /// + /// + [HttpGet("{signCode}")] + public async Task GetSignText(string signCode) + { + var signRawText = await _repository.Where(t => t.Code == signCode).Select(t => new { t.Code, t.Value, t.Id, ParentValue = t.Parent.Value, ParentValueCN = t.Parent.ValueCN }).FirstOrDefaultAsync(); + + if (signRawText == null) + { + return ResponseOutput.NotOk("未在系统中找到该签名场景的数据"); + } + + return ResponseOutput.Ok(new + { + SignCodeId = signRawText.Id, + SignCode = signRawText.Code, + SignText = signRawText.ParentValue.Replace("xxx", signRawText.Value), + SignTextCN = signRawText.ParentValueCN.Replace("xxx", signRawText.Value) + }); + } + + /// + /// 签名认证 + + /// + /// + [HttpPost] + public async Task VerifySignature(SignDTO signDTO) + { + + var user = await _repository.FirstOrDefaultAsync(u => u.UserName == signDTO.UserName && u.Password == signDTO.PassWord); + if (user == null) + { + return ResponseOutput.NotOk("password error"); + } + else if (user.Status == UserStateEnum.Disable) + { + return ResponseOutput.NotOk("The user has been disabled!"); + } + + //if (signDTO.IsAddSignData) + //{ + // 记录签名信息 + var add = await _repository.AddAsync(_mapper.Map(signDTO)); + + await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(add.Id); + //} + + //return ResponseOutput.Ok(); + + } + + + + + /// + /// 业务接口操作成功后, 让签署数据生效 + /// + /// + /// + [HttpPut("{signId:guid}")] + [Obsolete] + public async Task MakeSignEffective(Guid signId) + { + var success = await _repository.UpdateFromQueryAsync(t => t.Id == signId, u => new TrialSign() { IsCompleted = true }); + + return ResponseOutput.Result(success); + } + + + + /// + /// 签名确认 包括项目的三组配置 + QC问题确认 后修改状态 (适用于不会回退的,项目废除、状态修改, 存在回退 不在这里弄,提供单独接口修改状态) + /// + /// + public async Task TrialConfigSignatureConfirm(SignConfirmDTO signConfirmDTO) + { + + if (!await _trialRepository.AnyAsync(t => t.Id == signConfirmDTO.TrialId && (t.TrialStatusStr == StaticData.TrialInitializing|| t.TrialStatusStr == StaticData.TrialOngoing))) + { + return ResponseOutput.NotOk("项目不在Initializing/Ongoing,不允许确认配置"); + } + + if (signConfirmDTO.SignCode == ((int)SignEnum.TrialLogicConfim).ToString()) + { + await _trialRepository.UpdateFromQueryAsync(t => t.Id == signConfirmDTO.TrialId, u => new Trial() { IsTrialBasicLogicConfirmed = true }); + } + else if (signConfirmDTO.SignCode == ((int)SignEnum.TrialProcessConfim).ToString()) + { + await _trialRepository.UpdateFromQueryAsync(t => t.Id == signConfirmDTO.TrialId, u => new Trial() { IsTrialProcessConfirmed = true }); + } + else if (signConfirmDTO.SignCode == ((int)SignEnum.TrialUrgentConfim).ToString()) + { + await _trialRepository.UpdateFromQueryAsync(t => t.Id == signConfirmDTO.TrialId, u => new Trial() { IsTrialUrgentConfirmed = true }); + } + + else if (signConfirmDTO.SignCode == ((int)SignEnum.TrialQCQuestionConfirm).ToString()) + { + var showOrderList = await _repository.Where(t => t.TrialId == signConfirmDTO.TrialId).Select(t => + new { t.ShowOrder , ParentShowOrder=(int?) t.ParentQCQuestion.ShowOrder} ).ToListAsync(); + + if (showOrderList.Count() != showOrderList.Select(t=>t.ShowOrder).Distinct().Count()) + { + return ResponseOutput.NotOk("QC问题显示序号不允许重复"); + } + + if (showOrderList.Where(t=>t.ParentShowOrder != null).Any(t => t.ParentShowOrder > t.ShowOrder)) + { + return ResponseOutput.NotOk("父问题的序号要比子问题序号小,请确认"); + } + + await _trialRepository.UpdateFromQueryAsync(t => t.Id == signConfirmDTO.TrialId, u => new Trial() { QCQuestionConfirmedTime = DateTime.Now, QCQuestionConfirmedUserId = _userInfo.Id, IsQCQuestionConfirmed = true }); + } + + var signSuccess = await _repository.UpdateFromQueryAsync(t => t.Id == signConfirmDTO.SignId, u => new TrialSign() { IsCompleted = true }); + + return ResponseOutput.Result(signSuccess); + } + + + + /// + /// 更新项目状态 + /// + /// + /// + /// + /// + /// + /// + [HttpPut("{trialId:guid}/{signId:guid}/{trialStatusStr}/{reason?}")] + [UnitOfWork] + public async Task UpdateTrialState(Guid trialId, string trialStatusStr, Guid signId, string? reason, [FromServices] IEasyCachingProvider _provider) + { + + var trial = await _trialRepository.Where(t => t.Id == trialId, true).IgnoreQueryFilters().FirstOrDefaultAsync().IfNullThrowException(); + + if (trialStatusStr == StaticData.TrialOngoing) + { + if (trial.IsTrialBasicLogicConfirmed && trial.IsTrialProcessConfirmed && trial.IsTrialUrgentConfirmed && trial.VisitPlanConfirmed) + { + + } + else + { + return ResponseOutput.NotOk("项目 基础配置、流程配置、加急配置 、访视计划,有未确认项"); + } + } + + + trial.TrialStateChangeList.Add(new TrialStateChange() { OriginState = trial.TrialStatusStr, NowState = trialStatusStr, Reason = reason ?? String.Empty, TrialId = trialId }); + + trial.TrialStatusStr = trialStatusStr; + + + //Paused、 添加工总量 算医生读片中 + if (trialStatusStr.Contains(StaticData.TrialCompleted)) + { + await _repository.UpdateFromQueryAsync(u => u.TrialId == trialId, e => new Enroll + { + EnrollStatus = (int)EnrollStatus.Finished + }); + + await _trialRepository.UpdateFromQueryAsync(u => u.Id == trialId, s => new Trial { TrialFinishedTime = DateTime.UtcNow.AddHours(8) }); + + } + + await _provider.SetAsync(trialId.ToString(), trialStatusStr, TimeSpan.FromDays(7)); + + await _repository.SaveChangesAsync(); + + + var success = await _repository.UpdateFromQueryAsync(t => t.Id == signId, u => new TrialSign() { IsCompleted = true }); + + + return ResponseOutput.Result(success); + } + + /// + /// 项目状态 变更历史 + /// + /// + /// + + [HttpGet("{trialId:guid}")] + public async Task> GetTrialStateChangeList(Guid trialId) + { + return await _repository.Where(t => t.TrialId == trialId).ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + } + + + /// + /// 废除项目 + /// + /// + /// + /// + /// + [HttpPut("{trialId:guid}/{signId:guid}/{isAbandon:bool}")] + public async Task AbandonTrial(Guid trialId, Guid signId, bool isAbandon) + { + + await _trialRepository.UpdateFromQueryAsync(t => t.Id == trialId, u => new Trial() { IsDeleted = isAbandon }); + + + var success = await _repository.UpdateFromQueryAsync(t => t.Id == signId, u => new TrialSign() { IsCompleted = true }); + + + return ResponseOutput.Result(success); + } + + + + + /// + /// 获取 配置的所有信息 没有分多个接口 + /// + /// + /// + [HttpGet("{trialId:guid}")] + public async Task GetTrialConfigInfo(Guid trialId) + { + return await _trialRepository.Where(t => t.Id == trialId).ProjectTo(_mapper.ConfigurationProvider) + .FirstOrDefaultAsync().IfNullThrowException(); + } + + + /// + /// 配置 基础逻辑信息 + /// + /// + /// + [HttpPut] + public async Task ConfigTrialBasicInfo(BasicTrialConfig trialConfig) + { + if (!await _trialRepository.Where(t => t.Id == trialConfig.TrialId).IgnoreQueryFilters().AnyAsync(t => t.TrialStatusStr == StaticData.TrialOngoing || t.TrialStatusStr == StaticData.TrialInitializing)) + { + return ResponseOutput.NotOk(" only in Initializing or Ongoing State can operate "); + } + + var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == trialConfig.TrialId); + + if (trialInfo == null) return Null404NotFound(trialInfo); + + _mapper.Map(trialConfig, trialInfo); + + return ResponseOutput.Ok(await _repository.SaveChangesAsync()); + } + + /// + /// 配置流程 + /// + /// + /// + [HttpPut] + public async Task ConfigTrialProcessInfo(TrialProcessConfig trialConfig) + { + if (!await _trialRepository.Where(t => t.Id == trialConfig.TrialId).IgnoreQueryFilters().AnyAsync(t => t.TrialStatusStr == StaticData.TrialInitializing)) + { + return ResponseOutput.NotOk(" only in Initializing State can operate "); + } + + var trialInfo = await _trialRepository.Where(t => t.Id == trialConfig.TrialId, true).Include(t => t.TrialDicList.Where(u => u.KeyName == StaticData.Criterion)).FirstOrDefaultAsync(); + + if (trialInfo == null) return Null404NotFound(trialInfo); + + _mapper.Map(trialConfig, trialInfo); + + + + return ResponseOutput.Ok(await _repository.SaveChangesAsync()); + } + + /// + /// 配置加急信息 + /// + /// + /// + [HttpPut] + public async Task ConfigTrialUrgentInfo(TrialUrgentConfig trialConfig) + { + + if (!await _repository.Where(t => t.Id == trialConfig.TrialId).IgnoreQueryFilters().AnyAsync(t => t.TrialStatusStr == StaticData.TrialOngoing || t.TrialStatusStr == StaticData.TrialInitializing)) + { + return ResponseOutput.NotOk(" only in Initializing or Ongoing State can operate "); + } + + + + var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == trialConfig.TrialId); + + if (trialInfo == null) return Null404NotFound(trialInfo); + + //项目紧急 当前所有已提交,但未完成的访视,设置为加急。后续提交的访视也设置为加急 , + if (trialConfig.IsUrgent) + { + + await _repository.UpdateFromQueryAsync(t => t.TrialId == trialInfo.Id && t.SubmitState == SubmitStateEnum.Submitted && t.ForwardState < ForwardStateEnum.Forwarded, + s => new SubjectVisit() { IsUrgent = trialConfig.IsUrgent }); + } + else //之前设置为加急的访视状态不变。后续提交的访视,为不加急。 + { + + } + + _mapper.Map(trialConfig, trialInfo); + + return ResponseOutput.Ok(await _repository.SaveChangesAsync()); + } + + + + + + [Obsolete] + [HttpPost("{trialId:guid}/{type}")] + public async Task UploadTrialFile(IFormFile file, string type, Guid trialId, [FromServices] IWebHostEnvironment _hostEnvironment) + { + + var rootPath = Directory.GetParent(_hostEnvironment.ContentRootPath.TrimEnd('\\')).IfNullThrowException().FullName; + //上传根路径 + var _fileStorePath = Path.Combine(rootPath, StaticData.TrialDataFolder); + + //文件类型路径处理 + var uploadFolderPath = Path.Combine(_fileStorePath, trialId.ToString(), type); + if (!Directory.Exists(uploadFolderPath)) Directory.CreateDirectory(uploadFolderPath); + + + var realName = file.FileName; + var fileNameEX = Path.GetExtension(realName); + var trustedFileNameForFileStorage = Guid.NewGuid().ToString() + fileNameEX; + + var relativePath = $"/{StaticData.TrialDataFolder}/{trialId}/{type}/{trustedFileNameForFileStorage}"; + + var filePath = Path.Combine(uploadFolderPath, trustedFileNameForFileStorage); + using (FileStream fs = System.IO.File.Create(filePath)) + { + await file.CopyToAsync(fs); + fs.Flush(); + } + + return ResponseOutput.Ok(new + { + FilePath = relativePath, + FullFilePath = relativePath + "?access_token=" + _userInfo.UserToken + }); + + } + + } +} diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/TrialExternalUserService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialExternalUserService.cs new file mode 100644 index 00000000..41bcf7cf --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialExternalUserService.cs @@ -0,0 +1,480 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-03-04 13:33:56 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using IRaCIS.Core.Domain.Models; +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Application.Interfaces; +using IRaCIS.Core.Application.ViewModel; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Infrastructure; +using IRaCIS.Core.Domain.Share; +using MimeKit; +using MailKit.Security; +using Microsoft.AspNetCore.Authorization; +using Panda.DynamicWebApi.Attributes; + +namespace IRaCIS.Core.Application.Service +{ + /// + /// 项目外部人员 录入流程相关 + /// + [ApiExplorerSettings(GroupName = "Trial")] + public class TrialExternalUserService : BaseService, ITrialExternalUserService + { + private readonly IRepository _trialExternalUseRepository; + private readonly IRepository _userRepository; + private readonly IRepository _trialUserRepository; + private readonly IRepository _trialSiteSurveyUserRepository; + private readonly IRepository _trialSiteUserRepository; + + public TrialExternalUserService(IRepository trialExternalUseRepository, IRepository userRepository, IRepository trialUserRepository, + IRepository trialSiteSurveyUserRepository,IRepository trialSiteUserRepository) + { + _trialExternalUseRepository = trialExternalUseRepository; + _userRepository = userRepository; + _trialUserRepository = trialUserRepository; + _trialSiteSurveyUserRepository = trialSiteSurveyUserRepository; + _trialSiteUserRepository = trialSiteUserRepository; + } + + + [HttpPost] + public async Task> GetTrialExternalUserList(TrialExternalUserQuery queryTrialExternalUser) + { + + var trialExternalUserQueryable = _trialExternalUseRepository.Where(t => t.TrialId == queryTrialExternalUser.TrialId) + .WhereIf(!string.IsNullOrEmpty(queryTrialExternalUser.Phone), t => t.Phone.Contains(queryTrialExternalUser.Phone)) + .WhereIf(!string.IsNullOrEmpty(queryTrialExternalUser.Email), t => t.Email.Contains(queryTrialExternalUser.Email)) + .WhereIf(!string.IsNullOrEmpty(queryTrialExternalUser.Name), t => (t.LastName + " / " + t.FirstName).Contains(queryTrialExternalUser.Name)) + .ProjectTo(_mapper.ConfigurationProvider); + + return await trialExternalUserQueryable.ToListAsync(); + } + + /// + /// 验证 在系统中是否存在该类型的账户 返回true 表示 不存在 可以添加和更新|存在但是信息一致,false 需要提示不一致项(前端 可以直接用我返回的错误信息,或者根据返回的用户信息实体,自己设置格式显示) + /// + /// + /// + [HttpPost] + public async Task VerifyUserIsCanAddOrUpdate(VerifyUserAdd addOrEditTrialExternalUser) + { + + var existUser = await _userRepository.FirstOrDefaultAsync(t => t.EMail == addOrEditTrialExternalUser.Email && t.UserTypeId == addOrEditTrialExternalUser.UserTypeId); + + if (existUser != null) + { + if (existUser.LastName != addOrEditTrialExternalUser.LastName || existUser.FirstName != addOrEditTrialExternalUser.FirstName) + { + return ResponseOutput.NotOk($"该用户在系统中账户名为:{existUser.LastName + " / " + existUser.FirstName} 电话:{existUser.Phone},与填写信息存在不一致项, 现将界面信息修改为与系统一致,可进行保存", new { existUser.LastName, existUser.FirstName, existUser.Phone }); + } + + } + return ResponseOutput.Ok(); + + } + + + /// + /// 添加和更新接口 已验证邮箱和账户类型不允许添加重复项 + /// + /// + /// + public async Task AddOrUpdateTrialExternalUser(TrialExternalUserAddOrEdit addOrEditTrialExternalUser) + { + + //var verifyExp1 = new EntityVerifyExp() + //{ + // VerifyExp = t => t.Email == addOrEditTrialExternalUser.Email && t.UserTypeId == addOrEditTrialExternalUser.UserTypeId, + // VerifyMsg = "" + //}; + //var entity = await _trialExternalUseRepository.InsertOrUpdateAsync(addOrEditTrialExternalUser, false, verifyExp1); + + + if (addOrEditTrialExternalUser.Id == null) + { + var existSysUser = await _userRepository.FirstOrDefaultAsync(t => t.EMail == addOrEditTrialExternalUser.Email && t.UserTypeId == addOrEditTrialExternalUser.UserTypeId); + + if (existSysUser != null) + { + if (existSysUser.LastName != addOrEditTrialExternalUser.LastName || existSysUser.FirstName != addOrEditTrialExternalUser.FirstName) + { + return ResponseOutput.NotOk($"该用户在系统中账户名为:{existSysUser.LastName + " / " + existSysUser.FirstName} 电话:{existSysUser.Phone},与填写信息存在不一致项, 现将界面信息修改为与系统一致,可进行保存", new { existSysUser.LastName, existSysUser.FirstName, existSysUser.Phone }, ApiResponseCodeEnum.NeedTips); + } + + } + + //处理 生成账户 + + if (await _trialExternalUseRepository.AnyAsync(t => + t.Email == addOrEditTrialExternalUser.Email && + t.UserTypeId == addOrEditTrialExternalUser.UserTypeId && t.TrialId == addOrEditTrialExternalUser.TrialId)) + { + return ResponseOutput.NotOk("该外部用户表已存在 相同邮箱用户类型的账户"); + } + + + var addEntity = _mapper.Map(addOrEditTrialExternalUser); + + await _trialExternalUseRepository.AddAsync(addEntity); + + + var existUser = await _userRepository.FirstOrDefaultAsync(t => t.EMail == addOrEditTrialExternalUser.Email && t.UserTypeId == addOrEditTrialExternalUser.UserTypeId); + + if (existUser != null) + { + addEntity.IsSystemUser = true; + addEntity.SystemUserId = existUser.Id; + + } + else + { + //生成账户 并插入 + + var generateUser = _mapper.Map(addOrEditTrialExternalUser); + + generateUser.Code = _userRepository.Select(t => t.Code).DefaultIfEmpty().Max() + 1; + + + generateUser.UserCode = AppSettings.UserCodePrefix + generateUser.Code.ToString("D4"); + + generateUser.UserName = generateUser.UserCode; + + generateUser.UserTypeEnum = _repository.Where(t => t.Id == generateUser.UserTypeId).Select(t => t.UserTypeEnum).First(); + + generateUser.Password = MD5Helper.Md5("123456"); + + generateUser.Status = UserStateEnum.Disable; + + var newAddUser = await _repository.AddAsync(generateUser); + + + addEntity.IsSystemUser = false; + addEntity.SystemUserId = newAddUser.Id; + + + } + + await _trialExternalUseRepository.SaveChangesAsync(); + + return ResponseOutput.Ok(addEntity.Id.ToString()); + + } + else + { + return ResponseOutput.NotOk("这里不允许编辑,删除后再添加"); + + + if (await _trialExternalUseRepository.AnyAsync(t => + t.Email == addOrEditTrialExternalUser.Email && + t.UserTypeId == addOrEditTrialExternalUser.UserTypeId && t.Id != addOrEditTrialExternalUser.Id && t.TrialId == addOrEditTrialExternalUser.TrialId)) + { + return ResponseOutput.NotOk("该邮箱和用户类型,已存在该账户"); + } + + //if (addOrEditTrialExternalUser.IsSystemUser) + //{ + // return ResponseOutput.NotOk("系统账户不允许编辑"); + //} + + + var needUpdateEntity = + await _trialExternalUseRepository.FirstOrDefaultAsync(t => t.Id == addOrEditTrialExternalUser.Id); + + //更改之前 先验证是否在系统账户中存在 + + _mapper.Map(addOrEditTrialExternalUser, needUpdateEntity); + + + await _trialExternalUseRepository.SaveChangesAsync(); + + return ResponseOutput.Ok(needUpdateEntity.Id.ToString()); + + } + + + } + + + + + [HttpDelete("{trialExternalUserId:guid}/{isSystemUser:bool}/{systemUserId}")] + public async Task DeleteTrialExternalUser(Guid trialExternalUserId, bool isSystemUser, Guid systemUserId) + { + var success = await _trialExternalUseRepository.DeleteFromQueryAsync(t => t.Id == trialExternalUserId); + + if (isSystemUser == false) + { + await _userRepository.DeleteFromQueryAsync(t => t.Id == systemUserId); + } + + return ResponseOutput.Result(success); + } + + + /// + /// 勾选用户 批量发送邮件 + /// + /// + [HttpPost] + public async Task SendInviteEmail(TrialExternalUserSendEmail sendEmail) + { + + var trialInfo = await _repository.FirstOrDefaultAsync(t => t.Id == sendEmail.TrialId); + + foreach (var userInfo in sendEmail.SendUsers) + { + var messageToSend = new MimeMessage(); + //发件地址 + messageToSend.From.Add(new MailboxAddress("GRR", "iracis_grr@163.com")); + //收件地址 + messageToSend.To.Add(new MailboxAddress(String.Empty, userInfo.Email)); + //主题 + messageToSend.Subject = "GRR External User survey (Verification Code)"; + + var baseApiUrl = sendEmail.BaseUrl.Remove(sendEmail.BaseUrl.IndexOf("#")) + "api"; + + var sysUserInfo = await _userRepository.Where(t => t.Id == userInfo.SystemUserId).Include(t => t.UserTypeRole).FirstOrDefaultAsync(); + + + var builder = new BodyBuilder(); + + int verificationCode = new Random().Next(100000, 1000000); + + if (sysUserInfo.IsFirstAdd) + { + await _userRepository.UpdateFromQueryAsync(t => t.Id == sysUserInfo.Id, + u => new User() { Password = MD5Helper.Md5(verificationCode.ToString()) }); + } + + + builder.HtmlBody = @$" +
+
+
+ {sysUserInfo.LastName + "/" + sysUserInfo.FirstName}: +
+
+ 您参与的临床试验项目 {trialInfo.ExperimentName} ,独立影像评估相关工作将在网上进行。项目及账号信息为: +
+
+
+ 项目编号: {trialInfo.TrialCode} +
+
+ 试验方案号: {trialInfo.ResearchProgramNo} +
+
+ 试验名称: {trialInfo.ExperimentName} +
+
+ 用户名: {sysUserInfo.UserName} +
+
+ 密码: {(sysUserInfo.IsFirstAdd ? verificationCode.ToString()+"(请在登录后进行修改)" : "***(您已有账号, 若忘记密码, 请通过邮箱找回)")} +
+
+ 角色: {sysUserInfo.UserTypeRole.UserTypeShortName} +
+
+ 系统登录地址: {sendEmail.BaseUrl} (请确认加入后再登陆) +
+
+ + 查看并确认 + +
+
+ "; + + + messageToSend.Body = builder.ToMessageBody(); + + using (var smtp = new MailKit.Net.Smtp.SmtpClient()) + { + smtp.MessageSent += (sender, args) => + { + + _ = _trialExternalUseRepository.UpdateFromQueryAsync(t => t.Id == userInfo.Id, u => new TrialExternalUser() { InviteState = TrialExternalUserStateEnum.HasSend, ExpireTime = DateTime.Now.AddDays(7) }).Result; + + }; + + smtp.ServerCertificateValidationCallback = (s, c, h, e) => true; + + await smtp.ConnectAsync("smtp.163.com", 25, SecureSocketOptions.StartTls); + + await smtp.AuthenticateAsync("iracis_grr@163.com", "XLWVQKZAEKLDWOAH"); + + await smtp.SendAsync(messageToSend); + + await smtp.DisconnectAsync(true); + } + + + } + + return ResponseOutput.Ok(); + + } + + + + + /// + /// 不带Token 访问 用户选择 参与 不参与 Id: TrialExternalUserId + /// + /// + /// + [AllowAnonymous] + public async Task TrialExternalUserJoinTrial(TrialExternalUserConfirm editTrialUserPreparation) + { + + var needUpdate = await _trialExternalUseRepository.FirstOrDefaultAsync(t => t.Id == editTrialUserPreparation.Id); + + + if (DateTime.Now > needUpdate.ExpireTime) + { + return ResponseOutput.NotOk("邀请加入时间已过期,重新被邀请后才可以进行确认操作"); + } + + _mapper.Map(editTrialUserPreparation, needUpdate); + + needUpdate.InviteState = TrialExternalUserStateEnum.UserConfirmed; + + + var trialId = needUpdate.TrialId; + var userId = needUpdate.SystemUserId; + + //判断TrialUser中是否存在 不存在就插入 + if (!await _trialUserRepository.AnyAsync(t => t.TrialId == trialId && t.UserId == userId)) + { + + await _trialUserRepository.AddAsync(new TrialUser() { TrialId = trialId, UserId = userId }); + + await _trialExternalUseRepository.UpdateFromQueryAsync(t => t.TrialId == trialId && t.SystemUserId == userId, + u => new TrialExternalUser() { InviteState = TrialExternalUserStateEnum.UserConfirmed }); + + } + await _userRepository.UpdateFromQueryAsync(t => t.Id == userId, u => new User() { Status = UserStateEnum.Enable }); + + var success = await _trialExternalUseRepository.SaveChangesAsync(); + + return ResponseOutput.Ok(); + + } + + /// + /// 不带Token 访问 Site调研用户 加入项目 Id: TrialSiteSurveyUserId + /// + /// + /// + [AllowAnonymous] + public async Task TrialSiteSurveyUserJoinTrial(TrialExternalUserConfirm editInfo) + { + + var needUpdate = await _trialSiteSurveyUserRepository.Where(t => t.Id == editInfo.Id).Include(t=>t.TrialSiteSurvey).FirstOrDefaultAsync(); + + + if (DateTime.Now > needUpdate.ExpireTime) + { + return ResponseOutput.NotOk("邀请加入时间已过期,重新被邀请后才可以进行确认操作"); + } + + _mapper.Map(editInfo, needUpdate); + + //needUpdate.InviteState = TrialExternalUserStateEnum.UserConfirmed; + + + if (needUpdate.SystemUserId==null) + { + return ResponseOutput.NotOk("调研表系统用户Id 存储有问题"); + } + + var trialId = needUpdate.TrialSiteSurvey.TrialId; + var siteId= needUpdate.TrialSiteSurvey.SiteId; + var userId = (Guid)needUpdate.SystemUserId; + + + if (!_trialUserRepository.Where(t => t.TrialId == trialId && t.UserId == userId).Any()) + { + await _trialUserRepository.AddAsync(new TrialUser() { TrialId = trialId, UserId = userId }); + + await _trialSiteUserRepository.AddAsync(new TrialSiteUser() { TrialId = trialId, SiteId = siteId, UserId = userId }); + } + + await _userRepository.UpdateFromQueryAsync(t => t.Id == needUpdate.SystemUserId, u => new User() { Status = UserStateEnum.Enable }); + + + var success =await _trialExternalUseRepository.SaveChangesAsync(); + + return ResponseOutput.Ok(); + + } + + + + /// + /// 不带Token 访问 页面获取项目基本信息 和参与情况 (已经确认了 就不允许再次确认) Id: TrialExternalUserId/TrialSiteSurveyUserId + /// + /// + /// + [AllowAnonymous] + public async Task JoinBasicInfo(Guid id,bool isExternalUser) + { + if (isExternalUser) + { + return (await _trialExternalUseRepository.Where(t => t.Id == id) + .ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync()).IfNullThrowException(); + } + else + { + return (await _trialSiteSurveyUserRepository.Where(t => t.Id == id) + .ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync()).IfNullThrowException(); + } + + } + + + + + + /// + /// 加入项目 + /// + /// + /// + /// + [HttpGet("{trialId:guid}/{trialExternalUserId:guid}")] + [NonDynamicMethod] + public async Task UserConfirmJoinTrial(Guid trialId, Guid trialExternalUserId) + { + + var externalUser = await _trialExternalUseRepository.FirstOrDefaultAsync(t => t.Id == trialExternalUserId); + + + //判断TrialUser中是否存在 不存在就插入 + if (!await _repository.AnyAsync(t => t.TrialId == trialId && t.UserId == externalUser.SystemUserId)) + { + await _repository.AddAsync(new TrialUser() { TrialId = trialId, UserId = (Guid)externalUser.SystemUserId }); + + await _trialExternalUseRepository.UpdateFromQueryAsync(t => t.Id == trialExternalUserId, + u => new TrialExternalUser() { InviteState = TrialExternalUserStateEnum.UserConfirmed }); + + await _userRepository.UpdateFromQueryAsync(t => t.Id == externalUser.SystemUserId, u => new User() { Status = UserStateEnum.Enable }); + + await _userRepository.SaveChangesAsync(); + } + + + + return ResponseOutput.Ok(); + + + } + + + } +} diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/TrialMaintenanceService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialMaintenanceService.cs new file mode 100644 index 00000000..894a50fc --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialMaintenanceService.cs @@ -0,0 +1,247 @@ +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Domain.Share; +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Application.Contracts.DTO; +using IRaCIS.Application.Interfaces; + +namespace IRaCIS.Application.Services +{ + [ApiExplorerSettings(GroupName = "Trial")] + public class TrialMaintenanceService : BaseService, ITrialMaintenanceService + { + private readonly IRepository _trialUseRepository; + + public TrialMaintenanceService(IRepository trialUseRepository) + { + _trialUseRepository = trialUseRepository; + } + + /// + /// Setting页面 获取项目参与人员列表 + /// + /// + /// + [HttpPost] + public async Task> GetMaintenanceUserList(TrialMaintenanceQuery param) + { + + var query = _trialUseRepository.Where(t => t.TrialId == param.TrialId).IgnoreQueryFilters() + .WhereIf(!string.IsNullOrWhiteSpace(param.UserType), t => t.User.UserTypeRole.UserTypeShortName.Contains(param.UserType)) + .WhereIf(!string.IsNullOrWhiteSpace(param.UserName), t => t.User.UserName.Contains(param.UserName)) + + //.WhereIf( param.JoinTime!=null, t => t.JoinTime.to ==param.JoinTime.ToString("yyyy-MM-dd")) + //.WhereIf(param.RemoveTime!=null, t => t.RemoveTime.ToString("yyyy-MM-dd") == param.JoinTime.ToString("yyyy-MM-dd")) + + .WhereIf(!string.IsNullOrWhiteSpace(param.OrganizationName), t => t.User.OrganizationName.Contains(param.OrganizationName)) + .WhereIf(!string.IsNullOrWhiteSpace(param.UserRealName), t => (t.User.LastName + " / " + t.User.FirstName).Contains(param.UserRealName)) + .ProjectTo(_mapper.ConfigurationProvider); + + return await query.ToPagedListAsync(param.PageIndex, + param.PageSize, string.IsNullOrWhiteSpace(param.SortField) ? "UpdateTime" : param.SortField, param.Asc); + + } + + + + + + /// Setting页面 为 site 勾选CRC用户列表 + [HttpPost] + public async Task> GetSiteCRCScreeningList(SiteCRCQuery param) + { + // 最开始过滤site已经选择的用户 现在又改回去。。。 + + var query = _trialUseRepository.Where(t => t.TrialId == param.TrialId) + .Where(t => t.User.UserTypeEnum == UserTypeEnum.ClinicalResearchCoordinator) + .WhereIf(!string.IsNullOrWhiteSpace(param.UserType), t => t.User.UserTypeRole.UserTypeShortName.Contains(param.UserType)) + .WhereIf(!string.IsNullOrWhiteSpace(param.UserRealName), t => (t.User.LastName + " / " + t.User.FirstName).Contains(param.UserRealName)) + .ProjectTo(_mapper.ConfigurationProvider, new { siteId = param.SiteId }); + + return await query.ToPagedListAsync(param.PageIndex, + param.PageSize, string.IsNullOrWhiteSpace(param.SortField) ? "UpdateTime" : param.SortField, param.Asc); + + + //from userTrial in.Where(userTrialLambda) + // join user in _userRepository.Where(userLambda) on userTrial.UserId equals user.Id + // // 判断site下面的用户是否已经选择 + // join userId in _userTrialSiteRepository.Where(t => t.TrialId == param.TrialId && t.SiteId == param.SiteId).Select(t => t.UserId).Distinct() on user.Id equals userId into cc + // from userId in cc.DefaultIfEmpty() + // select new AssginSiteCRCListDTO() + // { + // //Id = userTrial.Id, + // SiteId = param.SiteId, + // UserId = userTrial.UserId, + // UserRealName = user.LastName + " / " + user.FirstName, + // UserName = user.UserName, + + // TrialId = userTrial.TrialId, + // UserTypeId = userTrial.UserTypeId, + // UserTypeEnum= user.UserTypeEnum, + + // //OrganizationTypeId = userTrial.OrganizationTypeId, + // //OrganizationType = userTrial.OrganizationType, + // //OrganizationId = userTrial.OrganizationId, + // OrganizationName = user.OrganizationName, + // UpdateTime = userTrial.UpdateTime, + // UserType = userTrial.UserType, + + // IsSelect = userId!=null, + // }; + + + } + + + /// Setting页面 项目参与人员勾选列表 + [HttpPost] + public async Task> GetTrialUserScreeningList(TrialUserQuery trialUserQuery) + { + //之前已经选择的用户 不放在列表中,现在又要改回去 废弃 + var query = _repository.Where(t => t.UserTypeEnum != UserTypeEnum.SuperAdmin) + .WhereIf(!string.IsNullOrWhiteSpace(trialUserQuery.UserRealName), t => (t.LastName + " / " + t.FirstName).Contains(trialUserQuery.UserRealName)) + .WhereIf(!string.IsNullOrWhiteSpace(trialUserQuery.UserName), t => t.UserName.Contains(trialUserQuery.UserName)) + //.WhereIf(!string.IsNullOrWhiteSpace(trialUserQuery.OrganizationName), t => t.OrganizationName.Contains(trialUserQuery.OrganizationName)) + .WhereIf(trialUserQuery.UserTypeEnum != null, t => t.UserTypeEnum == trialUserQuery.UserTypeEnum) + //.WhereIf(_userInfo.IsAdmin, t => t.UserTypeRole.Type == UserTypeGroup.TrialUser) + //.WhereIf(!_userInfo.IsAdmin, t => t.UserTypeRole.Type == UserTypeGroup.TrialUser || t.UserTypeEnum != UserTypeEnum.ProjectManager) + .ProjectTo(_mapper.ConfigurationProvider, new { trialId = trialUserQuery.TrialId }); + + return await query.ToPagedListAsync(trialUserQuery.PageIndex, + trialUserQuery.PageSize, string.IsNullOrWhiteSpace(trialUserQuery.SortField) ? "UserRealName" : trialUserQuery.SortField, trialUserQuery.Asc); + + + + + //IQueryable userQueryable = from user in _userRepository.Where(userLambda) + // join userType in _userTypeRoleRepository.AsQueryable() on user.UserTypeId equals userType.Id + // //下面左连接 判断是否已经选择了 + // join userId in _userTrialRepository.Where(t => t.TrialId == trialUserQuery.TrialId).Select(t=>t.UserId).Distinct() on user.Id equals userId into cc + // from userId in cc.DefaultIfEmpty() + // select new TrialUserScreeningDTO() + // { + // TrialId = trialUserQuery.TrialId, + // UserId = user.Id, + // UserRealName = user.LastName + " / " + user.FirstName, + // UserName = user.UserName, + // Sex = user.Sex, + + // Phone = user.Phone, + // EMail = user.EMail, + // UserTypeId = user.UserTypeId, + + // OrganizationName = user.OrganizationName, + // UserTypeEnum=user.UserTypeEnum, + // //OrganizationTypeId = user.OrganizationTypeId, + // //OrganizationType = user.OrganizationType, + // //OrganizationId = user.OrganizationId, + // DepartmentName = user.DepartmentName, + // PositionName = user.PositionName, + + // IsSelect = userId != null + // UserType = userType.UserTypeShortName, + // }; + + + + + + + } + + + /// + /// Setting页面 批量添加项目参与人员 + /// + /// + /// + //[TrialAudit(AuditType.TrialAudit, AuditOptType.AddTrialStaff)] + [HttpPost] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task AddTrialUsers(TrialUserAddCommand[] userTrialCommands) + { + var addArray = _mapper.Map(userTrialCommands); + + await _repository.AddRangeAsync(addArray); + + foreach (var item in addArray) + { + item.JoinTime = item.CreateTime; + } + + + var success = await _repository.SaveChangesAsync(); + + return ResponseOutput.Result(success); + } + + + + [TypeFilter(typeof(TrialResourceFilter))] + [HttpPut] + public async Task UpdateTrialUser(UpdateTrialUserCommand updateTrialUserCommand) + { + var trialUser = await _trialUseRepository.AsQueryable().IgnoreQueryFilters().FirstOrDefaultAsync(t => t.Id == updateTrialUserCommand.Id); + + if (trialUser == null) return Null404NotFound(trialUser); + + + if (updateTrialUserCommand.IsDeleted) + { + if (await _repository.AnyAsync(t => t.UserId == trialUser.UserId && t.TrialId == trialUser.TrialId)) + { + return ResponseOutput.NotOk("Participant has participated in site maintenance"); + } + + if (_userInfo.UserTypeEnumInt == (int)UserTypeEnum.IQC) + { + await _repository.UpdateFromQueryAsync(t => t.CurrentActionUserId == trialUser.UserId && t.TrialId == trialUser.TrialId && t.IsTake, u => new SubjectVisit() { CurrentActionUserId = null, CurrentActionUserExpireTime = null, IsTake = false }); + } + + } + + _mapper.Map(updateTrialUserCommand, trialUser); + + await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(); + + + } + + + /// 项目参与人员退出 其中IQC退出 回去释放工作量 + //[TrialAudit(AuditType.TrialAudit, AuditOptType.DeleteTrailStaff)] + [HttpDelete, Route("{id:guid}/{trialId:guid}/{isDelete:bool}")] + [TypeFilter(typeof(TrialResourceFilter))] + [UnitOfWork] + [Obsolete] + public async Task DeleteMaintenanceUser(Guid id, bool isDelete) + { + + var trialUser = await _trialUseRepository.AsQueryable().IgnoreQueryFilters().FirstOrDefaultAsync(t => t.Id == id); + + if (trialUser == null) return Null404NotFound(trialUser); + + if (await _repository.AnyAsync(t => t.UserId == trialUser.UserId && t.TrialId == trialUser.TrialId)) + { + return ResponseOutput.NotOk("Participant has participated in site maintenance"); + } + + if (_userInfo.UserTypeEnumInt == (int)UserTypeEnum.IQC && isDelete) + { + await _repository.UpdateFromQueryAsync(t => t.CurrentActionUserId == trialUser.UserId && t.TrialId == trialUser.TrialId && t.IsTake, u => new SubjectVisit() { CurrentActionUserId = null, CurrentActionUserExpireTime = null, IsTake = false }); + } + + await _trialUseRepository.UpdateFromQueryAsync(t => t.Id == id, u => new TrialUser() { IsDeleted = isDelete, RemoveTime = isDelete ? DateTime.Now : null }); + + await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(); + } + + + + } +} diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/TrialService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialService.cs new file mode 100644 index 00000000..6be1715e --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialService.cs @@ -0,0 +1,643 @@ +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Domain.Share; +using EasyCaching.Core; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Mvc; +using Panda.DynamicWebApi.Attributes; + +namespace IRaCIS.Application.Services +{ + + [ApiExplorerSettings(GroupName = "Trial")] + + public class TrialService : BaseService, ITrialService + { + + private readonly IEasyCachingProvider _provider; + private readonly IRepository _trialRepository; + public bool TrialExpeditedChange { get; set; } = false; + + + public TrialService(IEasyCachingProvider provider,IRepository trialRepository) + { + _provider = provider; + _trialRepository = trialRepository; + } + + /// + /// 分页获取临床项目列表 默认后台加急状态为3 查所有的 + /// + /// + /// + [HttpPost] + public async Task> GetTrialList(TrialQueryDTO searchParam) + { + var multiModalityIdSelectCount = searchParam.ModalityIds.Count; + var multiCriteriaSelectCount = searchParam.CriterionIds.Count; + var multiReviewTypeSelectCount = searchParam.ReviewTypeIds.Count; + + var query = _trialRepository.AsQueryable().IgnoreQueryFilters() + .WhereIf(!string.IsNullOrEmpty(searchParam.TrialStatusStr), o => o.TrialStatusStr.Contains(searchParam.TrialStatusStr)) + .WhereIf(searchParam.SponsorId != null, o => o.SponsorId == searchParam.SponsorId) + .WhereIf(searchParam.Expedited != null, o => o.Expedited == searchParam.Expedited) + .WhereIf(!string.IsNullOrEmpty(searchParam.Code), o => o.TrialCode.Contains(searchParam.Code)) + .WhereIf(!string.IsNullOrWhiteSpace(searchParam.Indication), o => o.Indication.Contains(searchParam.Indication)) + .WhereIf(!string.IsNullOrEmpty(searchParam.ResearchProgramNo), o => o.ResearchProgramNo.Contains(searchParam.ResearchProgramNo)) + .WhereIf(!string.IsNullOrWhiteSpace(searchParam.ExperimentName), o => o.ExperimentName.Contains(searchParam.ExperimentName)) + .WhereIf(searchParam.PhaseId != null, o => o.PhaseId == searchParam.PhaseId) + .WhereIf(searchParam.DeclarationTypeId != null, o => o.PhaseId == searchParam.DeclarationTypeId) + .WhereIf(searchParam.IndicationTypeId != null, o => o.PhaseId == searchParam.IndicationTypeId) + .WhereIf(searchParam.CROId != null, o => o.CROId == searchParam.CROId) + .WhereIf(searchParam.BeginDate != null, o => o.CreateTime >= searchParam.BeginDate) + .WhereIf(searchParam.EndDate != null, o => o.CreateTime <= searchParam.EndDate) + .WhereIf(searchParam.AttendedReviewerType != null, o => o.AttendedReviewerType == searchParam.AttendedReviewerType) + .WhereIf(multiModalityIdSelectCount > 0, t => t.TrialDicList.Count(t => t.KeyName == StaticData.Modality) == multiModalityIdSelectCount) + .WhereIf(multiCriteriaSelectCount > 0, t => t.TrialDicList.Count(t => t.KeyName == StaticData.Criterion) == multiCriteriaSelectCount) + .WhereIf(multiReviewTypeSelectCount > 0, t => t.TrialDicList.Count(t => t.KeyName == StaticData.ReviewType) == multiReviewTypeSelectCount) + .WhereIf(_userInfo.UserTypeEnumInt != (int)UserTypeEnum.SuperAdmin, t => t.TrialUserList.Any(t => t.UserId == _userInfo.Id)) + .WhereIf(_userInfo.UserTypeEnumInt != (int)UserTypeEnum.SuperAdmin && _userInfo.UserTypeEnumInt != (int)UserTypeEnum.ProjectManager && _userInfo.UserTypeEnumInt != (int)UserTypeEnum.APM, t => t.IsDeleted == false) + .ProjectTo(_mapper.ConfigurationProvider, new { userTypeEnumInt = _userInfo.UserTypeEnumInt, userId = _userInfo.Id }); + + return await query.ToPagedListAsync(searchParam.PageIndex, searchParam.PageSize, string.IsNullOrWhiteSpace(searchParam.SortField) ? "CreateTime" : searchParam.SortField, searchParam.Asc); + + + } + + //过滤废除的项目 + public async Task> GetTrialSelect() + { + return await _trialRepository.AsQueryable() + .WhereIf(_userInfo.UserTypeEnumInt != (int)UserTypeEnum.SuperAdmin, t => t.TrialUserList.Any(t => t.UserId == _userInfo.Id)) + .WhereIf(_userInfo.UserTypeEnumInt != (int)UserTypeEnum.SuperAdmin && _userInfo.UserTypeEnumInt != (int)UserTypeEnum.ProjectManager && _userInfo.UserTypeEnumInt != (int)UserTypeEnum.APM, t => t.IsDeleted == false) + .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + } + + + + /// + /// 获取项目基本信息 + /// + /// + /// + [HttpGet("{projectId:guid}")] + public async Task GetTrialInfoAndLockState(Guid projectId) + { + return await _trialRepository.Where(o => o.Id == projectId).IgnoreQueryFilters().ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync().IfNullThrowException(); + + } + + + [NonDynamicMethod] + public async Task GetTrialMaxState(Guid trialId) + { + return await _repository.Where(t => t.TrialId == trialId).MaxAsync(u => (int?)u.EnrollStatus) ?? 0; + } + + [HttpGet("{trialId:guid}")] + public async Task GetTrialInfoAndMaxTrialState(Guid trialId) + { + return new TrialAndTrialStateVieModel() + { + TrialView = await GetTrialInfoAndLockState(trialId), + TrialMaxState = await GetTrialMaxState(trialId) + }; + } + + + [HttpGet("{trialId:guid}")] + public async Task GetTrialExpeditedState(Guid trialId) + { + var trial = await _trialRepository.FirstOrDefaultAsync(u => u.Id == trialId).IfNullThrowConvertException(); + + return trial.Expedited; + } + + [HttpPut("{trialId:guid}/{isAbandon:bool}")] + public async Task AbandonTrial(Guid trialId, bool isAbandon) + { + + await _trialRepository.UpdateFromQueryAsync(t => t.Id == trialId, u => new Trial() { IsDeleted = isAbandon }); + + return ResponseOutput.Ok(); + } + + + + /// + /// 添加项目 + /// + /// + /// + [NonDynamicMethod] + public virtual async Task AddOrUpdateTrial(TrialCommand trialAddModel) + { + + + // 到时候 策略授权 统一改 归类 + if (!(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.APM || _userInfo.UserTypeEnumInt == (int)UserTypeEnum.ProjectManager || _userInfo.UserTypeEnumInt == (int)UserTypeEnum.SuperAdmin)) + { + return ResponseOutput.NotOk("仅PM/APM可以操作!"); + } + + if (trialAddModel.Id == Guid.Empty || trialAddModel.Id == null) + { + if (await _trialRepository.AnyAsync(u => u.TrialCode == trialAddModel.TrialCode)) + { + return ResponseOutput.NotOk("Same Trial ID already exists."); + } + + var dbMaxCode = await _trialRepository.Where(t => t.CreateTime.Year == DateTime.Now.Year && t.TrialType == trialAddModel.TrialType).Select(t => t.Code).DefaultIfEmpty().MaxAsync(); + + var currentYearMaxCodeNext = dbMaxCode + 1; + + //var test = _trialRepository.Where(t => t.CreateTime.Year == DateTime.Now.Year + 1).Select(t => t.Code).DefaultIfEmpty(1).ToList(); + + var trial = _mapper.Map(trialAddModel); + var yearStr = DateTime.Now.Year.ToString(); + + trial.Code = currentYearMaxCodeNext; + trial.TrialCode = (trial.TrialType == 1 ? yearStr.Substring(yearStr.Length - 2) : "T0") + trial.TrialCode + currentYearMaxCodeNext.ToString("D3"); + + + //多选信息 + trialAddModel.ModalityIds.ForEach(modalityId => trial.TrialDicList.Add(new TrialDictionary() { DictionaryId = modalityId, KeyName = StaticData.Modality, TrialId = trial.Id })); + trialAddModel.CriterionIds.ForEach(criterionId => trial.TrialDicList.Add(new TrialDictionary() { DictionaryId = criterionId, KeyName = StaticData.Criterion, TrialId = trial.Id })); + trialAddModel.ReviewTypeIds.ForEach(ReviewTypeId => trial.TrialDicList.Add(new TrialDictionary() { DictionaryId = ReviewTypeId, KeyName = StaticData.ReviewType, TrialId = trial.Id })); + + //添加项目后 项目状态变更为申请下载简历 + trial.TrialEnrollStatus = (int)TrialEnrollStatus.ChooseDoctor; + //trial.TrialStatusStr = StaticData.TrialInitializing; + + //状态变更详细表 + trial.ClinicalTrialProjectDetails.Add(new TrialStatusDetail() { TrialId = trial.Id, TrialStatus = (int)TrialEnrollStatus.ChooseDoctor }); + + trial = await _repository.AddAsync(trial); + + //如果是PM, 则需要将该人员添加到 运维人员表 + //添加运维人员PM + await _repository.AddAsync(new TrialUser() { TrialId = trial.Id, UserId = _userInfo.Id }); + + // 添加扩展信息表记录 + await _repository.AddAsync(new TrialPaymentPrice() { TrialId = trial.Id }); + + //添加访视 + await _repository.AddAsync(new VisitStage { TrialId = trial.Id, VisitNum = 0, BlindName = "B" + 0.ToString("D3"), VisitDay = 1, VisitName = "Baseline", IsBaseLine = true }); + await _repository.AddAsync(new VisitStage { TrialId = trial.Id, VisitNum = 1, BlindName = "B" + 10.ToString("D3"), VisitDay = 30, VisitName = "Visit 1" }); + + + var success = await _repository.SaveChangesAsync(); + + _provider.Set(trial.Id.ToString(), StaticData.TrialInitializing, TimeSpan.FromDays(7)); + + return ResponseOutput.Result(success, new { Id = trial.Id, TrialCode = trial.TrialCode }); + } + else + { + var updateModel = trialAddModel; + + if (!await _repository.AnyAsync(u => u.Id == trialAddModel.Id && (u.TrialStatusStr == StaticData.TrialInitializing || u.TrialStatusStr == StaticData.TrialOngoing))) + { + return ResponseOutput.NotOk("项目初始化或者进行中时 才允许操作"); + } + // 判断项目Id 是否已经存在 + if (await _repository.AnyAsync(u => u.TrialCode == updateModel.TrialCode && u.Id != updateModel.Id)) + { + return ResponseOutput.NotOk("Same Trial ID already exists."); + + } + + + + var trial = await _repository.FirstOrDefaultAsync(t => t.Id == updateModel.Id); + + + //删除中间表 Title对应的记录 + await _repository.DeleteFromQueryAsync(t => t.TrialId == updateModel.Id); + + //重新插入新的 Title记录 + + updateModel.ModalityIds.ForEach(modalityId => trial.TrialDicList.Add(new TrialDictionary() { DictionaryId = modalityId, KeyName = StaticData.Modality, TrialId = trial.Id })); + updateModel.CriterionIds.ForEach(criterionId => trial.TrialDicList.Add(new TrialDictionary() { DictionaryId = criterionId, KeyName = StaticData.Criterion, TrialId = trial.Id })); + updateModel.ReviewTypeIds.ForEach(ReviewTypeId => trial.TrialDicList.Add(new TrialDictionary() { DictionaryId = ReviewTypeId, KeyName = StaticData.ReviewType, TrialId = trial.Id })); + + + if (updateModel.Expedited != trial.Expedited && updateModel.Expedited != null) + { + TrialExpeditedChange = true; + await TrialExpeditedStatusChange(trial.Id, trial.Expedited, (int)updateModel.Expedited); + } + _mapper.Map(updateModel, trial); + + var success = await _repository.SaveChangesAsync(); + + return ResponseOutput.Result(success); + + + } + } + + + + + #region MyRegion + ///// + ///// 添加项目 + ///// + ///// + ///// + //[NonDynamicMethod] + //public virtual async Task AddOrUpdateTrial(TrialCommand trialAddModel) + //{ + + // if (trialAddModel.Id == Guid.Empty || trialAddModel.Id == null) + // { + // if (await _repository.AnyAsync(u => u.TrialCode == trialAddModel.TrialCode)) + // { + // return ResponseOutput.NotOk("Same Trial ID already exists."); + // } + + // var currentYearMaxCodeNext = await _repository.Where(t => t.CreateTime.Year == DateTime.Now.Year).Select(t => t.Code).DefaultIfEmpty().MaxAsync() + 1; + + // //var test = _trialRepository.Where(t => t.CreateTime.Year == DateTime.Now.Year + 1).Select(t => t.Code).DefaultIfEmpty(1).ToList(); + + // var trial = _mapper.Map(trialAddModel); + // var yearStr = DateTime.Now.Year.ToString(); + + // trial.Code = currentYearMaxCodeNext; + // trial.TrialCode = (trial.TrialType == 1 ? yearStr.Substring(yearStr.Length - 2) : "T0") + trial.TrialCode + currentYearMaxCodeNext.ToString("D3"); + + + // //多选信息 + // trialAddModel.ModalityIds.ForEach(modalityId => trial.TrialDicList.Add(new TrialDictionary() { DictionaryId = modalityId, KeyName = StaticData.Modality, TrialId = trial.Id })); + // trialAddModel.CriterionIds.ForEach(criterionId => trial.TrialDicList.Add(new TrialDictionary() { DictionaryId = criterionId, KeyName = StaticData.Criterion, TrialId = trial.Id })); + + // //添加项目后 项目状态变更为申请下载简历 + // trial.TrialStatus = (int)TrialEnrollStatus.ChooseDoctor; + // trial.TrialStatusStr = StaticData.TrialOngoing; + + // //状态变更详细表 + // trial.ClinicalTrialProjectDetails.Add(new TrialStatusDetail() { TrialId = trial.Id, TrialStatus = (int)TrialEnrollStatus.ChooseDoctor }); + + // trial = await _repository.AddAsync(trial); + // //如果是PM, 则需要将该人员添加到 运维人员表 + + + // //添加运维人员PM + // await _repository.AddAsync(new TrialUser() { TrialId = trial.Id, UserId = _userInfo.Id }); + + // // 添加扩展信息表记录 + // await _repository.AddAsync(new TrialPaymentPrice() { TrialId = trial.Id }); + + // //添加访视 + // await _repository.AddAsync(new VisitStage { TrialId = trial.Id, VisitNum = 0, BlindName = "B" + 0.ToString("D3"), VisitDay = 1, VisitName = "Baseline", IsBaseLine = true }); + // await _repository.AddAsync(new VisitStage { TrialId = trial.Id, VisitNum = 1, BlindName = "B" + 10.ToString("D3"), VisitDay = 30, VisitName = "Visit 1" }); + + + // var success = await _repository.SaveChangesAsync(); + + // return ResponseOutput.Result(success, new { Id = trial.Id, TrialCode = trial.TrialCode }); + // } + // else + // { + // var updateModel = trialAddModel; + // // 判断项目Id 是否已经存在 + // if (await _repository.AnyAsync(u => u.TrialCode == updateModel.TrialCode && u.Id != updateModel.Id)) + // { + // return ResponseOutput.NotOk("Same Trial ID already exists."); + + // } + + // var trial = await _repository.FirstOrDefaultAsync(t => t.Id == updateModel.Id); + + + // //删除中间表 Title对应的记录 + // await _repository.DeleteFromQueryAsync(t => t.TrialId == updateModel.Id); + + // //重新插入新的 Title记录 + // updateModel.ModalityIds.ForEach(async modalityId => await _repository.AddAsync(new TrialDictionary() { TrialId = updateModel.Id.Value, KeyName = StaticData.Modality, DictionaryId = modalityId })); + + // updateModel.CriterionIds.ForEach(async criterionId => await _repository.AddAsync(new TrialDictionary() { TrialId = updateModel.Id.Value, KeyName = StaticData.Criterion, DictionaryId = criterionId })); + + // if (updateModel.Expedited != trial.Expedited && updateModel.Expedited != null) + // { + // TrialExpeditedChange = true; + // await TrialExpeditedStatusChange(trial.Id, trial.Expedited, (int)updateModel.Expedited); + // } + // _mapper.Map(updateModel, trial); + + // var success = await _repository.SaveChangesAsync(); + + // return ResponseOutput.Result(success); + + + // } + //} + + #endregion + + + + + + // TODO: 需要优化,嵌套两层 switch case ? + [NonDynamicMethod] + private async Task TrialExpeditedStatusChange(Guid trialId, int oldState, int newState) + { + switch (oldState) + { + case (int)TrialExpedited.None: + switch (newState) + { + case (int)TrialExpedited.ExpeditedIn24H: + await _repository.UpdateFromQueryAsync(t => t.IsLock == false && t.TrialId == trialId, u => new Workload() + { + TimepointIn24H = u.Timepoint, + AdjudicationIn24H = u.Adjudication, + Timepoint = 0, + Adjudication = 0 + }); + break; + case (int)TrialExpedited.ExpeditedIn48H: + await _repository.UpdateFromQueryAsync(t => t.IsLock == false && t.TrialId == trialId, u => new Workload() + { + TimepointIn48H = u.Timepoint, + AdjudicationIn48H = u.Adjudication, + Timepoint = 0, + Adjudication = 0 + }); + break; + } + //_workloadRepository.Update(t => t.IsLock == false && t.TrialId == trialId, u => new Workload() + //{ + // Timepoint = 0, + // Adjudication = 0 + //}); + + break; + case (int)TrialExpedited.ExpeditedIn24H: + + switch (newState) + { + case (int)TrialExpedited.None: + await _repository.UpdateFromQueryAsync(t => t.IsLock == false && t.TrialId == trialId, u => new Workload() + { + Timepoint = u.TimepointIn24H, + Adjudication = u.AdjudicationIn24H, + TimepointIn24H = 0, + AdjudicationIn24H = 0 + }); + break; + case (int)TrialExpedited.ExpeditedIn48H: + await _repository.UpdateFromQueryAsync(t => t.IsLock == false && t.TrialId == trialId, u => new Workload() + { + TimepointIn48H = u.TimepointIn24H, + AdjudicationIn48H = u.AdjudicationIn24H, + TimepointIn24H = 0, + AdjudicationIn24H = 0 + }); + + break; + } + + //_workloadRepository.Update(t => t.IsLock == false && t.TrialId == trialId, u => new Workload() + //{ + // TimepointIn24H = 0, + // AdjudicationIn24H = 0 + //}); + + break; + case (int)TrialExpedited.ExpeditedIn48H: + switch (newState) + { + case (int)TrialExpedited.None: + await _repository.UpdateFromQueryAsync(t => t.IsLock == false && t.TrialId == trialId, u => new Workload() + { + Timepoint = u.TimepointIn48H, + Adjudication = u.AdjudicationIn48H, + TimepointIn48H = 0, + AdjudicationIn48H = 0 + }); + break; + case (int)TrialExpedited.ExpeditedIn24H: + await _repository.UpdateFromQueryAsync(t => t.IsLock == false && t.TrialId == trialId, u => new Workload() + { + TimepointIn24H = u.TimepointIn48H, + AdjudicationIn24H = u.AdjudicationIn48H, + TimepointIn48H = 0, + AdjudicationIn48H = 0 + }); + break; + } + + //_workloadRepository.Update(t => t.IsLock == false && t.TrialId == trialId, u => new Workload() + //{ + // TimepointIn48H = 0, + // AdjudicationIn48H = 0 + //}); + break; + } + } + + /// + /// 手动更新项目状态 + /// + /// 项目Id + /// 状态值 + /// + + [HttpPost("{trialId:guid}/{statusStr}")] + [TypeFilter(typeof(TrialResourceFilter))] + public virtual async Task UpdateTrialStatus(Guid trialId, string statusStr) + { + //Paused、 添加工总量 算医生读片中 + if (statusStr.Contains(StaticData.TrialCompleted) || statusStr.Contains(StaticData.TrialStopped)) + { + await _repository.UpdateFromQueryAsync(u => u.TrialId == trialId, e => new Enroll + { + EnrollStatus = (int)EnrollStatus.Finished + }); + } + + if (statusStr.Contains(StaticData.TrialCompleted)) + { + await _trialRepository.UpdateFromQueryAsync(u => u.Id == trialId, s => new Trial { TrialFinishedTime = DateTime.UtcNow.AddHours(8) }); + } + + //if (statusStr.Contains(StaticData.TrialOngoing) || statusStr.Contains(StaticData.TrialPaused)) + //{ + // _enrollRepository.Update(u => u.TrialId == trialId, e => new Enroll + // { + // EnrollStatus = (int)EnrollStatus.DoctorReading + // }); + //} + + + await _provider.SetAsync(trialId.ToString(), statusStr, TimeSpan.FromDays(7)); + return ResponseOutput.Result(await _repository.UpdateFromQueryAsync(u => u.Id == trialId, s => new Trial { TrialStatusStr = statusStr })); + + } + + /// 删除临床项目 + /// 临床试验项目Id + + [HttpDelete, Route("{trialId:guid}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task DeleteTrial(Guid trialId) + { + + + var trial = await _trialRepository.FirstOrDefaultAsync(u => u.Id == trialId); + if (trial == null) return Null404NotFound(trial); + + if (trial.VisitPlanConfirmed) + { + return ResponseOutput.NotOk("Trial访视计划已经确认,无法删除"); + } + + if (await _repository.AnyAsync(u => u.TrialId == trialId)) + { + return ResponseOutput.NotOk("该Trial有医生入组或在入组流程中,无法删除"); + } + + if (await _repository.AnyAsync(u => u.TrialId == trialId)) + { + return ResponseOutput.NotOk("该Trial下面有Site,无法删除"); + } + + //PM 可以删除项目 仅仅在没有site 参与者只有他自己的时候 + if (_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ProjectManager) + { + //参与者仅有他自己时,可以删除 + if (await _repository.GetQueryable().CountAsync(t => t.TrialId == trialId) == 1) + { + + var success1 = await _repository.DeleteFromQueryAsync(o => o.Id == trialId) || + await _repository.DeleteFromQueryAsync(t => t.TrialId == trialId) || + await _repository.DeleteFromQueryAsync(t => t.TrialId == trialId); + + return ResponseOutput.Result(success1); + } + } + + if (await _repository.AnyAsync(u => u.TrialId == trialId)) + { + return ResponseOutput.NotOk("该Trial下面有参与者,无法删除"); + } + var success = await _repository.DeleteFromQueryAsync(o => o.Id == trialId) || + await _repository.DeleteFromQueryAsync(t => t.TrialId == trialId) || + await _repository.DeleteFromQueryAsync(t => t.TrialId == trialId) || + await _repository.DeleteFromQueryAsync(t => t.TrialId == trialId); + + + return ResponseOutput.Result(success); + } + + + + [HttpPost] + public async Task> GetReviewerTrialListByEnrollmentStatus(TrialByStatusQueryDTO param) + { + + var query = _trialRepository.AsQueryable() + .WhereIf(param.Status == 5, t => t.EnrollList.Any(u => u.EnrollStatus == (int)EnrollStatus.HasCommittedToCRO)) + .WhereIf(param.Status == 8, t => t.EnrollList.Any(u => u.EnrollStatus == (int)EnrollStatus.InviteIntoGroup)) + .WhereIf(param.Status == 10, t => t.EnrollList.Any(u => u.EnrollStatus == (int)EnrollStatus.DoctorReading)) + .WhereIf(param.Status == 14, t => t.EnrollList.Any(u => u.EnrollStatus == (int)EnrollStatus.Finished)) + .ProjectTo(_mapper.ConfigurationProvider, new { userTypeEnumInt = _userInfo.UserTypeEnumInt, userId = _userInfo.Id }); + + return await query.ToPagedListAsync(param.PageIndex, param.PageSize, string.IsNullOrWhiteSpace(param.SortField) ? "CreateTime" : param.SortField, param.Asc); + + } + + /// + /// 根据项目Id 获取医生Id,用于出发计算费用 + /// + public async Task> GetTrialEnrollmentReviewerIds(Guid trialId) + { + return await _repository.Where(u => u.TrialId == trialId && + u.EnrollStatus >= (int)EnrollStatus.DoctorReading).Select(u => u.DoctorId).Distinct().ToListAsync(); + } + + [HttpPost("{trialId:guid}/{confirmOrCancel:bool}")] + [TrialAudit(AuditType.TrialAudit, AuditOptType.ConfirmTrialVisitPlan)] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task ConfirmTrialVisitPlan(Guid trialId, bool confirmOrCancel = true) + { + + //取消的时候要做验证 确认的话,前端操作多次也没关系 + if (!confirmOrCancel) + { + if (await _repository.AnyAsync(t => t.TrialId == trialId)) + { + return ResponseOutput.NotOk("VisitPlan has been generated for some subjects,can not revert editable state"); + } + } + + await _trialRepository.UpdateFromQueryAsync(u => u.Id == trialId, t => new Trial() { VisitPlanConfirmed = confirmOrCancel }); + + await _repository.UpdateFromQueryAsync(u => u.Id == trialId, t => new VisitStage() { IsConfirmed = true }); + + + return ResponseOutput.Ok(); + } + + + #region 医生用户接口 + + /// 分页获取医生参与的临床实验项目列表(查询条件) + /// + [HttpPost] + public async Task> GetTrialListByReviewer(ReviewerTrialQueryDTO searchModel) + { + var query = _trialRepository + .WhereIf(searchModel.EnrollStatus != null, o => (int)searchModel.EnrollStatus! == 10 ? o.EnrollList.Any(o => o.EnrollStatus >= 10 && o.EnrollStatus <= 13 && o.DoctorId == _userInfo.Id) : o.EnrollList.Any(o => o.EnrollStatus == searchModel.EnrollStatus && o.DoctorId == _userInfo.Id)) + .WhereIf(searchModel.Expedited != null, o => o.Expedited == searchModel.Expedited) + .WhereIf(!string.IsNullOrEmpty(searchModel.Code), o => o.TrialCode.Contains(searchModel.Code)) + .WhereIf(!string.IsNullOrWhiteSpace(searchModel.Indication), o => o.Indication.Contains(searchModel.Indication)) + .WhereIf(_userInfo.UserTypeEnumInt != (int)UserTypeEnum.SuperAdmin, t => t.TrialUserList.Any(t => t.UserId == _userInfo.Id)) + .ProjectTo(_mapper.ConfigurationProvider, new { userTypeEnumInt = _userInfo.UserTypeEnumInt, userId = _userInfo.Id }); + + + return await query.ToPagedListAsync(searchModel.PageIndex, searchModel.PageSize, string.IsNullOrWhiteSpace(searchModel.SortField) ? "CreateTime" : searchModel.SortField, searchModel.Asc); + + + + } + + + /// + /// 医生确认入组或拒绝入组 + /// + /// 项目Id + /// 9-拒绝入组,10-确认入组 + /// + [HttpPost("{trialId:guid}/{status:int}")] + + [TypeFilter(typeof(TrialResourceFilter))] + public async Task UpdateEnrollStatus(Guid trialId, int status) + { + await _repository.AddAsync(new EnrollDetail() + { + DoctorId = _userInfo.Id, + TrialId = trialId, + EnrollStatus = status, + OptUserType = (int)SystemUserType.DoctorUser, + }); + return ResponseOutput.Result(await _repository.UpdateFromQueryAsync(u => u.TrialId == trialId && u.DoctorId == _userInfo.Id, e => new Enroll + { + EnrollStatus = status + })); + } + + #endregion + + + + + + + } + +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/TrialSiteService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialSiteService.cs new file mode 100644 index 00000000..0e9f8f9d --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialSiteService.cs @@ -0,0 +1,330 @@ +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infrastructure.ExpressionExtend; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Application.Contracts.DTO; +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Application.Interfaces; + +namespace IRaCIS.Core.Application.Services +{ + [ApiExplorerSettings(GroupName = "Trial")] + public class TrialMaintenanceService : BaseService, ITrialSiteService + { + private readonly IRepository _trialSiteRepository; + + public TrialMaintenanceService(IRepository trialSiteRepository) + { + _trialSiteRepository = trialSiteRepository; + } + + /// Pannel 进去 SiteTab + [HttpPost] + public async Task> GetSiteCRCList(SiteCrcQueryDTO param) + { + + var siteStatQuery = _trialSiteRepository.Where(t => t.TrialId == param.TrialId) + .WhereIf(!string.IsNullOrWhiteSpace(param.SiteName), t => t.Site.SiteName.Contains(param.SiteName)) + .WhereIf(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ClinicalResearchCoordinator, t => t.CRCUserList.Any(k => k.UserId == _userInfo.Id)) + .WhereIf(!string.IsNullOrWhiteSpace(param.UserRealName), t => t.CRCUserList.Any(k => (k.User.LastName + " / " + k.User.FirstName).Contains(param.UserRealName))) + + .ProjectTo(_mapper.ConfigurationProvider); + + #region 统计 废弃 + //= from trialSite in + // join site in _siteRepository.Where() on trialSite.SiteId equals site.Id + // join userTrialStat in _userTrialSiteRepository + // .Where(t => t.TrialId == param.TrialId).GroupBy(u => u.SiteId) + // .Select(g => new { SiteId = g.Key, UserCount = g.Count() }) on site.Id equals userTrialStat.SiteId into kk + // from userTrialStat in kk.DefaultIfEmpty() + + // join subjectStat in _subjectRepository.Where(t => t.TrialId == param.TrialId).GroupBy(u => u.SiteId) + // .Select(g => new { SiteId = g.Key, SubjectCount = g.Count() }) on site.Id equals subjectStat + // .SiteId + // into ST + // from subjectStatItem in ST.DefaultIfEmpty() + + // ////加入 Visit统计 通过site下的受试者过滤visit 加入siteId 后修改 不用通过site下的用户过滤 + // //join subjectVisitStat in _subjectVisitRepository.Find(t=>t.TrialId==param.TrialId).GroupBy(t => t.SiteId).Select(g => new { SiteId = g.Key, VisitCount = g.Count() }) on site.Id equals subjectVisitStat.SiteId into SSV + // //from subjectVisitStatItem in SSV.DefaultIfEmpty() + + // //join subjectVisitStat2 in _subjectVisitRepository.Find(t => t.TrialId == param.TrialId&&t.VisitExecuted).GroupBy(t => t.SiteId).Select(g => new { SiteId = g.Key, VisitCount = g.Count() }) on site.Id equals subjectVisitStat2.SiteId into SSV2 + // //from subjectVisitStatItem2 in SSV2.DefaultIfEmpty() + + // join studyStat in _studyRepository.Where(t => t.TrialId == param.TrialId /*&& t.Status != (int)StudyStatus.Abandon*/).GroupBy(u => u.SiteId).Select(g => new { SiteId = g.Key, StudyCount = g.Count() }) on site.Id equals studyStat.SiteId into STT + // from studyStatItem in STT.DefaultIfEmpty() + // select new SiteStatDTO() + // { + // SiteId = site.Id, + // Site = site.SiteName, + // City = site.City, + // Country = site.Country, + + // TrialSiteCode = trialSite.TrialSiteCode, + + + // //VisitCount = subjectVisitStatItem2.VisitCount, + // //PlanVisitCount = subjectVisitStatItem.VisitCount, + + + // StudyCount = studyStatItem.StudyCount, + // UserCount = userTrialStat.UserCount, + // SubjectCount = subjectStatItem.SubjectCount, + + // UserList = trialSite.CRCUserList.Select(t => new UserTrialDTO() + // { + // Id = t.Id, + // SiteId = t.SiteId, + + // UserId = t.UserId, + // UserRealName = t.UserRealName, + + // TrialId = t.TrialId, + // UserTypeId = t.UserTypeId, + // UserType = t.UserType, + // //OrganizationTypeId = userTrial.OrganizationTypeId, + // //OrganizationType = userTrial.OrganizationType, + // //OrganizationId = userTrial.OrganizationId, + // OrganizationName = t.User.OrganizationName, + // UserName = t.User.UserName, + // Phone = t.User.Phone, + // UpdateTime = t.UpdateTime, + // }).ToList() + // }; + #endregion + + + return await siteStatQuery.ToPagedListAsync(param.PageIndex, + param.PageSize, string.IsNullOrWhiteSpace(param.SortField) ? "SiteCode" : param.SortField, param.Asc); + + + } + + + /// [new] setting页面Site列表,和getSiteCRCList对比 没有统计数据,增加了一些site信息 + [HttpPost] + public async Task> GetSiteCRCSimpleList(SiteCrcQueryDTO param) + { + + var siteStatQuery = _trialSiteRepository.Where(t => t.TrialId == param.TrialId) + .WhereIf(!string.IsNullOrWhiteSpace(param.SiteName), t => t.Site.SiteName.Contains(param.SiteName)) + .WhereIf(!string.IsNullOrWhiteSpace(param.TrialSiteAliasName), t => t.TrialSiteAliasName.Contains(param.TrialSiteAliasName)) + .WhereIf(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ClinicalResearchCoordinator, t => t.CRCUserList.Any(k => k.UserId == _userInfo.Id)) + .WhereIf(!string.IsNullOrWhiteSpace(param.UserRealName), t => t.CRCUserList.Any(k => (k.User.LastName + " / " + k.User.FirstName).Contains(param.UserRealName))) + + .ProjectTo(_mapper.ConfigurationProvider); + + #region 废弃 + //IQueryable siteStatQuery = from trialSite in _trialSiteRepository.Where(t => t.TrialId == param.TrialId) + // join site in _siteRepository.AsQueryable() on trialSite.SiteId equals site.Id + // join hospital in _hospitalRepository.AsQueryable() on site.HospitalId equals hospital.Id + // join userTrialStat in _userTrialSiteRepository + // .Where(t => t.TrialId == param.TrialId).GroupBy(u => u.SiteId) + // .Select(g => new { SiteId = g.Key, UserCount = g.Count() }) on site.Id equals userTrialStat.SiteId into kk + // from userTrialStat in kk.DefaultIfEmpty() + // select new SiteStatSimpleDTO() + // { + // Id = trialSite.Id, + // SiteId = site.Id, + // Site = site.SiteName, + // SiteCode = site.SiteCode, + // TrialSiteCode = trialSite.TrialSiteCode, + // Hospital = hospital.HospitalName, + // DirectorName = site.DirectorName, + // DirectorPhone = site.DirectorPhone, + // ContactName = site.ContactName, + // ContactPhone = site.ContactPhone, + // City = site.City, + // Country = site.Country, + // UpdateTime = trialSite.UpdateTime, + // UserCount = userTrialStat.UserCount, + + // UserList = trialSite.CRCUserList.Select(t => new UserTrialDTO() + // { + // Id = t.Id, + // SiteId = t.SiteId, + + // UserId = t.UserId, + // UserRealName = t.UserRealName, + + // TrialId = t.TrialId, + // UserTypeId = t.UserTypeId, + // UserType = t.UserType, + // //OrganizationTypeId = userTrial.OrganizationTypeId, + // //OrganizationType = userTrial.OrganizationType, + // //OrganizationId = userTrial.OrganizationId, + // OrganizationName = t.User.OrganizationName, + // UserName = t.User.UserName, + // Phone = t.User.Phone, + // UpdateTime = t.UpdateTime, + // }).ToList() + // }; + + + #endregion + + + var result = await siteStatQuery.ToPagedListAsync(param.PageIndex, + param.PageSize, string.IsNullOrWhiteSpace(param.SortField) ? "Site" : param.SortField, param.Asc); + + return result; + } + + + /// 获取某一Site下面的负责的CRC列表 + [HttpGet, Route("{trialId:guid}/{siteId:guid}")] + public async Task> GetTrialSiteCRCList(Guid trialId,Guid siteId) + { + var query = _repository.Where(t => t.TrialId == trialId && t.SiteId==siteId).IgnoreQueryFilters() + .ProjectTo(_mapper.ConfigurationProvider); + + return await query.ToListAsync(); + } + + + /// [new] Setting页面 Site勾选列表( + [HttpPost] + public async Task> GetTrialSiteScreeningList(TrialSiteQuery trialSiteQuery) + { + // 之前选择了的不能再次出现在列表,做的时候我就不建议这样搞,搞好了 现在又要改回去。。。 瞎折腾。。。。 + + + var siteQueryable = _repository.GetQueryable() + .WhereIf(!string.IsNullOrWhiteSpace(trialSiteQuery.SiteName.Trim()), t => t.SiteName.Contains(trialSiteQuery.SiteName.Trim())) + .ProjectTo(_mapper.ConfigurationProvider, new { trialId = trialSiteQuery.TrialId }); + + + return await siteQueryable.ToPagedListAsync(trialSiteQuery.PageIndex, + trialSiteQuery.PageSize, string.IsNullOrWhiteSpace(trialSiteQuery.SortField) ? "SiteName" : trialSiteQuery.SortField, trialSiteQuery.Asc); + + } + + + + + /// Setting页面 Site批量添加 + [HttpPost] + [UnitOfWork] + [TrialAudit(AuditType.TrialAudit, AuditOptType.AddTrialSite)] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task AddTrialSites(List trialSites) + { + var addArray = _mapper.Map>(trialSites); + + await _repository.AddRangeAsync(addArray); + + + return ResponseOutput.Result(await _repository.SaveChangesAsync()); + } + + + /// + /// 编辑项目中Site 基本信息 + /// + /// + /// + /// + /// + /// + [UnitOfWork] + [HttpPost("{trialId:guid}/{id:guid}/{trialSiteCode}/{trialSiteAliasName?}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task EditTrialSite(Guid id, Guid trialId, string trialSiteCode, string? trialSiteAliasName) + { + + if (await _trialSiteRepository.AnyAsync(t => t.Id != id && t.TrialSiteCode == trialSiteCode && t.TrialId == trialId)) + { + return ResponseOutput.NotOk("Code is not allowed to be repeated"); + } + + await _trialSiteRepository.UpdateFromQueryAsync(t => t.Id == id, u => new TrialSite() { TrialSiteCode = trialSiteCode, TrialSiteAliasName = trialSiteAliasName ?? String.Empty }); + await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(); + } + + + /// 删除 项目 下某一site + [HttpDelete("{id:guid}/{trialId:guid}")] + [TrialAudit(AuditType.TrialAudit, AuditOptType.DeleteTrialSite)] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task DeleteTrialSite(Guid id) + { + var relation = await _trialSiteRepository.FirstOrDefaultAsync(t => t.Id == id); + + if (relation == null) return Null404NotFound(relation); + + var trialId = relation.TrialId; + var siteId = relation.SiteId; + + if (await _repository.AnyAsync(t => t.TrialId == trialId && t.SiteId == siteId)) + { + return ResponseOutput.NotOk("The site has been associated with CRC, and couldn't be deleted."); + } + + if (await _repository.AnyAsync(t => t.SiteId == siteId && t.TrialId == trialId)) + { + return ResponseOutput.NotOk("The subjects has been added to this site, and couldn't be deleted."); + } + if (await _repository.AnyAsync(t => t.SiteId == siteId && t.TrialId == trialId)) + { + return ResponseOutput.NotOk("The site has been uploaded study, and couldn't be deleted."); + } + + await _repository.DeleteAsync(relation); + + return ResponseOutput.Result(await _repository.SaveChangesAsync()); + } + + + + /// 批量添加Site下 CRC的负责人 + [HttpPost] + [TrialAudit(AuditType.TrialAudit, AuditOptType.AddTrialSiteCRC)] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task AssignSiteCRC(List trialSiteCRCList) + { + var addArray = _mapper.Map>(trialSiteCRCList); + + await _repository.AddRangeAsync(addArray); + + return ResponseOutput.Result(await _repository.SaveChangesAsync()); + } + + /// 删除CRC人员 + [HttpDelete, Route("{id:guid}/{trialId:guid}/{isDelete:bool}")] + + //[TrialAudit(AuditType.TrialAudit, AuditOptType.DeleteTrialSiteCRC)] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task DeleteSiteCRC(Guid id, bool isDelete ) + { + var isSuccess = await _repository.UpdateFromQueryAsync(u => u.Id == id,u=>new TrialSiteUser() + { IsDeleted = isDelete, RemoveTime = isDelete ? DateTime.Now : null }); + + return ResponseOutput.Result(isSuccess); + + } + + /// + /// 获取项目下的 site 下拉框数据 CRC只看到他负责的 + /// + /// + /// + [HttpGet("{trialId:guid}")] + public async Task> GetTrialSiteSelect(Guid trialId) + { + //CRC只看到他负责的 + + var list = await _trialSiteRepository.Where(t => t.TrialId == trialId) + .WhereIf(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ClinicalResearchCoordinator, t => t.CRCUserList.Any(t => t.UserId == _userInfo.Id)) + .ProjectTo(_mapper.ConfigurationProvider).OrderBy(t => t.TrialSiteCode).ToListAsync(); + + + return list; + } + + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/TrialUserPreparationService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialUserPreparationService.cs new file mode 100644 index 00000000..b6daeece --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialUserPreparationService.cs @@ -0,0 +1,136 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-03-24 13:22:54 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using IRaCIS.Core.Domain.Models; +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Application.Interfaces; +using IRaCIS.Core.Application.ViewModel; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Authorization; +using Panda.DynamicWebApi.Attributes; + +namespace IRaCIS.Core.Application.Service +{ + /// + /// TrialUserPreparation Service + /// + [ApiExplorerSettings(GroupName = "Trial")] + public class TrialUserPreparationService : BaseService, ITrialUserPreparationService + { + private readonly IRepository _trialUserPreparationRepository; + private readonly IRepository _trialExternalUseRepository; + private readonly IRepository _userRepository; + private readonly IRepository _trialUserRepository; + private readonly IRepository _trialSiteUserSurveyRepository; + private readonly IRepository _trialSiteUseRepository; + + public TrialUserPreparationService(IRepository trialUserPreparationRepository, + IRepository trialExternalUseRepository, + IRepository userRepository, IRepository trialUserRepository, IRepository trialSiteUserSurveyRepository, IRepository trialSiteUseRepository) + { + _trialUserPreparationRepository = trialUserPreparationRepository; + _trialExternalUseRepository = trialExternalUseRepository; + _userRepository = userRepository; + _trialUserRepository = trialUserRepository; + _trialSiteUserSurveyRepository = trialSiteUserSurveyRepository; + _trialSiteUseRepository = trialSiteUseRepository; + } + + /// + /// 项目下 人员邀请 加入列表 + /// + /// + /// + public async Task> GetTrialUserPreparationList(TrialUserPreparationQuery queryTrialUserPreparation) + { + + + var trialUserPreparationQueryable = _trialUserPreparationRepository + .WhereIf(queryTrialUserPreparation.UserTypeId != null, t => t.User.UserTypeId == queryTrialUserPreparation.UserTypeId) + .ProjectTo(_mapper.ConfigurationProvider); + + return await trialUserPreparationQueryable.ToListAsync(); + } + + + /// + /// 不带Token访问 加入项目 记录 同意与否 + /// + /// + /// + [AllowAnonymous] + public async Task JoinTrial(JoinCommand joinCommand) + { + + + await UserJoinTrialAsync(joinCommand.TrialUserInfo); + } + + + + + + + + + + + /// + /// 用户加入项目 + /// + /// + /// + [NonDynamicMethod] + public async Task UserJoinTrialAsync(UserJoinTrialCommand joinTrialCommand) + { + var trialId = joinTrialCommand.TrialId; + var userId = joinTrialCommand.UserId; + + if (joinTrialCommand.IsExternal) + { + + //判断TrialUser中是否存在 不存在就插入 + if (!await _trialUserRepository.AnyAsync(t => t.TrialId == trialId && t.UserId == userId)) + { + + await _trialUserRepository.AddAsync(new TrialUser() { TrialId = trialId, UserId = userId }); + + + await _trialExternalUseRepository.UpdateFromQueryAsync(t => t.TrialId == trialId && t.SystemUserId == userId, + u => new TrialExternalUser() { InviteState = TrialExternalUserStateEnum.UserConfirmed }); + + + await _userRepository.SaveChangesAsync(); + } + } + else + { + if (joinTrialCommand.SiteId == null) + { + return ResponseOutput.NotOk("传参有误, SiteId必传"); + } + + if (!_trialUserRepository.Where(t => t.TrialId == trialId && t.UserId == userId).Any()) + { + await _trialUserRepository.AddAsync(new TrialUser() { TrialId = trialId, UserId = userId }); + + await _trialSiteUseRepository.AddAsync(new TrialSiteUser() { TrialId = trialId, SiteId =(Guid) joinTrialCommand.SiteId, UserId = userId }); + } + } + + await _userRepository.UpdateFromQueryAsync(t => t.Id == userId, u => new User() { Status = UserStateEnum.Enable }); + + + return ResponseOutput.Ok(); + + } + + + + + } +} diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/_MapConfig.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/_MapConfig.cs new file mode 100644 index 00000000..ff1b2bf3 --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/_MapConfig.cs @@ -0,0 +1,203 @@ +using AutoMapper; +using AutoMapper.EquivalencyExpression; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Application.Contracts.DTO; +using IRaCIS.Core.Application.ViewModel; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Domain.Share; + +namespace IRaCIS.Core.Application.Service +{ + public class TrialSiteUserConfig : Profile + { + public TrialSiteUserConfig() + { + //CreateMap(); + //CreateMap().ForMember(t => t.UserTypes, u => u.MapFrom(k => string.Join(',', k.UserTypeList))); + + + CreateMap(); + + CreateMap() + .ForMember(d => d.TrialSiteAliasName, u => u.MapFrom(s => s.SiteName)) + .ForMember(x => x.Id, x => x.Ignore()); + + CreateMap(); + + CreateMap(); + + CreateMap(); + + + //临床项目 + CreateMap().ForAllMembers(opt => opt.Condition((src, dest, srcMember) => srcMember != null)); + + CreateMap(); + + var userId = Guid.Empty; + var userTypeEnumInt = 0; + CreateMap() + .ForMember(d => d.DictionaryList, u => u.MapFrom(s => s.TrialDicList.Select(t => t.Dictionary).OrderBy(t => t.ShowOrder))) + //.ForMember(d => d.Code, u => u.MapFrom(s => s.TrialCode)) + .ForMember(d => d.Sponsor, u => u.MapFrom(s => s.Sponsor.SponsorName)) + .ForMember(d => d.Phase, u => u.MapFrom(s => s.Phase.Value)) + .ForMember(d => d.DeclarationType, u => u.MapFrom(s => s.DeclarationType.Value)) + .ForMember(d => d.IndicationType, u => u.MapFrom(s => s.IndicationType.Value)) + .ForMember(d => d.CRO, u => u.MapFrom(s => s.CRO.CROName)) + .ForMember(d => d.ReviewMode, u => u.MapFrom(s => s.ReviewMode.Value)) + //.ForMember(d => d.ReviewType, u => u.MapFrom(s => s.ReviewType.Value)) + .ForMember(d => d.IsLocked, u => u.MapFrom(s => s.WorkloadList.Any(u => u.DataFrom == (int)WorkLoadFromStatus.FinalConfirm))) + .ForMember(d => d.SiteCount, u => u.MapFrom(s => userTypeEnumInt == (int)UserTypeEnum.ClinicalResearchCoordinator ? s.TrialSiteUserList.Count(k => k.UserId == userId) : s.TrialSiteList.Count())) + .ForMember(d => d.StudyCount, u => u.MapFrom(s => userTypeEnumInt == (int)UserTypeEnum.ClinicalResearchCoordinator ? s.StudyList.Count(t => t.TrialSite.CRCUserList.Any(t => t.UserId == userId)) : s.StudyList.Count())) + .ForMember(d => d.SubjectCount, u => u.MapFrom(s => userTypeEnumInt == (int)UserTypeEnum.ClinicalResearchCoordinator ? s.SubjectList.Count(t => t.TrialSite.CRCUserList.Any(t => t.UserId == userId)) : s.SubjectList.Count())); + + + + + CreateMap().IncludeMembers(t => t.User) + .ForMember(d => d.UserRealName, u => u.MapFrom(s => s.User.LastName + " / " + s.User.FirstName)) + .ForMember(t => t.UserType, u => u.MapFrom(t => t.User.UserTypeRole.UserTypeShortName)); + CreateMap(); + + var siteId = Guid.Empty; + CreateMap().IncludeMembers(t => t.User) + .ForMember(d => d.UserRealName, u => u.MapFrom(s => s.User.LastName + " / " + s.User.FirstName)) + .ForMember(d => d.SiteId, u => u.MapFrom(t => siteId)) + .ForMember(d => d.UserType, u => u.MapFrom(t => t.User.UserTypeRole.UserTypeShortName)) + .ForMember(d => d.IsSelect, u => u.MapFrom(t => t.SiteList.Any(k => k.SiteId == siteId))); + CreateMap(); + + var trialId = Guid.Empty; + CreateMap() + .ForMember(d => d.UserRealName, u => u.MapFrom(s => s.LastName + " / " + s.FirstName)) + .ForMember(d => d.UserType, u => u.MapFrom(s => s.UserTypeRole.UserTypeShortName)) + .ForMember(d => d.UserId, u => u.MapFrom(s => s.Id)) + .ForMember(d => d.TrialId, u => u.MapFrom(s => trialId)) + .ForMember(d => d.IsSelect, u => u.MapFrom(t => t.UserTrials.Any(t => t.TrialId == trialId))); + + + CreateMap() + .ForMember(d => d.HospitalName, u => u.MapFrom(s => s.Hospital.HospitalName)); + + //trial site 选择列表 subjectVisit pannel 模式添加的时候 + CreateMap() + .ForMember(d => d.IsSelect, u => u.MapFrom(s => s.TrialSiteList.Where(t => t.TrialId == trialId).Any(k => k.SiteId == s.Id))); + + #region 项目 stie pannel + + #region site 也有country hospital 也有 注意区分 + CreateMap() + .ForMember(d => d.SiteCode, u => u.MapFrom(s => s.Site.SiteCode)) + .ForMember(d => d.City, u => u.MapFrom(s => s.Site.City)) + .ForMember(d => d.Country, u => u.MapFrom(s => s.Site.Country)) + .ForMember(d => d.Hospital, u => u.MapFrom(s => s.Site.Hospital.HospitalName)) + + + .ForMember(d => d.DirectorName, u => u.MapFrom(s => s.Site.DirectorName)) + .ForMember(d => d.DirectorPhone, u => u.MapFrom(s => s.Site.DirectorPhone)) + .ForMember(d => d.ContactPhone, u => u.MapFrom(s => s.Site.ContactPhone)) + .ForMember(d => d.Address, u => u.MapFrom(s => s.Site.Address)) + .ForMember(d => d.Site, u => u.MapFrom(s => s.Site.SiteName)) + .ForMember(d => d.VisitCount, u => u.MapFrom(s => s.SubjectVisitList.Count())) + .ForMember(d => d.SubjectCount, u => u.MapFrom(s => s.SubjectList.Count())) + + + + .ForMember(d => d.UserCount, u => u.MapFrom(s => s.CRCUserList.Count())) + .ForMember(d => d.UserNameList, u => u.MapFrom(s => s.CRCUserList.Where(t => t.IsDeleted == false).Select(u => u.User.LastName + " / " + u.User.FirstName))); + #endregion + + + + CreateMap(); + + CreateMap().IncludeMembers(t => t.Site) + .ForMember(d => d.Id, u => u.MapFrom(s => s.Id)) + .ForMember(d => d.UpdateTime, u => u.MapFrom(s => s.UpdateTime)) + .ForMember(d => d.Site, u => u.MapFrom(s => s.Site.SiteName)) + .ForMember(d => d.Hospital, u => u.MapFrom(s => s.Site.Hospital.HospitalName)) + .ForMember(d => d.UserCount, u => u.MapFrom(s => s.CRCUserList.Count())) + .ForMember(d => d.UserNameList, u => u.MapFrom(s => s.CRCUserList.Where(t=>t.IsDeleted==false).Select(u=>u.User.LastName+" / "+u.User.FirstName))); + CreateMap(); + + + + CreateMap().IncludeMembers(t => t.User) + .ForMember(d => d.UserType, u => u.MapFrom(s => s.User.UserTypeRole.UserTypeShortName)) + .ForMember(d => d.UserRealName, u => u.MapFrom(s => s.User.LastName + " / " + s.User.FirstName)); + CreateMap(); + + + + + #endregion + + + + CreateMap(); + CreateMap().ForMember(t=>t.TrialId,u=>u.MapFrom(c=>c.Id)) + .ForMember(t => t.CriterionIds, u => u.MapFrom(c => c.TrialDicList.Where(v=>v.KeyName== StaticData.Criterion).Select(r=>r.DictionaryId))); + CreateMap(); + CreateMap(); + + + CreateMap(); + + CreateMap().ForMember(t=>t.TrialDicList,u=>u.MapFrom(k=>k.CriterionIds)); + CreateMap().EqualityComparison((odto, o) => odto == o.DictionaryId) + .ForMember(t => t.DictionaryId, u => u.MapFrom(c => c)) + .ForMember(t => t.KeyName, u => u.MapFrom(c=> StaticData.Criterion) ); + + + CreateMap(); + + CreateMap(); + + CreateMap(); + + + CreateMap(); + + CreateMap() + .ForMember(t => t.UserRealName, u => u.MapFrom(c => c.User.LastName +" / "+c.User.FirstName)) + .ForMember(t => t.UserName, u => u.MapFrom(c => c.User.UserName)); + + + + CreateMap().ReverseMap(); + + CreateMap(); + + + CreateMap().ReverseMap(); + + + CreateMap() + + .ForMember(t => t.UserRealName, u => u.MapFrom(c => c.User.LastName + " / " + c.User.FirstName)) + .ForMember(t => t.UserName, u => u.MapFrom(c => c.User.UserName)) + .ForMember(t => t.UserTypeShortName, u => u.MapFrom(c => c.User.UserTypeRole.UserTypeShortName)); + + + CreateMap().IncludeMembers(t => t.Trial) + .ForMember(t => t.UserId, u => u.MapFrom(c => c.SystemUserId)); + CreateMap(); + + CreateMap().IncludeMembers(t => t.TrialSiteSurvey.Trial) + .ForMember(t => t.UserId, u => u.MapFrom(c => c.SystemUserId)) + .ForMember(t => t.TrialId, u => u.MapFrom(c => c.TrialSiteSurvey.TrialId)); + + + CreateMap(); + + CreateMap(); + + + + + + } + } + +} diff --git a/IRaCIS.Core.Application/Service/Visit/DTO/ClinicalStudySubjects.cs b/IRaCIS.Core.Application/Service/Visit/DTO/ClinicalStudySubjects.cs new file mode 100644 index 00000000..b589c394 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Visit/DTO/ClinicalStudySubjects.cs @@ -0,0 +1,163 @@ +using System; +using IRaCIS.Core.Infrastructure.Extention; +using IRaCIS.Core.Domain.Share; +using System.ComponentModel.DataAnnotations; + +namespace IRaCIS.Application.Contracts +{ + public class SubjectCommand + { + public Guid? Id { get; set; } + public string Code { get; set; } = String.Empty; + + public int? Age { get; set; } + public string Sex { get; set; } = string.Empty; + public Guid SiteId { get; set; } + public Guid TrialId { get; set; } + public string MedicalNo { get; set; } = string.Empty; + + public string ShortName { get; set; } = string.Empty; + + public string Height { get; set; } = string.Empty; + + public string Weight { get; set; } = string.Empty; + + public DateTime? BirthDate { get; set; } + public DateTime? SignDate { get; set; } + + public bool IsUrgent { get; set; } + + public DateTime? FirstGiveMedicineTime { get; set; } + + + + + //public DateTime? OutEnrollmentTime { get; set; } + //public DateTime? VisitOverTime { get; set; } + //public SubjectStatus Status { get; set; } + //public string Reason { get; set; } = string.Empty; + + + + } + + public class SubjectStatusChangeCommand + { + [NotDefault] + public Guid SubjectId { get; set; } + + public SubjectStatus Status { get; set; } + public DateTime? OutEnrollmentTime { get; set; } + public DateTime? VisitOverTime { get; set; } + public string Reason { get; set; } = string.Empty; + + + public Guid? FinalSubjectVisitId { get; set; } + } + + + public class TrialSubjectConfig + { + public string SubjectCodeRule { get; set; } = string.Empty; + public bool IsNoticeSubjectCodeRule { get; set; } + public bool IsHaveFirstGiveMedicineDate { get; set; } + public bool IsEnrollementQualificationConfirm { get; set; } + public bool IsHaveSubjectAge { get; set; } + public string OutEnrollmentVisitName { get; set; } = string.Empty; + public bool VisitPlanConfirmed { get; set; } + + public bool IsSubjectSexView { get; set; } = false; + public bool IsSubjectExpeditedView { get; set; } = false; + + public bool IsSubjectSecondCodeView { get; set; } = false; + } + + + public class SubjectQueryView: SubjectCommand + { + + public DateTime? OutEnrollmentTime { get; set; } + public DateTime? VisitOverTime { get; set; } + public SubjectStatus Status { get; set; } + public string Reason { get; set; } = string.Empty; + + + //public int? StudyCount { get; set; } + //public int? VisitCount { get; set; } + //public int? PlanVisitCount { get; set; } + + + //public int? InPlanDicomStudyUploadCount { get; set; } + //public int? OutPlanDicomStudyUploadCount { get; set; } + //public int? InPlanNoneDicomStudyUploadCount { get; set; } + //public int? OutPlanNoneDicomStudyUploadCount { get; set; } + + public Guid? FinalSubjectVisitId { get; set; } + + public int? TotalVisitCount => OutPlanVisitCount + InPlanVisitCount; + public int? TotalVisitSubmmitCount => OutPlanVisitSubmmitCount + InPlanVisitSubmmitCount; + + public int? OutPlanVisitCount { get; set; } + public int? OutPlanVisitSubmmitCount { get; set; } + + public int? InPlanVisitCount { get; set; } + public int? InPlanVisitSubmmitCount { get; set; } + + public int? MissingSubmmitCount { get; set; } + + public int? LostVisitCount { get; set; } + + public DateTime? CreateTime { get; set; } + public DateTime? UpdateTime { get; set; } + public string SiteName { get; set; } = string.Empty; + public string TrialSiteCode { get; set; } = string.Empty; + + public string LatestVisitName { get; set; } = string.Empty; + + public string LatestBlindName { get; set; } = string.Empty; + + public bool IsMissingImages => MissingSubmmitCount > 0; + + public Guid LatestSubmitSubjectVisitId { get; set; } + + + + } + + + + public class SubjectQueryParam : PageInput + { + [NotDefault] + public Guid TrialId { get; set; } + public string Code { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + + public string Sex { get; set; } = string.Empty; + + public Guid? SiteId { get; set; } + public SubjectStatus? Status { get; set; } + } + + + public class SubjectSelect + { + public Guid SubjectId { get; set; } + + public string Name => LastName + " / " + FirstName; + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + + public string Code { get; set; } = string.Empty; + + public string Sex { get; set; } = string.Empty; + + public int? Age { get; set; } + public SubjectStatus Status { get; set; } + + public DateTime? FirstGiveMedicineTime { get; set; } + + public string MRN { get; set; } = string.Empty; + } +} diff --git a/IRaCIS.Core.Application/Service/Visit/DTO/VisitPlanViewModel.cs b/IRaCIS.Core.Application/Service/Visit/DTO/VisitPlanViewModel.cs new file mode 100644 index 00000000..72582e0d --- /dev/null +++ b/IRaCIS.Core.Application/Service/Visit/DTO/VisitPlanViewModel.cs @@ -0,0 +1,85 @@ +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Contracts +{ + public class VisitStageSelectDTO + { + public Guid Id { get; set; } + + public decimal VisitNum { get; set; } + + public string VisitName { get; set; } = String.Empty; + public int VisitDay { get; set; } + + + public bool IsBaseLine { get; set; } = false; + + public string AnonymousVisitName { get; set; } = string.Empty; + + public int VisitWindowLeft { get; set; } + public int VisitWindowRight { get; set; } + } + + public class VisitStageDTO : VisitPlanCommand + { + public DateTime CreateTime { get; set; } + } + + public class VisitPlanView + { + public List VisitPlanList = new List(); + + public decimal TimePointsPerPatient { get; set; } + public bool VisitPlanConfirmed { get; set; } + + public bool IsHaveFirstGiveMedicineDate { get; set; } = true; + + //public bool SubjectHasAdded { get; set; } + } + + public class VisitPlanCommand + { + //public string BlindName => "B" + (VisitNum * 10).ToString("D3"); + + public string BlindName { get; set; } = String.Empty; + public bool IsConfirmed { get; set; } = false; + + public Guid? Id { get; set; } + public Guid TrialId { get; set; } + + public decimal VisitNum { get; set; } + public string VisitName { get; set; } = String.Empty; + public int VisitDay { get; set; } + public string Description { get; set; } = String.Empty; + public bool NeedGlobal { get; set; } = false; + + public bool IsBaseLine { get; set; } = false; + + + public string AnonymousVisitName { get; set; } = string.Empty; + + public int VisitWindowLeft { get; set; } + public int VisitWindowRight { get; set; } + } + + + public class VisitPlanQueryDTO : PageInput + { + public Guid TrialId { get; set; } = Guid.Empty; + public string Keyword { get; set; } = string.Empty; + } + + public class VisitPlanInfluenceSubjectVisitStatDTO + { + public Guid Id { get; set; } + public Guid CreateUserId { get; set; } + + public string CreateUser { get; set; } = String.Empty; + + public DateTime CreateTime { get; set; } + + public int InconsistentCount { get; set; } + } + + +} diff --git a/IRaCIS.Core.Application/Service/Visit/DTO/VisitPointViewModel.cs b/IRaCIS.Core.Application/Service/Visit/DTO/VisitPointViewModel.cs new file mode 100644 index 00000000..7f080d30 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Visit/DTO/VisitPointViewModel.cs @@ -0,0 +1,419 @@ +using IRaCIS.Core.Infrastructure.Extention; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Application.Contracts.Dicom.DTO; +using Newtonsoft.Json; +using System.ComponentModel.DataAnnotations; + +namespace IRaCIS.Core.Application.Contracts +{ + public class SubjectVisitCommand + { + public bool IsFinalVisit { get; set; } + + public string BlindName { get; set; } = string.Empty; + + public Guid? Id { get; set; } + public Guid TrialId { get; set; } + public Guid SubjectId { get; set; } + public Guid SiteId { get; set; } + public bool InPlan { get; set; } = true; + public VisitExecutedEnum VisitExecuted { get; set; } + public decimal VisitNum { get; set; } + public string VisitName { get; set; } = string.Empty; + public int? VisitDay { get; set; } + public string SVUPDES { get; set; } = string.Empty; + + public bool IsBaseLine { get; set; } = false; + public int VisitWindowLeft { get; set; } + public int VisitWindowRight { get; set; } + + public bool IsLostVisit { get; set; } + public PDStateEnum? PDState { get; set; } + + public bool? IsEnrollmentConfirm { get; set; } + public DateTime? SubjectFirstGiveMedicineTime { get; set; } + + //public bool IsOutEnromentVisit { get; set; } + + public Guid? OutPlanPreviousVisitId { get; set; } + + + + //public bool IsUrgent { get; set; } + + + } + public class SubjectVisitDTO : SubjectVisitCommand + { + public SubjectStatus SubjectStatus { get; set; } + + } + + public class SubjectVisitNewDTO : SubjectVisitDTO + { + public TrialQCProcess QCProcessEnum { get; set; } + public CheckStateEnum CheckState { get; set; } + public SubmitStateEnum SubmitState { get; set; } + public AuditStateEnum AuditState { get; set; } + + public string CurrentActionUser { get; set; } = String.Empty; + public DateTime? CurrentActionUserExpireTime { get; set; } + public string PreliminaryAuitUser { get; set; } = String.Empty; + public string ReviewAuitUser { get; set; } = String.Empty; + public DateTime? ReviewAuitTime { get; set; } + public DateTime? PreliminaryAuditTime { get; set; } + + + public int? StudyCount { get; set; } + + public String TrialSiteCode { get; set; } = String.Empty; + public DateTime UpdateTime { get; set; } + + public string SubjectSex { get; set; } = String.Empty; + + public int SubjectAge { get; set; } + + public string SubjectCode { get; set; } = String.Empty; + + + [JsonIgnore] + public string SubjectName => LastName + " / " + FirstName; + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + + public string SiteName { get; set; } = String.Empty; + + public string SiteCode { get; set; } = String.Empty; + + + + + } + + public class SubjectVisitSearchDTO : PageInput + { + public string SubjectInfo { get; set; } = String.Empty; + + public string VisitPlanInfo { get; set; } = String.Empty; + + public Guid? SubjectId { get; set; } + public Guid TrialId { get; set; } + public Guid? SiteId { get; set; } + } + + public class SubjectVisitSelectItem + { + public Guid Id { get; set; } + public decimal VisitNum { get; set; } + public string VisitName { get; set; } = String.Empty; + + public int VisitExecuted { get; set; } + } + + public class SubjectVisitSelectDTO + { + public Guid GuidId { get; set; } = Guid.NewGuid(); + public Guid SubjectVisitId { get; set; } = Guid.Empty; + public decimal VisitNum { get; set; } + public string VisitName { get; set; } = string.Empty; + + public string SVUPDES { get; set; } = string.Empty; + + public int? VisitDay { get; set; } + + //public Guid VisitStageId { get; set; } = Guid.Empty; + + public DateTime? SVSTDTC { get; set; } + + public DateTime? SVENDTC { get; set; } + + + //public bool IsSubjectVisit { get; set; } = false; + + public VisitExecutedEnum VisitExecuted { get; set; } + + + + public List StudyList = new List(); + } + + + public class SubjectVisitStudyDTO + { + public Guid SubjectVisitId { get; set; } + public Guid StudyId { get; set; } + public string StudyCode { get; set; } = String.Empty; + + public string Modalities { get; set; } = String.Empty; + + public int Status { get; set; } + + } + + + public class VisitStudyDTO + { + public Guid StudyId { get; set; } + public string StudyCode { get; set; } = String.Empty; + public string Modalities { get; set; } = String.Empty; + + public int SeriesCount { get; set; } + public int InstanceCount { get; set; } + + public List SeriesList { get; set; } = new List(); + } + + public class ArchiveStudyCommand + { + + + public Guid? AbandonStudyId { get; set; } + + + public Guid SubjectVisitId { get; set; } + + public string StudyInstanceUid { get; set; } = String.Empty; + + //public Guid TrialId { get; set; } + //public Guid SiteId { get; set; } + //public Guid SubjectId { get; set; } + //public DateTime? SVSTDTC { get; set; } + //public DateTime? SVENDTC { get; set; } + + //public string DTFPath { get; set; } + //public string FileRealName { get; set; } + + } + + + public class UploadDTFCommand + { + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public Guid SubjectId { get; set; } + public Guid SubjectVisitId { get; set; } + + } + + + public class StudyCommand + { + public Guid Id { get; set; } = Guid.Empty; + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public Guid SubjectId { get; set; } + public Guid SubjectVisitId { get; set; } + + } + + public class StudyEditCommand : StudyCommand + { + public Guid VisitStageId { get; set; } + public DateTime? SVSTDTC { get; set; } + public DateTime? SVENDTC { get; set; } + public string Comment { get; set; } = String.Empty; + } + + + + public class StudyStatDTO + { + + public int StudyCount { get; set; } + public decimal VisitNum { get; set; } + public string VisitName { get; set; } = String.Empty; + public int VisitDay { get; set; } + public string Description { get; set; } = String.Empty; + public Guid TrialId { get; set; } + public Guid SubjectVisitId { get; set; } + + } + + public class DistributeReviewerStudyStatusDTO + { + public string NameCN { get; set; } = String.Empty; + public string Name { get; set; } = String.Empty; + public string ReviewerCode { get; set; } = String.Empty; + public string StudyCode { get; set; } = String.Empty; + + public int Status { get; set; } + + } + + public class StudyStatusQueryDTO : PageInput + { + public Guid TrialId { get; set; } + public Guid? ReviewerId { get; set; } + + public int? StudyStatus { get; set; } + } + + + + + public class VerifyStudyUploadResult + { + public string StudyInstanceUid { get; set; } = String.Empty; + + public VerifyStudyDto? StudyInfo { get; set; } + + public bool AllowUpload { get; set; } = false; + + public bool AllowReUpload { get; set; } = false; + + public string ErrorMesseage { get; set; } = String.Empty; + + } + + public class VerifyUploadOrReupload + { + [NotDefault] + public Guid TrialId { get; set; } + + [NotDefault] + public Guid SubjectId { get; set; } + + [NotDefault] + public Guid SubjectVisitId { get; set; } + + + public decimal VisitNum { get; set; } + + public List StudyInstanceUidList { get; set; } = new List(); + } + + public class VerifyUploadStudyBasicInfo + { + public string StudyInstanceUid { get; set; } = String.Empty; + + public DateTime StudyDate { get; set; } + } + + + public class VerifyStudyDto + { + + public Guid Id { get; set; } + public string StudyCode { get; set; } = String.Empty; + + public Guid SubjectId { get; set; } + public Guid SubjectVisitId { get; set; } + + public string SubjectCode { get; set; } = String.Empty; + public string VisitName { get; set; } = String.Empty; + public string SubjectName => LastName + " / " + FirstName; + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + + public DateTime StudyTime { get; set; } + } + + public class StudyDTO + { + public Guid SubjectVisitId { get; set; } + public decimal VisitNum { get; set; } + public string VisitName { get; set; } = String.Empty; + public int? VisitDay { get; set; } + + public string SVUPDES { get; set; } = String.Empty; + + public DateTime? SVSTDTC { get; set; } + + public DateTime? SVENDTC { get; set; } + + + public Guid Id { get; set; } + public Guid TrialId { get; set; } + public string StudyCode { get; set; } = String.Empty; + + + public int StudyStatus { get; set; } + public DateTime? StudyDate { get; set; } + public string Modalities { get; set; } = String.Empty; + public int SeriesCount { get; set; } + public int InstanceCount { get; set; } + + public bool IsDoubleReview { get; set; } + + public string StudyDescription { get; set; } = String.Empty; + + public string BodyPartExamined { get; set; } = String.Empty; + + public string Comment { get; set; } = String.Empty; + + public DateTime UpdateTime { get; set; } + + public DateTime? UploadedTime { get; set; } + + public DateTime? DeadlineTime { get; set; } + + //public List DistributeReviewers { get; set; }=new List(); + + + public Guid SiteId { get; set; } + public string SiteName { get; set; } = String.Empty; + public string InstitutionName { get; set; } = String.Empty; + public string StudyId { get; set; } = String.Empty;//这个Id是dicom文件中的Id + public Guid SubjectId { get; set; } + public string SubjectCode { get; set; } = String.Empty; + + public string SubjectName => LastName + " / " + FirstName; + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public int? SubjectAge { get; set; } + + public SubjectStatus SubjectStatus { get; set; } + public string SubjectSex { get; set; } = String.Empty; + + public string SubjectMedicalNo { get; set; } = string.Empty; + + public string PatientId { get; set; } = String.Empty; + public string PatientName { get; set; } = String.Empty; + public string PatientAge { get; set; } = String.Empty; + public string PatientSex { get; set; } = String.Empty; + public string AccessionNumber { get; set; } = string.Empty; + public string PatientBirthDate { get; set; } = string.Empty; + + + //public int? NeedDealMessageCount { get; set; } + //public int? TotalMessageCount { get; set; } + public int? DTFCount { get; set; } + + + public string UploaderFirstName { get; set; } = String.Empty; + public string UploaderLastName { get; set; } = String.Empty; + } + + public class DistributeReviewer + { + public Guid StudyId { get; set; } + + public Guid ReviewerId { get; set; } + public string NameCN { get; set; } = String.Empty; + public string Name { get; set; } = String.Empty; + + public int Status { get; set; } + } + + public class StudyQueryDTO : PageInput + { + public Guid? SubjectId { get; set; } + + public Guid? SiteId { get; set; } + public Guid TrialId { get; set; } + public string SubjectInfo { get; set; } = String.Empty; + + public string VisitPlanInfo { get; set; }=String.Empty; + + public Guid? SubjectVisitId { get; set; } + + + public DateTime? StudyTimeBegin { get; set; } + + public DateTime? StudyTimeEnd { get; set; } + + public DateTime? UpdateTimeBegin { get; set; } + + public DateTime? UpdateTimeEnd { get; set; } + } +} diff --git a/IRaCIS.Core.Application/Service/Visit/Interface/ISubjectService.cs b/IRaCIS.Core.Application/Service/Visit/Interface/ISubjectService.cs new file mode 100644 index 00000000..412272b0 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Visit/Interface/ISubjectService.cs @@ -0,0 +1,12 @@ +using IRaCIS.Application.Contracts; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Application.Interfaces +{ + public interface ISubjectService + { + Task AddOrUpdateSubject([FromBody] SubjectCommand subjectCommand); + Task DeleteSubject(Guid id); + Task, TrialSubjectConfig>> GetSubjectList(SubjectQueryParam param); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Visit/Interface/ISubjectVisitService.cs b/IRaCIS.Core.Application/Service/Visit/Interface/ISubjectVisitService.cs new file mode 100644 index 00000000..81bee810 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Visit/Interface/ISubjectVisitService.cs @@ -0,0 +1,12 @@ +using IRaCIS.Core.Application.Contracts; + +namespace IRaCIS.Core.Application.Interfaces +{ + public interface ISubjectVisitService + { + Task AddOrUpdateSV(SubjectVisitCommand svCommand); + Task DeleteSV(Guid id); + Task> GetVisitStudyList(Guid trialId, Guid sujectVisitId, int isReading); + Task SetSVExecuted(Guid subjectVisitId); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Visit/Interface/IVisitPlanService.cs b/IRaCIS.Core.Application/Service/Visit/Interface/IVisitPlanService.cs new file mode 100644 index 00000000..3bf54f4b --- /dev/null +++ b/IRaCIS.Core.Application/Service/Visit/Interface/IVisitPlanService.cs @@ -0,0 +1,18 @@ +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Application.Interfaces +{ + public interface IVisitPlanService + { + Task AddOrUpdateVisitStage(VisitPlanCommand visitPlan); + Task ConfirmTrialVisitPlan(Guid trialId, [FromServices] IRepository _influnceStatRepository); + Task DeleteVisitStage(Guid id); + Task DownloadInflunceStudyList(Guid visitPlanInfluenceStatId, [FromServices] IRepository _influnceRepository); + Task> GetInfluenceHistoryList(Guid trialId, [FromServices] IRepository _influnceStatRepository); + Task> GetTrialVisitStageList(VisitPlanQueryDTO param); + Task> GetTrialVisitStageSelect(Guid trialId); + Task GetVisitStageList(Guid trialId); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Visit/SubjectService.cs b/IRaCIS.Core.Application/Service/Visit/SubjectService.cs new file mode 100644 index 00000000..9604fba5 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Visit/SubjectService.cs @@ -0,0 +1,252 @@ +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Domain.Share; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Application.Services +{ + [ApiExplorerSettings(GroupName = "Trial")] + public class SubjectService : BaseService, ISubjectService + { + private readonly IRepository _subjectRepository; + + public SubjectService(IRepository subjectRepository) + { + _subjectRepository = subjectRepository; + } + + /// + /// 添加或更新受试者信息[New] + /// + /// state:1-访视中,2-出组。0-全部 + /// + + [TrialAudit(AuditType.SubjectAudit, AuditOptType.AddOrUpdateSubject)] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task AddOrUpdateSubject([FromBody] SubjectCommand subjectCommand) + { + + if (await _repository.AnyAsync(t => t.Id == subjectCommand.TrialId && !t.VisitPlanConfirmed)) + { + return ResponseOutput.NotOk("The trial visit plan has not been confirmed yet.Please contact the project manager to confirm the visit plan before adding subject."); + } + + var verifyExp1 = new EntityVerifyExp() + { + VerifyExp = u => u.Code == subjectCommand.Code && u.TrialId == subjectCommand.TrialId, + VerifyMsg = "A subjects with the same subject ID already existed in this trial." + }; + + + + var mapedSubject = await _subjectRepository.InsertOrUpdateAsync(subjectCommand, false, verifyExp1/*, verifyExp2*/); + + if (subjectCommand.Id == null) //insert + { + var IsEnrollementQualificationConfirm = await _repository.Where(t => t.Id == mapedSubject.TrialId).Select(u => u.IsEnrollementQualificationConfirm).FirstOrDefaultAsync(); + + + + //添加受试者的时候,获取访视计划列表,添加到受试者访视表。 + var visitPlan = await _repository.Where(t => t.TrialId == subjectCommand.TrialId).ToListAsync(); + + var svlist = _mapper.Map>(visitPlan); + + svlist.ForEach(t => + { + t.TrialId = subjectCommand.TrialId; + t.SiteId = subjectCommand.SiteId; + t.IsEnrollmentConfirm = t.IsBaseLine ? IsEnrollementQualificationConfirm : false; + }); + + mapedSubject.SubjectVisitList = svlist; + } + else //update + { + //变更site + if (mapedSubject.SiteId != subjectCommand.SiteId) + { + + await _repository.UpdateFromQueryAsync(t => t.SubjectId == mapedSubject.Id, + u => new SubjectVisit() { SiteId = mapedSubject.SiteId }); + + await _repository.UpdateFromQueryAsync(t => t.SubjectId == mapedSubject.Id, + u => new DicomStudy() { SiteId = mapedSubject.SiteId }); + + + }; + ////如果访视结束了 需要删除计划外未执行的访视 + //if (mapedSubject.Status == SubjectStatus.EndOfVisit) + //{ + // await _repository.DeleteFromQueryAsync(t => t.VisitExecuted == VisitExecutedEnum.UnExecuted && t.SubjectId == mapedSubject.Id && t.InPlan == false); + //} + + ////如果是出组了 将受试者未执行的 设置为不可用 + //if (mapedSubject.Status == SubjectStatus.OutOfEnrollment) + //{ + // await _repository.UpdateFromQueryAsync(t => t.SubjectId == mapedSubject.Id && t.VisitExecuted == VisitExecutedEnum.UnExecuted, u => new SubjectVisit() { VisitExecuted = VisitExecutedEnum.Unavailable }); + //} + + } + + + + await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(mapedSubject.Id.ToString()); + + } + + [HttpPut("{trialId:guid}")] + public async Task UpdateSubjectStatus(SubjectStatusChangeCommand subjectStatusChangeCommand) + { + var subject = await _subjectRepository.FirstOrDefaultAsync(t => t.Id == subjectStatusChangeCommand.SubjectId); + + if (subject == null) return Null404NotFound(subject); + + _mapper.Map(subjectStatusChangeCommand, subject); + + ////如果访视结束了 需要删除计划外未执行的访视 + //if (subjectStatusChangeCommand.Status == SubjectStatus.EndOfVisit) + //{ + // await _repository.DeleteFromQueryAsync(t => t.VisitExecuted == VisitExecutedEnum.UnExecuted && t.SubjectId == subject.Id && t.InPlan == false); + //} + + //如果是出组了 + if (subjectStatusChangeCommand.Status == SubjectStatus.OutOfVisit) + { + + if (subjectStatusChangeCommand.FinalSubjectVisitId != null) + { + + if (await _repository.AnyAsync(t => t.SubjectId == subjectStatusChangeCommand.SubjectId && t.IsFinalVisit)) + { + return ResponseOutput.NotOk("该受试者已经有访视设置为末次访视,不允许将该访视设置为末次访视"); + } + + var sv = await _repository.FirstOrDefaultAsync(t => t.Id == subjectStatusChangeCommand.FinalSubjectVisitId).IfNullThrowException(); + + if (await _repository.AnyAsync(t => t.SubjectId == subjectStatusChangeCommand.SubjectId && t.VisitNum > sv.VisitNum && t.SubmitState == SubmitStateEnum.ToSubmit)) + { + return ResponseOutput.NotOk("该受试者此访视后有影像上传,该访视不允许设置为末次访视"); + } + + sv.IsFinalVisit = true; + + //末次访视后的 访视设置为不可用 + await _repository.UpdateFromQueryAsync(t => t.SubjectId == subjectStatusChangeCommand.SubjectId && t.VisitNum > sv.VisitNum, u => new SubjectVisit() { VisitExecuted = VisitExecutedEnum.Unavailable }); + + } + + + //将受试者未执行的 设置为不可用 + await _repository.UpdateFromQueryAsync(t => t.SubjectId == subject.Id && t.VisitExecuted == VisitExecutedEnum.UnExecuted, u => new SubjectVisit() { VisitExecuted = VisitExecutedEnum.Unavailable }); + } + else + { + + } + await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(); + + + } + + + [Obsolete] + [HttpGet("{trialId:guid}/{subjectId:guid}/{finalSubjectVisitId:guid}")] + public async Task VerifySubjectFinalVisit(Guid subjectId, Guid finalSubjectVisitId) + { + + if (await _repository.AnyAsync(t => t.SubjectId == subjectId && t.IsFinalVisit)) + { + return ResponseOutput.NotOk("该受试者已经有访视设置为末次访视,不允许将该访视设置为末次访视"); + } + + var sv = (await _repository.FirstOrDefaultAsync(t => t.Id == finalSubjectVisitId)).IfNullThrowException(); + + if (await _repository.AnyAsync(t => t.SubjectId == subjectId && t.VisitNum > sv.VisitNum && t.SubmitState == SubmitStateEnum.ToSubmit)) + { + return ResponseOutput.NotOk("该受试者此访视后有影像上传,该访视不允许设置为末次访视"); + } + + return ResponseOutput.Ok(); + } + + + [HttpDelete("{id:guid}/{trialId:guid}")] + + [TrialAudit(AuditType.SubjectAudit, AuditOptType.DeleteSubject)] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task DeleteSubject(Guid id) + { + + if (await _repository.AnyAsync(u => u.SubjectId == id && u.VisitExecuted == VisitExecutedEnum.Executed)) + { + return ResponseOutput.NotOk("This subject has executed a visit with uploading study images,and couldn't be deleted."); + } + + var isSuccess = await _subjectRepository.DeleteFromQueryAsync(u => u.Id == id); + await _repository.DeleteFromQueryAsync(u => u.SubjectId == id); + return ResponseOutput.Result(isSuccess); + } + + /// 分页获取受试者列表[New] + /// /// state:1-访视中,2-出组。0-全部 + [HttpPost] + public async Task, TrialSubjectConfig>> GetSubjectList(SubjectQueryParam param) + { + + var subjectQuery = _subjectRepository.Where(u => u.TrialId == param.TrialId) + .WhereIf(!string.IsNullOrWhiteSpace(param.Code), t => t.Code.Contains(param.Code)) + .WhereIf(!string.IsNullOrWhiteSpace(param.Name), t => t.ShortName.Contains(param.Name)) + .WhereIf(!string.IsNullOrWhiteSpace(param.Sex), t => t.Sex.Contains(param.Sex)) + .WhereIf(param.Status != null, t => t.Status == param.Status) + .WhereIf(param.SiteId != null, t => t.SiteId == param.SiteId) + // CRC 只负责他管理site的受试者 + .WhereIf(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ClinicalResearchCoordinator, t => t.TrialSite.CRCUserList.Any(t => t.UserId == _userInfo.Id)) + .ProjectTo(_mapper.ConfigurationProvider); + + + var pageList = await subjectQuery.ToPagedListAsync(param.PageIndex, param.PageSize, param.SortField == string.Empty ? "Code" : param.SortField, param.Asc); + + var trialConfig = await _repository.Where(t => t.Id == param.TrialId).ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync().IfNullThrowException(); + + return ResponseOutput.Ok(pageList, trialConfig); + + } + + + + /// + /// 计划外访视 获取受试者选择下拉框列表 + /// + [HttpGet("{siteId:guid}/{trialId:guid}")] + public List GetSubjectListBySiteId(Guid siteId, Guid trialId) + { + var query = _subjectRepository.Where(t => t.SiteId == siteId && t.TrialId == trialId && t.Status == SubjectStatus.OnVisit).Select(u => new SubjectSelect() + { + Code = u.Code, + FirstName = u.FirstName, + LastName = u.LastName, + Sex = u.Sex, + SubjectId = u.Id, + Age = u.Age, + MRN = u.MedicalNo, + Status = u.Status, + + FirstGiveMedicineTime = u.FirstGiveMedicineTime + + }); + return query.ToList(); + + + + } + + + } +} diff --git a/IRaCIS.Core.Application/Service/Visit/SubjectVisitService.cs b/IRaCIS.Core.Application/Service/Visit/SubjectVisitService.cs new file mode 100644 index 00000000..52c65013 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Visit/SubjectVisitService.cs @@ -0,0 +1,276 @@ +using IRaCIS.Core.Infrastructure.ExpressionExtend; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Application.Filter; + +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Application.Contracts.Dicom.DTO; +using Microsoft.AspNetCore.Authorization; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Application.Interfaces; + +namespace IRaCIS.Core.Application.Services +{ + [ApiExplorerSettings(GroupName = "Trial")] + public class SubjectVisitService : BaseService, ISubjectVisitService + { + private readonly IRepository _subjectVisitRepository; + + public SubjectVisitService(IRepository subjectVisitRepository) + { + _subjectVisitRepository = subjectVisitRepository; + } + + [HttpPost] + [TypeFilter(typeof(TrialResourceFilter))] + [UnitOfWork] + public async Task AddOrUpdateSV(SubjectVisitCommand svCommand) + { + + var verifyExp1 = new EntityVerifyExp() + { + VerifyExp = t => t.VisitNum == svCommand.VisitNum && t.SubjectId == svCommand.SubjectId, + VerifyMsg = "This subject's visit plan already contains a visit with the same visitnum." + }; + + var verifyExp2 = new EntityVerifyExp() + { + VerifyExp = t => t.SubjectId == svCommand.SubjectId && t.IsFinalVisit, + VerifyMsg = "该受试者已经有访视设置为末次访视,不允许将该访视设置为末次访视", + IsVerify=svCommand.IsFinalVisit + }; + + #region 废弃 + //var verifyExp2 = new EntityVerifyExp() + //{ + // verifyType = VerifyEnum.OnlyUpdate, + // VerifyExp = t => t.StudyList.Any(k => k.SubjectVisitId == svCommand.Id) && svCommand.SVSTDTC == null, + // VerifyMsg = "This visit is associated with the uploaded study images, and the visit start date is not allowed to be modified." + //}; + + //if (needDealEntity.SVSTDTC == null && needDealEntity.VisitExecuted == 1) + //{ + // return ResponseOutput.NotOk("VisitExecuted must have Start Date "); + //} + + + //if (needDealEntity.IsOutEnromentVisit) + //{ + // needDealEntity.VisitNum = _subjectVisitRepository.Where(t => t.SubjectId == svCommand.SubjectId).Max(t => t.VisitNum) + (decimal)0.1; + //} + + //没有EndOfVisit + //if (svCommand.IsFinalVisit) + //{ + // //if (svCommand.InPlan) + // //{ + // await _repository.UpdateFromQueryAsync(t => t.Id == svCommand.SubjectId, u => new Subject() { Status = SubjectStatus.EndOfVisit }); + // //} + // //else + // //{ + // // return ResponseOutput.NotOk("计划外访视不允许设置为最后一次访视"); + + // //} + //} + #endregion + + + svCommand.BlindName = "B" + ((int)(svCommand.VisitNum * 10)).ToString("D3"); + + var subject = (await _repository.FirstOrDefaultAsync(t => t.Id == svCommand.SubjectId)).IfNullThrowException(); + + + //不管是否 都是执行,只是失访的时候 设置Subject状态为失去访视 + //if (svCommand.IsLostVisit) + //{ + // svCommand.VisitExecuted = VisitExecutedEnum.Executed; + + // subject.IsMissingImages = true; + + //} + //else + //{ + // svCommand.VisitExecuted = VisitExecutedEnum.Executed; + //} + + svCommand.VisitExecuted = svCommand.IsLostVisit ? VisitExecutedEnum.Executed : svCommand.VisitExecuted; + + //设置末次评估后,不允许添加计划外访视 + if (svCommand.InPlan == false && svCommand.Id == null) + { + if (await _subjectVisitRepository.AnyAsync(t => t.SubjectId == svCommand.SubjectId && t.IsFinalVisit)) + { + return ResponseOutput.NotOk("设置末次评估后,不允许添加计划外访视"); + } + } + + //更新的时候,返回的是数据库此时的实体数据 + var needDealEntity = await _subjectVisitRepository.InsertOrUpdateAsync(svCommand, false, verifyExp1, verifyExp2); + + + if (svCommand.PDState != needDealEntity.PDState && svCommand.Id != null && needDealEntity.SubmitState == SubmitStateEnum.Submitted) + { + return ResponseOutput.NotOk("CRC提交后,不允许修改PD确认状态"); + } + + if (svCommand.PDState != needDealEntity.PDState && svCommand.Id != null && needDealEntity.RequestBackState == RequestBackStateEnum.PM_AgressBack) + { + return ResponseOutput.NotOk("回退的访视,不允许修改PD确认状态"); + } + + if (svCommand.IsFinalVisit) + { + + if (await _subjectVisitRepository.AnyAsync(t => t.SubjectId == svCommand.SubjectId && t.VisitNum > svCommand.VisitNum && t.SubmitState == SubmitStateEnum.ToSubmit)) + { + return ResponseOutput.NotOk("该受试者此访视后有影像上传,该访视不允许设置为末次访视"); + } + + + subject.Status = SubjectStatus.OutOfVisit; + + //末次访视后的 访视设置为不可用 + await _subjectVisitRepository.UpdateFromQueryAsync(t => t.SubjectId == svCommand.SubjectId && t.VisitNum > svCommand.VisitNum, u => new SubjectVisit() { VisitExecuted = VisitExecutedEnum.Unavailable }); + + } + else + { + //回退 + subject.Status = SubjectStatus.OnVisit; + + await _subjectVisitRepository.UpdateFromQueryAsync(t => t.SubjectId == svCommand.SubjectId && t.VisitExecuted == VisitExecutedEnum.Unavailable, u => new SubjectVisit() { VisitExecuted = VisitExecutedEnum.UnExecuted }); + } + + //更新受试者 首次给药日期 是否入组确认 + if (svCommand.SubjectFirstGiveMedicineTime != null) + { + + subject.FirstGiveMedicineTime = svCommand.SubjectFirstGiveMedicineTime; + + } + + if (svCommand.IsEnrollmentConfirm != null) + { + + await _subjectVisitRepository.UpdateFromQueryAsync(t => t.SubjectId == svCommand.SubjectId && t.IsBaseLine , u => new SubjectVisit() { IsEnrollmentConfirm = svCommand.IsEnrollmentConfirm.Value }); + + + //needDealEntity.IsUrgent = true; + } + + + await _repository.SaveChangesAsync(); + + // 保存数据后,重新算下是否缺失影像 应对状态撤回 + //if (svCommand.IsLostVisit == false) + //{ + + // var currentSubmittedVisitNum = await _repository.Where(t => t.SubmitState == SubmitStateEnum.Submitted).MaxAsync(t => t.VisitNum); + + // var isMissing = await _repository.AnyAsync(t => (t.VisitNum < currentSubmittedVisitNum && t.SubmitState != SubmitStateEnum.Submitted) || t.IsLostVisit); + + // await _repository.UpdateFromQueryAsync(t => t.Id == svCommand.SubjectId, u => new Subject() + // { + // IsMissingImages = isMissing + // }); + //} + + + + + return ResponseOutput.Ok(); + + + } + + + [HttpPut("{trialId:guid}/{subjectVisitId:guid}/{isUrgent:bool}")] + public async Task SetSubjectVisitUrgent(Guid subjectVisitId, bool isUrgent) + { + await _subjectVisitRepository.UpdateFromQueryAsync(t => t.Id == subjectVisitId, u => new SubjectVisit() { IsUrgent = isUrgent }); + + return ResponseOutput.Ok(); + } + + + [HttpDelete, Route("{id:guid}/{trialId:guid}")] + [TrialAudit(AuditType.SubjectAudit, AuditOptType.DeleteSubjectOutPlanVisit)] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task DeleteSV(Guid id) + { + if (await _repository.AnyAsync(t => t.SubjectVisitId == id)) + { + return ResponseOutput.NotOk("This visit is associated with the uploaded study images and couldn't be deleted."); + } + if (await _subjectVisitRepository.AnyAsync(t => t.Id == id && t.InPlan)) + { + return ResponseOutput.NotOk("This visit is InPlan and couldn't be deleted."); + } + return ResponseOutput.Result(await _repository.DeleteFromQueryAsync(s => s.Id == id)); + } + + /// + /// 设置受试者访视已执行 也就是将studyUploaded状态置为true 为了那些没有影像 人工设置准备 + /// + /// + /// + [HttpPost("{subjectVisitId:guid}/{trialId:guid}")] + [TrialAudit(AuditType.SubjectAudit, AuditOptType.SetSVExecuted)] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task SetSVExecuted(Guid subjectVisitId) + { + return ResponseOutput.Result(await _subjectVisitRepository.UpdateFromQueryAsync(s => s.Id == subjectVisitId, u => new SubjectVisit() { VisitExecuted = VisitExecutedEnum.Executed })); + } + + + /// + /// 获取访视下的Dicom 检查信息 分所有的, 阅片的 不阅片 isReading : 0 查询所有 1 查询仅仅阅片的 + /// + /// + /// + /// + /// + [HttpGet, Route("{trialId:guid}/{sujectVisitId:guid}/{isReading}")] + [AllowAnonymous] + public async Task> GetVisitStudyList(Guid trialId, Guid sujectVisitId, int isReading) + { + var studyList = await _repository.Where(t => t.TrialId == trialId && t.SubjectVisitId == sujectVisitId).Select(k => new VisitStudyDTO() + { + InstanceCount = k.InstanceCount, + Modalities = k.Modalities, + SeriesCount = k.SeriesCount, + StudyCode = k.StudyCode, + StudyId = k.Id + }).ToListAsync(); + var studyIds = studyList.Select(t => t.StudyId).ToList(); + + var instanceList = await _repository.Where(t => studyIds.Contains(t.StudyId)) + .Select(t => new { t.SeriesId, t.Id, t.InstanceNumber }).ToListAsync(); + + foreach (var t in studyList) + { + t.SeriesList = await _repository.Where(s => s.StudyId == t.StudyId) + .WhereIf(isReading == 1, s => s.IsReading).OrderBy(s => s.SeriesNumber). + ThenBy(s => s.SeriesTime) + .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + + + t.SeriesList.ForEach(series => series.InstanceList = instanceList.Where(t => t.SeriesId == series.Id).OrderBy(t => t.InstanceNumber).Select(k => k.Id).ToList()); + + //设置为阅片与否 不更改数据库检查 的instance数量 和 SeriesCount 所以这里要实时统计 + t.SeriesCount = t.SeriesList.Count(); + t.InstanceCount = t.SeriesList.SelectMany(t => t.InstanceList).Count(); + } + + + + + return studyList; + + //return ResponseOutput.Ok(studyList.Where(t => t.SeriesList.Count > 0).ToList()); + + } + + + } +} diff --git a/IRaCIS.Core.Application/Service/Visit/VisitPlanService.cs b/IRaCIS.Core.Application/Service/Visit/VisitPlanService.cs new file mode 100644 index 00000000..e575b3ce --- /dev/null +++ b/IRaCIS.Core.Application/Service/Visit/VisitPlanService.cs @@ -0,0 +1,437 @@ +using IRaCIS.Core.Infrastructure.ExpressionExtend; +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Application.Filter; +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Application.MediatR.CommandAndQueries; +using Magicodes.ExporterAndImporter.Core; +using Magicodes.ExporterAndImporter.Excel; +using Magicodes.ExporterAndImporter.Excel.AspNetCore; + +namespace IRaCIS.Application.Services +{ + [ApiExplorerSettings(GroupName = "Trial")] + public class VisitPlanService : BaseService, IVisitPlanService + { + private readonly IRepository _visitStageRepository; + private readonly IRepository _trialRepository; + + public VisitPlanService(IRepository visitStageRepository,IRepository trialRepository) + { + _visitStageRepository = visitStageRepository; + _trialRepository = trialRepository; + } + + + ///暂时不用 + /// 获取项目访视计划 + public async Task> GetTrialVisitStageList(VisitPlanQueryDTO param) + { + var visitStageQuery = _visitStageRepository.Where(u => u.TrialId == param.TrialId) + .WhereIf(!string.IsNullOrWhiteSpace(param.Keyword), t => t.VisitName.Contains(param.Keyword)) + .ProjectTo(_mapper.ConfigurationProvider); + + return await visitStageQuery.ToPagedListAsync(param.PageIndex, param.PageSize, "CreateTime", param.Asc); + + + } + + /// 根据项目Id,获取项目访视计划(不分页)[New] + [HttpGet("{trialId:guid}")] + public async Task GetVisitStageList(Guid trialId) + { + var query = _visitStageRepository.Where(u => u.TrialId == trialId) + .ProjectTo(_mapper.ConfigurationProvider).OrderBy(t => t.VisitNum); + var list = await query.ToListAsync(); + var trial = await _repository.FirstOrDefaultAsync(t => t.Id == trialId).IfNullThrowException(); + + return new VisitPlanView() + { + VisitPlanList = list, + TimePointsPerPatient = trial.TimePointsPerPatient, + VisitPlanConfirmed = trial.VisitPlanConfirmed, + IsHaveFirstGiveMedicineDate = trial.IsHaveFirstGiveMedicineDate, + //SubjectHasAdded = _subjectVisitRepository.Any(t => t.TrialId == trialId) + }; + } + + /// + /// 获取访视计划下拉框列表 + /// + /// + /// + [HttpGet("{trialId:guid}")] + public async Task> GetTrialVisitStageSelect(Guid trialId) + { + var query = _visitStageRepository.Where(u => u.TrialId == trialId) + .ProjectTo(_mapper.ConfigurationProvider).OrderBy(t => t.VisitNum); + var list = await query.ToListAsync(); + return list; + } + + + /// 添加或更新访视计划某项[New] + [HttpPost] + [UnitOfWork] + public async Task AddOrUpdateVisitStage(VisitPlanCommand visitPlan) + { + if (!await _repository.Where(t => t.Id == visitPlan.TrialId).IgnoreQueryFilters().AnyAsync(t => t.TrialStatusStr == StaticData.TrialOngoing || t.TrialStatusStr == StaticData.TrialInitializing)) + { + return ResponseOutput.NotOk(" only in Initializing or Ongoing State can operate "); + } + + var visitPlanList = await _visitStageRepository.Where(t => t.TrialId == visitPlan.TrialId) + .Select(t => new { t.Id, t.VisitNum, t.VisitDay }).OrderBy(t => t.VisitNum).ToListAsync(); + + //更新的时候,需要排除自己 + if (visitPlan.Id != null) + { + visitPlanList = visitPlanList.Where(t => t.Id != visitPlan.Id).ToList(); + } + + if (visitPlanList.Any(t => t.VisitNum < visitPlan.VisitNum)) + { + //比当前 visitNum小的 visitDay的最大值 还小 不允许添加 + if (visitPlan.VisitDay <= visitPlanList.Where(t => t.VisitNum < visitPlan.VisitNum).Select(t => t.VisitDay).Max()) + { + return ResponseOutput.NotOk("For the visit plan, the VisitDay with a larger VisitNum should be larger than the VisitDay with a smaller VisitNum."); + } + } + + if (visitPlanList.Any(t => t.VisitNum > visitPlan.VisitNum)) + { + if (visitPlan.VisitDay >= visitPlanList.Where(t => t.VisitNum > visitPlan.VisitNum).Select(t => t.VisitDay).Min()) + { + return ResponseOutput.NotOk("For the visit plan, the VisitDay with a larger VisitNum should be larger than the VisitDay with a smaller VisitNum."); + } + } + + + if (visitPlan.Id == Guid.Empty || visitPlan.Id == null)//add + { + + if (await _visitStageRepository.AnyAsync(t => t.TrialId == visitPlan.TrialId && (t.VisitName == visitPlan.VisitName || t.VisitNum == visitPlan.VisitNum))) + { + return ResponseOutput.NotOk("A visit with the same VisitName/VisitNum already existed in the current visit plan."); + } + + if (await _visitStageRepository.AnyAsync(t => t.TrialId == visitPlan.TrialId && t.IsBaseLine) && visitPlan.IsBaseLine) + { + return ResponseOutput.NotOk("A visit already is baseline in the current visit plan."); + } + + //已添加受试者 都不存在该新增的计划名称 那么该项目所有受试者都增加一个访视记录 + if (!await _repository.AnyAsync(t => t.VisitName == visitPlan.VisitName && t.TrialId == visitPlan.TrialId)) + { + var subjectSVS = await _repository.Where(t => t.TrialId == visitPlan.TrialId).Select(t => new { t.SubjectId, t.SiteId,t.IsFinalVisit }).Distinct().ToListAsync(); + + var subjects = subjectSVS.Select(t => new { t.SubjectId, t.SiteId }).Distinct().ToList(); + + foreach (var subject in subjects) + { + var svItem = _mapper.Map(visitPlan); + + svItem.SiteId = subject.SiteId; + + svItem.SubjectId = subject.SubjectId; + + //设置了末次访视,那么加访视计划的时候,设置为不可用 + if (subjectSVS.Any(t => t.SubjectId == svItem.SubjectId && t.IsFinalVisit)) + { + svItem.VisitExecuted = VisitExecutedEnum.Unavailable; + } + + await _repository.AddAsync(svItem); + } + } + + + var visitPlanItem = _mapper.Map(visitPlan); + + visitPlanItem.BlindName = "B" + ((int)visitPlanItem.VisitNum * 10).ToString("D3"); + + var result = await _repository.AddAsync(visitPlanItem); + + //更新项目访视计划状态为未确认 + await _repository.UpdateFromQueryAsync(u => u.Id == visitPlan.TrialId, t => new Trial() { VisitPlanConfirmed = false }); + + await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(result.Id.ToString()); + + + } + + else//update + { + + + if (await _visitStageRepository.AnyAsync(t => t.TrialId == visitPlan.TrialId && (t.VisitName == visitPlan.VisitName || t.VisitNum == visitPlan.VisitNum) && t.Id != visitPlan.Id)) + { + return ResponseOutput.NotOk("A visit with the same VisitName/VisitNum already existed in the current visit plan."); + } + + if (await _visitStageRepository.AnyAsync(t => t.TrialId == visitPlan.TrialId && t.IsBaseLine && t.Id != visitPlan.Id) && visitPlan.IsBaseLine) + { + return ResponseOutput.NotOk("A visit already is baseline in the current visit plan."); + } + + + + var stage = await _visitStageRepository.FirstOrDefaultAsync(t => t.Id == visitPlan.Id); + if (stage == null) return Null404NotFound(stage); + + //修改是否是基线 + if (stage.IsBaseLine && stage.IsBaseLine != visitPlan.IsBaseLine) + { + if (await _repository.Where(t => t.TrialId == visitPlan.TrialId).AnyAsync(v => v.IsBaseLine && v.SubmitState >= SubmitStateEnum.ToSubmit)) + { + return ResponseOutput.NotOk("有CRC已经为基线上传了影像数据,不允许修改基线"); + } + } + + _mapper.Map(visitPlan, stage); + + stage.BlindName = "B" + ((int)visitPlan.VisitNum * 10).ToString("D3"); + + //更新项目访视计划状态为未确认 + await _repository.UpdateFromQueryAsync(u => u.Id == visitPlan.TrialId, t => new Trial() { VisitPlanConfirmed = false }); + + await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(); + } + + } + + /// 删除项目计划某一项[New] + [HttpDelete("{id:guid}/{trialId:guid}")] + [TrialAudit(AuditType.TrialAudit, AuditOptType.DeleteTrialVisitPlanItem)] + + [TypeFilter(typeof(TrialResourceFilter))] + public async Task DeleteVisitStage(Guid id) + { + + var visitPlan = await _visitStageRepository.FirstOrDefaultAsync(t => t.Id == id); + + if (visitPlan == null) return Null404NotFound(visitPlan); + + if (await _repository.AnyAsync(t => t.VisitName == visitPlan.VisitName && t.TrialId == visitPlan.TrialId && t.VisitExecuted == VisitExecutedEnum.Executed)) + { + return ResponseOutput.NotOk("The visit plan has been assigned to the subjects and executed."); + } + + await _repository.DeleteFromQueryAsync(t => t.TrialId == visitPlan.TrialId && t.VisitName == visitPlan.VisitName); + + var result = await _visitStageRepository.DeleteFromQueryAsync(u => u.Id == id); + + + return ResponseOutput.Result(result); + } + + + [UnitOfWork] + [HttpPost("{trialId:guid}")] + [TrialAudit(AuditType.TrialAudit, AuditOptType.ConfirmTrialVisitPlan)] + + public async Task ConfirmTrialVisitPlan(Guid trialId, [FromServices] IRepository _influnceStatRepository) + { + if (!await _trialRepository.AnyAsync(t => t.Id == trialId &&( t.TrialStatusStr==StaticData.TrialInitializing || t.TrialStatusStr == StaticData.TrialOngoing))) + { + return ResponseOutput.NotOk("仅仅在项目初始化或者进行中时,才允许修改确认"); + } + + if (!await _visitStageRepository.AnyAsync(t => t.TrialId == trialId && t.IsBaseLine)) + { + return ResponseOutput.NotOk("没有基线,不允许确认"); + } + + if (!await _trialRepository.AnyAsync(t => t.Id == trialId && t.IsTrialBasicLogicConfirmed && t.IsTrialProcessConfirmed && t.IsTrialUrgentConfirmed)) + { + return ResponseOutput.NotOk("项目配置未确认,不允许确认访视计划"); + } + + var svList = await _visitStageRepository.Where(t => t.TrialId == trialId).Select(u => new { u.VisitDay, u.IsBaseLine }).ToListAsync(); + + if (svList.Min(t => t.VisitDay) != svList.Where(t => t.IsBaseLine).FirstOrDefault()?.VisitDay) + { + return ResponseOutput.NotOk("基线VisitDay 不是最小的, 不允许确认"); + } + + //更新项目访视计划状态为已确认 + await _trialRepository.UpdateFromQueryAsync(u => u.Id == trialId, t => new Trial() { VisitPlanConfirmed = true }); + + //找到访视计划修改的Item + var changedList = await _visitStageRepository.Where(t => t.TrialId == trialId && t.IsConfirmed == false) + .Select(t => new { t.Trial.IsHaveFirstGiveMedicineDate, t.Id, t.VisitName, t.VisitWindowLeft, t.VisitWindowRight, t.VisitDay, t.VisitNum, t.IsBaseLine }).ToListAsync(); + + + //访视计划 整体状态变更为 确认 + await _visitStageRepository.UpdateFromQueryAsync(u => u.TrialId == trialId, t => new VisitStage() { IsConfirmed = true }); + + var stat = new VisitPlanInfluenceStat() { TrialId = trialId }; + + foreach (var changedItem in changedList) + { + //找到该项目 访视已经执行,并且配置了有首次给药日期 并且更新后超窗的访视,要把超窗之前的值也要查询出来 + var qcPassedVisitList = await _repository.Where(t => t.TrialId == trialId + && t.VisitExecuted == VisitExecutedEnum.Executed + && t.AuditState == AuditStateEnum.QCPassed + && t.Trial.IsHaveFirstGiveMedicineDate == true + && t.VisitStageId == changedItem.Id + && t.Subject.FirstGiveMedicineTime != null + ).Select(k => new + { + SubjectVisitId = k.Id, + SelfWindowLeft = k.Subject.FirstGiveMedicineTime!.Value.AddDays(k.VisitDay + k.VisitWindowLeft), + SelfWindowRight = k.Subject.FirstGiveMedicineTime!.Value.AddDays(k.VisitDay + k.VisitWindowRight + 1).AddSeconds(-1), + + NowWindowLeft = k.Subject.FirstGiveMedicineTime!.Value.AddDays(changedItem.VisitDay + changedItem.VisitWindowLeft), + NowWindowRight = k.Subject.FirstGiveMedicineTime!.Value.AddDays(changedItem.VisitDay + changedItem.VisitWindowRight + 1).AddSeconds(-1), + + NoneDicomStudyList = + k.NoneDicomStudyList //之前是查询调整之后超窗的 现在调整前超窗 调整后 没超窗的也要记录 + //.Where(study => study.ImageDate k.Subject.FirstGiveMedicineTime.Value.AddDays(changedItem.VisitDay + changedItem.VisitWindowRight??0 + 1).AddSeconds(-1)) + .Select(t => new { NoneDicomStudyId = t.Id, t.Modality, StudyTime = t.ImageDate }), + + + DicomStudyList = k.StudyList + //.Where(study => study.StudyTime k.Subject.FirstGiveMedicineTime.Value.AddDays(changedItem.VisitDay + changedItem.VisitWindowRight??0 + 1).AddSeconds(-1)) + .Select(t => new { StudyId = t.Id, Modality = t.Modalities, t.StudyTime }) + + }).ToListAsync(); + + + foreach (var visit in qcPassedVisitList) + { + //找到本身没有超窗的数据 修改后超窗的 + visit.DicomStudyList.Where(t => (t.StudyTime > visit.SelfWindowLeft && t.StudyTime < visit.SelfWindowRight) && (t.StudyTime < visit.NowWindowLeft || t.StudyTime > visit.NowWindowRight)).ForEach(t => + { + stat.InconsistentCount++; + stat.InfluenceStudyList.Add(new VisitPlanInfluenceStudy() + { + IsOverWindowNowNotOverWindow = false, + Modality = t.Modality, + SubjectVisitId = visit.SubjectVisitId, + StudyId = t.StudyId, + IsDicomStudy = true, + StudyTime = t.StudyTime, + TrialId = trialId, + HistoryWindow = visit.SelfWindowLeft.ToString("yyyy-MM-dd") + " ~ " + visit.SelfWindowRight.ToString("yyyy-MM-dd"), + NowWindow = visit.NowWindowLeft.ToString("yyyy-MM-dd") + " ~ " + visit.NowWindowRight.ToString("yyyy-MM-dd") + + }); + + }); + + visit.NoneDicomStudyList.Where(t => (t.StudyTime > visit.SelfWindowLeft && t.StudyTime < visit.SelfWindowRight) && (t.StudyTime < visit.NowWindowLeft || t.StudyTime > visit.NowWindowRight)).ForEach(t => + { + stat.InconsistentCount++; + stat.InfluenceStudyList.Add(new VisitPlanInfluenceStudy() + { + IsOverWindowNowNotOverWindow = false, + Modality = t.Modality, + SubjectVisitId = visit.SubjectVisitId, + StudyId = t.NoneDicomStudyId, + IsDicomStudy = false, + StudyTime = t.StudyTime, + TrialId = trialId, + HistoryWindow = visit.SelfWindowLeft.ToString("yyyy-MM-dd") + " ~ " + visit.SelfWindowRight.ToString("yyyy-MM-dd"), + NowWindow = visit.NowWindowLeft.ToString("yyyy-MM-dd") + " ~ " + visit.NowWindowRight.ToString("yyyy-MM-dd") + + }); + }); + + //本身超窗 修改后没超窗的 + visit.DicomStudyList.Where(t => (t.StudyTime < visit.SelfWindowLeft || t.StudyTime > visit.SelfWindowRight) && (t.StudyTime > visit.NowWindowLeft && t.StudyTime < visit.NowWindowRight)).ForEach(t => + { + stat.InconsistentCount++; + stat.InfluenceStudyList.Add(new VisitPlanInfluenceStudy() + { + IsOverWindowNowNotOverWindow = true, + Modality = t.Modality, + SubjectVisitId = visit.SubjectVisitId, + StudyId = t.StudyId, + IsDicomStudy = true, + StudyTime = t.StudyTime, + TrialId = trialId, + HistoryWindow = visit.SelfWindowLeft.ToString("yyyy-MM-dd") + " ~ " + visit.SelfWindowRight.ToString("yyyy-MM-dd"), + NowWindow = visit.NowWindowLeft.ToString("yyyy-MM-dd") + " ~ " + visit.NowWindowRight.ToString("yyyy-MM-dd") + + }); + }); + + visit.NoneDicomStudyList.Where(t => (t.StudyTime < visit.SelfWindowLeft || t.StudyTime > visit.SelfWindowRight) && (t.StudyTime > visit.NowWindowLeft && t.StudyTime < visit.NowWindowRight)).ForEach(t => + { + stat.InconsistentCount++; + stat.InfluenceStudyList.Add(new VisitPlanInfluenceStudy() + { + IsOverWindowNowNotOverWindow = true, + Modality = t.Modality, + SubjectVisitId = visit.SubjectVisitId, + StudyId = t.NoneDicomStudyId, + IsDicomStudy = false, + StudyTime = t.StudyTime, + TrialId = trialId, + HistoryWindow = visit.SelfWindowLeft.ToString("yyyy-MM-dd") + " ~ " + visit.SelfWindowRight.ToString("yyyy-MM-dd"), + NowWindow = visit.NowWindowLeft.ToString("yyyy-MM-dd") + " ~ " + visit.NowWindowRight.ToString("yyyy-MM-dd") + + }); + }); + } + + //变更某一访视计划Item 受试者访视相关字段 + await _repository.UpdateFromQueryAsync(t => t.TrialId == trialId && t.VisitStageId == changedItem.Id, k => new SubjectVisit() + { + IsBaseLine = changedItem.IsBaseLine, + VisitName = changedItem.VisitName, + VisitNum = changedItem.VisitNum, + VisitDay = changedItem.VisitDay, + VisitWindowLeft = changedItem.VisitWindowLeft, + VisitWindowRight = changedItem.VisitWindowRight + }); + + } + + + await _repository.AddAsync(stat); + await _repository.SaveChangesAsync(); + return ResponseOutput.Ok(); + + } + + + + + [HttpGet("{trialId:guid}")] + public async Task> GetInfluenceHistoryList(Guid trialId, [FromServices] IRepository _influnceStatRepository) + { + var list = await _influnceStatRepository.Where(t => t.TrialId == trialId).ProjectTo(_mapper.ConfigurationProvider).OrderByDescending(t => t.CreateTime).ToListAsync(); + + return list; + } + + [HttpGet("{visitPlanInfluenceStatId:guid}")] + public async Task DownloadInflunceStudyList(Guid visitPlanInfluenceStatId, [FromServices] IRepository _influnceRepository) + { + var list = _influnceRepository.Where(t => t.VisitPlanInfluenceStatId == visitPlanInfluenceStatId) + .ProjectTo(_mapper.ConfigurationProvider).ToList(); + + + IExporter exporter = new ExcelExporter(); + + var result = await exporter.ExportAsByteArray(list); + + + return new XlsxFileResult(bytes: result, fileDownloadName: $"检查导出_{DateTime.Now.ToString("yyyy-MM-dd:hh:mm:ss")}.xlsx"); + + + } + + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/Visit/_MapConfig.cs b/IRaCIS.Core.Application/Service/Visit/_MapConfig.cs new file mode 100644 index 00000000..4021ee4f --- /dev/null +++ b/IRaCIS.Core.Application/Service/Visit/_MapConfig.cs @@ -0,0 +1,95 @@ +using AutoMapper; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Application.Contracts; +using IRaCIS.Core.Application.MediatR.CommandAndQueries; +using IRaCIS.Core.Domain.Share; + +namespace IRaCIS.Core.Application.Service +{ + public class VisitMapConfig : Profile + { + public VisitMapConfig() + { + CreateMap(); + CreateMap(); + CreateMap(); + + CreateMap(); + + CreateMap(); + CreateMap() + .ForMember(d => d.Id, t => t.Ignore()) + .ForMember(d => d.VisitStageId, t =>t.MapFrom(u=>u.Id)); + + + CreateMap() + .ForMember(d => d.CreateUser, u => u.MapFrom(g => g.CreateUser.LastName + " / " + g.CreateUser.FirstName)); + + + + CreateMap(); + CreateMap(); + + CreateMap() + .ForMember(d => d.SiteName, u => u.MapFrom(s => s.TrialSite.Site.SiteName)) + .ForMember(d => d.TrialSiteCode, u => u.MapFrom(s => s.TrialSite.TrialSiteCode)) + .ForMember(d => d.LatestBlindName, u => u.MapFrom(s => s.LatestSubjectVisit.BlindName)) + .ForMember(d => d.LatestVisitName, u => u.MapFrom(s => s.LatestSubjectVisit.VisitName)) + //.ForMember(d => d.IsSubjectSexView, u => u.MapFrom(s => s.Trial.IsSubjectSexView)) + //.ForMember(d => d.IsSubjectExpeditedView, u => u.MapFrom(s => s.Trial.IsSubjectExpeditedView)) + + //不能对包含聚合或子查询的表达式执行聚合函数 + //.ForMember(d => d.InPlanStudyCount, u => u.MapFrom(s => s.SubjectVisitList.Where(t => t.InPlan).Sum(k => k.StudyList.Count()))) + //.ForMember(d => d.OutPlanStudyCount, u => u.MapFrom(s => s.SubjectVisitList.Where(t => t.InPlan==false).Sum(k => k.StudyList.Count()))) + + //.ForMember(d => d.InPlanDicomStudyUploadCount, u => u.MapFrom(s => s.SubjectVisitList.Where(t => t.InPlan).SelectMany(k => k.StudyList).Count())) + //.ForMember(d => d.OutPlanDicomStudyUploadCount, u => u.MapFrom(s => s.SubjectVisitList.Where(t => t.InPlan == false).SelectMany(k => k.StudyList).Count())) + + //.ForMember(d => d.InPlanNoneDicomStudyUploadCount, u => u.MapFrom(s => s.SubjectVisitList.Where(t => t.InPlan).SelectMany(k => k.NoneDicomStudyList).Count())) + //.ForMember(d => d.OutPlanNoneDicomStudyUploadCount, u => u.MapFrom(s => s.SubjectVisitList.Where(t => t.InPlan == false).SelectMany(k => k.NoneDicomStudyList).Count())) + .ForMember(d => d.FinalSubjectVisitId, u => u.MapFrom(s => s.SubjectVisitList.Where(t => t.IsFinalVisit).Select(c=>(Guid?) c.Id).FirstOrDefault())) + .ForMember(d => d.InPlanVisitCount, u => u.MapFrom(s => s.SubjectVisitList.Count(t => t.InPlan))) + .ForMember(d => d.OutPlanVisitCount, u => u.MapFrom(s => s.SubjectVisitList.Count(t => t.InPlan == false))) + //执行不一定上传了 可能是失访 实际执行过了 + + + .ForMember(d => d.MissingSubmmitCount, u => u.MapFrom(s => s.SubjectVisitList.Count(t => t.VisitNum d.InPlanVisitSubmmitCount, u => u.MapFrom(s => s.SubjectVisitList.Count(t => t.SubmitState == SubmitStateEnum.Submitted && t.InPlan == true))) + .ForMember(d => d.LostVisitCount, u => u.MapFrom(s => s.SubjectVisitList.Count(t => t.IsLostVisit))) + .ForMember(d => d.InPlanVisitSubmmitCount, u => u.MapFrom(s => s.SubjectVisitList.Count(t => t.SubmitState == SubmitStateEnum.Submitted && t.InPlan == true))) + .ForMember(d => d.OutPlanVisitSubmmitCount, u => u.MapFrom(s => s.SubjectVisitList.Count(t => t.SubmitState == SubmitStateEnum.Submitted && t.InPlan == false))); + // .ForMember(d => d.InPlanVisitSubmmitCount, u => u.MapFrom(s => s.SubjectVisitList.Count(t => t.SubmitState>=SubmitStateEnum.ToSubmit && t.InPlan == true))) + //.ForMember(d => d.OutPlanVisitSubmmitCount, u => u.MapFrom(s => s.SubjectVisitList.Count(t => t.SubmitState >= SubmitStateEnum.ToSubmit && t.InPlan == false))); + //.ForMember(d => d.InPlanVisitUploadCount, u => u.MapFrom(s => s.SubjectVisitList.Count(t => t.VisitExecuted == VisitExecutedEnum.Executed && t.InPlan))) + //.ForMember(d => d.OutPlanVisitUploadCount, u => u.MapFrom(s => s.SubjectVisitList.Count(t => t.VisitExecuted == VisitExecutedEnum.Executed && t.InPlan == false))); + + + //审计信息 这里不用IncludeMembers 也可以识别 是以导航属性名称开头 + // 还有 外键? COALESCE([t0].[SubjectId], '00000000-0000-0000-0000-000000000000') 因为destination 是Guid + CreateMap() + .ForMember(d => d.SubjectName, u => u.MapFrom(s => s.Subject.LastName + " / " + s.Subject.FirstName)) + .ForMember(d => d.SubjectCode, u => u.MapFrom(s => s.Subject.Code)); + + + CreateMap().ForAllMembers(opt => opt.Condition((src, dest, srcMember) => srcMember != null)); + + + + CreateMap().IncludeMembers(t=>t.Trial) + .ForMember(d => d.SiteName, u => u.MapFrom(s => s.Site.SiteName)) + .ForMember(d => d.SiteCode, u => u.MapFrom(s => s.Site.SiteCode)) + .ForMember(d => d.SubjectVisitId, u => u.MapFrom(s => s.Id)) + .ForMember(d => d.TrialSiteCode, u => u.MapFrom(s => s.TrialSite.TrialSiteCode)) + .ForMember(d => d.TrialCode, u => u.MapFrom(s => s.Trial.TrialCode)) + .ForMember(d => d.Sponsor, u => u.MapFrom(s => s.Trial.Sponsor.SponsorName)); + CreateMap(); + + CreateMap() + .ForMember(d => d.SubjectVisitId, u => u.MapFrom(s => s.Id)) + .ForMember(d => d.SubjectCode, u => u.MapFrom(s => s.SubjectVisit.Subject.Code)) + .ForMember(d => d.VisitName, u => u.MapFrom(s => s.SubjectVisit.VisitName)) + .ForMember(d => d.TrialSiteCode, u => u.MapFrom(s => s.SubjectVisit.TrialSite.TrialSiteCode)); + } + } + +} diff --git a/IRaCIS.Core.Application/Service/WorkLoad/DTO/DoctorWorkLoadViewModel.cs b/IRaCIS.Core.Application/Service/WorkLoad/DTO/DoctorWorkLoadViewModel.cs new file mode 100644 index 00000000..e7eebc3e --- /dev/null +++ b/IRaCIS.Core.Application/Service/WorkLoad/DTO/DoctorWorkLoadViewModel.cs @@ -0,0 +1,231 @@ +using System.ComponentModel.DataAnnotations; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Contracts +{ + /// + ///后台 工作量审核视图模型 + /// + public class WorkLoadDetailDTO : WorkloadDTO + { + public DateTime CreateTime { get; set; } + public string ChineseName { get; set; } = String.Empty; + + public string FirstName { get; set; } = String.Empty; + public string LastName { get; set; } = String.Empty; + public string Code { get; set; } = String.Empty; + public string Indication { get; set; } = String.Empty; + public int RowIntId { get; set; } + public Guid RowGuid { get; set; } + public string YearMonthStr { get; set; } = String.Empty; + + public string WorkTimeStr => YearMonthStr; + + public bool IsLock { get; set; } = false; + } + + public class WorkLoadDetailViewModel : WorkloadDTO + { + public DateTime CreateTime { get; set; } + public string ChineseName { get; set; } = String.Empty; + public string FirstName { get; set; } = String.Empty; + public string LastName { get; set; } = String.Empty; + public string Code { get; set; } = String.Empty; + public string Indication { get; set; } = String.Empty; + + public Guid RowGuid { get; set; } + public string YearMonthStr { get; set; } = String.Empty; + + public bool IsLock { get; set; } = false; + public string DeclareTimeStr => CreateTime.ToString("yyyy-MM-dd"); + + public string WorkTimeStr => WorkTime.ToString("yyyy-MM-dd"); + } + + + + public class CalculatePaymentDTO : WorkloadDTO + { + public string TrialCode { get; set; } = String.Empty; + public decimal? TrialAdditional { get; set; } + public decimal AdjustmentMultiple { get; set; } + //public double RankPrice { get; set; } + public decimal PersonalAdditional { get; set; } + public decimal TimepointPrice { get; set; } + public decimal TimepointIn24HPrice { get; set; } + public decimal TimepointIn48HPrice { get; set; } + public decimal AdjudicationPrice { get; set; } + public decimal AdjudicationIn24HPrice { get; set; } + public decimal AdjudicationIn48HPrice { get; set; } + public decimal GlobalPrice { get; set; } + public decimal TrainingPrice { get; set; } + public decimal DowntimePrice { get; set; } + public decimal RefresherTrainingPrice { get; set; } + } + + /// + /// 工作量审核数据库查询模型 + /// + public class WorkloadDTO + { + public Guid? Id { get; set; } + + public Guid TrialId { get; set; } + + public Guid DoctorId { get; set; } + + public int DataFrom { get; set; } + + public DateTime WorkTime { get; set; } + + public int Training { get; set; } + + public int Downtime { get; set; } + + public int Timepoint { get; set; } + + public int TimepointIn24H { get; set; } + + public int TimepointIn48H { get; set; } + + public int Adjudication { get; set; } + + public int AdjudicationIn24H { get; set; } + + public int AdjudicationIn48H { get; set; } + + public int Global { get; set; } + public int RefresherTraining { get; set; } + + + } + + /// + /// 工作量分页列表模型 医生端查询模型 + /// + public class WorkLoadQueryDTO : PageInput + { + public Guid TrialId { get; set; } + public int WorkLoadFromStatus { get; set; } // 关联枚举 + public DateTime? SearchBeginDateTime { get; set; } + public DateTime? SearchEndDateTime { get; set; } + } + + /// + /// 后台查询模型 + /// + public class WorkLoadStatsQueryDTO : PageInput + { + public Guid? TrialId { get; set; } + public Guid? DoctorId { get; set; } = Guid.Empty; + public List WorkLoadFromStatus { get; set; } = new List(); // 关联枚举 + public DateTime? SearchBeginDateTime { get; set; } + public DateTime? SearchEndDateTime { get; set; } + } + + + public class WorkLoadDetailQueryDTO + { + public Guid TrialId { get; set; } = Guid.Empty; + public Guid DoctorId { get; set; } = Guid.Empty; + public string YearMonthStr { get; set; } = String.Empty; + } + + public class CalculateDoctorAndMonthDTO + { + [Required(ErrorMessage = "需要有效的时间")] + public DateTime CalculateMonth { get; set; } + + [Required(ErrorMessage = "需要有效的医生列表")] + public List NeedCalculateReviewers { get; set; } = new List(); + } + + public class WorkLoadDoctorQueryDTO : PageInput + { + public Guid TrialId { get; set; } = Guid.Empty; + } + + public class WorkLoadCommand : WorkloadDTO + { + public Guid CreateUserId { get; set; } + public int CreateUserType { get; set; } + public Guid UpdateUserId { get; set; } + } + + + public class ExistWorkloadViewModel + { + public bool IsExist { get; set; } + public WorkloadDTO WorkLoad { get; set; } = new WorkloadDTO(); + } + + public class WorkloadCommand : WorkloadDTO + { + } + + public class WorkloadExistQueryDTO + { + public Guid TrialId { get; set; } + public Guid DoctorId { get; set; } + public DateTime WorkDate { get; set; } + } + + + + public class WorkLoadAndTrainingDTO + { + + public Guid DoctorId { get; set; }/*=Guid.Empty;*/ + public string Code { get; set; } = String.Empty; + public string FirstName { get; set; } = String.Empty; + public string LastName { get; set; } = String.Empty; + public string ChineseName { get; set; } = String.Empty; + + public DateTime? EnrollTime { get; set; } + + public int ReviewerReadingType { get; set; } + + public string? EnrollTimeStr => EnrollTime?.ToString("yyyy-MM-dd HH:mm:ss"); + + public DateTime? UpdateTime { get; set; } + + public string? UpdateTimeStr => UpdateTime?.ToString("yyyy-MM-dd HH:mm:ss"); + + public int? TrainingTimes { get; set; } + public int? Downtime { get; set; } + + //读片点 + public int? Timepoint { get; set; } + public int? TimepointIn24H { get; set; } + public int? TimepointIn48H { get; set; } + + //裁判阅片 + public int? Adjudication { get; set; } + public int? AdjudicationIn24H { get; set; } + public int? AdjudicationIn48H { get; set; } + + //全局阅片 + public int? Global { get; set; } + + public int? RefresherTraining { get; set; } + + + + } + + + public class WorkLoadAndAgreementDTO : WorkLoadAndTrainingDTO + { + public DateTime? OutEnrollTime { get; set; } + public Guid AgreementId { get; set; } + + public string AgreementPath { get; set; } = String.Empty; + + public string? AgreementFileName => AgreementPath?.Split('/').Last(); + + public string AgreementFullPath => AgreementPath; + } + + + +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/WorkLoad/DTO/EnrollViewModel.cs b/IRaCIS.Core.Application/Service/WorkLoad/DTO/EnrollViewModel.cs new file mode 100644 index 00000000..0aafd7d1 --- /dev/null +++ b/IRaCIS.Core.Application/Service/WorkLoad/DTO/EnrollViewModel.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IRaCIS.Core.Application.Service.WorkLoad.DTO +{ + public class EnrollViewModel + { + [StringLength(50)] + public string ChineseName { get; set; } = string.Empty; + + + [StringLength(100)] + public string FirstName { get; set; } = string.Empty; + + + [StringLength(100)] + public string LastName { get; set; } = string.Empty; + + public decimal? AdjustmentMultiple { get; set; } = 0; + + public Guid DoctorId { get; set; } + + public Guid TrialId { get; set; } + + public Guid Id { get; set; } + + public int? Training { get; set; } + + public int? RefresherTraining { get; set; } + + public int? Timepoint { get; set; } + + public int? Timepoint48H { get; set; } + + public int? Timepoint24H { get; set; } + + public int? Adjudication { get; set; } + + public int? Adjudication48H { get; set; } + + public int? Adjudication24H { get; set; } + + public int? Global { get; set; } + + + public int? Downtime { get; set; } + + public string ReviewerCode { get; set; } = string.Empty; + + public int? Code { get; set; } + + } + + + public class EnrollGetQuery:PageInput + { + public Guid TrialId { get; set; } + + + + + + + } + + public class EnrollCommand + { + public Guid Id { get; set; } + + + public decimal? Training { get; set; } + + public decimal? RefresherTraining { get; set; } + + public decimal? Timepoint { get; set; } + + public decimal? Timepoint48H { get; set; } + + public decimal? Timepoint24H { get; set; } + + public decimal? Adjudication { get; set; } + + public decimal? Adjudication48H { get; set; } + + public decimal? Adjudication24H { get; set; } + + public decimal? Global { get; set; } + + + public decimal? Downtime { get; set; } + + + } + } diff --git a/IRaCIS.Core.Application/Service/WorkLoad/DTO/WorkloadDistribution.cs b/IRaCIS.Core.Application/Service/WorkLoad/DTO/WorkloadDistribution.cs new file mode 100644 index 00000000..c577802b --- /dev/null +++ b/IRaCIS.Core.Application/Service/WorkLoad/DTO/WorkloadDistribution.cs @@ -0,0 +1,143 @@ +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Contracts +{ + #region TP + public class WorkloadTPDTO + { + public Guid Id { get; set; } + public Guid SiteId { get; set; } + public string SiteName { get; set; } = String.Empty; + public Guid SubjectId { get; set; } + public string SubjectCode { get; set; } = String.Empty; + public Guid SubjectVisitId { get; set; } + public string VisitName { get; set; } = String.Empty; + public decimal VisitNum { get; set; } + public Guid StudyId { get; set; } + public string StudyCode { get; set; } = String.Empty; + public string TimepointCode { get; set; } = String.Empty; + public Guid ReviewerId { get; set; } + public string ReviewerCode { get; set; } = String.Empty; + public string ReviewerFirstName { get; set; } = String.Empty; + public string ReviewerLastName { get; set; } = String.Empty; + public string ReviewerChineseName { get; set; } = String.Empty; + public int Status { get; set; } + public DateTime UpdateTime { get; set; } + } + #endregion + + #region Global + public class WorkloadGlobalDTO + { + public Guid Id { get; set; } + public Guid SiteId { get; set; } + public string SiteName { get; set; } = String.Empty; + public Guid VisitId { get; set; } + public string VisitName { get; set; } = String.Empty; + + public Guid TrialId { get; set; } + public Guid SubjectId { get; set; } + public string SubjectCode { get; set; } = String.Empty; + public decimal VisitNum { get; set; } + + public string GlobalCode { get; set; } = String.Empty; + public Guid ReviewerId { get; set; } + public string ReviewerCode { get; set; } = String.Empty; + public string ReviewerFirstName { get; set; } = String.Empty; + public string ReviewerLastName { get; set; } = String.Empty; + public string ReviewerChineseName { get; set; } = String.Empty; + public int Status { get; set; } + public DateTime UpdateTime { get; set; } + } + #endregion + + #region AD + public class WorkloadADDTO + { + public Guid Id { get; set; } + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public string SiteName { get; set; }=String.Empty; + public Guid SubjectId { get; set; } + public string SubjectCode { get; set; } = String.Empty; + public string ADCode { get; set; } = String.Empty; + public Guid ReviewerId { get; set; } + public string ReviewerCode { get; set; } = String.Empty; + public string ReviewerFirstName { get; set; } = String.Empty; + public string ReviewerLastName { get; set; } = String.Empty; + public string ReviewerChineseName { get; set; } = String.Empty; + public int Status { get; set; } + public DateTime UpdateTime { get; set; } + public Guid Global1Id { get; set; } + public Guid Global2Id { get; set; } + } + #endregion + + public class WorkloadDistributionQueryParam : PageInput + { + public Guid TrialId { get; set; } = Guid.Empty; + public Guid? SiteId { get; set; } = Guid.Empty; + public string SubjectCode { get; set; } = string.Empty; + public string WorkloadCode { get; set; } = string.Empty; + public string Reviewer { get; set; } = string.Empty; + public int? Status { get; set; } + public int? GroupId { get; set; } = 0; + } + + public class WorkloadTPInfo + { + public Guid Id { get; set; } + public Guid StudyId { get; set; } + } + public class WorkloadTPCommand + { + public List TpList { get; set; } = new List(); + public Guid ReviewerId { get; set; } + public string ReviewerName { get; set; } = String.Empty; + } + + public class WorkloadGlobalInfo + { + public Guid Id { get; set; } + public Guid SubjectId { get; set; } + public decimal VisitNum { get; set; } + } + public class WorkloadGlobalCommand + { + public List GlobalList { get; set; } = new List(); + public Guid ReviewerId { get; set; } + public string ReviewerName { get; set; } = String.Empty; + } + public class WorkloadAdCommand + { + public List IdList { get; set; } = new List(); + public Guid ReviewerId { get; set; } + public string ReviewerName { get; set; } = String.Empty; + } + + //public class WorkloadDistributionUpdateCommand + //{ + // public Guid Id { get; set; } + // public Guid? ReviewerId { get; set; } + //} + + public class WorkloadDetailDTO + { + public Guid Id { get; set; } + public Guid WorkloadId { get; set; } + public string OptUserName { get; set; } = String.Empty; + public DateTime OptTime { get; set; } + public string ReviewerFirstName { get; set; } = String.Empty; + public string ReviewerLastName { get; set; } = String.Empty; + public string ReviewerChineseName { get; set; } = String.Empty; + public int Status { get; set; } + } + + //public class WorkloadDetailQueryParam + //{ + // public int WorkloadType { get; set; } //1-tp,2-global,3-ad + // public Guid WorkloadId { get; set; } + + //} + +} diff --git a/IRaCIS.Core.Application/Service/WorkLoad/DoctorWorkloadService.cs b/IRaCIS.Core.Application/Service/WorkLoad/DoctorWorkloadService.cs new file mode 100644 index 00000000..a50053c4 --- /dev/null +++ b/IRaCIS.Core.Application/Service/WorkLoad/DoctorWorkloadService.cs @@ -0,0 +1,800 @@ +using AutoMapper; +using IRaCIS.Core.Infrastructure.ExpressionExtend; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Share; +using System.Linq.Expressions; +using IRaCIS.Core.Application.Filter; +using Microsoft.AspNetCore.Mvc; + +namespace IRaCIS.Application.Services +{ + [ApiExplorerSettings(GroupName = "Trial")] + public class DoctorWorkloadService : BaseService, IDoctorWorkloadService + { + private readonly IRepository _trialRepository; + private readonly IRepository _enrollRepository; + private readonly IRepository _doctorRepository; + private readonly IRepository _doctorWorkloadRepository; + private readonly IRepository _attachmentRepository; + private readonly IRepository _costStatisticsRepository; + private readonly IRepository _trialRevenuesPriceRepository; + private readonly IRepository _trialRevenuesPriceVerificationRepository; + + + public DoctorWorkloadService(IRepository clinicalTrialProjectRepository, + IRepository intoGroupRepository, + IRepository doctorInfoRepository, + IRepository doctorWorkloadRepository, + IRepository attachmentRepository, + IRepository costStatisticsRepository, + IRepository trialRevenuesPriceRepository, + IRepository trialRevenuesPriceVerificationRepository, + IMapper mapper) + { + _trialRepository = clinicalTrialProjectRepository; + _enrollRepository = intoGroupRepository; + _doctorRepository = doctorInfoRepository; + _doctorWorkloadRepository = doctorWorkloadRepository; + _attachmentRepository = attachmentRepository; + _costStatisticsRepository = costStatisticsRepository; + _trialRevenuesPriceRepository = trialRevenuesPriceRepository; + _trialRevenuesPriceVerificationRepository = trialRevenuesPriceVerificationRepository; + + } + + #region 入组工作量统计列表 具体详情 增删改查相关 上传\删除协议 + /// + /// 保存协议- ack Sow [AUTH] + /// + [HttpPost("{trialId}")] + + [TypeFilter(typeof(TrialResourceFilter))] + public async Task UploadReviewerAckSOW(Guid trialId, + ReviewerAckDTO attachmentViewModel) + { + + var intoGroupItem = await _enrollRepository.FirstOrDefaultAsync(t => t.TrialId == trialId && t.DoctorId == attachmentViewModel.DoctorId); + + if (intoGroupItem == null) return Null404NotFound(intoGroupItem); + + if (attachmentViewModel.Id != Guid.Empty) + { + await _attachmentRepository.DeleteFromQueryAsync(t => t.Id == attachmentViewModel.Id); + } + + var attach = _mapper.Map(attachmentViewModel); + var attachment = await _attachmentRepository.AddAsync(attach); + + intoGroupItem.AttachmentId = attachment.Id; + await _enrollRepository.UpdateAsync(intoGroupItem); + + + var success = await _enrollRepository.SaveChangesAsync(); + + return ResponseOutput.Result(success, attachment.Id.ToString()); + } + + /// + /// 删除协议 + /// + [HttpDelete, Route("deleteReviewerAckSOW/{trialId}/{doctorId}/{attachmentId}")] + + [TypeFilter(typeof(TrialResourceFilter))] + public async Task DeleteReviewerAckSOW(Guid trialId, Guid doctorId, Guid attachmentId) + { + var success1 = await _attachmentRepository.DeleteFromQueryAsync(a => a.Id == attachmentId); + var success2 = await _enrollRepository.UpdateFromQueryAsync(t => t.TrialId == trialId && t.DoctorId == doctorId, u => + new Enroll() + { + AttachmentId = Guid.Empty + }); + return ResponseOutput.Result(success1 && success2); + } + + /// + /// 0代表裁判和Tp 都可以 1、代表Tp 2 代表裁判 + /// + /// + [HttpPost("{trialId}/{doctorId}/{reviewerReadingType}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task UpdateReviewerReadingType(Guid trialId, Guid doctorId, int type) + { + var success2 = await _enrollRepository.UpdateFromQueryAsync(t => t.TrialId == trialId && t.DoctorId == doctorId, u => + new Enroll() + { + ReviewerReadingType = type + }); + return ResponseOutput.Result(success2); + } + + /// + /// 获取某个项目入组的医生工作量统计列表 + /// + [HttpPost] + public async Task> GetTrialEnrollmentWorkloadStats(WorkLoadDoctorQueryDTO doctorSearchModel) + { + //https://blog.csdn.net/sunshineblog/article/details/78636389 + + var trialId = doctorSearchModel.TrialId; + + var doctorIntoGroupQueryable = + + from intoGroup in _enrollRepository.Where(x => x.TrialId == trialId && x.EnrollStatus >= (int)EnrollStatus.ConfirmIntoGroup) + join doctor in _doctorRepository.AsQueryable() on intoGroup.DoctorId equals doctor.Id + join attachmentItem in _attachmentRepository.AsQueryable() on intoGroup.AttachmentId equals attachmentItem.Id into cc + from attachment in cc.DefaultIfEmpty() + join doctorWorkLoad in _doctorWorkloadRepository.Where(t => t.TrialId == trialId && t.DataFrom == (int)WorkLoadFromStatus.FinalConfirm).GroupBy(t => t.DoctorId).Select( + g => new + { + DoctorId = g.Key, + + Training = g.Sum(t => t.Training), + Downtime = g.Sum(t => t.Downtime), + Timepoint = g.Sum(t => t.Timepoint), + TimepointIn24H = g.Sum(t => t.TimepointIn24H), + TimepointIn48H = g.Sum(t => t.TimepointIn48H), + Global = g.Sum(t => t.Global), + Adjudication = g.Sum(t => t.Adjudication), + AdjudicationIn24H = g.Sum(t => t.AdjudicationIn24H), + AdjudicationIn48H = g.Sum(t => t.AdjudicationIn48H), + RefresherTraining = g.Sum(t => t.RefresherTraining) + } + + ) on intoGroup.DoctorId equals doctorWorkLoad.DoctorId into tt //连接为了得到入组时间 + from doctorWorkLoad in tt.DefaultIfEmpty()//不是每个医生都有工作量 + + select new WorkLoadAndAgreementDTO() + { + + DoctorId = doctor.Id, + Code = doctor.ReviewerCode, + FirstName = doctor.FirstName, + LastName = doctor.LastName, + ChineseName = doctor.ChineseName, + + OutEnrollTime = intoGroup.OutEnrollTime, + EnrollTime = intoGroup.EnrollTime, + AgreementId = intoGroup.AttachmentId, + ReviewerReadingType = intoGroup.ReviewerReadingType, + AgreementPath = attachment.Path, + + Timepoint = doctorWorkLoad.Timepoint, + TimepointIn24H = doctorWorkLoad.TimepointIn24H, + TimepointIn48H = doctorWorkLoad.TimepointIn48H, + + Adjudication = doctorWorkLoad.Adjudication, + AdjudicationIn24H = doctorWorkLoad.AdjudicationIn24H, + AdjudicationIn48H = doctorWorkLoad.AdjudicationIn48H, + + Global = doctorWorkLoad.Global, + + RefresherTraining = doctorWorkLoad.RefresherTraining, + TrainingTimes = doctorWorkLoad.Training, + Downtime = doctorWorkLoad.Downtime + + }; + + var count = await doctorIntoGroupQueryable.CountAsync(); + if (string.IsNullOrWhiteSpace(doctorSearchModel.SortField)) + { + doctorIntoGroupQueryable = doctorIntoGroupQueryable.OrderByDescending("EnrollTime").ThenBy(u => u.Code); + } + else + { + if (doctorSearchModel.SortField == "FirstName" || doctorSearchModel.SortField == "LastName") + { + if (doctorSearchModel.Asc) + { + doctorIntoGroupQueryable = doctorIntoGroupQueryable.OrderBy("LastName").ThenBy(u => u.FirstName); + } + else + { + doctorIntoGroupQueryable = doctorIntoGroupQueryable.OrderByDescending("LastName").ThenByDescending(u => u.FirstName); + } + } + else + { + var propName = doctorSearchModel.SortField == "" ? "EnrollTime" : doctorSearchModel.SortField; + doctorIntoGroupQueryable = doctorSearchModel.Asc + ? doctorIntoGroupQueryable.OrderBy(propName) + : doctorIntoGroupQueryable.OrderByDescending(propName); + } + } + //分页 + doctorIntoGroupQueryable = doctorIntoGroupQueryable + .Skip((doctorSearchModel.PageIndex - 1) * doctorSearchModel.PageSize) + .Take(doctorSearchModel.PageSize); + //从数据库提取数据 + var intoGroupList = doctorIntoGroupQueryable.ToList(); + + return new PageOutput(doctorSearchModel.PageIndex, + doctorSearchModel.PageSize, count, intoGroupList); + + } + + /// + /// 获取入组某个项目的医生的最近几个月的工作量详情(带有填充数据) + /// + [HttpPost] + public PageOutput GetEnrollmentWorkloadStatsDetail(WorkLoadStatsQueryDTO workLoadSearch) + { + #region 条件查询 + Expression> workloadLambda = x => true; + if (workLoadSearch.TrialId != Guid.Empty && workLoadSearch.TrialId != null) + { + workloadLambda = workloadLambda.And(x => x.TrialId == workLoadSearch.TrialId); + } + if (workLoadSearch.DoctorId != Guid.Empty && workLoadSearch.DoctorId != null) + { + workloadLambda = workloadLambda.And(t => t.DoctorId == workLoadSearch.DoctorId); + } + if (workLoadSearch.SearchBeginDateTime != null) + { + var bDate = workLoadSearch.SearchBeginDateTime; + + var beginDate = new DateTime(bDate.Value.Year, bDate.Value.Month, 1); + + workloadLambda = workloadLambda.And(t => + t.WorkTime >= beginDate); + } + if (workLoadSearch.SearchEndDateTime != null) + { + var eDate = workLoadSearch.SearchEndDateTime.Value.AddMonths(1); + + DateTime endDate = new DateTime(eDate.Year, eDate.Month, 1); + + workloadLambda = workloadLambda.And(t => + t.WorkTime < endDate); + } + + + #endregion + + IQueryable? workLoadQueryable = default; + + var tempWorkload = new List(); + if (workLoadSearch.WorkLoadFromStatus.Contains(1)) + { + workLoadQueryable = from workLoad in _doctorWorkloadRepository.Where(workloadLambda.And(t => t.DataFrom == (int)WorkLoadFromStatus.CRO)) + join doctor in _doctorRepository.AsQueryable() on workLoad.DoctorId equals doctor.Id + join trial in _trialRepository.AsQueryable() on workLoad.TrialId equals trial.Id + + select new WorkLoadDetailDTO() + { + Id = workLoad.Id, + DoctorId = workLoad.DoctorId, + ChineseName = doctor.ChineseName, + FirstName = doctor.FirstName, + LastName = doctor.LastName, + + YearMonthStr = workLoad.YearMonth, + CreateTime = workLoad.CreateTime, + WorkTime = workLoad.WorkTime, + + DataFrom = workLoad.DataFrom, + TrialId = workLoad.TrialId, + Indication = trial.Indication, + Code = trial.TrialCode, + + Timepoint = workLoad.Timepoint, + TimepointIn24H = workLoad.TimepointIn24H, + TimepointIn48H = workLoad.TimepointIn48H, + Global = workLoad.Global, + RefresherTraining = workLoad.RefresherTraining, + Adjudication = workLoad.Adjudication, + AdjudicationIn24H = workLoad.AdjudicationIn24H, + AdjudicationIn48H = workLoad.AdjudicationIn48H, + + + Training = workLoad.Training, + Downtime = workLoad.Downtime, + IsLock = workLoad.IsLock + }; + tempWorkload.AddRange(workLoadQueryable.ToList()); + } + + + if (workLoadSearch.WorkLoadFromStatus.Contains(2)) + { + workLoadQueryable = from workLoad in _doctorWorkloadRepository.Where(workloadLambda.And(t => t.DataFrom == (int)WorkLoadFromStatus.FinalConfirm)) + join doctor in _doctorRepository.AsQueryable() on workLoad.DoctorId equals doctor.Id + join trial in _trialRepository.AsQueryable() on workLoad.TrialId equals trial.Id + + select new WorkLoadDetailDTO() + { + Id = workLoad.Id, + DoctorId = workLoad.DoctorId, + ChineseName = doctor.ChineseName, + FirstName = doctor.FirstName, + LastName = doctor.LastName, + + YearMonthStr = workLoad.YearMonth, + CreateTime = workLoad.CreateTime, + WorkTime = workLoad.WorkTime, + + DataFrom = workLoad.DataFrom, + TrialId = workLoad.TrialId, + Indication = trial.Indication, + Code = trial.TrialCode, + + Timepoint = workLoad.Timepoint, + TimepointIn24H = workLoad.TimepointIn24H, + TimepointIn48H = workLoad.TimepointIn48H, + Global = workLoad.Global, + RefresherTraining = workLoad.RefresherTraining, + Adjudication = workLoad.Adjudication, + AdjudicationIn24H = workLoad.AdjudicationIn24H, + AdjudicationIn48H = workLoad.AdjudicationIn48H, + + Training = workLoad.Training, + Downtime = workLoad.Downtime, + IsLock = workLoad.IsLock + }; + tempWorkload.AddRange(workLoadQueryable.ToList()); + } + + if (workLoadSearch.WorkLoadFromStatus.Contains(0)) + { + workLoadQueryable = + from workLoad in (from workLoadItem in _doctorWorkloadRepository.Where(workloadLambda.And(t => t.DataFrom == (int)WorkLoadFromStatus.Doctor)) + group workLoadItem by new { workLoadItem.YearMonth, workLoadItem.DoctorId, workLoadItem.TrialId, workLoadItem.IsLock } into g + select new + { + Id = Guid.NewGuid(), + DoctorId = g.Key.DoctorId, + DataFrom = 0, + TrialId = g.Key.TrialId, + IsLock = g.Key.IsLock, + YearMonthStr = g.Key.YearMonth, + Timepoint = g.Sum(workLoad => workLoad.Timepoint), + TimepointIn24H = g.Sum(workLoad => workLoad.TimepointIn24H), + TimepointIn48H = g.Sum(workLoad => workLoad.TimepointIn48H), + Global = g.Sum(workLoad => workLoad.Global), + RefresherTraining = g.Sum(workLoad => workLoad.RefresherTraining), + Adjudication = g.Sum(workLoad => workLoad.Adjudication), + AdjudicationIn24H = g.Sum(workLoad => workLoad.AdjudicationIn24H), + AdjudicationIn48H = g.Sum(workLoad => workLoad.AdjudicationIn48H), + + Training = g.Sum(workLoad => workLoad.Training), + Downtime = g.Sum(workLoad => workLoad.Downtime) + }) + join doctor in _doctorRepository.AsQueryable() on workLoad.DoctorId equals doctor.Id + join trial in _trialRepository.AsQueryable() on workLoad.TrialId equals trial.Id + select new WorkLoadDetailDTO() + { + Id = workLoad.Id, + DoctorId = workLoad.DoctorId, + ChineseName = doctor.ChineseName, + FirstName = doctor.FirstName, + LastName = doctor.LastName, + YearMonthStr = workLoad.YearMonthStr, + + DataFrom = workLoad.DataFrom, + TrialId = workLoad.TrialId, + Indication = trial.Indication, + Code = trial.TrialCode, + + Timepoint = workLoad.Timepoint, + TimepointIn24H = workLoad.TimepointIn24H, + TimepointIn48H = workLoad.TimepointIn48H, + Global = workLoad.Global, + RefresherTraining = workLoad.RefresherTraining, + Adjudication = workLoad.Adjudication, + AdjudicationIn24H = workLoad.AdjudicationIn24H, + AdjudicationIn48H = workLoad.AdjudicationIn48H, + + Training = workLoad.Training, + Downtime = workLoad.Downtime, + IsLock = workLoad.IsLock + }; + tempWorkload.AddRange(workLoadQueryable.ToList()); + } + + //workLoadQueryable = workLoadQueryable.OrderByDescending(t => t.YearMonthStr).ThenBy(t => t.DataFrom); + + //取选取月份的数据 + var month = (workLoadSearch.SearchEndDateTime?.Year - workLoadSearch.SearchBeginDateTime?.Year) * 12 + (workLoadSearch.SearchEndDateTime?.Month - workLoadSearch.SearchBeginDateTime?.Month) + 1; + var count = (month ?? 1) * workLoadSearch.WorkLoadFromStatus.Count; + //tempWorkload = tempWorkload.Take(count); + tempWorkload.OrderByDescending(t => t.YearMonthStr).ThenBy(t => t.DataFrom).Take(count); + //var workLoadList = workLoadQueryable.ToList(); + return BuildFullData(tempWorkload, workLoadSearch); + } + + /// + /// 获取来自Reviewer自己的数据,某个月添加的多条 + /// + [HttpPost] + public async Task> GetReviewerWorkLoadListDetail( + WorkLoadDetailQueryDTO workLoadSearch) + { + Expression> workloadLambda = t => t.DataFrom == (int)WorkLoadFromStatus.Doctor && t.YearMonth == workLoadSearch.YearMonthStr; + + if (workLoadSearch.TrialId != Guid.Empty) + { + workloadLambda = workloadLambda.And(x => x.TrialId == workLoadSearch.TrialId); + } + + if (workLoadSearch.DoctorId != Guid.Empty) + { + workloadLambda = workloadLambda.And(t => t.DoctorId == workLoadSearch.DoctorId); + } + + var workLoadQueryable = from workLoad in _doctorWorkloadRepository.Where(workloadLambda) + join doctor in _doctorRepository.AsQueryable() on workLoad.DoctorId equals doctor.Id + join trial in _trialRepository.AsQueryable() on workLoad.TrialId equals trial.Id + + select new WorkLoadDetailViewModel() + { + Id = workLoad.Id, + DoctorId = workLoad.DoctorId, + ChineseName = doctor.ChineseName, + FirstName = doctor.FirstName, + LastName = doctor.LastName, + YearMonthStr = workLoad.YearMonth, + CreateTime = workLoad.CreateTime, + WorkTime = workLoad.WorkTime, + + DataFrom = workLoad.DataFrom, + TrialId = workLoad.TrialId, + Indication = trial.Indication, + Code = trial.TrialCode, + + Timepoint = workLoad.Timepoint, + TimepointIn24H = workLoad.TimepointIn24H, + TimepointIn48H = workLoad.TimepointIn48H, + Global = workLoad.Global, + Adjudication = workLoad.Adjudication, + AdjudicationIn24H = workLoad.AdjudicationIn24H, + AdjudicationIn48H = workLoad.AdjudicationIn48H, + RefresherTraining = workLoad.RefresherTraining, + + Training = workLoad.Training, + Downtime = workLoad.Downtime, + IsLock = workLoad.IsLock + }; + + workLoadQueryable = workLoadQueryable.OrderBy("WorkTime"); + + var workLoadList = await workLoadQueryable.ToListAsync(); + + workLoadList.ForEach(t => t.RowGuid = Guid.NewGuid()); + + return workLoadList; + + } + + /// + /// 工作量是否存在,用于判断只能添加一条的工作量记录 + /// + /// + /// 查询某个医生是否在某天有某个项目的工作量数据,处理添加来自医生自己的工作量数据 + /// + [HttpPost] + public async Task> WorkloadExist(WorkloadExistQueryDTO param) + { + var isExist = false; + var exist = await _doctorWorkloadRepository.FirstOrDefaultAsync(u => u.TrialId == param.TrialId && u.DoctorId == param.DoctorId + && u.WorkTime == param.WorkDate && u.DataFrom == 0); + if (exist != null) + { + isExist = true; + } + return ResponseOutput.Ok(new ExistWorkloadViewModel + { + IsExist = isExist, + WorkLoad = _mapper.Map(exist), + }); + } + + /// + /// 添加或更新工作量 + /// + public async Task AddOrUpdateWorkload(WorkloadCommand workLoadAddOrUpdateModel, + Guid userId) + { + var yearMonthStr = workLoadAddOrUpdateModel.WorkTime.ToString("yyyy-MM"); + + + if (workLoadAddOrUpdateModel.Id == Guid.Empty || workLoadAddOrUpdateModel.Id == null) + { + //确定是那个医生 那个项目 那个月份做的 + Expression> workloadLambda = x => x.DoctorId == workLoadAddOrUpdateModel.DoctorId && x.TrialId == workLoadAddOrUpdateModel.TrialId && x.YearMonth == yearMonthStr; + + //只要是来自CRO或者最终确认的 只能有一条 + switch (workLoadAddOrUpdateModel.DataFrom) + { + case (int)WorkLoadFromStatus.CRO: //1 来自CRO的 + workloadLambda = workloadLambda.And(t => t.DataFrom == (int)WorkLoadFromStatus.CRO); + break; + + case (int)WorkLoadFromStatus.FinalConfirm: //2 最终确认的 + workloadLambda = workloadLambda.And(t => t.DataFrom == (int)WorkLoadFromStatus.FinalConfirm); + + var isExist = await _costStatisticsRepository.AnyAsync(u => u.YearMonth == yearMonthStr && u.DoctorId == workLoadAddOrUpdateModel.DoctorId); + if (!isExist) + { + await _costStatisticsRepository.AddAsync(new Payment + { + DoctorId = workLoadAddOrUpdateModel.DoctorId, + YearMonth = yearMonthStr, + YearMonthDate = DateTime.Parse(yearMonthStr), + IsLock = false + }); + } + + //处理 验证项目收入价格是否都填写了 + + var revenuesPriceExist = await _trialRevenuesPriceRepository.AsQueryable() + .FirstOrDefaultAsync(t => t.TrialId == workLoadAddOrUpdateModel.TrialId); + + //没填写收入价格 插入记录 所有的都是false + if (revenuesPriceExist == null) + { + await _trialRevenuesPriceVerificationRepository.AddAsync(new TrialRevenuesPriceVerification() + { + TrialId = workLoadAddOrUpdateModel.TrialId, + ReviewerId = workLoadAddOrUpdateModel.DoctorId, + YearMonth = yearMonthStr, + WorkLoadDate = workLoadAddOrUpdateModel.WorkTime + }); + } + else //有价格 校验 看全不全 + { + + var tpNeedSetPrice = workLoadAddOrUpdateModel.Timepoint > 0 && + revenuesPriceExist.Timepoint <= 0; + var tp24HNeedSetPrice = workLoadAddOrUpdateModel.TimepointIn24H > 0 && + revenuesPriceExist.TimepointIn24H <= 0; + var tp48NeedSetPrice = workLoadAddOrUpdateModel.TimepointIn48H > 0 && + revenuesPriceExist.TimepointIn48H <= 0; + var adNeedSetPrice = workLoadAddOrUpdateModel.Adjudication > 0 && + revenuesPriceExist.Adjudication <= 0; + var ad24NeedSetPrice = workLoadAddOrUpdateModel.AdjudicationIn24H > 0 && + revenuesPriceExist.AdjudicationIn24H <= 0; + var ad48NeedSetPrice = workLoadAddOrUpdateModel.AdjudicationIn48H > 0 && + revenuesPriceExist.AdjudicationIn48H <= 0; + var downtimeNeedSetPrice = workLoadAddOrUpdateModel.Downtime > 0 && + revenuesPriceExist.Downtime <= 0; + var globalNeedSetPrice = workLoadAddOrUpdateModel.Global > 0 && + revenuesPriceExist.Global <= 0; + var trainingNeedSetPrice = workLoadAddOrUpdateModel.Training > 0 && + revenuesPriceExist.Training <= 0; + + var refresherTrainingTrainingNeedSetPrice = workLoadAddOrUpdateModel.RefresherTraining > 0 && + revenuesPriceExist.RefresherTraining <= 0; + + + + + var needAdd = tpNeedSetPrice || tp24HNeedSetPrice || tp48NeedSetPrice || adNeedSetPrice || ad24NeedSetPrice || + ad48NeedSetPrice || downtimeNeedSetPrice || globalNeedSetPrice && trainingNeedSetPrice || refresherTrainingTrainingNeedSetPrice; + + //验证不通过 增加 + if (needAdd) + { + await _trialRevenuesPriceVerificationRepository.AddAsync(new TrialRevenuesPriceVerification() + { + TrialId = workLoadAddOrUpdateModel.TrialId, + ReviewerId = workLoadAddOrUpdateModel.DoctorId, + YearMonth = yearMonthStr, + WorkLoadDate = workLoadAddOrUpdateModel.WorkTime, + //需要设置价格和 有价格是相反的 数据库此字段存储的时候对应项是否有价格 + Timepoint = !tpNeedSetPrice, + TimepointIn24H = !tp24HNeedSetPrice, + TimepointIn48H = !tp48NeedSetPrice, + Adjudication = !adNeedSetPrice, + AdjudicationIn24H = !ad24NeedSetPrice, + AdjudicationIn48H = !ad48NeedSetPrice, + Downtime = !downtimeNeedSetPrice, + Global = !globalNeedSetPrice, + Training = !trainingNeedSetPrice, + RefresherTraining = !refresherTrainingTrainingNeedSetPrice + }); + } + + } + + + break; + } + if (workLoadAddOrUpdateModel.DataFrom != (int)WorkLoadFromStatus.Doctor) + { + if (await _doctorWorkloadRepository.AnyAsync(workloadLambda)) + { + + return ResponseOutput.NotOk("This type of data can only have one"); + + } + } + if (workLoadAddOrUpdateModel.DataFrom == (int)WorkLoadFromStatus.Doctor) + { + Expression> doctorworkloadLambda = x => x.DoctorId == workLoadAddOrUpdateModel.DoctorId && x.TrialId == workLoadAddOrUpdateModel.TrialId && x.YearMonth == yearMonthStr && workLoadAddOrUpdateModel.WorkTime == x.WorkTime; + + doctorworkloadLambda = doctorworkloadLambda.And(t => t.DataFrom == (int)WorkLoadFromStatus.Doctor); + if (await _doctorWorkloadRepository.AnyAsync(doctorworkloadLambda)) + { + return ResponseOutput.NotOk("This type of data can only have one"); + } + } + + var workLoad = _mapper.Map(workLoadAddOrUpdateModel); + + workLoad.YearMonth = yearMonthStr; + + await _doctorWorkloadRepository.AddAsync(workLoad); + + await _enrollRepository.UpdateFromQueryAsync( + t => t.DoctorId == workLoadAddOrUpdateModel.DoctorId && t.TrialId == workLoadAddOrUpdateModel.TrialId, u => new Enroll() + { + EnrollStatus = (int)EnrollStatus.DoctorReading, + UpdateTime = DateTime.Now + }); + + var success = await _doctorWorkloadRepository.SaveChangesAsync(); + + return ResponseOutput.Result(success, workLoad.Id); + + } + else + { + //判断是否已经被锁定 + var isLocked = await _costStatisticsRepository.AnyAsync(u => u.DoctorId == workLoadAddOrUpdateModel.DoctorId && + u.YearMonth == yearMonthStr && u.IsLock); + if (isLocked) + { + + return ResponseOutput.NotOk("Expenses have been settled and workload cannot be modified."); + + } + + var success = await _doctorWorkloadRepository.UpdateFromQueryAsync(t => t.Id == workLoadAddOrUpdateModel.Id, + u => new Workload() + { + Timepoint = workLoadAddOrUpdateModel.Timepoint, + TimepointIn24H = workLoadAddOrUpdateModel.TimepointIn24H, + TimepointIn48H = workLoadAddOrUpdateModel.TimepointIn48H, + + Adjudication = workLoadAddOrUpdateModel.Adjudication, + AdjudicationIn24H = workLoadAddOrUpdateModel.AdjudicationIn24H, + AdjudicationIn48H = workLoadAddOrUpdateModel.AdjudicationIn48H, + + RefresherTraining = workLoadAddOrUpdateModel.RefresherTraining, + + Downtime = workLoadAddOrUpdateModel.Downtime, + Training = workLoadAddOrUpdateModel.Training, + Global = workLoadAddOrUpdateModel.Global, + UpdateTime = DateTime.Now, + UpdateUserId = userId, + + WorkTime = workLoadAddOrUpdateModel.WorkTime, + YearMonth = yearMonthStr + }); + return ResponseOutput.Result(success); + + } + } + + /// + /// 删除工作量 + /// + public async Task DeleteWorkload(Guid workloadId) + { + return ResponseOutput.Result(await _doctorWorkloadRepository.DeleteFromQueryAsync(t => t.Id == workloadId)); + } + + /// + /// 获取工作量详情(用于判断工作量锁定时,调用) + /// + public async Task GetWorkloadDetailById(Guid id) + { + var workload = await _doctorWorkloadRepository.FirstOrDefaultAsync(u => u.Id == id); + return _mapper.Map(workload); + } + + #endregion + + private PageOutput BuildFullData( + List workLoadList, WorkLoadStatsQueryDTO workLoadSearch) + { + var doctor = _doctorRepository.Where(t => t.Id == workLoadSearch.DoctorId).ProjectTo(_mapper.ConfigurationProvider).FirstOrDefault().IfNullThrowException(); + //查询 1-3月的 共3个月 + var month = (workLoadSearch.SearchEndDateTime?.Year - workLoadSearch.SearchBeginDateTime?.Year) * 12 + + (workLoadSearch.SearchEndDateTime?.Month - workLoadSearch.SearchBeginDateTime?.Month) + 1; + + var needCount = month * workLoadSearch.WorkLoadFromStatus.Count ?? 1; + + if (workLoadSearch.WorkLoadFromStatus.Contains((int)WorkLoadFromStatus.FinalConfirm)) + { + if (workLoadList.Count != needCount) + { + for (int i = 0; i < month; i++) + { + var yearMonthStr = workLoadSearch.SearchBeginDateTime?.AddMonths(i).ToString("yyyy-MM"); + if (!workLoadList.Any(t => t.YearMonthStr == yearMonthStr && + t.DataFrom == (int)WorkLoadFromStatus.FinalConfirm)) + { + workLoadList.Add(new WorkLoadDetailDTO() + { + Id = Guid.Empty, + DataFrom = (int)WorkLoadFromStatus.FinalConfirm, + YearMonthStr = yearMonthStr, + ChineseName = doctor.ChineseName, + FirstName = doctor.FirstName, + LastName = doctor.LastName, + Code = doctor.ReviewerCode + }); + } + } + } + } + if (workLoadSearch.WorkLoadFromStatus.Contains((int)WorkLoadFromStatus.CRO)) + { + if (workLoadList.Count != needCount) + { + for (int i = 0; i < month; i++) + { + var yearMonthStr = workLoadSearch.SearchBeginDateTime?.AddMonths(i).ToString("yyyy-MM"); + if (!workLoadList.Any(t => t.YearMonthStr == yearMonthStr && + t.DataFrom == (int)WorkLoadFromStatus.CRO)) + { + workLoadList.Add(new WorkLoadDetailDTO() + { + Id = Guid.Empty, + DataFrom = (int)WorkLoadFromStatus.CRO, + YearMonthStr = yearMonthStr, + ChineseName = doctor.ChineseName, + FirstName = doctor.FirstName, + LastName = doctor.LastName, + Code = doctor.ReviewerCode + }); + } + } + } + } + if (workLoadSearch.WorkLoadFromStatus.Contains((int)WorkLoadFromStatus.Doctor)) + { + if (workLoadList.Count != needCount) + { + for (int i = 0; i < month; i++) + { + var yearMonthStr = workLoadSearch.SearchBeginDateTime?.AddMonths(i).ToString("yyyy-MM"); + if (!workLoadList.Any(t => t.YearMonthStr == yearMonthStr && + t.DataFrom == (int)WorkLoadFromStatus.Doctor)) + { + workLoadList.Add(new WorkLoadDetailDTO() + { + Id = Guid.Empty, + DataFrom = (int)WorkLoadFromStatus.Doctor, + YearMonthStr = yearMonthStr, + ChineseName = doctor.ChineseName, + FirstName = doctor.FirstName, + LastName = doctor.LastName, + Code = doctor.ReviewerCode + }); + } + } + } + + } + + workLoadList = workLoadList.OrderByDescending(t => t.YearMonthStr).ThenBy(t => t.DataFrom) + .ToList(); + int tempRowId = 0; + foreach (var workLoad in workLoadList) + { + workLoad.RowGuid = Guid.NewGuid(); + workLoad.RowIntId = tempRowId + 1; + tempRowId++; + } + + return new PageOutput(1, workLoadList.Count, + workLoadList.Count, workLoadList); + + + } + + + } +} diff --git a/IRaCIS.Core.Application/Service/WorkLoad/EnrollService.cs b/IRaCIS.Core.Application/Service/WorkLoad/EnrollService.cs new file mode 100644 index 00000000..bab9fcb5 --- /dev/null +++ b/IRaCIS.Core.Application/Service/WorkLoad/EnrollService.cs @@ -0,0 +1,455 @@ +using IRaCIS.Application.Interfaces; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Application.Filter; +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Application.Service.WorkLoad.DTO; + +namespace IRaCIS.Application.Services +{ + [ApiExplorerSettings(GroupName = "Enroll")] + public class EnrollService : BaseService, IEnrollService + { + private readonly IRepository _trialRepository; + private readonly IRepository _trialDetailRepository; + + private readonly IRepository _TrialPaymentPriceRepository; + private readonly IRepository _enrollRepository; + private readonly IRepository _doctorRepository; + private readonly IRepository _enrollDetailRepository; + private readonly IRepository _workloadRepository; + + + public EnrollService(IRepository clinicalTrialProjectRepository, + IRepository clinicalProjectDetailRepository, + IRepository intoGroupRepository, + IRepository doctorRepository, + IRepository TrialPaymentPriceRepository, + IRepository intoGroupDetailRepository, IRepository workloadRepository) + { + _trialRepository = clinicalTrialProjectRepository; + _TrialPaymentPriceRepository = TrialPaymentPriceRepository; + _doctorRepository = doctorRepository; + + _trialDetailRepository = clinicalProjectDetailRepository; + _enrollRepository = intoGroupRepository; + _enrollDetailRepository = intoGroupDetailRepository; + _workloadRepository = workloadRepository; + + } + + + + /// + /// 添加或更新项目医生项目价格 + /// + [HttpPost] + public async Task AddOrUpdateEnroll(EnrollCommand addOrUpdateModel) + { + + var trialDoctoritem = await _enrollRepository.FirstOrDefaultAsync(u => u.Id == addOrUpdateModel.Id); + if (trialDoctoritem == null)//insert + { + var trialExt = _mapper.Map(addOrUpdateModel); + await _enrollRepository.AddAsync(trialExt); + } + else//update + { + await _enrollRepository.UpdateAsync(_mapper.Map(addOrUpdateModel, trialDoctoritem)); + } + var success = await _enrollRepository.SaveChangesAsync(); + return ResponseOutput.Result(success); + } + + + + /// + /// 获取医生项目列表 + /// + /// + /// + [HttpPost] + public async Task> GetTrialDoctorList(EnrollGetQuery challengeQuery) + { + var costStatisticsQueryable = from enroll in _enrollRepository.Where(t => t.TrialId == challengeQuery.TrialId) + join dociorc in _doctorRepository.Where() on enroll.DoctorId equals dociorc.Id + join price in _TrialPaymentPriceRepository.Where() on enroll.TrialId equals price.TrialId + select new EnrollViewModel() + { + ChineseName = dociorc.ChineseName, + AdjustmentMultiple = enroll.AdjustmentMultiple, + FirstName = dociorc.FirstName, + LastName = dociorc.LastName, + DoctorId = dociorc.Id, + TrialId = challengeQuery.TrialId, + Id = enroll.Id, + Training = enroll.Training, + RefresherTraining = enroll.RefresherTraining, + Timepoint = enroll.Timepoint, + Timepoint48H = enroll.Timepoint48H, + Timepoint24H = enroll.Timepoint24H, + Adjudication = enroll.Adjudication, + Adjudication48H = enroll.Adjudication48H, + Adjudication24H = enroll.Adjudication24H, + Global = enroll.Global, + Code = dociorc.Code, + ReviewerCode = dociorc.ReviewerCode, + Downtime = enroll.Downtime, + }; + return await costStatisticsQueryable.ToPagedListAsync(challengeQuery.PageIndex, challengeQuery.PageSize, "ChineseName"); + + + + //var query2 = _repository.Where(x => x.TrialId == challengeQuery.TrialId) + // .WhereIf(challengeQuery.TrialId != null, t => t.TrialId == challengeQuery.TrialId) + // .WhereIf(challengeQuery.DoctorId != null, t => t.DoctorId == challengeQuery.DoctorId) + + // .ProjectTo(_mapper.ConfigurationProvider); + + //var pageList = await query2.ToListAsync(); + + + // return new PageOutput(challengeQuery.pageIndex, challengeQuery.pageSize, + //costStatisticsQueryable); + + + } + + + #region Reviewer 入组审核流程(select-submit-approve-confirm) + + /// 为项目筛选医生 提交 【select】 + /// 项目Id + /// 医生Id数组 + /// + + [HttpPost("{trialId:guid}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task SelectReviewers(Guid trialId, Guid[] doctorIdArray) + { + var trial = await _trialRepository.FirstOrDefaultAsync(t => t.Id == trialId); + + if (trial == null) return Null404NotFound(trial); + + //_trialRepository.Attach(trial); + + //更新项目状态 + trial.TrialEnrollStatus = (int)TrialEnrollStatus.HasApplyDownLoadResume; + + //_trialRepository.Update(trial); + + //添加项目状态变化记录 + var trialDetail = new TrialStatusDetail() + { + + TrialId = trial.Id, + TrialStatus = (int)TrialEnrollStatus.HasApplyDownLoadResume, + }; + await _trialDetailRepository.AddAsync(trialDetail); + + // 入组表 入组状态跟踪表 + foreach (var doctorId in doctorIdArray) + { + await _enrollRepository.AddAsync(new Enroll() + { + DoctorId = doctorId, + TrialId = trialId, + EnrollStatus = (int)EnrollStatus.HasApplyDownloadResume, + }); + + await _enrollDetailRepository.AddAsync(new EnrollDetail() + { + DoctorId = doctorId, + TrialId = trialId, + EnrollStatus = (int)EnrollStatus.HasApplyDownloadResume, + OptUserType = (int)SystemUserType.AdminUser, + }); + } + + return ResponseOutput.Result(await _enrollRepository.SaveChangesAsync()); + + } + + + + + /// + /// 入组流程-向CRO提交医生[Submit] + /// + + [HttpPost("{trialId:guid}/{commitState:int}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task SubmitReviewer(Guid trialId, Guid[] doctorIdArray, int commitState) + { + + var trial = await _trialRepository.FirstOrDefaultAsync(t => t.Id == trialId); + + + if (trial != null) + { + if (commitState == 1) //确认提交CRO + { + //更新项目状态 + trial.TrialEnrollStatus = (int)TrialEnrollStatus.HasCommitCRO; + + //添加项目详细记录 + var trialDetail = new TrialStatusDetail() + { + TrialId = trial.Id, + TrialStatus = (int)TrialEnrollStatus.HasCommitCRO + }; + await _trialDetailRepository.AddAsync(trialDetail); + + //更新入组表 跟踪了 所以不用下面的_enrollRepository.Update(intoGroupItem); + var intoGroupList = await _enrollRepository.Where(t => t.TrialId == trialId, true).ToListAsync(); + + foreach (var intoGroupItem in intoGroupList) + { + if (doctorIdArray.Contains(intoGroupItem.DoctorId)) + { + intoGroupItem.EnrollStatus = (int)EnrollStatus.HasCommittedToCRO; + //_enrollRepository.Update(intoGroupItem); + + await _enrollDetailRepository.AddAsync(new EnrollDetail() + { + TrialDetailId = trialDetail.Id, + DoctorId = intoGroupItem.DoctorId, + TrialId = trialId, + EnrollStatus = (int)EnrollStatus.HasCommittedToCRO, + OptUserType = (int)SystemUserType.AdminUser, //后台用户 + }); + } + } + + + } + else if (commitState == 0)//回退上一步 + { + //更新入组表 + var intoGroupList = await _enrollRepository.Where(t => t.TrialId == trialId, true).ToListAsync(); + + foreach (var intoGroupItem in intoGroupList) + { + if (doctorIdArray.Contains(intoGroupItem.DoctorId)) + { + intoGroupItem.EnrollStatus = (int)EnrollStatus.HasApplyDownloadResume; + + //_enrollRepository.Update(intoGroupItem); + + + var deleteItem = await _enrollDetailRepository.FirstOrDefaultAsync(t => + t.TrialId == trialId && t.DoctorId == intoGroupItem.DoctorId && + t.EnrollStatus == (int)EnrollStatus.HasCommittedToCRO); + + await _enrollDetailRepository.DeleteAsync(deleteItem); + } + } + + + } + return ResponseOutput.Result(await _enrollRepository.SaveChangesAsync()); + } + return ResponseOutput.NotOk($"Cannot find trial {trialId}"); + } + + /// + /// 入组流程-CRO确定医生名单 [ Approve] + /// + + [HttpPost("{trialId:guid}/{auditState:int}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task ApproveReviewer(Guid trialId, Guid[] doctorIdArray, int auditState) + { + + var trial = await _trialRepository.FirstOrDefaultAsync(t => t.Id == trialId); + + if (trial == null) return Null404NotFound(trial); + + if (auditState == 1) //确认入组 + { + //var existItem = _trialRepository.FindSingleOrDefault(u => u.Id == trialId && u.TrialStatus >= (int)TrialEnrollStatus.HasConfirmedDoctorNames); + //if (existItem == null) + //{ + // trial.TrialStatus = (int)TrialEnrollStatus.HasConfirmedDoctorNames; + // trial.TrialStatusStr = "Approved"; + //} + //_trialRepository.Update(trial); + + //更新项目状态 + trial.TrialEnrollStatus = (int)TrialEnrollStatus.HasConfirmedDoctorNames; + + //添加项目详细记录 + var trialDetail = new TrialStatusDetail() + { + TrialId = trialId, + TrialStatus = (int)TrialEnrollStatus.HasConfirmedDoctorNames + }; + await _trialDetailRepository.AddAsync(trialDetail); + + //更新入组表 跟踪方式,不用下面的_enrollRepository.Update(intoGroupItem); + var intoGroupList = _enrollRepository.Where(t => t.TrialId == trialId, true).ToList(); + + + foreach (var intoGroupItem in intoGroupList) + { + if (doctorIdArray.Contains(intoGroupItem.DoctorId)) + { + intoGroupItem.EnrollStatus = (int)EnrollStatus.InviteIntoGroup; + //_enrollRepository.Update(intoGroupItem); + + await _enrollDetailRepository.AddAsync(new EnrollDetail() + { + DoctorId = intoGroupItem.DoctorId, + TrialId = trialId, + EnrollStatus = (int)EnrollStatus.InviteIntoGroup, + OptUserType = (int)SystemUserType.AdminUser, //后台用户 + }); + } + } + + } + else if (auditState == 0)//回退上一步 + { + //更新入组表 + var intoGroupList = _enrollRepository.Where(t => t.TrialId == trialId, true).ToList(); + + foreach (var intoGroupItem in intoGroupList) + { + if (doctorIdArray.Contains(intoGroupItem.DoctorId)) + { + intoGroupItem.EnrollStatus = (int)EnrollStatus.HasCommittedToCRO; + + //_enrollRepository.Update(intoGroupItem); + + var deleteItem = await _enrollDetailRepository.FirstOrDefaultAsync(t => + t.TrialId == trialId && t.DoctorId == intoGroupItem.DoctorId && + t.EnrollStatus == (int)EnrollStatus.InviteIntoGroup); + + await _enrollDetailRepository.DeleteAsync(deleteItem); + } + } + + + } + return ResponseOutput.Result(await _enrollRepository.SaveChangesAsync()); + + + + } + + /// + /// 入组流程-后台确认医生入组[Confirm] + /// + + [HttpPost("{trialId:guid}/{confirmState:int}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task ConfirmReviewer(Guid trialId, Guid[] doctorIdArray, int confirmState) + { + //var trial = _trialRepository.FirstOrDefault(t => t.Id == trialId); + //var existItem = _trialRepository.FindSingleOrDefault(u => u.Id == trialId && u.TrialStatus >= (int)TrialEnrollStatus.HasConfirmedDoctorNames); + + //trial.TrialStatusStr = "Reading"; + ////trial.TrialStatus = (int)TrialStatus.HasConfirmedDoctorNames; + //_trialRepository.Update(trial); + + var trial = await _trialRepository.FirstOrDefaultAsync(t => t.Id == trialId); + + if (trial == null) return Null404NotFound(trial); + + + //更新入组表 + var intoGroupList = await _enrollRepository.Where(t => t.TrialId == trialId).ToListAsync(); + + if (confirmState == 1) //确认入组 + { + foreach (var intoGroupItem in intoGroupList) + { + if (doctorIdArray.Contains(intoGroupItem.DoctorId)) + { + intoGroupItem.EnrollStatus = (int)EnrollStatus.ConfirmIntoGroup; + intoGroupItem.EnrollTime = DateTime.Now; + await _enrollRepository.UpdateAsync(intoGroupItem); + + await _enrollDetailRepository.AddAsync(new EnrollDetail() + { + DoctorId = intoGroupItem.DoctorId, + TrialId = trialId, + EnrollStatus = (int)EnrollStatus.ConfirmIntoGroup, + OptUserType = (int)SystemUserType.AdminUser, //后台用户 + }); + } + } + + } + else if (confirmState == 0)//回退上一步 + { + foreach (var intoGroupItem in intoGroupList) + { + if (doctorIdArray.Contains(intoGroupItem.DoctorId)) + { + intoGroupItem.EnrollStatus = (int)EnrollStatus.InviteIntoGroup; + intoGroupItem.EnrollTime = null; + await _enrollRepository.UpdateAsync(intoGroupItem); + + + var deleteItem = await _enrollDetailRepository.FirstOrDefaultAsync(t => + t.TrialId == trialId && t.DoctorId == intoGroupItem.DoctorId && + t.EnrollStatus == (int)EnrollStatus.ConfirmIntoGroup); + + await _enrollDetailRepository.DeleteAsync(deleteItem); + } + } + + + } + return ResponseOutput.Result(await _enrollRepository.SaveChangesAsync()); + + } + + /// + /// optType 0表示回退,回退之后,列表没这条数据了, 1表示出组,需要填写出组时间 + /// + /// + /// + /// + /// + /// + [HttpPost("{trialId:guid}/{doctorId:guid}/{optType:int}")] + [TypeFilter(typeof(TrialResourceFilter))] + public async Task EnrollBackOrOut(Guid trialId, Guid doctorId, int optType, DateTime? outEnrollTime) + { + var intoGroupItem = await _enrollRepository.FirstOrDefaultAsync(t => t.TrialId == trialId && t.DoctorId == doctorId); + if (optType == 0) + { + var sum = _workloadRepository.Where(t => + t.TrialId == trialId && t.DoctorId == doctorId && + t.DataFrom == (int)WorkLoadFromStatus.FinalConfirm) + .Sum(u => u.Adjudication + u.AdjudicationIn24H + u.AdjudicationIn48H + u.Timepoint + + u.TimepointIn24H + u.TimepointIn48H + u.RefresherTraining + u.Training + u.Global + + u.Downtime); + if (sum != 0) + { + return ResponseOutput.NotOk("Reviewers with workload cannot go back"); + } + + intoGroupItem.EnrollStatus = (int)EnrollStatus.InviteIntoGroup; + intoGroupItem.EnrollTime = null; + await _enrollRepository.UpdateAsync(intoGroupItem); + + var deleteItem = await _enrollDetailRepository.FirstOrDefaultAsync(t => + t.TrialId == trialId && t.DoctorId == intoGroupItem.DoctorId && + t.EnrollStatus == (int)EnrollStatus.ConfirmIntoGroup); + + await _enrollDetailRepository.DeleteAsync(deleteItem); + + } + else if (optType == 1) + { + intoGroupItem.OutEnrollTime = outEnrollTime; + } + return ResponseOutput.Result(await _enrollRepository.SaveChangesAsync()); + } + #endregion + + } +} diff --git a/IRaCIS.Core.Application/Service/WorkLoad/Interface/IDoctorWorkloadService.cs b/IRaCIS.Core.Application/Service/WorkLoad/Interface/IDoctorWorkloadService.cs new file mode 100644 index 00000000..7af152ad --- /dev/null +++ b/IRaCIS.Core.Application/Service/WorkLoad/Interface/IDoctorWorkloadService.cs @@ -0,0 +1,18 @@ +using IRaCIS.Application.Contracts; + +namespace IRaCIS.Application.Services +{ + public interface IDoctorWorkloadService + { + Task AddOrUpdateWorkload(WorkloadCommand workLoadAddOrUpdateModel, Guid userId); + Task DeleteReviewerAckSOW(Guid trialId, Guid doctorId, Guid attachmentId); + Task DeleteWorkload(Guid workloadId); + PageOutput GetEnrollmentWorkloadStatsDetail(WorkLoadStatsQueryDTO workLoadSearch); + Task> GetReviewerWorkLoadListDetail(WorkLoadDetailQueryDTO workLoadSearch); + Task> GetTrialEnrollmentWorkloadStats(WorkLoadDoctorQueryDTO doctorSearchModel); + Task GetWorkloadDetailById(Guid id); + Task UpdateReviewerReadingType(Guid trialId, Guid doctorId, int type); + Task UploadReviewerAckSOW(Guid trialId, ReviewerAckDTO attachmentViewModel); + Task> WorkloadExist(WorkloadExistQueryDTO param); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/WorkLoad/Interface/IEnrollService.cs b/IRaCIS.Core.Application/Service/WorkLoad/Interface/IEnrollService.cs new file mode 100644 index 00000000..2ef888d9 --- /dev/null +++ b/IRaCIS.Core.Application/Service/WorkLoad/Interface/IEnrollService.cs @@ -0,0 +1,15 @@ +using IRaCIS.Core.Application.Service.WorkLoad.DTO; + +namespace IRaCIS.Application.Services +{ + public interface IEnrollService + { + Task AddOrUpdateEnroll(EnrollCommand addOrUpdateModel); + Task ApproveReviewer(Guid trialId, Guid[] doctorIdArray, int auditState); + Task ConfirmReviewer(Guid trialId, Guid[] doctorIdArray, int confirmState); + Task EnrollBackOrOut(Guid trialId, Guid doctorId, int optType, DateTime? outEnrollTime); + Task> GetTrialDoctorList(EnrollGetQuery challengeQuery); + Task SelectReviewers(Guid trialId, Guid[] doctorIdArray); + Task SubmitReviewer(Guid trialId, Guid[] doctorIdArray, int commitState); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/Service/WorkLoad/Interface/ITrialEnrollmentService.cs b/IRaCIS.Core.Application/Service/WorkLoad/Interface/ITrialEnrollmentService.cs new file mode 100644 index 00000000..e0e5374e --- /dev/null +++ b/IRaCIS.Core.Application/Service/WorkLoad/Interface/ITrialEnrollmentService.cs @@ -0,0 +1,29 @@ +using System; +using IRaCIS.Core.Application.Service.WorkLoad.DTO; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Application.Interfaces +{ + public interface ITrialEnrollmentService + { + IResponseOutput AddOrUpdateEnroll(EnrollCommand addOrUpdateModel); + + Task< PageOutput> GetTrialDoctorList(EnrollGetQuery challengeQuery); + + /// 入组流程-筛选医生 [select] + IResponseOutput SelectReviewers( Guid trialId, Guid[] doctorIdArray); + + /// 入组流程-向CRO提交医生[Submit] + IResponseOutput SubmitReviewer( Guid trialId, Guid[] doctorIdArray, int commitState); + + /// 入组流程-CRO确定医生名单 [ Approve] + IResponseOutput ApproveReviewer( Guid trialId, Guid[] doctorIdArray, int auditState); + + /// 入组流程-向CRO提交医生[Submit] + IResponseOutput ConfirmReviewer(Guid trialId, Guid[] doctorIdArray, int confirmState); + + + IResponseOutput EnrollBackOrOut( Guid trialId, Guid doctorId, int optType,DateTime? outEnrollTime); + + } +} diff --git a/IRaCIS.Core.Application/Service/WorkLoad/Interface/IWorkloadDistributionService.cs b/IRaCIS.Core.Application/Service/WorkLoad/Interface/IWorkloadDistributionService.cs new file mode 100644 index 00000000..7043447d --- /dev/null +++ b/IRaCIS.Core.Application/Service/WorkLoad/Interface/IWorkloadDistributionService.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Core.Application.Contracts +{ + public interface IWorkloadDistributionService + { + PageOutput GetWorkloadTPList(WorkloadDistributionQueryParam param); + IResponseOutput DistributeTP(WorkloadTPCommand workloadTPCommand); + IResponseOutput UpdateDistributeTP(Guid tpId, Guid ReviewerId, Guid studyId); + + PageOutput GetWorkloadGlobalList(WorkloadDistributionQueryParam param); + IResponseOutput DistributeGlobal(WorkloadGlobalCommand workloadGlobalCommand); + IResponseOutput UpdateDistributeGlobal(Guid tpId, Guid ReviewerId,Guid subjectId, decimal visitNum); + + PageOutput GetWorkloadADList(WorkloadDistributionQueryParam param); + IResponseOutput DistributeAD(WorkloadAdCommand workloadTPCommand); + IResponseOutput UpdateDistributeAD(Guid tpId, Guid ReviewerId); + + IResponseOutput> GetWorkloadDetail(Guid WorkloadId); + + IResponseOutput UpdateGlobalStatus(Guid globalId); + + } +} diff --git a/IRaCIS.Core.Application/Service/WorkLoad/WorkloadDistributionService.cs b/IRaCIS.Core.Application/Service/WorkLoad/WorkloadDistributionService.cs new file mode 100644 index 00000000..ddc07353 --- /dev/null +++ b/IRaCIS.Core.Application/Service/WorkLoad/WorkloadDistributionService.cs @@ -0,0 +1,573 @@ +//using IRaCIS.Application.Contracts; +//using IRaCIS.Core.Application.Contracts; +//using System.Linq.Expressions; +//using IRaCIS.Core.Infrastructure.ExpressionExtend; +//using IRaCIS.Core.Infra.EFCore; +//using IRaCIS.Core.Domain.Models; +//using IRaCIS.Core.Domain.Share; +//using IRaCIS.Core.Infrastructure.Extention; +//using Microsoft.AspNetCore.Mvc; + +//namespace IRaCIS.Core.Application.Services +//{ +// /// +// /// TP、Global、AD 工作量分配查看 +// /// +// [ ApiExplorerSettings(GroupName = "Trial")] +//#pragma warning disable +// public class WorkloadDistributionService : BaseService, IWorkloadDistributionService +// { +// private readonly IWorkloadTPRepository _workloadTpRepository; +// private readonly IWorkloadGlobalRepository _workloadGlobalRepository; +// private readonly IWorkloadADRepository _workloadAdRepository; +// private readonly IRepository _subjectVisitRepository; +// private readonly IRepository _studyRepository; +// private readonly IRepository _doctorRepository; +// private readonly IRepository _subjectRepository; +// private readonly IRepository _siteRepository; +// private readonly IWorkloadDetailRepository _workloadDetailRepository; + + +// public WorkloadDistributionService(IWorkloadTPRepository workloadTpRepository, +// IWorkloadGlobalRepository workloadGlobalRepository, +// IWorkloadADRepository workloadAdRepository, +// IRepository subjectVisitRepository, IRepository studyRepository, +// IRepository doctorRepository, IRepository subjectRepository, +// IRepository siteRepository, +// IWorkloadDetailRepository workloadDetailRepository, +// IUserInfo userInfo) +// { +// _workloadTpRepository = workloadTpRepository; +// _workloadGlobalRepository = workloadGlobalRepository; +// _workloadAdRepository = workloadAdRepository; +// _subjectVisitRepository = subjectVisitRepository; +// _studyRepository = studyRepository; +// _doctorRepository = doctorRepository; +// _subjectRepository = subjectRepository; +// _siteRepository = siteRepository; +// _workloadDetailRepository = workloadDetailRepository; + +// } + +// /// +// /// 批量分配Tp +// /// +// /// +// /// +// [HttpPost] +// public IResponseOutput DistributeTP(WorkloadTPCommand workloadTPCommand) +// { +// //当前采用的是没有提示到具体的TP,如有需要在修改下 +// var studyIdList = workloadTPCommand.TpList.Select(u => u.StudyId); +// var temp = _workloadTpRepository.Where(u => studyIdList.Contains(u.StudyId) && u.ReviewerId == workloadTPCommand.ReviewerId); +// if (temp.Any() ) +// { +// return ResponseOutput.NotOk("The TPs of different Arms of the same subject couldn't be assigned to the same reviewer."); +// } +// var success = false; +// workloadTPCommand.TpList.ForEach(t => +// { +// _workloadDetailRepository.Add(new WorkloadDetail +// { +// WorkloadId = t.Id, +// OptUserName = _userInfo.RealName, +// OptTime = DateTime.Now, +// Status = (int)WorkloadStatus.Distributed, +// ReviewerId = workloadTPCommand.ReviewerId +// }); +// _workloadDetailRepository.SaveChanges(); +// success = _workloadTpRepository.Update(u => u.Id == t.Id, k => new WorkloadTP() +// { +// ReviewerId = workloadTPCommand.ReviewerId, +// Status = (int)WorkloadStatus.Distributed +// }); +// }); +// return ResponseOutput.Result(success); +// } + + +// /// +// /// 批量分配AD +// /// +// /// +// /// +// [HttpPost] +// public IResponseOutput DistributeAD(WorkloadAdCommand workloadTPCommand) +// { +// var success = false; +// workloadTPCommand.IdList.ForEach(t => +// { +// _workloadDetailRepository.Add(new WorkloadDetail +// { +// WorkloadId = t, +// OptUserName = _userInfo.RealName, +// OptTime = DateTime.Now, +// Status = (int)WorkloadStatus.Distributed, +// ReviewerId = workloadTPCommand.ReviewerId +// }); +// _workloadDetailRepository.SaveChanges(); +// success = _workloadAdRepository.Update(u => u.Id == t, k => new WorkloadAD() +// { +// ReviewerId = workloadTPCommand.ReviewerId, +// Status = (int)WorkloadStatus.Distributed +// }); +// }); +// return ResponseOutput.Result(success); +// } + +// /// +// /// 批量分配Global +// /// +// /// +// /// +// [HttpPost] +// public IResponseOutput DistributeGlobal(WorkloadGlobalCommand workloadGCommand) +// { +// var temp = _workloadGlobalRepository.Where(u => workloadGCommand.GlobalList.Select(s => s.SubjectId).Contains(u.SubjectId) +// && workloadGCommand.GlobalList.Select(s => s.VisitNum).Contains(u.VisitNum) && +// u.ReviewerId == workloadGCommand.ReviewerId); +// if (temp.Any()) +// { +// return ResponseOutput.NotOk("The Globals of different Arms of the same subject couldn't be assigned to the same reviewer."); +// } + +// var success = false; +// workloadGCommand.GlobalList.ForEach(t => +// { +// _workloadDetailRepository.Add(new WorkloadDetail +// { +// WorkloadId = t.Id, +// OptUserName = _userInfo.RealName, +// OptTime = DateTime.Now, +// Status = (int)WorkloadStatus.Distributed, +// ReviewerId = workloadGCommand.ReviewerId +// }); +// _workloadDetailRepository.SaveChanges(); +// success = _workloadGlobalRepository.Update(u => u.Id == t.Id, k => new WorkloadGlobal() +// { +// ReviewerId = workloadGCommand.ReviewerId, +// Status = (int)WorkloadStatus.Distributed +// }); +// }); +// return ResponseOutput.Result(success); +// } + +// [HttpPost] +// public PageOutput GetWorkloadGlobalList(WorkloadDistributionQueryParam param) +// { +// IQueryable query = null; +// Expression> workloadTPLambda = x => x.TrialId == param.TrialId; +// if (param.SiteId != null) +// { +// workloadTPLambda = workloadTPLambda.And(t => t.SiteId == param.SiteId); +// } + +// if (param.Status != null) +// { +// workloadTPLambda = workloadTPLambda.And(t => t.Status == param.Status); +// } +// if (!string.IsNullOrEmpty(param.WorkloadCode)) +// { +// var globalCode = param.WorkloadCode.Trim(); +// workloadTPLambda = workloadTPLambda.And(t => t.GlobalCode.Contains(globalCode)); +// } +// if (param.GroupId != null && param.GroupId > 0) +// { +// var groupCode = "G0" + param.GroupId; +// workloadTPLambda = workloadTPLambda.And(t => t.GlobalCode.Contains(groupCode)); +// } + +// Expression> subjectLambda = x => x.TrialId == param.TrialId; +// if (!string.IsNullOrEmpty(param.SubjectCode)) +// { +// var subjectCode = param.SubjectCode.Trim(); +// subjectLambda = subjectLambda.And(t => t.Code.Contains(subjectCode)); +// } +// Expression> doctorLambda = x => true; +// if (!string.IsNullOrEmpty(param.Reviewer)) +// { +// var reviewer = param.Reviewer.Trim(); + +// doctorLambda = doctorLambda.And(t => t.ChineseName.Contains(reviewer) +// || t.FirstName.Contains(reviewer) || t.LastName.Contains(reviewer)); +// query = from workloadG in _workloadGlobalRepository.Where(workloadTPLambda) +// join subject in _subjectRepository.Where(subjectLambda) on workloadG.SubjectId equals subject.Id +// join site in _siteRepository.AsQueryable() on workloadG.SiteId equals site.Id +// join doctor in _doctorRepository.Where(doctorLambda) on workloadG.ReviewerId equals doctor.Id +// select new WorkloadGlobalDTO() +// { +// Id = workloadG.Id, +// ReviewerCode = doctor.ReviewerCode, +// ReviewerChineseName = doctor.ChineseName, +// ReviewerFirstName = doctor.FirstName, +// ReviewerId = doctor.Id, +// ReviewerLastName = doctor.LastName, +// SiteId = workloadG.SiteId, +// SiteName = site.SiteName, +// Status = workloadG.Status, +// GlobalCode = workloadG.GlobalCode, +// SubjectCode = subject.Code, +// VisitId = workloadG.VisitId, +// UpdateTime = workloadG.UpdateTime, +// SubjectId = workloadG.SubjectId, +// VisitNum = workloadG.VisitNum, +// VisitName = workloadG.VisitName +// }; +// } +// else +// { +// query = from workloadG in _workloadGlobalRepository.Where(workloadTPLambda) +// join subject in _subjectRepository.Where(subjectLambda) on workloadG.SubjectId equals subject.Id +// join site in _siteRepository.AsQueryable() on workloadG.SiteId equals site.Id +// join doctor in _doctorRepository.Where(doctorLambda) on workloadG.ReviewerId equals doctor.Id into cc +// from doctor in cc.DefaultIfEmpty() +// select new WorkloadGlobalDTO() +// { +// Id = workloadG.Id, +// ReviewerCode = doctor.ReviewerCode, +// ReviewerChineseName = doctor.ChineseName, +// ReviewerFirstName = doctor.FirstName, +// ReviewerId = doctor.Id, +// ReviewerLastName = doctor.LastName, +// SiteId = workloadG.SiteId, +// SiteName = site.SiteName, +// Status = workloadG.Status, +// GlobalCode = workloadG.GlobalCode, +// SubjectCode = subject.Code, +// VisitId = workloadG.VisitId, +// UpdateTime = workloadG.UpdateTime, +// SubjectId = workloadG.SubjectId, +// VisitNum = workloadG.VisitNum, +// VisitName = workloadG.VisitName +// }; +// } + +// var count = query.Count(); + +// var propName = param.SortField == string.Empty ? "GlobalCode" : param.SortField; + +// query = param.Asc +// ? query.OrderBy(propName).ThenBy(t => t.SiteName).ThenBy(t => t.SubjectCode) +// : query.OrderByDescending(propName).ThenBy(t => t.SiteName).ThenBy(t => t.SubjectCode); + +// query = query.Skip((param.PageIndex - 1) * param.PageSize).Take(param.PageSize); +// var list = query.ToList(); + +// return new PageOutput(param.PageIndex, +// param.PageSize, count, list); +// } + +// [HttpPost] +// public PageOutput GetWorkloadADList(WorkloadDistributionQueryParam param) +// { +// IQueryable query = null; +// Expression> workloadAdLambda = x => x.TrialId == param.TrialId; +// if (param.SiteId != null) +// { +// workloadAdLambda = workloadAdLambda.And(t => t.SiteId == param.SiteId); +// } + +// if (param.Status != null) +// { +// workloadAdLambda = workloadAdLambda.And(t => t.Status == param.Status); +// } +// if (!string.IsNullOrEmpty(param.WorkloadCode)) +// { +// var adCode = param.WorkloadCode.Trim(); +// workloadAdLambda = workloadAdLambda.And(t => t.ADCode.Contains(adCode)); +// } + +// Expression> subjectLambda = x => x.TrialId == param.TrialId; +// if (!string.IsNullOrEmpty(param.SubjectCode)) +// { +// var subjectCode = param.SubjectCode.Trim(); +// subjectLambda = subjectLambda.And(t => t.Code.Contains(subjectCode)); + +// } +// Expression> doctorLambda = x => true; +// if (!string.IsNullOrEmpty(param.Reviewer)) +// { +// var reviewer = param.Reviewer.Trim(); +// doctorLambda = doctorLambda.And(t => t.ChineseName.Contains(reviewer) +// || t.FirstName.Contains(reviewer) || t.LastName.Contains(reviewer)); +// query = from workloadAd in _workloadAdRepository.Where(workloadAdLambda) +// join subject in _subjectRepository.Where(subjectLambda) on workloadAd.SubjectId equals subject.Id +// join site in _siteRepository.AsQueryable() on workloadAd.SiteId equals site.Id +// join doctor in _doctorRepository.Where(doctorLambda) on workloadAd.ReviewerId equals doctor.Id +// select new WorkloadADDTO() +// { +// Id = workloadAd.Id, +// ReviewerCode = doctor.ReviewerCode, +// ReviewerChineseName = doctor.ChineseName, +// ReviewerFirstName = doctor.FirstName, +// ReviewerId = doctor.Id, +// ReviewerLastName = doctor.LastName, +// SiteId = workloadAd.SiteId, +// SiteName = site.SiteName, +// Status = workloadAd.Status, +// ADCode = workloadAd.ADCode, +// SubjectCode = subject.Code, +// SubjectId = workloadAd.SubjectId, +// UpdateTime = workloadAd.UpdateTime +// }; +// } +// else +// { +// query = from workloadAd in _workloadAdRepository.Where(workloadAdLambda) +// join subject in _subjectRepository.Where(subjectLambda) on workloadAd.SubjectId equals subject.Id + +// join site in _siteRepository.AsQueryable() on workloadAd.SiteId equals site.Id +// join doctor in _doctorRepository.Where(doctorLambda) on workloadAd.ReviewerId equals doctor.Id into cc +// from doctor in cc.DefaultIfEmpty() +// select new WorkloadADDTO() +// { +// Id = workloadAd.Id, +// ReviewerCode = doctor.ReviewerCode, +// ReviewerChineseName = doctor.ChineseName, +// ReviewerFirstName = doctor.FirstName, +// ReviewerId = doctor.Id, +// ReviewerLastName = doctor.LastName, +// SiteId = workloadAd.SiteId, +// SiteName = site.SiteName, +// Status = workloadAd.Status, +// ADCode = workloadAd.ADCode, +// SubjectCode = subject.Code, +// SubjectId = workloadAd.SubjectId, +// UpdateTime = workloadAd.UpdateTime +// }; + +// } +// var count = query.Count(); + +// var propName = param.SortField == string.Empty ? "ADCode" : param.SortField; + +// query = param.Asc +// ? query.OrderBy(propName).ThenBy(t => t.SiteName).ThenBy(t => t.SubjectCode) +// : query.OrderByDescending(propName).ThenBy(t => t.SiteName).ThenBy(t => t.SubjectCode); + +// query = query.Skip((param.PageIndex - 1) * param.PageSize).Take(param.PageSize); +// var list = query.ToList(); + +// return new PageOutput(param.PageIndex, +// param.PageSize, count, list); +// } + +// [HttpPost] +// public PageOutput GetWorkloadTPList(WorkloadDistributionQueryParam param) +// { +// IQueryable query = null; +// Expression> workloadTPLambda = x => x.TrialId == param.TrialId; +// if (param.SiteId != null) +// { +// workloadTPLambda = workloadTPLambda.And(t => t.SiteId == param.SiteId); +// } + +// if (param.Status != null) +// { +// workloadTPLambda = workloadTPLambda.And(t => t.Status == param.Status); +// } +// if (!string.IsNullOrEmpty(param.WorkloadCode)) +// { +// var timepoint = param.WorkloadCode.Trim(); + +// workloadTPLambda = workloadTPLambda.And(t => t.TimepointCode.Contains(timepoint)); +// } +// if (param.GroupId != null && param.GroupId > 0) +// { +// var groupCode = "T0" + param.GroupId; +// workloadTPLambda = workloadTPLambda.And(t => t.TimepointCode.Contains(groupCode)); +// } + +// Expression> subjectLambda = x => x.TrialId == param.TrialId; +// if (!string.IsNullOrEmpty(param.SubjectCode)) +// { +// var subjectCode = param.SubjectCode.Trim(); +// subjectLambda = subjectLambda.And(t => t.Code.Contains(subjectCode)); +// } +// Expression> doctorLambda = x => true; +// if (!string.IsNullOrEmpty(param.Reviewer)) +// { +// var reviewer = param.Reviewer.Trim(); +// doctorLambda = doctorLambda.And(t => t.ChineseName.Contains(reviewer) +// || t.FirstName.Contains(reviewer) || t.LastName.Contains(reviewer)); +// query = from workloadTp in _workloadTpRepository.Where(workloadTPLambda) +// join subject in _subjectRepository.Where(subjectLambda) on workloadTp.SubjectId equals subject.Id +// join subjectVisit in _subjectVisitRepository.AsQueryable() on workloadTp.SubjectVisitId equals subjectVisit.Id +// join study in _studyRepository.AsQueryable() on workloadTp.StudyId equals study.Id +// join site in _siteRepository.AsQueryable() on workloadTp.SiteId equals site.Id +// join doctor in _doctorRepository.Where(doctorLambda) on workloadTp.ReviewerId equals doctor.Id + +// select new WorkloadTPDTO() +// { +// Id = workloadTp.Id, +// ReviewerCode = doctor.ReviewerCode, +// ReviewerChineseName = doctor.ChineseName, +// ReviewerFirstName = doctor.FirstName, +// ReviewerId = doctor.Id, +// ReviewerLastName = doctor.LastName, +// SiteId = workloadTp.SiteId, +// SiteName = site.SiteName, +// Status = workloadTp.Status, +// StudyCode = study.StudyCode, +// StudyId = workloadTp.StudyId, +// TimepointCode = workloadTp.TimepointCode, +// SubjectCode = subject.Code, +// SubjectVisitId = workloadTp.SubjectVisitId, +// SubjectId = workloadTp.SubjectId, +// VisitNum = subjectVisit.VisitNum, +// VisitName = subjectVisit.VisitName, +// UpdateTime = workloadTp.UpdateTime +// }; +// } +// else +// { +// query = from workloadTp in _workloadTpRepository.Where(workloadTPLambda) +// join subject in _subjectRepository.Where(subjectLambda) on workloadTp.SubjectId equals subject.Id +// join subjectVisit in _subjectVisitRepository.AsQueryable() on workloadTp.SubjectVisitId equals subjectVisit.Id +// join study in _studyRepository.AsQueryable() on workloadTp.StudyId equals study.Id +// join site in _siteRepository.AsQueryable() on workloadTp.SiteId equals site.Id +// join doctor in _doctorRepository.Where(doctorLambda) on workloadTp.ReviewerId equals doctor.Id into cc +// from doctor in cc.DefaultIfEmpty() +// select new WorkloadTPDTO() +// { +// Id = workloadTp.Id, +// ReviewerCode = doctor.ReviewerCode, +// ReviewerChineseName = doctor.ChineseName, +// ReviewerFirstName = doctor.FirstName, +// ReviewerId = doctor.Id, +// ReviewerLastName = doctor.LastName, +// SiteId = workloadTp.SiteId, +// SiteName = site.SiteName, +// Status = workloadTp.Status, +// StudyCode = study.StudyCode, +// StudyId = workloadTp.StudyId, +// TimepointCode = workloadTp.TimepointCode, +// SubjectCode = subject.Code, +// SubjectVisitId = workloadTp.SubjectVisitId, +// SubjectId = workloadTp.SubjectId, +// VisitNum = subjectVisit.VisitNum, +// VisitName = subjectVisit.VisitName, +// UpdateTime = workloadTp.UpdateTime +// }; + +// } +// var count = query.Count(); + +// var propName = param.SortField == string.Empty ? "TimepointCode" : param.SortField; +// query = param.Asc +// ? query.OrderBy(propName).ThenBy(t => t.SiteName).ThenBy(t => t.SubjectCode) +// : query.OrderByDescending(propName).ThenBy(t => t.SiteName).ThenBy(t => t.SubjectCode); +// query = query.Skip((param.PageIndex - 1) * param.PageSize).Take(param.PageSize); +// var list = query.ToList(); + +// return new PageOutput(param.PageIndex, +// param.PageSize, count, list); +// } + +// //修改单个TP +// [HttpPost("{tpId:guid}/{reviewerId:guid}")] +// public IResponseOutput UpdateDistributeAD(Guid tpId, Guid reviewerId) +// { +// _workloadDetailRepository.Add(new WorkloadDetail +// { +// WorkloadId = tpId, +// OptUserName = _userInfo.RealName, +// OptTime = DateTime.Now, +// Status = (int)WorkloadStatus.Distributed, +// ReviewerId = reviewerId +// }); +// _workloadDetailRepository.SaveChanges(); +// var success= _workloadAdRepository.Update(t => t.Id == tpId, u => new WorkloadAD() +// { +// ReviewerId = reviewerId, +// Status = (int)WorkloadStatus.Distributed +// }); + +// return ResponseOutput.Result(success); +// } + +// //修改单个Global +// [HttpPost("{tpId:guid}/{reviewerId:guid}/{subjectId:guid}/{visitNum}")] +// public IResponseOutput UpdateDistributeGlobal(Guid tpId, Guid reviewerId, Guid subjectId, decimal visitNum) +// { +// var temp = _workloadGlobalRepository.Where(u => u.SubjectId == subjectId && +// u.VisitNum == visitNum && +// u.ReviewerId == reviewerId && u.Id != tpId); +// if (temp.Any()) +// { +// return ResponseOutput.NotOk("The Global of the other arm of this subject has already been assigned to this reviewer, and the assignment couldn't be performed."); +// } + +// _workloadDetailRepository.Add(new WorkloadDetail +// { +// WorkloadId = tpId, +// OptUserName = _userInfo.RealName, +// OptTime = DateTime.Now, +// Status = (int)WorkloadStatus.Distributed, +// ReviewerId = reviewerId +// }); +// _workloadDetailRepository.SaveChanges(); +// return ResponseOutput.Result(_workloadGlobalRepository.Update(t => t.Id == tpId, u => new WorkloadGlobal() +// { +// ReviewerId = reviewerId, +// Status = (int)WorkloadStatus.Distributed +// })); +// } + +// //修改单个TP +// [HttpPost("{tpId:guid}/{reviewerId:guid}/{studyId:guid}")] +// public IResponseOutput UpdateDistributeTP(Guid tpId, Guid reviewerId, Guid studyId) +// { +// var temp = _workloadTpRepository.Where(u => u.StudyId == studyId && u.ReviewerId == reviewerId && u.Id != tpId); +// if (temp.Any()) +// { +// return ResponseOutput.NotOk("The TP of the other arm of this subject has already been assigned to this reviewer, and the assignment couldn't be performed."); +// } + +// _workloadDetailRepository.Add(new WorkloadDetail +// { +// WorkloadId = tpId, +// OptUserName = _userInfo.RealName, +// OptTime = DateTime.Now, +// Status = (int)WorkloadStatus.Distributed, +// ReviewerId = reviewerId +// }); +// _workloadDetailRepository.SaveChanges(); +// return ResponseOutput.Result(_workloadTpRepository.Update(t => t.Id == tpId, u => new WorkloadTP() +// { +// ReviewerId = reviewerId, +// Status = (int)WorkloadStatus.Distributed +// })); +// } + +// [HttpGet("{workloadId:guid}")] +// public IResponseOutput> GetWorkloadDetail(Guid workloadId) +// { +// IQueryable query = null; +// query = from detail in _workloadDetailRepository.Where(u => u.WorkloadId == workloadId) +// join doctor in _doctorRepository.AsQueryable() on detail.ReviewerId equals doctor.Id into cc +// from doctor in cc.DefaultIfEmpty() +// select new WorkloadDetailDTO() +// { +// OptTime = detail.OptTime, +// OptUserName = detail.OptUserName, +// ReviewerChineseName = doctor.ChineseName, +// ReviewerFirstName = doctor.FirstName, +// ReviewerLastName = doctor.LastName, +// Status = detail.Status +// }; +// var list = query.OrderByDescending(u => u.OptTime).ToList(); +// return ResponseOutput.Ok(list); +// } + +// [HttpPost("UpdateGlobalStatus/{globalId:guid}")] +// public IResponseOutput UpdateGlobalStatus(Guid globalId) +// { +// return ResponseOutput.Result(_workloadGlobalRepository.Update(u => u.Id == globalId, t => new WorkloadGlobal() +// { +// Status = 0 +// })); +// } + +// } +//} diff --git a/IRaCIS.Core.Application/Service/WorkLoad/_MapConfig.cs b/IRaCIS.Core.Application/Service/WorkLoad/_MapConfig.cs new file mode 100644 index 00000000..3e9e5514 --- /dev/null +++ b/IRaCIS.Core.Application/Service/WorkLoad/_MapConfig.cs @@ -0,0 +1,21 @@ +using AutoMapper; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Application.Service.WorkLoad.DTO; +using IRaCIS.Core.Domain.Models; + +namespace IRaCIS.Core.Application.Service +{ + public class WorkLoadConfig : Profile + { + public WorkLoadConfig() + { + CreateMap(); + + CreateMap(); + + CreateMap(); + + } + } + +} diff --git a/IRaCIS.Core.Application/TestService.cs b/IRaCIS.Core.Application/TestService.cs new file mode 100644 index 00000000..6a93b15d --- /dev/null +++ b/IRaCIS.Core.Application/TestService.cs @@ -0,0 +1,39 @@ +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Mvc; +using System.Globalization; +using System.ComponentModel.DataAnnotations; + +namespace IRaCIS.Application.Services +{ + [ ApiExplorerSettings(GroupName = "Institution")] + public class TestService : BaseService + { + [HttpPost] + public string Get(testModel testModel) + { + CultureInfo culture = CultureInfo.CurrentUICulture; + + var a = 123; + + var b = _localizer["test{0}", "测试"]; + return _localizer["test{0}","测试"]; + } + } + + public class testModel + { + + //[Required] + //public string Id { get; set; } + } +} + + +namespace Localization +{ + public class SharedResource + { + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/WebAppConfig.cs b/IRaCIS.Core.Application/WebAppConfig.cs new file mode 100644 index 00000000..639e67cd --- /dev/null +++ b/IRaCIS.Core.Application/WebAppConfig.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Configuration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Json; + +namespace IRaCIS.Application +{ + //public class WebAppConfig + //{ + // public static string RootUrl = ConfigurationManager.AppSettings["rootUrl"].ToString(); + // public static string CodePrefix = ConfigurationManager.AppSettings["CodePrefix"].ToString(); + // public static string UserCodePrefix = ConfigurationManager.AppSettings["UserCodePrefix"].ToString(); + // public static string VisitPlanNumPrefix = ConfigurationManager.AppSettings["VisitPlanNumPrefix"].ToString(); + + // public static int CodeLength { get; private set; } + // public static List ExcludeCodeList { get; private set; } + + //} + + + // .net core 迁移替换 + public class WebAppConfig + { + public static string RootUrl { get; set; } + public static string CodePrefix { get; set; } + public static string UserCodePrefix { get; set; } + public static string VisitPlanNumPrefix { get; set; } + + public static int CodeLength { get; private set; } + public static List ExcludeCodeList { get; private set; } + static WebAppConfig() + { + var configuration = new ConfigurationBuilder() + .Add(new JsonConfigurationSource { Path = "appsettings.json", ReloadOnChange = true }) + .Build(); + + RootUrl = configuration.GetSection("RootUrl").Value; + + CodePrefix = configuration.GetSection("CodePrefix").Value; + + UserCodePrefix = configuration.GetSection("UserCodePrefix").Value; + + VisitPlanNumPrefix = configuration.GetSection("VisitPlanNumPrefix").Value; + } + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/_MediatR/CommandAndQueries/ConsistencyVerificationRequest.cs b/IRaCIS.Core.Application/_MediatR/CommandAndQueries/ConsistencyVerificationRequest.cs new file mode 100644 index 00000000..f1808766 --- /dev/null +++ b/IRaCIS.Core.Application/_MediatR/CommandAndQueries/ConsistencyVerificationRequest.cs @@ -0,0 +1,103 @@ +using Magicodes.ExporterAndImporter.Core; +using MediatR; +using MiniExcelLibs.Attributes; + +namespace IRaCIS.Core.Application.MediatR.CommandAndQueries +{ + public class ConsistencyVerificationRequest : IRequest + { + public List ETCList { get; set; } = new List(); + + public Guid TrialId { get; set; } + } + + public class CheckDBModel : CheckViewModel + { + + public Guid SubjectVisitId { get; set; } + + public Guid StudyId { get; set; } + } + + public class CheckViewModel + { + [ExcelColumnName("中心编号")] + public string SiteCode { get; set; } = string.Empty; + [ExcelColumnName("受试者筛选号")] + public string SubjectCode { get; set; } = string.Empty; + [ExcelColumnName("访视名称")] + public string VisitName { get; set; } = string.Empty; + [ExcelColumnName("检查日期")] + public string StudyDate { get; set; } = string.Empty; + [ExcelColumnName("检查技术")] + public string Modality { get; set; } = string.Empty; + + public override bool Equals(object? obj) + { + if (obj == null) return false; + + var checkModel = obj as CheckViewModel; + + if (checkModel is not null) + { + return SiteCode == checkModel.SiteCode && SubjectCode == checkModel.SubjectCode && VisitName == checkModel.VisitName && StudyDate == checkModel.StudyDate && Modality == checkModel.Modality; + } + else + { + return false; + } + } + public override int GetHashCode() + { + return (SiteCode + SubjectCode + VisitName + StudyDate + Modality).GetHashCode(); + } + } + + public class VisitPlanInfluenceSubjectVisitDTO + { + [ExporterHeader(IsIgnore = true)] + public Guid StudyId { get; set; } + + [ExporterHeader(IsIgnore = true)] + public Guid TrialId { get; set; } + + [ExporterHeader(IsIgnore = true)] + public Guid SubjectVisitId { get; set; } + + [ExporterHeader(DisplayName = "中心编号")] + public string TrialSiteCode { get; set; } = string.Empty; + + [ExporterHeader(DisplayName = "受试者")] + public string SubjectCode { get; set; } = string.Empty; + + [ExporterHeader(DisplayName = "访视名称")] + public string VisitName { get; set; } = string.Empty; + + [ExporterHeader(DisplayName = "检查时间", Format = "yyyy-mm-dd hh:mm:ss")] + public DateTime StudyTime { get; set; } + + [ExporterHeader(DisplayName = "检查技术")] + public string Modality { get; set; } = string.Empty; + + [ExporterHeader(IsIgnore = true)] + public bool IsDicomStudy { get; set; } + + [ExporterHeader(DisplayName = "影像类型")] + public string ImageType => IsDicomStudy ? "Dicom" : "非Dicom"; + + [ExporterHeader(DisplayName = "历史窗口")] + public string HistoryWindow { get; set; } = string.Empty; + + [ExporterHeader(DisplayName = "之前超窗调整后没超窗")] + [ValueMapping(text: "yes", true)] + [ValueMapping(text: "no", false)] + public bool IsOverWindowNowNotOverWindow { get; set; } + + [ExporterHeader(DisplayName = "目前窗口")] + public string NowWindow { get; set; } = string.Empty; + + [ExporterHeader(IsIgnore = true)] + public DateTime CreateTime { get; set; } + } + +} diff --git a/IRaCIS.Core.Application/_MediatR/CommandAndQueries/QAMessageRequest.cs b/IRaCIS.Core.Application/_MediatR/CommandAndQueries/QAMessageRequest.cs new file mode 100644 index 00000000..2482c257 --- /dev/null +++ b/IRaCIS.Core.Application/_MediatR/CommandAndQueries/QAMessageRequest.cs @@ -0,0 +1,23 @@ +using IRaCIS.Core.Domain.Share; +using MediatR; +using System; + +namespace IRaCIS.Core.Application.MediatR.CommandAndQueries +{ + + public class QAMessageRequest:IRequest + { + //有一部分QA 消息 在 AutoFac AOP中,但是由于直接服务生成API 上面切入的AOP会失效, + //因为采用进程内消息通信,可以解耦代码,避免写在一起,影响阅读,专注于业务逻辑 + public NoticeType MessageType { get; set; } + + public Guid SubjectVisitId { get; set; } + + //用于关联关系记录 删除的时候,处理消息 + public Guid QCChallengeId { get; set; } + + //用于关联关系记录 删除的时候,处理消息 + public Guid QCChallengeReplyId { get; set; } + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Application/_MediatR/Handlers/AnonymizeCacheHandler.cs b/IRaCIS.Core.Application/_MediatR/Handlers/AnonymizeCacheHandler.cs new file mode 100644 index 00000000..476daeb9 --- /dev/null +++ b/IRaCIS.Core.Application/_MediatR/Handlers/AnonymizeCacheHandler.cs @@ -0,0 +1,44 @@ +using EasyCaching.Core; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infra.EFCore; +using MediatR; + +namespace IRaCIS.Core.Application.MediatR.Handlers +{ + + public class AnonymizeCacheRequest : IRequest + { + + } + public class AnonymizeCacheHandler : IRequestHandler + { + private readonly IRepository _repository; + + private readonly IEasyCachingProvider _provider; + + /// + /// 构造函数注入 + /// + public AnonymizeCacheHandler(IRepository repository, IEasyCachingProvider provider) + { + _repository = repository; + + _provider = provider; + } + + + public Task Handle(AnonymizeCacheRequest request, CancellationToken cancellationToken) + { + var systemAnonymizationList = _repository.Where(t => t.IsEnable).ToList(); + + _provider.Set(StaticData.Anonymize_AddFixedFiled, systemAnonymizationList.Where(t => t.IsAdd && t.IsFixed).ToList(), TimeSpan.FromDays(7)); + _provider.Set(StaticData.Anonymize_AddIRCInfoFiled, systemAnonymizationList.Where(t => t.IsAdd && t.IsFixed == false).ToList(), TimeSpan.FromDays(7)); + _provider.Set(StaticData.Anonymize_FixedField, systemAnonymizationList.Where(t => t.IsAdd == false && t.IsFixed).ToList(), TimeSpan.FromDays(7)); + _provider.Set(StaticData.Anonymize_IRCInfoField, systemAnonymizationList.Where(t => t.IsAdd == false && t.IsFixed == false).ToList(), TimeSpan.FromDays(7)); + + return Task.FromResult(true); + } + } + + +} diff --git a/IRaCIS.Core.Application/_MediatR/Handlers/ConsistencyVerificationHandler.cs b/IRaCIS.Core.Application/_MediatR/Handlers/ConsistencyVerificationHandler.cs new file mode 100644 index 00000000..2f6677c5 --- /dev/null +++ b/IRaCIS.Core.Application/_MediatR/Handlers/ConsistencyVerificationHandler.cs @@ -0,0 +1,151 @@ +using AutoMapper; +using IRaCIS.Core.Application.MediatR.CommandAndQueries; +using IRaCIS.Core.Domain.Share; + +using IRaCIS.Core.Infra.EFCore; +using MediatR; +using System.Linq.Expressions; + +namespace IRaCIS.Core.Application.MediatR.Handlers +{ + public class ConsistencyVerificationHandler : IRequestHandler + { + private readonly IRepository _studyRepository; + private readonly IUserInfo _userInfo; + private readonly IRepository _subjectRepository; + private readonly IRepository _subjectVisitRepository; + private readonly IRepository _trialSiteRepository; + private readonly IMapper _mapper; + private readonly IRepository _noneDicomStudyRepository; + + /// + /// 构造函数注入 + /// + + public ConsistencyVerificationHandler(IRepository studyRepository, IUserInfo userInfo, + IRepository subjectRepository, IRepository subjectVisitRepository, + IRepository trialSiteRepository, IRepository noneDicomStudyRepository, + IMapper mapper) + { + _noneDicomStudyRepository = noneDicomStudyRepository; + _studyRepository = studyRepository; + _userInfo = userInfo; + _subjectRepository = subjectRepository; + _subjectVisitRepository = subjectVisitRepository; + _trialSiteRepository = trialSiteRepository; + _mapper = mapper; + } + + async Task IRequestHandler.Handle(ConsistencyVerificationRequest request, CancellationToken cancellationToken) + { + var trialId = request.TrialId; + + //处理Excel大小写 + request.ETCList.ForEach(t => { t.Modality = t.Modality.ToUpper().Trim(); t.StudyDate = Convert.ToDateTime(t.StudyDate).ToString("yyyy-MM-dd"); t.SiteCode = t.SiteCode.ToUpper().Trim(); t.VisitName = t.VisitName.ToUpper().Trim(); t.SubjectCode = t.SubjectCode.ToUpper().Trim(); }); + var etcList = request.ETCList; + + //Expression> subjectVisitLambda2 = x => x.TrialId == request.TrialId; + + //subjectVisitLambda2= subjectVisitLambda2.And(x => x.CheckState == CheckStateEnum.ToCheck && x.AuditState == AuditStateEnum.QCPassed || (x.CheckState == CheckStateEnum.CVIng && x.AuditState == AuditStateEnum.QCPassed)); + + Expression> subjectVisitLambda = x => x.TrialId == request.TrialId && + (x.CheckState == CheckStateEnum.ToCheck && x.AuditState == AuditStateEnum.QCPassed || (x.CheckState == CheckStateEnum.CVIng && x.AuditState == AuditStateEnum.QCPassed)); + + var dicomQuery = from sv in _subjectVisitRepository.Where(subjectVisitLambda) + join trialSite in _trialSiteRepository.Where(t => t.TrialId == trialId) on sv.SiteId equals trialSite.SiteId + join subject in _subjectRepository.AsQueryable() on sv.SubjectId equals subject.Id + join study in _studyRepository.AsQueryable() on sv.Id equals study.SubjectVisitId + select new CheckDBModel() + { + SubjectVisitId = sv.Id, + SiteCode = trialSite.TrialSiteCode, + StudyDate = study.StudyTime.ToString("yyyy-MM-dd"), + StudyId = study.Id, + Modality = study.Modalities, + SubjectCode = subject.Code, + VisitName = sv.VisitName, + }; + + var noneDicomQuey = from sv in _subjectVisitRepository.Where(subjectVisitLambda) + join trialSite in _trialSiteRepository.Where(t => t.TrialId == trialId) on sv.SiteId equals trialSite.SiteId + join subject in _subjectRepository.AsQueryable() on sv.SubjectId equals subject.Id + join noneDicomStudy in _noneDicomStudyRepository.AsQueryable() on sv.Id equals noneDicomStudy.SubjectVisitId + select new CheckDBModel() + { + SubjectVisitId = sv.Id, + SiteCode = trialSite.TrialSiteCode, + StudyDate = noneDicomStudy.ImageDate.ToString("yyyy-MM-dd"), + StudyId = noneDicomStudy.Id, + Modality = noneDicomStudy.Modality, + SubjectCode = subject.Code, + VisitName = sv.VisitName, + }; + + var dbList = (await dicomQuery.ToListAsync()).Union(await noneDicomQuey.ToListAsync()); + + //处理数据库 大小写 + dbList.ForEach(t => { t.Modality = t.Modality.ToUpper().Trim(); t.SiteCode = t.SiteCode.ToUpper().Trim(); t.VisitName = t.VisitName.ToUpper().Trim(); t.SubjectCode = t.SubjectCode.ToUpper().Trim(); }); + + var dbCheckList = _mapper.Map>(dbList); + + //按照数据库数据访视分组 + var svGroup = dbList.GroupBy(t => new { t.SubjectVisitId, t.SiteCode, t.SubjectCode, t.VisitName }) + .Select(g => new { g.Key.SubjectCode, g.Key.VisitName, g.Key.SiteCode, g.Key.SubjectVisitId, StudyList = g.ToList() }).ToList(); + + foreach (var sv in svGroup) + { + //找到etc 当前visit site 和subject 一致的检查列表 + var etcVisitStudyList = etcList.Where(t => t.SubjectCode == sv.SubjectCode && t.SiteCode == sv.SiteCode && t.VisitName == sv.VisitName).ToList(); + var dbVisitStudyList = dbCheckList.Where(t => t.SubjectCode == sv.SubjectCode && t.SiteCode == sv.SiteCode && t.VisitName == sv.VisitName).ToList(); + + var dbSV = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == sv.SubjectVisitId).IfNullThrowException(); + + if (etcVisitStudyList.Count == 0) + { + + dbSV.CheckResult = "当前访视在EDC表中未找到数据,请核对 SubjectCode、 SiteCode 、VisitName 是否和ETC系统保持一致"; + dbSV.CheckState = CheckStateEnum.CVIng; + dbSV.ForwardState = ForwardStateEnum.ToForward; + dbSV.CheckChallengeState = CheckChanllengeTypeEnum.PMWaitCRCReply; + dbSV.CheckChallengeDialogList.Add(new CheckChallengeDialog() { SubjectVisitId = sv.SubjectVisitId, TalkContent = dbSV.CheckResult, UserTypeEnum = (UserTypeEnum)_userInfo.UserTypeEnumInt }); + } + else + { + //etc 和数据库 并集 + var unionList = dbVisitStudyList.Union(etcVisitStudyList); + var dbExceptExcel = dbVisitStudyList.Except(etcVisitStudyList); + var excelExceptDB = etcVisitStudyList.Except(dbCheckList); + + //两者没有差别 + if (dbExceptExcel.Count() == 0) + { + dbSV.CheckState = CheckStateEnum.CVPassed; + dbSV.CheckPassedTime = DateTime.Now; + dbSV.CheckResult = "核对EDC数据,完全一致"; + dbSV.CheckChallengeDialogList.Add(new CheckChallengeDialog() { SubjectVisitId = sv.SubjectVisitId, TalkContent = dbSV.CheckResult, UserTypeEnum = (UserTypeEnum)_userInfo.UserTypeEnumInt }); + } + else + { + dbSV.CheckResult = "根据导入的一致性核查数据,请确认本访视以下不一致检查项信息:" + String.Join(" | ", dbExceptExcel.Select(t => $"EDC 缺少:{t.StudyDate} {t.Modality} ")) + " | " + + String.Join(" | ", excelExceptDB.Select(t => $"IRC 缺少:{t.StudyDate} {t.Modality}")); + dbSV.CheckState = CheckStateEnum.CVIng; + dbSV.CheckChallengeState = CheckChanllengeTypeEnum.PMWaitCRCReply; + + //讲核查结果发送消息给CRC + dbSV.CheckChallengeDialogList.Add(new CheckChallengeDialog() { SubjectVisitId = sv.SubjectVisitId, TalkContent = dbSV.CheckResult, UserTypeEnum = (UserTypeEnum)_userInfo.UserTypeEnumInt }); + } + + } + dbSV.CheckTime = DateTime.Now; + await _subjectVisitRepository.SaveChangesAsync(); + + } + + return "OK"; + + } + + } + + +} diff --git a/IRaCIS.Core.Application/_MediatR/Handlers/QAMessageHandler.cs b/IRaCIS.Core.Application/_MediatR/Handlers/QAMessageHandler.cs new file mode 100644 index 00000000..3a497034 --- /dev/null +++ b/IRaCIS.Core.Application/_MediatR/Handlers/QAMessageHandler.cs @@ -0,0 +1,234 @@ +//using IRaCIS.Core.Application.MediatR.CommandAndQueries; +//using MediatR; +//using System.Threading; +//using System.Threading.Tasks; +//using IRaCIS.Core.Domain.Share; +//using IRaCIS.Core.Infra.EFCore; +//using IRaCIS.Core.Domain.Models; +//using System.Linq; +//using System; + + +//namespace IRaCIS.Core.Application.MediatR.Handlers +//{ + +// public class QAMessageHandler : IRequestHandler +// { +// private readonly IRepository _userTrialRepository; +// private readonly IRepository _userTrialSiteRepository; +// private readonly IRepository _studyRepository; +// private readonly IRepository _qaNoticeRepository; +// private readonly IUserInfo _userInfo; +// private readonly IRepository _qaRecordRepository; +// private readonly IRepository _subjectRepository; +// private readonly IRepository _subjectVisitRepository; + +// /// +// /// 构造函数注入 +// /// + +// public QAMessageHandler(IRepository studyRepository, IRepository userTrialRepository, IRepository userTrialSiteRepository, IRepository qaNoticeRepository, IUserInfo userInfo, +// IRepository qaRecordRepository, IRepository subjectRepository, IRepository subjectVisitRepository) +// { +// _userTrialSiteRepository = userTrialSiteRepository; +// _userTrialRepository = userTrialRepository; +// _studyRepository = studyRepository; +// _qaNoticeRepository = qaNoticeRepository; +// _userInfo = userInfo; +// _qaRecordRepository = qaRecordRepository; +// _subjectRepository = subjectRepository; +// _subjectVisitRepository = subjectVisitRepository; +// } + +// public Task Handle(QAMessageRequest request, CancellationToken cancellationToken) +// { +// var qaRecord = _qaRecordRepository.FirstOrDefault(t => t.Id == request.QCChallengeId); + +// var subjectQuery = +// from sv in _subjectVisitRepository.Where(t => t.Id == qaRecord.SubjectVisitId) +// join sub in _subjectRepository.AsQueryable() on sv.SubjectId equals sub.Id +// select new +// { +// sv.VisitName, +// sv.VisitNum, +// SubjectName = sub.LastName + " / " + sub.FirstName, +// sub.SiteId +// }; + +// var subject = subjectQuery.First(); + + + +// //查询项目的参与者 和 负责site下CRC用户 +// var trialUserList = _userTrialRepository.Where(t => t.TrialId == qaRecord.TrialId).ToList(); + + +// var qaList = trialUserList.Where(t => t.User.UserTypeEnum == UserTypeEnum.IQC).ToList(); + + +// if (request.MessageType == NoticeType.QA_AddQARecord_NoticeCRC) +// { + + +// //找出当前操作的QA 如果是pm 或者admin 代替操作 此时会有问题 所以 谁代替,就以谁的名义执行 +// //var currentQA = qaList.First(t => t.UserId == _userInfo.Id); +// //var currentQA = trialUserList.First(t => t.UserId == _userInfo.Id); + +// //在项目CRC列表中筛选出 负责该study关联 site的CRC + +// var siteCRCList = _userTrialSiteRepository.Where(t => +// t.SiteId == subject.SiteId && t.User.UserTypeEnum == UserTypeEnum.ClinicalResearchCoordinator && t.TrialId == qaRecord.TrialId).ToList(); + +// //var siteCRCList = userList.Where(t => t.SiteId == subject.SiteId && t.UserTypeEnum == UserType.ClinicalResearchCoordinator).ToList(); + + +// var notice = new QANotice() +// { +// TrialId = qaRecord.TrialId, +// SubjectVisitId = qaRecord.SubjectVisitId, + +// FromUser = _userInfo.RealName, +// FromUserId = _userInfo.Id, +// FromUserType = _userInfo.UserTypeShortName, +// NoticeTypeEnum = NoticeType.QA_AddQARecord_NoticeCRC, +// NeedDeal = true, +// Message = $"QA -> CRC : {_userInfo.RealName} add QA Record", +// SendTime = DateTime.Now, + +// RelationId = request.QCChallengeId +// }; + +// siteCRCList.ForEach(t => notice.QANoticeUserList.Add(new QANoticeUser() +// { +// QANoticeId = notice.Id, +// SubjectVisitId = qaRecord.SubjectVisitId, +// ToUser = t.User.LastName + " / " + t.User.FirstName, +// ToUserId = t.UserId, +// ToUserType = t.UserTypeRole.UserTypeShortName +// })); + +// //添加 发送给CRC的消息 消息和CRC是 一对多 +// _qaNoticeRepository.Add(notice); + +// } + +// else if (request.MessageType == NoticeType.QARecordDialogPost) +// { +// #region QA通知处理 新建一条QA通知消息 需要分用户类型 记录关联Id 为了删除操作 + +// //CRC =>QA +// if (_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ClinicalResearchCoordinator) +// { +// //PM 或者admin可以代替CRC角色 不能从CRC列表中查询用户 +// //var currentCRC = trialUserList.First(t => t.UserId == _userInfo.Id); + +// var notice = new QANotice() +// { +// TrialId = qaRecord.TrialId, +// SubjectVisitId = qaRecord.SubjectVisitId, +// //FromUser = currentCRC.UserRealName, +// //FromUserId = _userInfo.Id, +// //FromUserType = currentCRC.UserType, +// FromUser = _userInfo.RealName, +// FromUserId = _userInfo.Id, +// FromUserType = _userInfo.UserTypeShortName, +// NoticeTypeEnum = NoticeType.CRC_QARecordDialogPost_NoticeQA, +// NeedDeal = true, +// Message = $"CRC -> QA : {_userInfo.RealName} post or reply message for QA record about SubjectName:{subject.SubjectName } VisitName: {subject.VisitName} VisitNum:{subject.VisitName} !", +// SendTime = DateTime.Now, +// RelationId = request.QCChallengeReplyId +// }; + +// qaList.ForEach(t => notice.QANoticeUserList.Add(new QANoticeUser() +// { +// QANoticeId = notice.Id, +// SubjectVisitId = qaRecord.SubjectVisitId, +// ToUser = t.User.LastName + " / " + t.User.FirstName, +// ToUserId = t.UserId, +// ToUserType = t.User.UserTypeRole.UserTypeShortName +// })); + +// _qaNoticeRepository.Add(notice); + +// //这里作为 设置QA 设置 Inqa 状态的回复 或者QA和CRC对话的 +// var needDealNoticeList = _qaNoticeRepository.AsQueryable() +// .Where(t => t.SubjectVisitId == qaRecord.SubjectVisitId && t.NeedDeal && (t.NoticeTypeEnum == NoticeType.QA_InQA_NoticeCRC +// || t.NoticeTypeEnum == NoticeType.QA_QARecordDialogPost_NoticeCRC || t.NoticeTypeEnum == NoticeType.QA_AddQARecord_NoticeCRC)).ToList(); + +// needDealNoticeList.ForEach(t => +// { +// t.NeedDeal = false; +// t.DealTime = DateTime.Now; +// _qaNoticeRepository.Update(t); +// }); + +// } + +// if (_userInfo.UserTypeEnumInt == (int)UserTypeEnum.IQC) +// { + +// //找出当前操作的QA 如果是pm 或者admin 代替操作 此时会有问题 所以 谁代替,就以谁的名义执行 +// //var currentQA = qaList.First(t => t.UserId == _userInfo.Id); +// //var currentQA = trialUserList.First(t => t.UserId == _userInfo.Id); + +// //在项目CRC列表中筛选出 负责该study关联 site的CRC +// //var siteCRCList = userList.Where(t => t.SiteId == subject.SiteId && t.UserTypeEnum == UserType.ClinicalResearchCoordinator).ToList(); + +// var siteCRCList = _userTrialSiteRepository.Where(t => +// t.SiteId == subject.SiteId && t.User.UserTypeEnum == UserTypeEnum.ClinicalResearchCoordinator && t.TrialId == qaRecord.TrialId).ToList(); + +// var notice = new QANotice() +// { +// TrialId = qaRecord.TrialId, +// SubjectVisitId = qaRecord.SubjectVisitId, +// //FromUser = currentQA.UserRealName, +// //FromUserId = _userInfo.Id, +// //FromUserType = currentQA.UserType, +// FromUser = _userInfo.RealName, +// FromUserId = _userInfo.Id, +// FromUserType = _userInfo.UserTypeShortName, +// NoticeTypeEnum = NoticeType.QA_QARecordDialogPost_NoticeCRC, +// NeedDeal = true, +// Message = $"QA -> CRC : {_userInfo.RealName} post or reply message for QA record about SubjectName:{subject.SubjectName } VisitName: {subject.VisitName} VisitNum:{subject.VisitName} !", +// SendTime = DateTime.Now, +// RelationId = request.QCChallengeReplyId +// }; + +// siteCRCList.ForEach(t => notice.QANoticeUserList.Add(new QANoticeUser() +// { +// QANoticeId = notice.Id, +// SubjectVisitId = qaRecord.SubjectVisitId, +// ToUser = t.User.LastName + " / " + t.User.FirstName, +// ToUserId = t.UserId, +// ToUserType = t.UserTypeRole.UserTypeShortName +// })); + +// //添加 发送给CRC的消息 消息和CRC是 一对多 +// _qaNoticeRepository.Add(notice); + +// //这里作为 CRC post的对话的回复 +// var needDealNoticeList = _qaNoticeRepository.AsQueryable() +// .Where(t => t.SubjectVisitId == qaRecord.SubjectVisitId && t.NeedDeal && t.NoticeTypeEnum == NoticeType.CRC_QARecordDialogPost_NoticeQA).ToList(); + +// needDealNoticeList.ForEach(t => +// { +// t.NeedDeal = false; +// t.DealTime = DateTime.Now; +// _qaNoticeRepository.Update(t); +// }); +// } + + +// #endregion +// } +// else +// { + +// } + + +// return Task.FromResult(true); + +// } +// } +//} \ No newline at end of file diff --git a/IRaCIS.Core.Application/_MediatR/Handlers/TrialStateCacheHandler.cs b/IRaCIS.Core.Application/_MediatR/Handlers/TrialStateCacheHandler.cs new file mode 100644 index 00000000..151f78e8 --- /dev/null +++ b/IRaCIS.Core.Application/_MediatR/Handlers/TrialStateCacheHandler.cs @@ -0,0 +1,45 @@ +using EasyCaching.Core; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infra.EFCore; +using MediatR; + +namespace IRaCIS.Core.Application.MediatR.Handlers +{ + + public class TrialStateCacheRequest : IRequest + { + + } + public class TrialStateCacheHandler : IRequestHandler + { + private readonly IRepository _trialRepository; + + private readonly IEasyCachingProvider _provider; + + /// + /// 构造函数注入 + /// + public TrialStateCacheHandler(IRepository trialRepository, IEasyCachingProvider provider) + { + _trialRepository = trialRepository; + + _provider = provider; + + } + + + public Task Handle(TrialStateCacheRequest request, CancellationToken cancellationToken) + { + //项目启动,将项目状态缓存,因为hangfire 加入后台任务,还是向队列添加任务,执行都有延迟,效果不好 + var list = _trialRepository.Select(t => new { TrialId = t.Id, TrialStatusStr = t.TrialStatusStr }).ToList(); + + // 每天都会有任务刷新状态,项目编辑 添加时 都会处理到缓存中 + list.ForEach(t => _provider.Set(t.TrialId.ToString(), t.TrialStatusStr, TimeSpan.FromDays(7))); + + + return Task.FromResult(true); + } + } + + +} diff --git a/IRaCIS.Core.Domain.Share/Common/DataTypeEnum.cs b/IRaCIS.Core.Domain.Share/Common/DataTypeEnum.cs new file mode 100644 index 00000000..306311e3 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/Common/DataTypeEnum.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IRaCIS.Core.Domain.Share +{ + public enum DataTypeEnum + { + //医生相关 + Reviewer = 0, + + //项目基本信息相关 + TrialBasicDate= 1, + + + //项目Site调研相关 + //TrialSiteSurvey=2, + + + ////项目QC相关 + //TrialQCChanlengeType = 2, + + } +} diff --git a/IRaCIS.Core.Domain.Share/Common/EmailScenarioEnum.cs b/IRaCIS.Core.Domain.Share/Common/EmailScenarioEnum.cs new file mode 100644 index 00000000..05959a61 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/Common/EmailScenarioEnum.cs @@ -0,0 +1,38 @@ +using System.ComponentModel; + +namespace IRaCIS.Core.Domain.Share +{ + public enum EmailScenarioEnum + { + + None=0, + + [Description("用户体系")] + User =1, + + [Description("中心调研邀请")] + SiteSurveyInvite =2, + + [Description("中心调研审批通知")] + SiteSurveyApproval = 3, + + } + + + + + + + + + + + public enum BasicDataTypeEnum + { + None =0, + + Email=1, + + Sign=2 + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain.Share/Common/SignEnum.cs b/IRaCIS.Core.Domain.Share/Common/SignEnum.cs new file mode 100644 index 00000000..43cbcf9f --- /dev/null +++ b/IRaCIS.Core.Domain.Share/Common/SignEnum.cs @@ -0,0 +1,34 @@ + + +namespace IRaCIS.Core.Domain.Share +{ + public enum SignEnum + { + //Trial 逻辑配置确认 + + TrialLogicConfim = 101, + + + + TrialProcessConfim = 102, + + + + TrialUrgentConfim = 103, + + + + TrialLogicConfimUpdate = 104, + + + TrialProcessUpdate = 105, + + + TrialUrgentConfimUpdate = 106, + + + TrialQCQuestionConfirm=107, + + } + +} diff --git a/IRaCIS.Core.Domain.Share/Common/VerifyType.cs b/IRaCIS.Core.Domain.Share/Common/VerifyType.cs new file mode 100644 index 00000000..6592b44d --- /dev/null +++ b/IRaCIS.Core.Domain.Share/Common/VerifyType.cs @@ -0,0 +1,13 @@ + + +namespace IRaCIS.Core.Domain.Share +{ + public enum VerifyType + { + Email = 0, + + Phone = 1 + + } + +} diff --git a/IRaCIS.Core.Domain.Share/Doctor/DoctorStatus.cs b/IRaCIS.Core.Domain.Share/Doctor/DoctorStatus.cs new file mode 100644 index 00000000..d6867c26 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/Doctor/DoctorStatus.cs @@ -0,0 +1,35 @@ +namespace IRaCIS.Core.Domain.Share +{ + public enum ContractorStatusEnum + { + None = 0,//搜索时不条件查询--未添加信息时数据库的默认值 + Cooperation = 1, + Noncooperation = 2 + } + + public enum ReviewerInformationConfirmStatus + { + None = 0, + ConfirmPass = 1, + ConfirmRefuse = 2 + } + + + public enum ReviewerEnrollStatus + { + Yes = 1, + No = 0 + } + public enum ResumeStatusEnum + { + None = 0, + + Pass = 1, + + Failed = 2 + } + + + + +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain.Share/Doctor/DoctorTrialState.cs b/IRaCIS.Core.Domain.Share/Doctor/DoctorTrialState.cs new file mode 100644 index 00000000..d7914222 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/Doctor/DoctorTrialState.cs @@ -0,0 +1,30 @@ +namespace IRaCIS.Core.Domain.Share +{ + public enum DoctorTrialState + { + NotApply = 0, + + //已申请 + HasAppliedDownLoad = 1, + + //审核通过 + AuditPass = 2, + + AuditFailed = 3, + + //已下载 + ResumeHasDownLoad = 4, + + //已邀请--已确认该名单 + Inviting = 5, + + //同意入组 + InviteConfirmed = 6, + + //拒绝入组 + InviteRefused = 7, + + HasUploadAgreement = 8 + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain.Share/Doctor/WorkLoadStatus.cs b/IRaCIS.Core.Domain.Share/Doctor/WorkLoadStatus.cs new file mode 100644 index 00000000..0edddb8c --- /dev/null +++ b/IRaCIS.Core.Domain.Share/Doctor/WorkLoadStatus.cs @@ -0,0 +1,15 @@ +namespace IRaCIS.Core.Domain.Share +{ + public enum WorkloadStatus + { + ToBeAllocated=0,//待分配 + + Distributed = 30,//已分配 + + Reading = 40,//都片中,未提交读片报告之前 + + ReviewFinish = 80,//读片完成 + + //Archived=100,//流程结束,归档锁库 + } +} diff --git a/IRaCIS.Core.Domain.Share/IRaCIS.Core.Domain.Share.csproj b/IRaCIS.Core.Domain.Share/IRaCIS.Core.Domain.Share.csproj new file mode 100644 index 00000000..d9335b8b --- /dev/null +++ b/IRaCIS.Core.Domain.Share/IRaCIS.Core.Domain.Share.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + + + + ..\bin + + + + + + + + + diff --git a/IRaCIS.Core.Domain.Share/NetResource.cs b/IRaCIS.Core.Domain.Share/NetResource.cs new file mode 100644 index 00000000..85834fc3 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/NetResource.cs @@ -0,0 +1,50 @@ +using System.Runtime.InteropServices; + +namespace IRaCIS.Core.Domain.Share +{ + [StructLayout(LayoutKind.Sequential)] + public class NetResource + { + public int dwScope; + public int dwType; + public int dwDisplayType; + public int dwUsage; + public string lpLocalName; + public string lpRemoteName; + public string lpComment; + public string lpProvider; + } + + + public class WNetAddConnectionHelper + { + [DllImport("mpr.dll", EntryPoint = "WNetAddConnection2")] + private static extern uint WNetAddConnection2(NetResource lpNetResource, string lpPassword, string lpUsername, uint dwFlags); + + [DllImport("mpr.dll")] + public static extern int WNetCancelConnection2A(string sharename, int dwFlags, int fForce); + + public static void Connect() + { + + var remoteName = @"\\192.168.1.119\Potomac_01"; + NetResource netResource = new NetResource(); + netResource.dwScope = 1; + netResource.dwType = 1; + netResource.dwDisplayType = 3; + netResource.dwUsage = 1; + netResource.lpLocalName = "W:"; + netResource.lpRemoteName = remoteName.TrimEnd('\\'); + + WNetAddConnection2(netResource, "Everest@suzhou406", "share", 1); + + } + + public static int Disconnect() + { + return WNetCancelConnection2A("W:", 1, 1); + } + + } +} + diff --git a/IRaCIS.Core.Domain.Share/QC/AuditStateEnum.cs b/IRaCIS.Core.Domain.Share/QC/AuditStateEnum.cs new file mode 100644 index 00000000..06b6b66e --- /dev/null +++ b/IRaCIS.Core.Domain.Share/QC/AuditStateEnum.cs @@ -0,0 +1,29 @@ + + +namespace IRaCIS.Core.Domain.Share +{ + + public enum AuditStateEnum + { + //不可用 + None = 0, + + ToAudit = 3, + + //1st QC进行了操作 + InPrimaryQC = 4, + + PrimaryQCPassed = 5, + + //2nd QC进行操作 + InSecondaryQC = 6, + + + //任何QC设置为QC Failed + QCFailed = 7, + + //2nd QC设置为QC Passed + QCPassed = 8, + } + +} diff --git a/IRaCIS.Core.Domain.Share/QC/ChallengeStateEnum.cs b/IRaCIS.Core.Domain.Share/QC/ChallengeStateEnum.cs new file mode 100644 index 00000000..c7bbda8a --- /dev/null +++ b/IRaCIS.Core.Domain.Share/QC/ChallengeStateEnum.cs @@ -0,0 +1,15 @@ + + +namespace IRaCIS.Core.Domain.Share +{ + + public enum ChallengeStateEnum + { + No = 0, + + HaveAndAllClosed = 1, + + HaveAndHaveNotClosed = 2 + } + +} diff --git a/IRaCIS.Core.Domain.Share/QC/CheckChanllengeTypeEnum.cs b/IRaCIS.Core.Domain.Share/QC/CheckChanllengeTypeEnum.cs new file mode 100644 index 00000000..d242c1f2 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/QC/CheckChanllengeTypeEnum.cs @@ -0,0 +1,22 @@ + + +namespace IRaCIS.Core.Domain.Share +{ + + + public enum CheckChanllengeTypeEnum + { + //不可用 + None = 0, + + //CRC 已回复 PM 待回复 + CRCWaitPMReply = 1, + + //PM 已回复 CRC 待回复 + PMWaitCRCReply = 2, + + Closed = 3 + + } + +} diff --git a/IRaCIS.Core.Domain.Share/QC/CheckStateEnum.cs b/IRaCIS.Core.Domain.Share/QC/CheckStateEnum.cs new file mode 100644 index 00000000..390c52dc --- /dev/null +++ b/IRaCIS.Core.Domain.Share/QC/CheckStateEnum.cs @@ -0,0 +1,20 @@ + + +namespace IRaCIS.Core.Domain.Share +{ + + public enum CheckStateEnum + { + //不可用 + None = 0, + + ToCheck = 9, + + + //核查中 + CVIng = 10, + + // 通过CV,等待转发 + CVPassed = 11, + } +} diff --git a/IRaCIS.Core.Domain.Share/QC/ForwardStateEnum.cs b/IRaCIS.Core.Domain.Share/QC/ForwardStateEnum.cs new file mode 100644 index 00000000..b5754e87 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/QC/ForwardStateEnum.cs @@ -0,0 +1,19 @@ + + +namespace IRaCIS.Core.Domain.Share +{ + + + + public enum ForwardStateEnum + { + //不可用 + None = 0, + + ToForward = 14, + + Forwarded = 15, + + ForwardFailed = 16 + } +} diff --git a/IRaCIS.Core.Domain.Share/QC/NoticeType.cs b/IRaCIS.Core.Domain.Share/QC/NoticeType.cs new file mode 100644 index 00000000..99801256 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/QC/NoticeType.cs @@ -0,0 +1,40 @@ +namespace IRaCIS.Core.Domain.Share +{ + public enum NoticeType + { + NotNeedNotice=0, + + CRC_RequestToQA_NoticeQA=1, + + QA_AddQARecord_NoticeCRC=2, + + QARecordDialogPost = 3, + + CRC_QARecordDialogPost_NoticeQA =4, + + QA_QARecordDialogPost_NoticeCRC = 5, + + + + QA_InQA_NoticeCRC = 7, + + CRC_ReUpload_NoticeQA=8, + + QA_QAPass_NoticeQA=9, + + QA_Anonymized_NoticeQA = 11,//匿名化完成 + + //QA_Anonymized_NoticePM = 11,//匿名化完成 + + // PM 不会转发消息 只作为匿名化消息的边界 + + //PM_Forwarded = 12,//已经转发 + + //删除操作 只是将以前的发送的消息删除 + //QA_DeleteQARecord_RemoveNotice=3, + //QA_QARecordDialogDelete_RemoveNotice =6, + + // QA不通过 QA和CRC 对话的消息置为已处理 + //QA_QANotPass_NoticeCRC = 10, + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain.Share/QC/PDStateEnum.cs b/IRaCIS.Core.Domain.Share/QC/PDStateEnum.cs new file mode 100644 index 00000000..ae1aba95 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/QC/PDStateEnum.cs @@ -0,0 +1,12 @@ +namespace IRaCIS.Core.Domain.Share +{ + public enum PDStateEnum + { + //不可用 + None = 0, + + PDProgress=1 + } + + +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain.Share/QC/QAItemStatus.cs b/IRaCIS.Core.Domain.Share/QC/QAItemStatus.cs new file mode 100644 index 00000000..70a1e98b --- /dev/null +++ b/IRaCIS.Core.Domain.Share/QC/QAItemStatus.cs @@ -0,0 +1,13 @@ +namespace IRaCIS.Core.Domain.Share +{ + public enum QAItemStatus + { + Yes=1, + + No=2, + + Uncertain=3, + + Undefined=4 + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain.Share/QC/QATrialTemplateStatus.cs b/IRaCIS.Core.Domain.Share/QC/QATrialTemplateStatus.cs new file mode 100644 index 00000000..2cfd0a54 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/QC/QATrialTemplateStatus.cs @@ -0,0 +1,9 @@ +namespace IRaCIS.Core.Domain.Share +{ + public enum QATrialTemplateStatus + { + HasQuote=1, + + CanEditOrUpdate=0 + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain.Share/QC/QCChallengeCloseEnum.cs b/IRaCIS.Core.Domain.Share/QC/QCChallengeCloseEnum.cs new file mode 100644 index 00000000..69c84f99 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/QC/QCChallengeCloseEnum.cs @@ -0,0 +1,20 @@ + + +namespace IRaCIS.Core.Domain.Share +{ + + public enum QCChallengeCloseEnum + { + //不可用 + None = 0, + + // 问题已解决 + ProblemSolved = 1, + + //问题无法解决 + Unresolvable = 2, + + OtherReason=3 + + } +} diff --git a/IRaCIS.Core.Domain.Share/QC/RequestBackStateEnum.cs b/IRaCIS.Core.Domain.Share/QC/RequestBackStateEnum.cs new file mode 100644 index 00000000..5f8d797b --- /dev/null +++ b/IRaCIS.Core.Domain.Share/QC/RequestBackStateEnum.cs @@ -0,0 +1,20 @@ + + +namespace IRaCIS.Core.Domain.Share +{ + + + + public enum RequestBackStateEnum + { + + NotRequest = 0, + + //CRC 申请,PM待同意 + CRC_RequestBack = 1, + + //PM 已同意 CRC + PM_AgressBack = 2 + } + +} diff --git a/IRaCIS.Core.Domain.Share/QC/SubmitStateEnum.cs b/IRaCIS.Core.Domain.Share/QC/SubmitStateEnum.cs new file mode 100644 index 00000000..9569eaf0 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/QC/SubmitStateEnum.cs @@ -0,0 +1,18 @@ + + +namespace IRaCIS.Core.Domain.Share +{ + + public enum SubmitStateEnum + { + //不可用 + None = 0, + + //上传了影像或PDF,但没有点击Request QC + ToSubmit = 1, + + //点击了Request QC,没有人处理 + Submitted = 2, + + } +} diff --git a/IRaCIS.Core.Domain.Share/QC/TrialQCProcess.cs b/IRaCIS.Core.Domain.Share/QC/TrialQCProcess.cs new file mode 100644 index 00000000..bffe7530 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/QC/TrialQCProcess.cs @@ -0,0 +1,40 @@ +namespace IRaCIS.Core.Domain.Share +{ + public enum TrialQCProcess + { + NotAudit=0, + + SingleAudit=1, + + DoubleAudit=2 + } + + public enum CurrentQC + { + + First = 1, + + Second = 2 + } + + + public enum QCChanllengeReuploadEnum + { + None=0, + + CRCRequestReupload=1, + + QCAgreeUpload=2, + + + CRCReuploaded=3 + } + + public enum CRCReuploadType + { + AdditionalUpload = 0, + + ReUpload = 1, + } + +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain.Share/QC/VisitExecutedEnum.cs b/IRaCIS.Core.Domain.Share/QC/VisitExecutedEnum.cs new file mode 100644 index 00000000..ce94aca8 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/QC/VisitExecutedEnum.cs @@ -0,0 +1,16 @@ + + +namespace IRaCIS.Core.Domain.Share +{ + public enum VisitExecutedEnum + { + UnExecuted = 0, + + Executed = 1, + + //不可用 + Unavailable = 2 + + } + +} diff --git a/IRaCIS.Core.Domain.Share/Trial/EnrollStatus.cs b/IRaCIS.Core.Domain.Share/Trial/EnrollStatus.cs new file mode 100644 index 00000000..c989ccda --- /dev/null +++ b/IRaCIS.Core.Domain.Share/Trial/EnrollStatus.cs @@ -0,0 +1,36 @@ +namespace IRaCIS.Core.Domain.Share +{ + public enum EnrollStatus + { + None = 0, + + HasApplyDownloadResume = 1,//下载简历 申请 + + AuditRefuseDownload = 2,//下载简历审核 不同意 + + HasAuditPassDownload = 3,//下载简历审核 同意 + + HasDownloadResume = 4, // 下载简历 完成 + + HasCommittedToCRO = 5, //已提交CRO + + RefuseCommitCRO = 6, //已提交CRO + + RefuseInviteIntoGroup = 7,// 不邀请入组 + + InviteIntoGroup = 8, //名单被确认 邀请入组 + + RefuseIntoGroup = 9, // 拒绝入组 + + ConfirmIntoGroup = 10, // 确认入组 + + //HasUploadAgreement = 11, // 已上传协议 + + DoctorTrained = 12, // 参加培训 + + DoctorReading = 13, //正在读 + + Finished = 14 // 结束,锁库 + + } +} diff --git a/IRaCIS.Core.Domain.Share/Trial/ProjectStatusEnum.cs b/IRaCIS.Core.Domain.Share/Trial/ProjectStatusEnum.cs new file mode 100644 index 00000000..5d7a3e30 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/Trial/ProjectStatusEnum.cs @@ -0,0 +1,16 @@ +namespace IRaCIS.Core.Domain.Share +{ + public enum ProjectStatusEnum + { + Created = 0, //创建项目 + ChooseDoctor = 1, //晒选医生 + ApplyDownLoadResume = 2, //申请下载简历 + DownloadApproved = 3,//下载审核已通过 + Downloaded = 4,//已下载 + ConfirmDoctorNames = 5,//确认医生名单 + DoctorConfirm = 6,//医生确认完毕 + EndInGroup = 7, //结束入组 + Going = 8,//进行中 + End = 9 // 结束,锁库 + } +} diff --git a/IRaCIS.Core.Domain.Share/Trial/StudyStatus.cs b/IRaCIS.Core.Domain.Share/Trial/StudyStatus.cs new file mode 100644 index 00000000..d42a91f3 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/Trial/StudyStatus.cs @@ -0,0 +1,75 @@ +namespace IRaCIS.Core.Domain.Share +{ + public enum StudyStatus + { + + //NeedReupload = 15,//需要重传15 + + //QAReuploaded = 20,//QA没通过后重新上传完成 + + Pending=0, //配合前端 查询待处理的消息 + + Uploading = 1,// study记录插入 默认状态 正在上传 + + Uploaded = 5,//已上传 + + QARequested=7, + + QAing = 10,//QA中 (各种对话回复、重传啥的中间过程不管) + + + //Abandon = 16,//废弃16 重传的和之前的不是同一份影像,前一份设置为废弃 + + + QAFinish = 25,//QA完成25,合格 + + QAFInishNotPass = 26, //QA结束,不合格 + + QAOver=27,// 为了前端 同时查询 25 26两个状态的Study记录 + + + Anonymizing=28,//匿名化中 + + Anonymized = 30,//匿名化完成 + + AnonymizeFailed = 32,//匿名化失败 + + Forwarding = 34,//转发中 + + Forwarded = 36,//已经转发 + + ForwardFailed = 38,//转发失败 + + + + + + Distributed = 42,//已分配读片任务 + + Reading = 48,//都片中,未提交读片报告之前 + + NeedAd = 52, + + AdDistributed = 58, + + AdReading = 64, + + Review = 80,//待审核 + + Archived = 100,//流程结束,归档锁库 + + + + + //Anonymize = 28,//匿名化完成 30 + //Forwarded = 29,//已经转发 36 + //Distributed = 30,//已分配读片任务 42 + ////Distributed2 = 32, + //Reading = 40,//都片中,未提交读片报告之前 + //NeedAd = 45, + //AdDistributed = 50, + //AdReading = 60, + //Review = 80,//待审核 + //Archived = 100,//流程结束,归档锁库 + } +} diff --git a/IRaCIS.Core.Domain.Share/Trial/SubjectStatus.cs b/IRaCIS.Core.Domain.Share/Trial/SubjectStatus.cs new file mode 100644 index 00000000..e0b55323 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/Trial/SubjectStatus.cs @@ -0,0 +1,11 @@ +namespace IRaCIS.Core.Domain.Share +{ + public enum SubjectStatus + { + OnVisit = 1, + + OutOfVisit = 2, + + EndOfVisit = 3, + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain.Share/Trial/TrialEnrollStatus.cs b/IRaCIS.Core.Domain.Share/Trial/TrialEnrollStatus.cs new file mode 100644 index 00000000..6d85724b --- /dev/null +++ b/IRaCIS.Core.Domain.Share/Trial/TrialEnrollStatus.cs @@ -0,0 +1,25 @@ +namespace IRaCIS.Core.Domain.Share +{ + public enum TrialEnrollStatus + { + //Created = 0, //创建项目 + + ChooseDoctor = 1, //筛选医生 + + HasApplyDownLoadResume = 2, //已申请下载简历 + + HasApprovedDownload = 3, //已审核通过 + + //HasDownloaded = 4, //已下载 --进入医生挑选阶段 + + HasCommitCRO = 5, //已提交CRO + + HasConfirmedDoctorNames = 6, //已确认医生名单 + + EndInGroup = 9, //结束入组--参与项目的医生都确认了 + + ProjectGoing = 10, //进行中 + + Finished = 11 // 结束,锁库 + } +} diff --git a/IRaCIS.Core.Domain.Share/Trial/TrialExpedited.cs b/IRaCIS.Core.Domain.Share/Trial/TrialExpedited.cs new file mode 100644 index 00000000..808fc08d --- /dev/null +++ b/IRaCIS.Core.Domain.Share/Trial/TrialExpedited.cs @@ -0,0 +1,14 @@ +namespace IRaCIS.Core.Domain.Share +{ + public enum TrialExpedited + { + + All=-1, + + None=0, + + ExpeditedIn24H = 1, + + ExpeditedIn48H = 2 + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain.Share/Trial/TrialExpeditedStatus.cs b/IRaCIS.Core.Domain.Share/Trial/TrialExpeditedStatus.cs new file mode 100644 index 00000000..9e4b9399 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/Trial/TrialExpeditedStatus.cs @@ -0,0 +1,9 @@ +namespace IRaCIS.Core.Domain.Share +{ + public enum TrialExpeditedStatus + { + Normal = 0, //不加急 + In24HourExpedited = 1,//24小时 加急 + In48HourExpedited = 2,//48小时 加急 + } +} diff --git a/IRaCIS.Core.Domain.Share/Trial/TrialSiteSurveyEnum.cs b/IRaCIS.Core.Domain.Share/Trial/TrialSiteSurveyEnum.cs new file mode 100644 index 00000000..5a426e60 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/Trial/TrialSiteSurveyEnum.cs @@ -0,0 +1,23 @@ +namespace IRaCIS.Core.Domain.Share +{ + + + public enum TrialSiteSurveyEnum + { + + ToSubmit = 0, + + + CRCSubmitted = 1, + + + SPMApproved = 2, + + //账号生成成功的 没成功的状态不变 + PMCreatedAndLock = 3,// Created + + + } + + +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain.Share/Trial/WorkLoadFromStatus.cs b/IRaCIS.Core.Domain.Share/Trial/WorkLoadFromStatus.cs new file mode 100644 index 00000000..e8e8ed77 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/Trial/WorkLoadFromStatus.cs @@ -0,0 +1,10 @@ +namespace IRaCIS.Core.Domain.Share +{ + public enum WorkLoadFromStatus + { + Doctor = 0, + CRO = 1, + FinalConfirm = 2, + All = 3 + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain.Share/User/SystemUserType.cs b/IRaCIS.Core.Domain.Share/User/SystemUserType.cs new file mode 100644 index 00000000..f022ed01 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/User/SystemUserType.cs @@ -0,0 +1,12 @@ + + +namespace IRaCIS.Core.Domain.Share +{ + + public enum SystemUserType + { + AdminUser = 0, + + DoctorUser = 1 + } +} diff --git a/IRaCIS.Core.Domain.Share/User/TrialExternalEnum.cs b/IRaCIS.Core.Domain.Share/User/TrialExternalEnum.cs new file mode 100644 index 00000000..5bf02d9b --- /dev/null +++ b/IRaCIS.Core.Domain.Share/User/TrialExternalEnum.cs @@ -0,0 +1,17 @@ + + +namespace IRaCIS.Core.Domain.Share +{ + public enum TrialExternalUserStateEnum + { + //待发送 + WaitSent = 0, + + //已发送 + HasSend = 1, + + //用户已确认 + UserConfirmed = 2 + } + +} diff --git a/IRaCIS.Core.Domain.Share/User/UserStateEnum.cs b/IRaCIS.Core.Domain.Share/User/UserStateEnum.cs new file mode 100644 index 00000000..cd0d9934 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/User/UserStateEnum.cs @@ -0,0 +1,12 @@ + + +namespace IRaCIS.Core.Domain.Share +{ + + public enum UserStateEnum + { + Disable = 0, + + Enable = 1 + } +} diff --git a/IRaCIS.Core.Domain.Share/User/UserType.cs b/IRaCIS.Core.Domain.Share/User/UserType.cs new file mode 100644 index 00000000..8f5310c6 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/User/UserType.cs @@ -0,0 +1,80 @@ +namespace IRaCIS.Core.Domain.Share +{ + + + public enum UserTypeEnum + { + + //PM + ProjectManager=1, + + //CRC + ClinicalResearchCoordinator=2, + + //IQC + IQC = 3, + + + ////简历管理人员 + //ResumeManager=4, + ////简历运维人员 + //ReviewerCoordinator = 5, + + ReviewerCoordinator = 4, + + // 大屏展示 + Dashboard = 6, + + // 超级管理员用户类型,用于取代 SuperAdmin字段 数据库不内置这个用户类型和角色的配置,因为只允许有一个 + SuperAdmin=8, + + + CRA=9, + + SPM=10, + + APM=11, + + CPM=12, + + IndependentReviewer=13, + // 医学影像经理 + MIM = 14, + + + QA=15, + + EA=16, + + MW=17, + + + SMM=18, + + CMM=19, + + //医生用户类型暂不处理 + + ShareImage = 15, + + Undefined=0 + + + } + + + public enum UserTypeSelectEnum + { + + None=0, + + ExternalUser=1, + + InnerUser=2, + + SiteSurvey=3, + } + + + +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain.Share/User/UserTypeGroup.cs b/IRaCIS.Core.Domain.Share/User/UserTypeGroup.cs new file mode 100644 index 00000000..916bf18a --- /dev/null +++ b/IRaCIS.Core.Domain.Share/User/UserTypeGroup.cs @@ -0,0 +1,14 @@ + + +namespace IRaCIS.Core.Domain.Share +{ + + public enum UserTypeGroup + { + TrialUser = 1, + + ResumeUser = 2, + + OtherUser = 3, + } +} diff --git a/IRaCIS.Core.Domain.Share/_AppSettings.cs b/IRaCIS.Core.Domain.Share/_AppSettings.cs new file mode 100644 index 00000000..eb7038f2 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/_AppSettings.cs @@ -0,0 +1,82 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Json; +using System; +using System.Collections.Generic; + +namespace IRaCIS.Core.Domain.Share +{ + public class AppSettings + { + public static string RootUrl { get; set; } + public static string CodePrefix { get; set; } + public static string UserCodePrefix { get; set; } + public static string VisitPlanNumPrefix { get; set; } + + public static int CodeLength { get; private set; } + public static List ExcludeCodeList { get; private set; } + public static List AnonymizeTagList = new List(); + public static int LoginExpiredTimeSpan { get; private set; } = 15; + public static bool OpenLog { get; set; } = true; + public static bool AddClinicalInfo { get; set; } = true; + public static bool Share { get; set; } = true; + + + static AppSettings() + { + var configuration = new ConfigurationBuilder() + .Add(new JsonConfigurationSource + { + Path = "appsettings.json", + ReloadOnChange = true + }) + .Add(new JsonConfigurationSource + { + Path = "AnonymizeTagSetting.json", + ReloadOnChange = true + }) + .Build(); + + RootUrl = configuration.GetSection("RootUrl").Value; + + CodePrefix = configuration.GetSection("CodePrefix").Value; + + UserCodePrefix = configuration.GetSection("UserCodePrefix").Value; + + VisitPlanNumPrefix = configuration.GetSection("VisitPlanNumPrefix").Value; + try + { + int tempLoginExpiredTimeSpan = 15; + if (int.TryParse(configuration.GetSection("LoginExpiredTimeSpan").Value, out tempLoginExpiredTimeSpan)) + { + LoginExpiredTimeSpan = tempLoginExpiredTimeSpan; + } + OpenLog = Convert.ToBoolean(configuration.GetSection("OpenLog").Value); + AddClinicalInfo = Convert.ToBoolean(configuration.GetSection("AddClinicalInfo").Value); + configuration.GetSection("needAnonymizeTag").Bind(AnonymizeTagList); + Share = Convert.ToBoolean(configuration.GetSection("OpenLog").Value); + } + catch (Exception) + { + + } + + } + + } + + public class AnonymizeTag + { + public string Group { get; set; } + public string Element { get; set; } + public string ReplaceValue { get; set; } + public bool Enable { get; set; } + public AnonymizeTag() { } + public AnonymizeTag(string group, string element, string value, bool enable) + { + Group = group; + Element = element; + ReplaceValue = value; + Enable = enable; + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain.Share/_StaticData.cs b/IRaCIS.Core.Domain.Share/_StaticData.cs new file mode 100644 index 00000000..60bd0409 --- /dev/null +++ b/IRaCIS.Core.Domain.Share/_StaticData.cs @@ -0,0 +1,72 @@ +namespace IRaCIS.Core.Domain.Share +{ + public static class StaticData + { + /// + /// 用户默认密码 + /// + public static readonly string DefaultPassword = "123456"; + + public static string StudyMaxCode = "StudyMaxCode"; + + public static string Question = "Question"; + + #region 字典表项固定值 + public static readonly string Title = "Title"; + public static readonly string ReadingType = "ReadingType"; + public static readonly string Subspeciality = "Subspeciality"; + public static readonly string Modality = "Modality"; + public static readonly string Criterion = "Criterion"; + public static readonly string ReviewType = "ReviewType"; + public static readonly string ReadingStandard = "ReadingStandard"; + + + + public static readonly string TrialDataFolder = "TrialData"; + public static readonly string DicomFolder = "Dicom"; + public static readonly string NoneDicomFolder = "NoneDicom"; + public static readonly string UploadFileFolder = "UploadFile"; + public static readonly string TreatmenthistoryFolder = "Treatmenthistory"; + + + + #endregion + + #region Reviewer Degree + public const string Bachelor = "Bachelor"; + public const string Master = "Master"; + public const string Doctorate = "Doctorate"; + #endregion + + + + #region TrialStatusStr + + public const string TrialInitializing = "Initializing"; + + public const string TrialOngoing = "Ongoing"; + + public const string TrialCompleted = "Completed"; + + public const string TrialStopped = "Stopped"; + + #endregion + + + + #region 匿名化 + + public const string Anonymize_FixedField = "Anonymize_FixedField"; + + public const string Anonymize_IRCInfoField = "Anonymize_IRCInfoField"; + + public const string Anonymize_AddFixedFiled = "Anonymize_AddFixedFiled"; + + public const string Anonymize_AddIRCInfoFiled = "Anonymize_AddIRCInfoFiled"; + + + #endregion + } + + +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain/BaseModel/Entity.cs b/IRaCIS.Core.Domain/BaseModel/Entity.cs new file mode 100644 index 00000000..b8e9a1e6 --- /dev/null +++ b/IRaCIS.Core.Domain/BaseModel/Entity.cs @@ -0,0 +1,31 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + public abstract class Entity : IEntity + { + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get ; set ; } + } + + public interface IEntity + { + /// + /// 编号 + /// + + + + abstract TKey Id { get; set; } + } + + + //public class Entity : IEntity + //{ + + //} +} diff --git a/IRaCIS.Core.Domain/BaseModel/IAuditAdd.cs b/IRaCIS.Core.Domain/BaseModel/IAuditAdd.cs new file mode 100644 index 00000000..a9d71ae1 --- /dev/null +++ b/IRaCIS.Core.Domain/BaseModel/IAuditAdd.cs @@ -0,0 +1,25 @@ +using System; + +namespace IRaCIS.Core.Domain.Models +{ + public interface IAuditAdd where TKey: struct + { + TKey CreateUserId { get; set; } + + DateTime CreateTime { get; set; } + + } + + + + public interface IAuditAdd: IAuditAdd + { + + } + + public interface IAuditAddWithUserName : IAuditAdd + { + string CreateUser { get; set; } + } + +} diff --git a/IRaCIS.Core.Domain/BaseModel/IAuditUpdate.cs b/IRaCIS.Core.Domain/BaseModel/IAuditUpdate.cs new file mode 100644 index 00000000..ab9de675 --- /dev/null +++ b/IRaCIS.Core.Domain/BaseModel/IAuditUpdate.cs @@ -0,0 +1,16 @@ +using System; + +namespace IRaCIS.Core.Domain.Models +{ + public interface IAuditUpdate where TKey : struct + { + TKey UpdateUserId { get; set; } + //string UpdateUserName { get; set; } + DateTime UpdateTime { get; set; } + } + + public interface IAuditUpdate : IAuditUpdate + { + + } +} diff --git a/IRaCIS.Core.Domain/BaseModel/ISoftDelete.cs b/IRaCIS.Core.Domain/BaseModel/ISoftDelete.cs new file mode 100644 index 00000000..b8da28ad --- /dev/null +++ b/IRaCIS.Core.Domain/BaseModel/ISoftDelete.cs @@ -0,0 +1,11 @@ +using System; + +namespace IRaCIS.Core.Domain.Models +{ + + + public interface ISoftDelete + { + bool IsDeleted { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Common/Dictionary.cs b/IRaCIS.Core.Domain/Common/Dictionary.cs new file mode 100644 index 00000000..9a4a1603 --- /dev/null +++ b/IRaCIS.Core.Domain/Common/Dictionary.cs @@ -0,0 +1,63 @@ +using IRaCIS.Core.Domain.Share; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("Dictionary")] + public partial class Dictionary : Entity, IAuditUpdate, IAuditAdd + { + + public List DoctorDicRelationList { get; set; } = new List(); + + + + + [StringLength(50)] + public string KeyName { get; set; } = string.Empty; + + + [StringLength(100)] + public string Value { get; set; } = string.Empty; + + + [StringLength(100)] + public string ValueCN { get; set; } = string.Empty; + + + [StringLength(512)] + public string Description { get; set; } = string.Empty; + + public int ShowOrder { get; set; } + + public string Type { get; set; } + + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } + + + public string Code { get; set; } + + public Guid? ParentId { get; set; } + + public bool IsEnable { get; set; } + + + public bool IsConfig { get; set; } + + public Guid? ConfigTypeId { get; set; } + + + [ForeignKey("ConfigTypeId")] + public Dictionary ConfigDictionary { get; set; } + + + [ForeignKey("ParentId")] + public Dictionary Parent { get; set; } + + } +} diff --git a/IRaCIS.Core.Domain/Common/EmailNoticeConfig.cs b/IRaCIS.Core.Domain/Common/EmailNoticeConfig.cs new file mode 100644 index 00000000..19d5f3e8 --- /dev/null +++ b/IRaCIS.Core.Domain/Common/EmailNoticeConfig.cs @@ -0,0 +1,109 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-02-15 11:55:43 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using IRaCIS.Core.Domain.Share; +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///EmailNoticeConfig + /// + [Table("EmailNoticeConfig")] + public class EmailNoticeConfig : Entity, IAuditUpdate, IAuditAdd + { + + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + /// + /// UpdateUserId + /// + [Required] + public Guid UpdateUserId { get; set; } + + + [Required] + public Guid ScenarioId { get; set; } + + + [ForeignKey("ScenarioId")] + public SystemBasicData Scenario { get; set; } + + /// + /// UpdateTime + /// + [Required] + public DateTime UpdateTime { get; set; } + + /// + /// Title + /// + [Required] + public string Title { get; set; } = String.Empty; + + /// + /// Body + /// + [Required] + public string Body { get; set; } = String.Empty; + + /// + /// FromEmail + /// + [Required] + public string FromEmail { get; set; } = String.Empty; + + /// + /// ReceiveEmail + /// + [Required] + public string ReceiveEmail { get; set; } = String.Empty; + + /// + /// CopyEmail + /// + [Required] + public string CopyEmail { get; set; } = String.Empty; + + /// + /// IsReturnRequired + /// + [Required] + public bool IsReturnRequired { get; set; } + + /// + /// IsUrgent + /// + [Required] + public bool IsUrgent { get; set; } + + /// + /// IsEnable + /// + [Required] + public bool IsEnable { get; set; } + + + public string AuthorizationCode { get; set; } = String.Empty; + + public string Code { get; set; } = String.Empty; + + public bool IsAutoSend { get; set; } + + } + +} diff --git a/IRaCIS.Core.Domain/Common/Message.cs b/IRaCIS.Core.Domain/Common/Message.cs new file mode 100644 index 00000000..51689a9b --- /dev/null +++ b/IRaCIS.Core.Domain/Common/Message.cs @@ -0,0 +1,18 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("Message")] + public class Message : Entity + { + public Guid ToDoctorId { get; set; } + public Guid FromUserId { get; set; } + public string Title { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + public DateTime MessageTime { get; set; } + public bool HasRead { get; set; } + public string Memo { get; set; } = string.Empty; + + } +} diff --git a/IRaCIS.Core.Domain/Common/SystemBasicData.cs b/IRaCIS.Core.Domain/Common/SystemBasicData.cs new file mode 100644 index 00000000..95e3a6e0 --- /dev/null +++ b/IRaCIS.Core.Domain/Common/SystemBasicData.cs @@ -0,0 +1,93 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-02-15 15:45:52 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using IRaCIS.Core.Domain.Share; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///SystemBasicData + /// + [Table("SystemBasicData")] + public class SystemBasicData : Entity, IAuditUpdate, IAuditAdd + { + + /// + /// Name + /// + [Required] + public string Name { get; set; }=string.Empty; + + /// + /// Value + /// + [Required] + public string Value { get; set; } = string.Empty; + + /// + /// Description + /// + [Required] + public string Description { get; set; } = string.Empty; + + /// + /// ShowOrder + /// + [Required] + public int ShowOrder { get; set; } + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + /// + /// UpdateTime + /// + [Required] + public DateTime UpdateTime { get; set; } + + /// + /// UpdateUserId + /// + [Required] + public Guid UpdateUserId { get; set; } + + /// + /// Code + /// + [Required] + public string Code { get; set; } + + + public Guid? ParentId { get; set; } + + [ForeignKey("ParentId")] + public SystemBasicData Parent { get; set; } + + + public string ValueCN { get; set; } = string.Empty; + + public bool IsEnable { get; set; } + + + public BasicDataTypeEnum BasicDataTypeEnum { get; set; } + + + + + } + +} diff --git a/IRaCIS.Core.Domain/Common/SystemLog.cs b/IRaCIS.Core.Domain/Common/SystemLog.cs new file mode 100644 index 00000000..37e6e248 --- /dev/null +++ b/IRaCIS.Core.Domain/Common/SystemLog.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("SystemLog")] + public partial class SystemLog : Entity + { + public string ApiPath { get; set; } = string.Empty; + public string Params { get; set; } = string.Empty; + public string Result { get; set; } = string.Empty; + public DateTime RequestTime { get; set; } = DateTime.Now; + public long ElapsedMilliseconds { get; set; } = 0; + public Guid OptUserId { get; set; } = Guid.Empty; + public string OptUserName { get; set; } = string.Empty; + public string ClientIP { get; set; } = string.Empty; + public bool Status { get; set; } = true; + public string Message { get; set; } = string.Empty; + public string LogCategory { get; set; } = string.Empty; + } +} diff --git a/IRaCIS.Core.Domain/Common/VerificationCode.cs b/IRaCIS.Core.Domain/Common/VerificationCode.cs new file mode 100644 index 00000000..d5aefbd5 --- /dev/null +++ b/IRaCIS.Core.Domain/Common/VerificationCode.cs @@ -0,0 +1,24 @@ +using IRaCIS.Core.Domain.Share; +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("VerificationCode")] + public class VerificationCode:Entity + { + //public Guid Id { get; set; } + + public Guid UserId { get; set; } = Guid.Empty; + + public string Code { get; set; } + + public VerifyType CodeType { get; set; } + + public bool HasSend { get; set; } + + public string EmailOrPhone { get; set; } + + public DateTime ExpirationTime { get; set; } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain/Dcotor/Attachment.cs b/IRaCIS.Core.Domain/Dcotor/Attachment.cs new file mode 100644 index 00000000..bf61dd5e --- /dev/null +++ b/IRaCIS.Core.Domain/Dcotor/Attachment.cs @@ -0,0 +1,34 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + //public enum AttachmentType + //{ + // Avatar=1,//ͷ + // Resume=2,// + // GCP=3,//GCP֤ + // MedicalLicence=4,//ҽʦʸ֤ + // PracticeCertificate=5,//ִҵʸ֤ + // LargeEquipmentWorkingCertificate=6,//еϸ֤ + // HighestDegreeCertificate=7//ѧ֤ + //} + [Table("Attachment")] + public partial class Attachment : Entity, IAuditAdd + { + public Guid? DoctorId { get; set; } + public string Type { get; set; } + public bool IsOfficial { get; set; } = false; + public string Path { get; set; } = string.Empty; + public string Code { get; set; } = string.Empty; + public DateTime? ExpiryDate { get; set; } + public string FileName { get; set; } = string.Empty; + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } = Guid.Empty; + //language=1 ģ 2ΪӢ + public int Language { get; set; } = 0; + + //public Guid CreateUserId { get; set; } = Guid.Empty; + //public DateTime? CreateTime { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Dcotor/Doctor.cs b/IRaCIS.Core.Domain/Dcotor/Doctor.cs new file mode 100644 index 00000000..723a128f --- /dev/null +++ b/IRaCIS.Core.Domain/Dcotor/Doctor.cs @@ -0,0 +1,181 @@ +using IRaCIS.Core.Domain.Share; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("Doctor")] + public partial class Doctor : Entity, IAuditUpdate, IAuditAdd + { + + // + public List DoctorDicRelationList { get; set; }=new List(); + + public List TrialExperienceCriteriaList { get; set; } + + // UserDoctor һҽ ɱû + + public List UserList { get; set; } + + + public List EnrollList { get; set; } + + + + public string ReviewerCode { get; set; } + + public int Code { get; set; } + + + [StringLength(100)] + public string Phone { get; set; } = string.Empty; + + + [StringLength(100)] + public string Password { get; set; } + + + [StringLength(50)] + public string ChineseName { get; set; } = string.Empty; + + + [StringLength(100)] + public string FirstName { get; set; } = string.Empty; + + + [StringLength(100)] + public string LastName { get; set; } = string.Empty; + + public int Sex { get; set; } + + + [StringLength(100)] + public string EMail { get; set; } = string.Empty; + + + [StringLength(100)] + public string WeChat { get; set; } = string.Empty; + + + [StringLength(1000)] + public string Introduction { get; set; } = string.Empty; + + public Guid? DepartmentId { get; set; } = Guid.Empty; + + + [StringLength(100)] + public string DepartmentOther { get; set; } = string.Empty; + + + + public Guid? RankId { get; set; } = Guid.Empty; + + + [StringLength(100)] + public string RankOther { get; set; } = string.Empty; + + + + public Guid? PositionId { get; set; } = Guid.Empty; + + + [StringLength(100)] + public string PositionOther { get; set; } = string.Empty; + + // Ƿɿ ζ һֱ + public Guid? HospitalId { get; set; } = Guid.Empty; + + + [StringLength(200)] + public string HospitalOther { get; set; } = string.Empty; + + + + + + [StringLength(100)] + public string ReadingTypeOther { get; set; } = string.Empty; + + + + + [StringLength(100)] + public string SubspecialityOther { get; set; } = string.Empty; + + public int GCP { get; set; } + + public Guid? GCPId { get; set; } = Guid.Empty; + + public Guid OrganizationId { get; set; } = Guid.Empty; + + public string OtherClinicalExperience { get; set; } = string.Empty; + public string OtherClinicalExperienceCN { get; set; } = string.Empty; + + public ContractorStatusEnum CooperateStatus { get; set; } = ContractorStatusEnum.Noncooperation; + + public ResumeStatusEnum ResumeStatus { get; set; } = ResumeStatusEnum.Failed; + + + + [StringLength(512)] + public string PhotoPath { get; set; } = string.Empty; + + [StringLength(512)] + public string ResumePath { get; set; } = string.Empty; + public Guid? SpecialityId { get; set; } = Guid.Empty; + + public string SpecialityOther { get; set; } = string.Empty; + + public string AdminComment { get; set; } = string.Empty; + + public int ReviewStatus { get; set; } = 2; + + public bool AcceptingNewTrial { get; set; } = false; + public bool ActivelyReading { get; set; } = false; + + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } + + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + + public DateTime? LastLoginTime { get; set; } + + public Guid AuditUserId { get; set; } = Guid.Empty; + + public DateTime? AuditTime { get; set; } + + public int Nation { get; set; } = 0; // ֧ͣ0-йҽCN1-ҽUS + + + public string ReadingTypeOtherCN { get; set; } = string.Empty; + public string HospitalOtherCN { get; set; } = string.Empty; + public string PositionOtherCN { get; set; } = string.Empty; + public string RankOtherCN { get; set; } = string.Empty; + public string DepartmentOtherCN { get; set; } = string.Empty; + public string SubspecialityOtherCN { get; set; } = string.Empty; + public string SpecialityOtherCN { get; set; } = string.Empty; + + + [ForeignKey("HospitalId")] + public Hospital Hospital { get; set; } + + [ForeignKey("SpecialityId")] + public virtual Dictionary Speciality { get; set; } + + [ForeignKey("DepartmentId")] + public virtual Dictionary Department { get; set; } + + + [ForeignKey("RankId")] + public virtual Dictionary Rank { get; set; } + + + [ForeignKey("PositionId")] + public virtual Dictionary Position { get; set; } + + public List AttachmentList { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Dcotor/DoctorDictionary.cs b/IRaCIS.Core.Domain/Dcotor/DoctorDictionary.cs new file mode 100644 index 00000000..7738b7ab --- /dev/null +++ b/IRaCIS.Core.Domain/Dcotor/DoctorDictionary.cs @@ -0,0 +1,23 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("DoctorDictionary")] + public partial class DoctorDictionary : Entity + { + + [ForeignKey("DoctorId")] + public Doctor Doctor { get; set; } + [ForeignKey("DictionaryId")] + public Dictionary Dictionary { get; set; } + + [StringLength(50)] + public string KeyName { get; set; } = string.Empty; + + public Guid DoctorId { get; set; } + + public Guid DictionaryId { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Dcotor/DoctorWorkload.cs b/IRaCIS.Core.Domain/Dcotor/DoctorWorkload.cs new file mode 100644 index 00000000..46186c58 --- /dev/null +++ b/IRaCIS.Core.Domain/Dcotor/DoctorWorkload.cs @@ -0,0 +1,54 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("DoctorWorkload")] + public partial class Workload : Entity, IAuditUpdate, IAuditAdd + { + [Required] + public Guid TrialId { get; set; } + + public Guid DoctorId { get; set; } + + public int DataFrom { get; set; } + + public DateTime WorkTime { get; set; } + + public int Training { get; set; } + + public int Downtime { get; set; } + + public int Timepoint { get; set; } + + public int TimepointIn24H { get; set; } + + public int TimepointIn48H { get; set; } + + public int Adjudication { get; set; } + + public int AdjudicationIn24H { get; set; } + + public int AdjudicationIn48H { get; set; } + + public int Global { get; set; } + public int RefresherTraining { get; set; } + + public int CreateUserType { get; set; } + + [Required] + public string YearMonth { get; set; } + + public bool IsLock { get; set; } = false; + + + public Guid CreateUserId { get; set; } + + public DateTime CreateTime { get; set; } + + public Guid UpdateUserId { get; set; } + + public DateTime UpdateTime { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Dcotor/Education.cs b/IRaCIS.Core.Domain/Dcotor/Education.cs new file mode 100644 index 00000000..498478f4 --- /dev/null +++ b/IRaCIS.Core.Domain/Dcotor/Education.cs @@ -0,0 +1,69 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("Education")] + public partial class Education : Entity, IAuditUpdate, IAuditAdd + { + public Guid DoctorId { get; set; } + + [Column(TypeName = "date")] + public DateTime? BeginDate { get; set; } + [Column(TypeName = "date")] + public DateTime? EndDate { get; set; } + + [StringLength(50)] + public string Degree { get; set; } = string.Empty; + [StringLength(100)] + public string Major { get; set; } = string.Empty; + + [StringLength(100)] + public string Organization { get; set; } = string.Empty; + + + [StringLength(50)] + public string Country { get; set; } = string.Empty; + + + [StringLength(50)] + public string Province { get; set; } = string.Empty; + + + [StringLength(50)] + public string City { get; set; } = string.Empty; + + + [StringLength(50)] + public string DegreeCN { get; set; } = string.Empty; + [StringLength(100)] + public string MajorCN { get; set; } = string.Empty; + + [StringLength(100)] + public string OrganizationCN { get; set; } = string.Empty; + + + [StringLength(50)] + public string CountryCN { get; set; } = string.Empty; + + + [StringLength(50)] + public string ProvinceCN { get; set; } = string.Empty; + + + [StringLength(50)] + public string CityCN { get; set; } = string.Empty; + + + + public int ShowOrder { get; set; } + + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } = DateTime.Now; + + + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Dcotor/Postgraduate.cs b/IRaCIS.Core.Domain/Dcotor/Postgraduate.cs new file mode 100644 index 00000000..7486316b --- /dev/null +++ b/IRaCIS.Core.Domain/Dcotor/Postgraduate.cs @@ -0,0 +1,66 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("Postgraduate")] + public partial class Postgraduate : Entity, IAuditUpdate, IAuditAdd + { + // public Guid Id { get; set; } + + public Guid DoctorId { get; set; } + + [Column(TypeName = "date")] + public DateTime? BeginDate { get; set; } + + [Column(TypeName = "date")] + public DateTime? EndDate { get; set; } + + + [StringLength(50)] + public string Training { get; set; } = string.Empty; + + [StringLength(100)] + public string Major { get; set; } = string.Empty; + + [StringLength(100)] + public string Hospital { get; set; } = string.Empty; + + [StringLength(100)] + public string School { get; set; } = string.Empty; + [StringLength(100)] + public string Country { get; set; } = string.Empty; + + [StringLength(100)] + public string Province { get; set; } = string.Empty; + + [StringLength(100)] + public string City { get; set; } = string.Empty; + + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } = DateTime.Now; + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + + [StringLength(50)] + public string TrainingCN { get; set; } = string.Empty; + + [StringLength(100)] + public string MajorCN { get; set; } = string.Empty; + + [StringLength(100)] + public string HospitalCN { get; set; } = string.Empty; + + [StringLength(100)] + public string SchoolCN { get; set; } = string.Empty; + [StringLength(100)] + public string CountryCN { get; set; } = string.Empty; + + [StringLength(100)] + public string ProvinceCN { get; set; } = string.Empty; + + [StringLength(100)] + public string CityCN { get; set; } = string.Empty; + } +} diff --git a/IRaCIS.Core.Domain/Dcotor/ResearchPublication.cs b/IRaCIS.Core.Domain/Dcotor/ResearchPublication.cs new file mode 100644 index 00000000..f86f505b --- /dev/null +++ b/IRaCIS.Core.Domain/Dcotor/ResearchPublication.cs @@ -0,0 +1,24 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("ResearchPublication")] + public partial class ResearchPublication : Entity, IAuditUpdate, IAuditAdd + { + public Guid DoctorId { get; set; } + public string Research { get; set; } = string.Empty; + public string Grants { get; set; } = string.Empty; + public string Publications { get; set; } = string.Empty; + public string AwardsHonors { get; set; } = string.Empty; + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } = DateTime.Now; + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; }=DateTime.Now; + + public string ResearchCN { get; set; } = string.Empty; + public string GrantsCN { get; set; } = string.Empty; + public string PublicationsCN { get; set; } = string.Empty; + public string AwardsHonorsCN { get; set; } = string.Empty; + } +} diff --git a/IRaCIS.Core.Domain/Dcotor/TrialExperience.cs b/IRaCIS.Core.Domain/Dcotor/TrialExperience.cs new file mode 100644 index 00000000..cba50202 --- /dev/null +++ b/IRaCIS.Core.Domain/Dcotor/TrialExperience.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("TrialExperience")] + public partial class TrialExperience : Entity, IAuditUpdate, IAuditAdd + { + + public List ExperienceCriteriaList { get; set; } + + + public Guid DoctorId { get; set; } + + //[StringLength(100)] + //public string Term { get; set; } + //[StringLength(100)] + //public string EvaluationCriteria { get; set; } + + public Guid? PhaseId { get; set; } + + public Dictionary Phase { get; set; } + + [StringLength(512)] + public string EvaluationContent { get; set; } + + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } = DateTime.Now; + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Dcotor/TrialExperienceCriteria.cs b/IRaCIS.Core.Domain/Dcotor/TrialExperienceCriteria.cs new file mode 100644 index 00000000..7328aa35 --- /dev/null +++ b/IRaCIS.Core.Domain/Dcotor/TrialExperienceCriteria.cs @@ -0,0 +1,18 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + public class TrialExperienceCriteria:Entity + { + public Guid DoctorId { get; set; } + public Guid TrialExperienceId { get; set; } + public Guid EvaluationCriteriaId { get; set; } + + [ForeignKey("EvaluationCriteriaId")] + public Dictionary EvaluationCriteria { get; set; } + + + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain/Dcotor/UserDoctor.cs b/IRaCIS.Core.Domain/Dcotor/UserDoctor.cs new file mode 100644 index 00000000..0f93551b --- /dev/null +++ b/IRaCIS.Core.Domain/Dcotor/UserDoctor.cs @@ -0,0 +1,22 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + /// + /// ûҽϵ - ʵ + /// + [Table("UserDoctor")] + public partial class UserDoctor : Entity + { + [ForeignKey("User")] + public Guid UserId { get; set; } + + [ForeignKey("Doctor")] + public Guid DoctorId { get; set; } + + public Doctor Doctor { get; set; } + + public User User { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Dcotor/Vacation.cs b/IRaCIS.Core.Domain/Dcotor/Vacation.cs new file mode 100644 index 00000000..ae5670c3 --- /dev/null +++ b/IRaCIS.Core.Domain/Dcotor/Vacation.cs @@ -0,0 +1,18 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("Vacation")] + public class Vacation : Entity, IAuditUpdate, IAuditAdd + { + public Guid DoctorId { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public int Status { get; set; } = 1; + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Document/SystemDocConfirmedUser.cs b/IRaCIS.Core.Domain/Document/SystemDocConfirmedUser.cs new file mode 100644 index 00000000..a6cd58a8 --- /dev/null +++ b/IRaCIS.Core.Domain/Document/SystemDocConfirmedUser.cs @@ -0,0 +1,49 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-01-14 15:04:22 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///SystemDocConfirmedUser + /// + [Table("SystemDocConfirmedUser")] + public class SystemDocConfirmedUser : Entity + { + + public SystemDocument SystemDocument { get; set; } + + [ForeignKey("ConfirmUserId")] + public User User { get; set; } + + /// + /// TrialDocumentId + /// + [Required] + public Guid SystemDocumentId { get; set; } + + /// + /// ConfirmTime + /// + public DateTime? ConfirmTime { get; set; } + + /// + /// ConfirmUserId + /// + [Required] + public Guid ConfirmUserId { get; set; } + + /// + /// SignFirstViewTime + /// + public DateTime? SignFirstViewTime { get; set; } + + public string SignText { get; set; } = string.Empty; + + } + +} diff --git a/IRaCIS.Core.Domain/Document/SystemDocNeedConfirmedUserType.cs b/IRaCIS.Core.Domain/Document/SystemDocNeedConfirmedUserType.cs new file mode 100644 index 00000000..51023cc6 --- /dev/null +++ b/IRaCIS.Core.Domain/Document/SystemDocNeedConfirmedUserType.cs @@ -0,0 +1,37 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-01-14 15:04:23 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///SystemDocNeedConfirmedUserType + /// + [Table("SystemDocNeedConfirmedUserType")] + public class SystemDocNeedConfirmedUserType : Entity + { + + [ForeignKey("NeedConfirmUserTypeId")] + public UserType UserTypeRole { get; set; } + + + public SystemDocument SystemDocument { get; set; } + /// + /// SystemDocumentId + /// + [Required] + public Guid SystemDocumentId { get; set; } + + /// + /// NeedConfirmUserTypeId + /// + [Required] + public Guid NeedConfirmUserTypeId { get; set; } + + } + +} diff --git a/IRaCIS.Core.Domain/Document/SystemDocument.cs b/IRaCIS.Core.Domain/Document/SystemDocument.cs new file mode 100644 index 00000000..0577b630 --- /dev/null +++ b/IRaCIS.Core.Domain/Document/SystemDocument.cs @@ -0,0 +1,76 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-01-05 09:11:49 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///SystemDocument + /// + [Table("SystemDocument")] + public class SystemDocument : Entity, IAuditUpdate, IAuditAdd + { + + public List SystemDocConfirmedUserList { get; set; } + + public List NeedConfirmedUserTypeList { get; set; } + + /// + /// Type + /// + [Required] + public string Type { get; set; } = string.Empty; + + /// + /// Name + /// + [Required] + public string Name { get; set; } = string.Empty; + + + public bool IsAbandon { get; set; } + + public int SignViewMinimumMinutes { get; set; } + + /// + /// Path + /// + [Required] + public string Path { get; set; } = string.Empty; + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + /// + /// UpdateTime + /// + [Required] + public DateTime UpdateTime { get; set; } + + /// + /// UpdateUserId + /// + [Required] + public Guid UpdateUserId { get; set; } + + + + + + } + +} diff --git a/IRaCIS.Core.Domain/Document/TrialDocNeedConfirmedUserType.cs b/IRaCIS.Core.Domain/Document/TrialDocNeedConfirmedUserType.cs new file mode 100644 index 00000000..bd87c683 --- /dev/null +++ b/IRaCIS.Core.Domain/Document/TrialDocNeedConfirmedUserType.cs @@ -0,0 +1,42 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-01-05 09:11:50 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///TrialDocumentUserConfirm + /// + [Table("TrialDocNeedConfirmedUserType")] + public class TrialDocNeedConfirmedUserType : Entity + { + [ForeignKey("NeedConfirmUserTypeId")] + public UserType UserTypeRole { get; set; } + + + public TrialDocument TrialDocument { get; set; } + + + /// + /// TrialDocumentId + /// + [Required] + public Guid TrialDocumentId { get; set; } + + + [Required] + public Guid NeedConfirmUserTypeId { get; set; } + + + //[ForeignKey("NeedConfirmUserTypeId")] + + //public UserTypeRole UserTypeRole { get; set; } + + } + +} diff --git a/IRaCIS.Core.Domain/Document/TrialDocUserTypeConfirmedUser.cs b/IRaCIS.Core.Domain/Document/TrialDocUserTypeConfirmedUser.cs new file mode 100644 index 00000000..7ff5dc89 --- /dev/null +++ b/IRaCIS.Core.Domain/Document/TrialDocUserTypeConfirmedUser.cs @@ -0,0 +1,53 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-01-05 18:02:45 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///TrialDocUserTypeConfirmUser + /// + [Table("TrialDocUserTypeConfirmedUser")] + public class TrialDocUserTypeConfirmedUser : Entity + { + //public Guid TrialId { get; set; } + + //public TrialUser TrialUser { get; set; } + + public TrialDocument TrialDocument { get; set; } + + + /// + /// TrialDocumentId + /// + [Required] + public Guid TrialDocumentId { get; set; } + + /// + /// ConfirmTime + /// + public DateTime? ConfirmTime { get; set; } + + /// + /// ConfirmUserId + /// + [Required] + public Guid ConfirmUserId { get; set; } + + [ForeignKey("ConfirmUserId")] + public User User { get; set; } + + + public DateTime? SignFirstViewTime { get; set; } + + + public string SignText { get; set; } = string.Empty; + + } + + +} diff --git a/IRaCIS.Core.Domain/Document/TrialDocument.cs b/IRaCIS.Core.Domain/Document/TrialDocument.cs new file mode 100644 index 00000000..2a8c9276 --- /dev/null +++ b/IRaCIS.Core.Domain/Document/TrialDocument.cs @@ -0,0 +1,93 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-01-05 09:11:50 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///TrialDocument + /// + [Table("TrialDocument")] + public class TrialDocument : Entity, IAuditUpdate, IAuditAdd + { + + //需要确认的项目用户 通过TrialId 关联 用中间表过滤 + + + public List TrialDocConfirmedUserList { get; set; } + + public List NeedConfirmedUserTypeList { get; set; } + + public Trial Trial { get; set; } + + //public List TrialUserList { get; set; } + + /// + /// Type + /// + [Required] + public string Type { get; set; } = string.Empty; + + /// + /// Name + /// + [Required] + public string Name { get; set; } = string.Empty; + + /// + /// Path + /// + [Required] + public string Path { get; set; } = string.Empty; + + /// + /// TrialId + /// + [Required] + public Guid TrialId { get; set; } + + /// + /// Description + /// + [Required] + public string Description { get; set; } = string.Empty; + + + public bool IsAbandon { get; set; } + + public int SignViewMinimumMinutes { get; set; } + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + /// + /// UpdateTime + /// + [Required] + public DateTime UpdateTime { get; set; } + + /// + /// UpdateUserId + /// + [Required] + public Guid UpdateUserId { get; set; } + + + + } + +} diff --git a/IRaCIS.Core.Domain/Financial/CalculateTask.cs b/IRaCIS.Core.Domain/Financial/CalculateTask.cs new file mode 100644 index 00000000..d46027c7 --- /dev/null +++ b/IRaCIS.Core.Domain/Financial/CalculateTask.cs @@ -0,0 +1,16 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("CalculateTask")] + public class CalculateTask : Entity + { + public Guid ReviewerId { get; set; } + + [Required] + public string YearMonth { get; set; } + public bool IsLock { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Financial/ExchangeRate.cs b/IRaCIS.Core.Domain/Financial/ExchangeRate.cs new file mode 100644 index 00000000..c77e3ce9 --- /dev/null +++ b/IRaCIS.Core.Domain/Financial/ExchangeRate.cs @@ -0,0 +1,18 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("ExchangeRate")] + public class ExchangeRate : Entity, IAuditUpdate, IAuditAdd + { + public string YearMonth { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal Rate { get; set; } + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Financial/Payment.cs b/IRaCIS.Core.Domain/Financial/Payment.cs new file mode 100644 index 00000000..800ce71b --- /dev/null +++ b/IRaCIS.Core.Domain/Financial/Payment.cs @@ -0,0 +1,49 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + + [Table("Payment")] + public partial class Payment : Entity, IAuditUpdate, IAuditAdd + { + public Guid DoctorId { get; set; } + public string YearMonth { get; set; } = string.Empty; + public bool IsLock { get; set; } + public DateTime YearMonthDate { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal PaymentUSD { get; set; } + + [Column(TypeName = "decimal(18,4)")] + public decimal PaymentCNY { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal ExchangeRate { get; set; } + + + [Column(TypeName = "decimal(18,4)")] + public decimal AdjustmentCNY { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal AdjustmentUSD { get; set; } + public DateTime CalculateTime { get; set; } = DateTime.Now; + + [StringLength(100)] + public string CalculateUser { get; set; } = string.Empty; + + [StringLength(500)] + public string Note { get; set; } = string.Empty; + + //public double TaxCNY { get; set; } + //public double ActuallyPaidCNY { get; set; } + //public double BankTransferCNY { get; set; } + + + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Financial/PaymentAdjustment.cs b/IRaCIS.Core.Domain/Financial/PaymentAdjustment.cs new file mode 100644 index 00000000..44d47554 --- /dev/null +++ b/IRaCIS.Core.Domain/Financial/PaymentAdjustment.cs @@ -0,0 +1,33 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("PaymentAdjustment")] + public partial class PaymentAdjustment : Entity, IAuditUpdate, IAuditAdd + { + public Guid ReviewerId { get; set; } + + public DateTime YearMonthDate { get; set; } + public string YearMonth { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal AdjustmentUSD { get; set; } + + [Column(TypeName = "decimal(18,4)")] + public decimal AdjustmentCNY { get; set; } + + + public Guid TrialId { get; set; } = Guid.Empty; + + [Column(TypeName = "decimal(18,2)")] + public decimal ExchangeRate { get; set; } + public bool IsLock { get; set; } = false; + public string Note { get; set; } = string.Empty; + + public Guid CreateUserId { get; set; } = Guid.Empty; + public DateTime CreateTime { get; set; } = DateTime.Now; + public Guid UpdateUserId { get; set; } = Guid.Empty; + public DateTime UpdateTime { get; set; } = DateTime.Now; + } +} diff --git a/IRaCIS.Core.Domain/Financial/PaymentDetail.cs b/IRaCIS.Core.Domain/Financial/PaymentDetail.cs new file mode 100644 index 00000000..feb8e504 --- /dev/null +++ b/IRaCIS.Core.Domain/Financial/PaymentDetail.cs @@ -0,0 +1,48 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("PaymentDetail")] + public partial class PaymentDetail : Entity, IAuditUpdate, IAuditAdd + { + public Guid PaymentId { get; set; } + public Guid DoctorId { get; set; } + public string YearMonth { get; set; } + public Guid TrialId { get; set; } + + [StringLength(50)] + public string TrialCode { get; set; } + + [StringLength(50)] + public string PaymentType { get; set; } + public int Count { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal BasePrice { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal PersonalAdditional { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal TrialAdditional { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal ExchangeRate { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal PaymentUSD { get; set; } + + [Column(TypeName = "decimal(18,4)")] + public decimal PaymentCNY { get; set; } + + public int ShowTypeOrder { get; set; } + public int ShowCodeOrder { get; set; } + + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Financial/RankPrice.cs b/IRaCIS.Core.Domain/Financial/RankPrice.cs new file mode 100644 index 00000000..01daf2b7 --- /dev/null +++ b/IRaCIS.Core.Domain/Financial/RankPrice.cs @@ -0,0 +1,49 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("RankPrice")] + public partial class RankPrice : Entity, IAuditUpdate, IAuditAdd + { + [StringLength(200)] + public string RankName { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal Timepoint { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal TimepointIn24H { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal TimepointIn48H { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal Adjudication { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal AdjudicationIn24H { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal AdjudicationIn48H { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal Global { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal Training { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal Downtime { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal RefresherTraining { get; set; } + public int ShowOrder { get; set; } + + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } = Guid.Empty; + public DateTime UpdateTime { get; set; } + public Guid UpdateUserId { get; set; } = Guid.Empty; + } +} diff --git a/IRaCIS.Core.Domain/Financial/ReviewerPayInformation.cs b/IRaCIS.Core.Domain/Financial/ReviewerPayInformation.cs new file mode 100644 index 00000000..ebf486ab --- /dev/null +++ b/IRaCIS.Core.Domain/Financial/ReviewerPayInformation.cs @@ -0,0 +1,32 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("DoctorPayInformation")] + public partial class ReviewerPayInformation : Entity, IAuditAdd, IAuditUpdate + { + public Guid DoctorId { get; set; } + [StringLength(200)] + public string DoctorNameInBank { get; set; } + + [StringLength(100)] + public string IDCard { get; set; } + + [StringLength(100)] + public string BankCardNumber { get; set; } + + [StringLength(200)] + public string BankName { get; set; } + public Guid RankId { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal Additional { get; set; } + + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } = Guid.Empty; + public DateTime UpdateTime { get; set; } + public Guid UpdateUserId { get; set; } = Guid.Empty; + } +} diff --git a/IRaCIS.Core.Domain/Financial/TrialPaymentPrice.cs b/IRaCIS.Core.Domain/Financial/TrialPaymentPrice.cs new file mode 100644 index 00000000..f883a570 --- /dev/null +++ b/IRaCIS.Core.Domain/Financial/TrialPaymentPrice.cs @@ -0,0 +1,34 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("TrialPaymentPrice")] + public partial class TrialPaymentPrice : Entity, IAuditAdd, IAuditUpdate + { + public Guid TrialId { get; set; } + + public Trial Trial { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal TrialAdditional { get; set; } = 0; + + public string SowName { get; set; } = string.Empty; + public string SowPath { get; set; } = string.Empty; + + [Column(TypeName = "decimal(18,2)")] + public decimal AdjustmentMultiple { get; set; } = 1; + + + + /// + /// Ƿ ΪĿ + /// + public bool? IsNewTrial { get; set; } = false; + + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid UpdateUserId { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Financial/TrialRevenuesPrice.cs b/IRaCIS.Core.Domain/Financial/TrialRevenuesPrice.cs new file mode 100644 index 00000000..10557226 --- /dev/null +++ b/IRaCIS.Core.Domain/Financial/TrialRevenuesPrice.cs @@ -0,0 +1,46 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("TrialRevenuesPrice")] + public class TrialRevenuesPrice : Entity, IAuditUpdate, IAuditAdd + { + public Guid TrialId { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal Timepoint { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal TimepointIn24H { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal TimepointIn48H { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal Adjudication { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal AdjudicationIn24H { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal AdjudicationIn48H { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal Global { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal Training { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal Downtime { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal RefresherTraining { get; set; } + + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Financial/TrialRevenuesPriceVerification.cs b/IRaCIS.Core.Domain/Financial/TrialRevenuesPriceVerification.cs new file mode 100644 index 00000000..822c312d --- /dev/null +++ b/IRaCIS.Core.Domain/Financial/TrialRevenuesPriceVerification.cs @@ -0,0 +1,33 @@ +using System; + +namespace IRaCIS.Core.Domain.Models +{ + public class TrialRevenuesPriceVerification : Entity + { + public Guid TrialId { get; set; } + + public Guid ReviewerId { get; set; } + + public string YearMonth { get; set; } + + public bool Training { get; set; } = false; + + public bool Downtime { get; set; } = false; + + public bool Global { get; set; } = false; + + public bool Timepoint { get; set; } = false; + + public bool TimepointIn24H { get; set; } = false; + + public bool TimepointIn48H { get; set; } = false; + + public bool Adjudication { get; set; } = false; + + public bool AdjudicationIn24H { get; set; } = false; + + public bool AdjudicationIn48H { get; set; } = false; + public bool RefresherTraining { get; set; } = false; + public DateTime WorkLoadDate { get; set; } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain/Financial/VolumeReward.cs b/IRaCIS.Core.Domain/Financial/VolumeReward.cs new file mode 100644 index 00000000..937d56ed --- /dev/null +++ b/IRaCIS.Core.Domain/Financial/VolumeReward.cs @@ -0,0 +1,18 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("VolumeReward")] + public partial class VolumeReward : Entity, IAuditUpdate, IAuditAdd + { + [Column(TypeName = "decimal(18,2)")] + public decimal Price { get; set; } + public int Min { get; set; } + public int Max { get; set; } + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/IRaCIS.Core.Domain.csproj b/IRaCIS.Core.Domain/IRaCIS.Core.Domain.csproj new file mode 100644 index 00000000..b9aa9e09 --- /dev/null +++ b/IRaCIS.Core.Domain/IRaCIS.Core.Domain.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + + + + ..\bin + + + + + + + + + + + + diff --git a/IRaCIS.Core.Domain/Image/DicomInstance.cs b/IRaCIS.Core.Domain/Image/DicomInstance.cs new file mode 100644 index 00000000..10c865c2 --- /dev/null +++ b/IRaCIS.Core.Domain/Image/DicomInstance.cs @@ -0,0 +1,50 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("DicomInstance")] + public class DicomInstance : Entity, IAuditAdd, IAuditUpdate + { + [ForeignKey("SeriesId")] + public DicomSeries DicomSerie { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + + public Guid SeqId { get; set; } + public Guid StudyId { get; set; } + public Guid SeriesId { get; set; } + public string StudyInstanceUid { get; set; } + public string SeriesInstanceUid { get; set; } + public string SopInstanceUid { get; set; } + public int InstanceNumber { get; set; } + public DateTime InstanceTime { get; set; } + public bool CPIStatus { get; set; } + public int ImageRows { get; set; } + public int ImageColumns { get; set; } + public int SliceLocation { get; set; } + + + public string SliceThickness { get; set; } + public int NumberOfFrames { get; set; } + public string PixelSpacing { get; set; } + + public string ImagerPixelSpacing { get; set; } + public string FrameOfReferenceUID { get; set; } + public string WindowCenter { get; set; } + public string WindowWidth { get; set; } + + + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public Guid SubjectId { get; set; } + public Guid SubjectVisitId { get; set; } + public bool Anonymize { get; set; } + + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } = DateTime.Now; + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } = DateTime.Now; + } +} diff --git a/IRaCIS.Core.Domain/Image/DicomSeries.cs b/IRaCIS.Core.Domain/Image/DicomSeries.cs new file mode 100644 index 00000000..8096be17 --- /dev/null +++ b/IRaCIS.Core.Domain/Image/DicomSeries.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("DicomSeries")] + public class DicomSeries : Entity, IAuditAdd, IAuditUpdate, ISoftDelete + { + [ForeignKey("StudyId")] + public DicomStudy DicomStudy { get; set; } + + + public List DicomInstanceList { get; set; } + + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid SeqId { get; set; } + public Guid StudyId { get; set; } + public string StudyInstanceUid { get; set; } + public string SeriesInstanceUid { get; set; } + public int SeriesNumber { get; set; } + public DateTime SeriesTime { get; set; } + public string Modality { get; set; } + public string Description { get; set; } + public int InstanceCount { get; set; } + public string SliceThickness { get; set; } + + + + + public string ImagePositionPatient { get; set; } + public string ImageOrientationPatient { get; set; } + public string BodyPartExamined { get; set; } + public string SequenceName { get; set; } + public string ProtocolName { get; set; } + public string ImagerPixelSpacing { get; set; } + + + public string AcquisitionTime { get; set; } = string.Empty; + public string AcquisitionNumber { get; set; } = string.Empty; + public string TriggerTime { get; set; } = string.Empty; + + + + + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public Guid SubjectId { get; set; } + public Guid SubjectVisitId { get; set; } + + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } = DateTime.Now; + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } = DateTime.Now; + + public bool IsDeleted {get;set;} + + public bool IsReading { get; set; } = true; + + public string BodyPartForEdit { get; set; } = string.Empty; + } +} diff --git a/IRaCIS.Core.Domain/Image/DicomStudy.cs b/IRaCIS.Core.Domain/Image/DicomStudy.cs new file mode 100644 index 00000000..6325d73c --- /dev/null +++ b/IRaCIS.Core.Domain/Image/DicomStudy.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("DicomStudy")] + public class DicomStudy : Entity, IAuditUpdate, IAuditAdd, ISoftDelete + { + //一个检查 由多个人管理 + //public List TrialSiteUserList { get; set; } = new List(); + + public List DicomStudyMonitorList { get; set; } = new List(); + public List StudyDTFList { get; set;} = new List(); + public TrialSite TrialSite { get; set; } + public Site Site { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid SeqId { get; set; } + + public Guid TrialId { get; set; } + + public Guid SiteId { get; set; } + + public Guid SubjectId { get; set; } + + public Guid SubjectVisitId { get; set; } + + public int Code { get; set; } = 0; + + public string StudyCode { get; set; } = string.Empty; + + public int Status { get; set; } = 1; + + public string StudyInstanceUid { get; set; } = string.Empty; + public DateTime StudyTime { get; set; } + public string Modalities { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + public int SeriesCount { get; set; } = 0; + public int InstanceCount { get; set; } = 0; + + //public bool SoftDelete { get; set; } = false; + + public string InstitutionName { get; set; } = string.Empty; + public string PatientId { get; set; } = string.Empty; + public string PatientName { get; set; } = string.Empty; + public string PatientAge { get; set; } = string.Empty; + public string PatientSex { get; set; } = string.Empty; + + public string StudyId { get; set; } = string.Empty; + public string AccessionNumber { get; set; } = string.Empty; + public string PatientBirthDate { get; set; } = string.Empty; + public string AcquisitionTime { get; set; } = string.Empty; + public string AcquisitionNumber { get; set; } = string.Empty; + public string TriggerTime { get; set; } = string.Empty; + + + //0 未知 1 单重 2 双重 + public bool IsDoubleReview { get; set; } + + public string Comment { get; set; } = string.Empty;//上传的时候的 + + public string BodyPartExamined { get; set; } = string.Empty; + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } = DateTime.Now; + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } = DateTime.Now; + + [ForeignKey("CreateUserId")] + public User Uploader { get; set; } + + //public DateTime? UploadedTime { get; set; } + + //public string Uploader { get; set; } = string.Empty; + + + + + public DateTime? DeadlineTime { get; set; } + + public string QAComment { get; set; } = string.Empty; + + + public string BodyPartForEdit { get; set; } = string.Empty; + + + public bool CheckPassed { get; set; } + + public string CheckResult { get; set; }=string.Empty; + + + [ForeignKey("SubjectId")] + public Subject Subject { get; set; } + + [ForeignKey("SubjectVisitId")] + public SubjectVisit SubjectVisit { get; set; } + + //软删除 + public bool IsDeleted { get; set; } + + + } +} diff --git a/IRaCIS.Core.Domain/Image/DicomStudyMonitor.cs b/IRaCIS.Core.Domain/Image/DicomStudyMonitor.cs new file mode 100644 index 00000000..61756a95 --- /dev/null +++ b/IRaCIS.Core.Domain/Image/DicomStudyMonitor.cs @@ -0,0 +1,79 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-01-25 13:26:03 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///DicomStudyMonitor + /// + [Table("StudyMonitor")] + public class StudyMonitor : Entity, IAuditAdd + { + [ForeignKey("StudyId")] + public DicomStudy DicomStudy { get; set; } + + [ForeignKey("StudyId")] + public NoneDicomStudy NoneDicomStudy { get; set; } + + public DateTime CreateTime { get; set; } + + public Guid CreateUserId { get; set; } + + //可能是Dicom 也可能是非Dicom + public Guid StudyId { get; set; } + + + public DateTime UploadStartTime { get; set; } + + + public DateTime UploadFinishedTime { get; set; } + + + public decimal FileSize { get; set; } + + public string IP { get; set; } + + + public bool IsDicomReUpload { get; set; } + + public bool IsDicom { get; set; } + + public int FileCount { get; set; } + + + + public Guid TrialId { get; set; } + + public Guid SiteId { get; set; } + + public Guid SubjectId { get; set; } + + public Guid SubjectVisitId { get; set; } + + + [ForeignKey("SubjectId")] + public Subject Subject { get; set; } + + [ForeignKey("SubjectVisitId")] + public SubjectVisit SubjectVisit { get; set; } + + + public TrialSite TrialSite { get; set; } + + [ForeignKey("SiteId")] + public Site Site { get; set; } + + [ForeignKey("TrialId")] + public Trial Trial { get; set; } + + [ForeignKey("CreateUserId")] + public User Uploader { get; set; } + + } + +} diff --git a/IRaCIS.Core.Domain/Image/ImageLabel.cs b/IRaCIS.Core.Domain/Image/ImageLabel.cs new file mode 100644 index 00000000..2b985400 --- /dev/null +++ b/IRaCIS.Core.Domain/Image/ImageLabel.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("ImageLabel")] + public class ImageLabel : Entity + { + public string TpCode { get; set; } = string.Empty; + public Guid StudyId { get; set; } = Guid.Empty; + public Guid SeriesId { get; set; } = Guid.Empty; + public Guid InstanceId { get; set; } = Guid.Empty; + public string LabelValue { get; set; } = string.Empty; + } +} diff --git a/IRaCIS.Core.Domain/Image/ImageQA.cs b/IRaCIS.Core.Domain/Image/ImageQA.cs new file mode 100644 index 00000000..a24d2f9c --- /dev/null +++ b/IRaCIS.Core.Domain/Image/ImageQA.cs @@ -0,0 +1,16 @@ +using System; + +namespace IRaCIS.Core.Domain.Models +{ + public class ImageQA : Entity, IAuditAdd + { + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public Guid StudyId { get; set; } + public Guid ParentId { get; set; } + public string CommunicationRecord { get; set; } + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } = DateTime.Now; + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain/Image/ImageShare.cs b/IRaCIS.Core.Domain/Image/ImageShare.cs new file mode 100644 index 00000000..b688db37 --- /dev/null +++ b/IRaCIS.Core.Domain/Image/ImageShare.cs @@ -0,0 +1,17 @@ +using System; + +namespace IRaCIS.Core.Domain.Models +{ + public class ImageShare: Entity + { + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public Guid SubjectId { get; set; } + public Guid StudyId { get; set; } + + public DateTime ExpireTime { get; set; } + + public string Password { get; set; } + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain/Image/KeyInstance.cs b/IRaCIS.Core.Domain/Image/KeyInstance.cs new file mode 100644 index 00000000..bea65ae0 --- /dev/null +++ b/IRaCIS.Core.Domain/Image/KeyInstance.cs @@ -0,0 +1,18 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("KeyInstance")] + public class KeyInstance : Entity, IAuditAdd, IAuditUpdate + { + + public string TpCode { get; set; } + public Guid SeriesId { get; set; } + public Guid InstanceId { get; set; } + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } = DateTime.Now; + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } = DateTime.Now; + } +} diff --git a/IRaCIS.Core.Domain/Image/NoneDicomFile.cs b/IRaCIS.Core.Domain/Image/NoneDicomFile.cs new file mode 100644 index 00000000..a7e01f59 --- /dev/null +++ b/IRaCIS.Core.Domain/Image/NoneDicomFile.cs @@ -0,0 +1,16 @@ +using System; + +namespace IRaCIS.Core.Domain.Models +{ + + //public class NoneDicomFile : Entity, IAuditAdd + //{ + // public Guid StudyId { get; set; } + + // public string FileName { get; set; } + + // public string Path { get; set; } + // public Guid CreateUserId { get; set; } + // public DateTime CreateTime { get; set; } = DateTime.Now; + //} +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain/Image/StudyDTF.cs b/IRaCIS.Core.Domain/Image/StudyDTF.cs new file mode 100644 index 00000000..0523fa86 --- /dev/null +++ b/IRaCIS.Core.Domain/Image/StudyDTF.cs @@ -0,0 +1,24 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + public class StudyDTF : Entity, IAuditAdd + { + [ForeignKey("StudyId")] + public DicomStudy DicomStudy { get; set; } + public Guid StudyId { get; set; } = Guid.Empty; + + public string FileName { get; set; } = string.Empty; + + public string Path { get; set; } = string.Empty; + + + //public byte[] RowVersion { get; set; } = default; + + + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } = DateTime.Now; + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain/Image/StudyReviewer.cs b/IRaCIS.Core.Domain/Image/StudyReviewer.cs new file mode 100644 index 00000000..8dc51927 --- /dev/null +++ b/IRaCIS.Core.Domain/Image/StudyReviewer.cs @@ -0,0 +1,20 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("StudyReviewer")] + public class StudyReviewer : Entity, IAuditAdd + { + public Guid StudyId { get; set; } + public Guid ReviewerId { get; set; } + + public Guid TrialId { get; set; } + + // 2是 ad 1是tp + public int WorkloadType { get; set; } = 0; + public int Status { get; set; } = 30; + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } = DateTime.Now; + } +} diff --git a/IRaCIS.Core.Domain/Image/StudyStatusDetail.cs b/IRaCIS.Core.Domain/Image/StudyStatusDetail.cs new file mode 100644 index 00000000..825d3807 --- /dev/null +++ b/IRaCIS.Core.Domain/Image/StudyStatusDetail.cs @@ -0,0 +1,15 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("StudyStatusDetail")] + public class StudyStatusDetail : Entity + { + public Guid StudyId { get; set; } + public int Status { get; set; } = 0; + public string OptUserName { get; set; } = string.Empty; + public DateTime OptTime { get; set; } = DateTime.Now; + public string Note { get; set; } = string.Empty; + } +} diff --git a/IRaCIS.Core.Domain/Image/SystemAnonymization.cs b/IRaCIS.Core.Domain/Image/SystemAnonymization.cs new file mode 100644 index 00000000..c57917ab --- /dev/null +++ b/IRaCIS.Core.Domain/Image/SystemAnonymization.cs @@ -0,0 +1,91 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-03-03 15:26:35 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///SystemAnonymization + /// + [Table("SystemAnonymization")] + public class SystemAnonymization : Entity, IAuditUpdate, IAuditAdd + { + + + + /// + /// Group + /// + [Required] + public string Group { get; set; } = String.Empty; + + /// + /// Element + /// + [Required] + public string Element { get; set; } = String.Empty; + + /// + /// TagDescription + /// + [Required] + public string TagDescription { get; set; } = String.Empty; + + /// + /// TagDescriptionCN + /// + [Required] + public string TagDescriptionCN { get; set; } + + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + /// + /// UpdateTime + /// + [Required] + public DateTime UpdateTime { get; set; } + + /// + /// UpdateUserId + /// + [Required] + public Guid UpdateUserId { get; set; } + + /// + /// ReplaceValue + /// + [Required] + public string ReplaceValue { get; set; } = String.Empty; + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// ValueRepresentation + /// + [Required] + public string ValueRepresentation { get; set; } = String.Empty; + + + public bool IsAdd { get; set; } + + public bool IsEnable { get; set; } + + + public bool IsFixed { get; set; } + + + } + +} diff --git a/IRaCIS.Core.Domain/Institution/CRO.cs b/IRaCIS.Core.Domain/Institution/CRO.cs new file mode 100644 index 00000000..654b29fe --- /dev/null +++ b/IRaCIS.Core.Domain/Institution/CRO.cs @@ -0,0 +1,17 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("CROCompany")] + public partial class CRO : Entity, IAuditUpdate, IAuditAdd + { + public string CROName { get; set; } = string.Empty; + public string CROCode { get; set; } + + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Institution/Hospital.cs b/IRaCIS.Core.Domain/Institution/Hospital.cs new file mode 100644 index 00000000..6c30476a --- /dev/null +++ b/IRaCIS.Core.Domain/Institution/Hospital.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("Hospital")] + public class Hospital : Entity, IAuditUpdate, IAuditAdd + { + public string HospitalName { get; set; } = string.Empty; + public string UniversityAffiliated { get; set; } = string.Empty; + public string Country { get; set; } = string.Empty; + public string Province { get; set; } = string.Empty; + public string City { get; set; } = string.Empty; + + public string HospitalNameCN { get; set; } = string.Empty; + public string UniversityAffiliatedCN { get; set; } = string.Empty; + public string CountryCN { get; set; } = string.Empty; + public string ProvinceCN { get; set; } = string.Empty; + public string CityCN { get; set; } = string.Empty; + + + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } = Guid.Empty; + + public DateTime UpdateTime { get; set; } = DateTime.Now; + public Guid UpdateUserId { get; set; } = Guid.Empty; + + public List DoctorList { get; set; } + + } +} diff --git a/IRaCIS.Core.Domain/Institution/Site.cs b/IRaCIS.Core.Domain/Institution/Site.cs new file mode 100644 index 00000000..e18ffed9 --- /dev/null +++ b/IRaCIS.Core.Domain/Institution/Site.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("Site")] + public partial class Site : Entity, IAuditUpdate, IAuditAdd + { + public Hospital Hospital { get; set; } + public string SiteName { get; set; } + public string SiteCode { get; set; } + + public string City { get; set; } + public string Country { get; set; } + public Guid? HospitalId { get; set; } + public int State { get; set; } + + public string UniqueCode { get; set; } = string.Empty; + + public string Address { get; set; } + + public string DirectorName { get; set; } = string.Empty; + public string DirectorPhone { get; set; } = string.Empty; + public string ContactName { get; set; } = string.Empty; + public string ContactPhone { get; set; } = string.Empty; + + public Guid CreateUserId { get; set; } = Guid.Empty; + public DateTime CreateTime { get; set; } = DateTime.Now; + public Guid UpdateUserId { get; set; } = Guid.Empty; + public DateTime UpdateTime { get; set; } = DateTime.Now; + + public List SubjectList { get; set; } + + // + public List TrialSiteList { get; set; } + + public List< TrialSiteUser> TrialSiteUserList { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Institution/Sponsor.cs b/IRaCIS.Core.Domain/Institution/Sponsor.cs new file mode 100644 index 00000000..c91cdb35 --- /dev/null +++ b/IRaCIS.Core.Domain/Institution/Sponsor.cs @@ -0,0 +1,16 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("Sponsor")] + public partial class Sponsor : Entity, IAuditUpdate, IAuditAdd + { + public string SponsorName { get; set; } = String.Empty; + + public DateTime CreateTime { get; set; } = DateTime.Now; + public Guid CreateUserId { get; set; } = Guid.Empty; + public DateTime UpdateTime { get; set; } = DateTime.Now; + public Guid UpdateUserId { get; set; } = Guid.Empty; + } +} diff --git a/IRaCIS.Core.Domain/Management/Menu.cs b/IRaCIS.Core.Domain/Management/Menu.cs new file mode 100644 index 00000000..983638b9 --- /dev/null +++ b/IRaCIS.Core.Domain/Management/Menu.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("Menu")] + public class Menu : Entity, IAuditUpdate, IAuditAdd + { + public List UserTypeMenuList { get; set; } + + + //上级菜单 + public Guid? ParentId { get; set; } = Guid.Empty; + + // 类型(M目录 C菜单 F按钮 L链接) + public string MenuType { get; set; } = string.Empty; + + public string MenuIcon { get; set; } + + public string MenuName { get; set; } = string.Empty; + + //路由地址 + public string Path { get; set; } = string.Empty; + + //组件路径 + public string Component { get; set; } = string.Empty; + + public int ShowOrder { get; set; } + + //启用 禁用 + public bool IsEnable { get; set; } = true; + + public bool IsCache { get; set; } = false; + + public bool IsDisplay { get; set; } + + public bool IsInTabDisplay { get; set; } + + public bool IsExternalLink { get; set; } + + //权限点 + public string PermissionStr { get; set; } + + //Api 接口地址 + public string ApiPath { get; set; } + + public string Note { get; set; } = string.Empty; + + public string Meta { get; set; } = string.Empty; + + public string Redirect { get; set; } = string.Empty; + + public DateTime CreateTime { get; set; } = DateTime.Now; + public Guid CreateUserId { get; set; } = Guid.Empty; + public DateTime UpdateTime { get; set; } = DateTime.Now; + public Guid UpdateUserId { get; set; } = Guid.Empty; + + } +} diff --git a/IRaCIS.Core.Domain/Management/Role.cs b/IRaCIS.Core.Domain/Management/Role.cs new file mode 100644 index 00000000..e110af3d --- /dev/null +++ b/IRaCIS.Core.Domain/Management/Role.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("Role")] + public partial class Role : Entity, IAuditUpdate, IAuditAdd + { + + public string RoleName { get; set; } = string.Empty; + + public string RoleDescription { get; set; } = string.Empty; + + public int Status { get; set; } + public int PrivilegeLevel { get; set; } //Ȩ޼ + + public DateTime CreateTime { get; set; } = DateTime.Now; + public Guid CreateUserId { get; set; } = Guid.Empty; + public DateTime UpdateTime { get; set; } = DateTime.Now; + public Guid UpdateUserId { get; set; } = Guid.Empty; + } +} diff --git a/IRaCIS.Core.Domain/Management/User.cs b/IRaCIS.Core.Domain/Management/User.cs new file mode 100644 index 00000000..0dff11de --- /dev/null +++ b/IRaCIS.Core.Domain/Management/User.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using IRaCIS.Core.Domain.Share; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("User")] + public partial class User : Entity, IAuditAdd, IAuditUpdate + { + [ForeignKey("UserTypeId")] + public UserType UserTypeRole { get; set; } + + public List SystemDocConfirmedList { get; set; } + + public List UserDoctors { get; set; } = new List(); + public List UserRoleList { get; set; } = new List(); + public List UserTrials { get; set; } = new List(); + + + [StringLength(255)] + public string UserName { get; set; } + + [StringLength(255)] + public string Password { get; set; } + [StringLength(255)] + + public string LastName { get; set; } + public string FirstName { get; set; } + + public string Phone { get; set; } = string.Empty; + public string EMail { get; set; } = string.Empty; + public int Sex { get; set; } + public UserStateEnum Status { get; set; } = UserStateEnum.Enable; + + public DateTime? LastLoginTime { get; set; } + + public Guid UserTypeId { get; set; } = Guid.Empty; + public bool IsZhiZhun { get; set; } + + public UserTypeEnum UserTypeEnum { get; set; } + + public string OrganizationName { get; set; } = String.Empty; + + public bool PasswordChanged { get; set; } + + //public Guid OrganizationId { get; set; } = Guid.Empty; + //public Guid OrganizationTypeId { get; set; } = Guid.Empty; + //public string OrganizationType { get; set; } = String.Empty; + //public string UserType { get; set; } = string.Empty; + //public bool SuperAdmin { get; set; } = false; + //public string RealName { get; set; } + + public string UserCode { get; set; } = string.Empty; + + public int Code { get; set; } + + + public string DepartmentName { get; set; } = String.Empty; + + public string PositionName { get; set; } = String.Empty; + + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + + public bool IsFirstAdd { get; set; } = true; + + + + + } +} diff --git a/IRaCIS.Core.Domain/Management/UserRole.cs b/IRaCIS.Core.Domain/Management/UserRole.cs new file mode 100644 index 00000000..e76b4027 --- /dev/null +++ b/IRaCIS.Core.Domain/Management/UserRole.cs @@ -0,0 +1,15 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("UserRole")] + public partial class UserRole : Entity + { + public Role Role { get; set; } + + public User User { get; set; } + public Guid UserId { get; set; } + public Guid RoleId { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Management/UserType.cs b/IRaCIS.Core.Domain/Management/UserType.cs new file mode 100644 index 00000000..e53ed493 --- /dev/null +++ b/IRaCIS.Core.Domain/Management/UserType.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using IRaCIS.Core.Domain.Share; + +namespace IRaCIS.Core.Domain.Models +{ + public class UserType:Entity + { + //public Guid RoleId { get; set; } + //public List TrialUserList { get; set; } + + + + public UserTypeEnum UserTypeEnum { get; set; } + + public string UserTypeName { get; set; } + + public string Description { get; set; } + + public int Order { get; set; } + + public string UserTypeShortName { get; set; } = string.Empty; + + public bool IsEnable { get; set; } = true; + + + public string PermissionStr { get; set; } + + + + + public List UserTypeMenuList { get; set; } + + public List UserTypeGroupList { get; set; } + + public List SystemDocNeedConfirmedUserTypeList { get; set; } + + public List UserList { get; set; } + + + + + //public bool IsInternal { get; set; } + //public UserTypeGroup Type { get; set; } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain/Management/UserTypeGroup.cs b/IRaCIS.Core.Domain/Management/UserTypeGroup.cs new file mode 100644 index 00000000..9bf1138d --- /dev/null +++ b/IRaCIS.Core.Domain/Management/UserTypeGroup.cs @@ -0,0 +1,40 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-03-16 09:50:51 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using IRaCIS.Core.Domain.Share; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///UserTypeGroup + /// + [Table("UserTypeGroup")] + public class UserTypeGroup : Entity + { + + /// + /// UserTypeId + /// + [Required] + public Guid UserTypeId { get; set; } + + /// + /// DictionaryId + /// + [Required] + public Guid DictionaryId { get; set; } + + + [ForeignKey("DictionaryId")] + public Dictionary Group { get; set; } + + [ForeignKey("UserTypeId")] + public UserType UserType { get; set; } + + } + +} diff --git a/IRaCIS.Core.Domain/Management/UserTypeMenu.cs b/IRaCIS.Core.Domain/Management/UserTypeMenu.cs new file mode 100644 index 00000000..52753db7 --- /dev/null +++ b/IRaCIS.Core.Domain/Management/UserTypeMenu.cs @@ -0,0 +1,19 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; +using IRaCIS.Core.Domain.Share; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("UserTypeMenu")] + public partial class UserTypeMenu : Entity + { + + public Menu Menu { get; set; } + public Guid UserTypeId { get; set; } + public Guid MenuId { get; set; } + + [ForeignKey("UserTypeId")] + public UserType UserType { get; set; } + + } +} diff --git a/IRaCIS.Core.Domain/QC/CheckChallengeDialog.cs b/IRaCIS.Core.Domain/QC/CheckChallengeDialog.cs new file mode 100644 index 00000000..657388d5 --- /dev/null +++ b/IRaCIS.Core.Domain/QC/CheckChallengeDialog.cs @@ -0,0 +1,24 @@ +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Domain.Share; +using System; +using System.Collections.Generic; +using System.Text; + +namespace IRaCIS.Core.Domain.Models +{ + + public class CheckChallengeDialog : Entity, IAuditAddWithUserName + { + public string TalkContent { get; set; } = string.Empty; + + + public Guid SubjectVisitId { get; set; } + + public string CreateUser { get; set; } + + public DateTime CreateTime { get; set; } = DateTime.Now; + public Guid CreateUserId { get; set; } = Guid.Empty; + + public UserTypeEnum UserTypeEnum { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/QC/ClinicalData/PreviousHistory.cs b/IRaCIS.Core.Domain/QC/ClinicalData/PreviousHistory.cs new file mode 100644 index 00000000..b35626de --- /dev/null +++ b/IRaCIS.Core.Domain/QC/ClinicalData/PreviousHistory.cs @@ -0,0 +1,88 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-22 11:15:18 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///PreviousHistory + /// + [Table("PreviousHistory")] + public class PreviousHistory : Entity, IAuditAddWithUserName + { + + public SubjectVisit SubjectVisit { get; set; } + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// CreateUser + /// + [Required] + public string CreateUser { get; set; } + + /// + /// StartTime + /// + public DateTime? StartTime { get; set; } + + /// + /// EndTime + /// + public DateTime? EndTime { get; set; } + + /// + /// IsPD + /// + public int? IsPD { get; set; } + + /// + /// SubjectVisitId + /// + [Required] + public Guid SubjectVisitId { get; set; } + + /// + /// IsSubjectLevel + /// + [Required] + public bool IsSubjectLevel { get; set; } + + /// + /// Path + /// + [Required] + public string Path { get; set; } = String.Empty; + + /// + /// FileName + /// + [Required] + public string FileName { get; set; } = String.Empty; + + /// + /// Position + /// + [Required] + public string Position { get; set; } = String.Empty; + + + //[Required] + //public Guid SubjectId { get; set; } + } + + //移动到DBContext文件中 +} diff --git a/IRaCIS.Core.Domain/QC/ClinicalData/PreviousOther.cs b/IRaCIS.Core.Domain/QC/ClinicalData/PreviousOther.cs new file mode 100644 index 00000000..04e33a0b --- /dev/null +++ b/IRaCIS.Core.Domain/QC/ClinicalData/PreviousOther.cs @@ -0,0 +1,90 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-22 11:15:18 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///PreviousOther + /// + [Table("PreviousOther")] + public class PreviousOther : Entity, IAuditAddWithUserName + { + + + public SubjectVisit SubjectVisit { get; set; } + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// CreateUser + /// + [Required] + public string CreateUser { get; set; } + + /// + /// StartTime + /// + public DateTime? StartTime { get; set; } + + /// + /// EndTime + /// + public DateTime? EndTime { get; set; } + + /// + /// IsPD + /// + [Required] + public bool IsPD { get; set; } + + /// + /// SubjectVisitId + /// + [Required] + public Guid SubjectVisitId { get; set; } + + /// + /// IsSubjectLevel + /// + [Required] + public bool IsSubjectLevel { get; set; } + + /// + /// Path + /// + [Required] + public string Path { get; set; } = String.Empty; + + /// + /// FileName + /// + [Required] + public string FileName { get; set; } = String.Empty; + + /// + /// TreatmentType + /// + [Required] + public string TreatmentType { get; set; } = String.Empty; + + + //public Guid SubjectId { get; set; } + + } + + //移动到DBContext文件中 +} diff --git a/IRaCIS.Core.Domain/QC/ClinicalData/PreviousPDF.cs b/IRaCIS.Core.Domain/QC/ClinicalData/PreviousPDF.cs new file mode 100644 index 00000000..f14f2f07 --- /dev/null +++ b/IRaCIS.Core.Domain/QC/ClinicalData/PreviousPDF.cs @@ -0,0 +1,53 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-09 11:35:31 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///PreviousPDF + /// + [Table("PreviousPDF")] + public class PreviousPDF : Entity, IAuditAdd + { + + public SubjectVisit SubjectVisit { get; set; } + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// SubjectVisitId + /// + [Required] + public Guid SubjectVisitId { get; set; } + + /// + /// Path + /// + [Required] + public string Path { get; set; } + + /// + /// FileName + /// + [Required] + public string FileName { get; set; } + + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + } + + +} diff --git a/IRaCIS.Core.Domain/QC/ClinicalData/PreviousSurgery.cs b/IRaCIS.Core.Domain/QC/ClinicalData/PreviousSurgery.cs new file mode 100644 index 00000000..bc4724be --- /dev/null +++ b/IRaCIS.Core.Domain/QC/ClinicalData/PreviousSurgery.cs @@ -0,0 +1,80 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-22 11:15:18 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///PreviousSurgery + /// + [Table("PreviousSurgery")] + public class PreviousSurgery : Entity, IAuditAddWithUserName + { + + + + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// CreateUser + /// + [Required] + public string CreateUser { get; set; } + + /// + /// OperationTime + /// + public DateTime? OperationTime { get; set; } + + /// + /// SubjectVisitId + /// + [Required] + public Guid SubjectVisitId { get; set; } + + /// + /// IsSubjectLevel + /// + [Required] + public bool IsSubjectLevel { get; set; } + + /// + /// Path + /// + [Required] + public string Path { get; set; } = String.Empty; + + /// + /// FileName + /// + [Required] + public string FileName { get; set; } = String.Empty; + + /// + /// OperationName + /// + [Required] + public string OperationName { get; set; } = String.Empty; + + + //[Required] + //public Guid SubjectId { get; set; } + + } + + +} diff --git a/IRaCIS.Core.Domain/QC/NoneDicom/NoneDicomStudy.cs b/IRaCIS.Core.Domain/QC/NoneDicom/NoneDicomStudy.cs new file mode 100644 index 00000000..f3531cb0 --- /dev/null +++ b/IRaCIS.Core.Domain/QC/NoneDicom/NoneDicomStudy.cs @@ -0,0 +1,105 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-06 10:49:39 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///NoneDicomStudy + /// + [Table("NoneDicomStudy")] + public class NoneDicomStudy : Entity, IAuditUpdate, IAuditAdd + { + public List NoneDicomFileList { get; set; } + + public SubjectVisit SubjectVisit { get; set; } + + public TrialSite TrialSite { get; set; } + + public Subject Subject { get; set; } + + public int Code { get; set; } + + /// + /// TrialId + /// + [Required] + public Guid TrialId { get; set; } + + /// + /// SiteId + /// + [Required] + public Guid SiteId { get; set; } + + /// + /// SubjectId + /// + [Required] + public Guid SubjectId { get; set; } + + /// + /// SubjectVisitId + /// + [Required] + public Guid SubjectVisitId { get; set; } + + /// + /// BodyPart + /// + [Required] + public string BodyPart { get; set; } + + /// + /// Modality + /// + [Required] + public string Modality { get; set; } + + /// + /// ImageDate + /// + [Required] + public DateTime ImageDate { get; set; } + + /// + /// UpdateTime + /// + [Required] + public DateTime UpdateTime { get; set; } + + /// + /// UpdateUserId + /// + [Required] + public Guid UpdateUserId { get; set; } + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + [ForeignKey("CreateUserId")] + public User CreateUser { get; set; } + + + /// + /// Description + /// + public string Description { get; set; } + + } + +} diff --git a/IRaCIS.Core.Domain/QC/NoneDicom/NoneDicomStudyFile.cs b/IRaCIS.Core.Domain/QC/NoneDicom/NoneDicomStudyFile.cs new file mode 100644 index 00000000..03ac1245 --- /dev/null +++ b/IRaCIS.Core.Domain/QC/NoneDicom/NoneDicomStudyFile.cs @@ -0,0 +1,53 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-06 10:49:39 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///NoneDicomStudyFile + /// + [Table("NoneDicomStudyFile")] + public class NoneDicomStudyFile : Entity, IAuditAdd + { + [ForeignKey("NoneDicomStudyId")] + + public NoneDicomStudy NoneDicomStudy { get; set; } + /// + /// NoneDicomStudyId + /// + [Required] + public Guid NoneDicomStudyId { get; set; } + /// + /// Path + /// + [Required] + public string Path { get; set; } + + /// + /// FileName + /// + [Required] + public string FileName { get; set; } + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + + + } + +} diff --git a/IRaCIS.Core.Domain/QC/QANotice.cs b/IRaCIS.Core.Domain/QC/QANotice.cs new file mode 100644 index 00000000..ef784043 --- /dev/null +++ b/IRaCIS.Core.Domain/QC/QANotice.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using IRaCIS.Core.Domain.Share; + +namespace IRaCIS.Core.Domain.Models +{ + public class QANotice : Entity + { + public Guid TrialId { get; set; } + + public Guid SubjectVisitId { get; set; } + + public Guid RelationId { get; set; }=Guid.Empty; + + public string Message { get; set; } + + public string StudyStatusStr { get; set; } = string.Empty; + + public DateTime SendTime { get; set; } + + public DateTime? DealTime { get; set; } + + public Guid FromUserId { get; set; } + + public string FromUser { get; set; } + + public string FromUserType { get; set; } + + public NoticeType NoticeTypeEnum { get; set; } + + public bool NeedDeal { get; set; } + + public virtual ICollection QANoticeUserList { get; set; } + + public QANotice() + { + QANoticeUserList=new HashSet(); + } + + } + + + +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain/QC/QANoticeUser.cs b/IRaCIS.Core.Domain/QC/QANoticeUser.cs new file mode 100644 index 00000000..0248745a --- /dev/null +++ b/IRaCIS.Core.Domain/QC/QANoticeUser.cs @@ -0,0 +1,19 @@ +using System; + +namespace IRaCIS.Core.Domain.Models +{ + public class QANoticeUser:Entity + { + public Guid SubjectVisitId { get; set; } + + public Guid QANoticeId { get; set; } = Guid.Empty; + + public Guid ToUserId { get; set; } = Guid.Empty; + + public string ToUser { get; set; } + + public string ToUserType { get; set; } + + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain/QC/QCChallenge.cs b/IRaCIS.Core.Domain/QC/QCChallenge.cs new file mode 100644 index 00000000..c6f22c66 --- /dev/null +++ b/IRaCIS.Core.Domain/QC/QCChallenge.cs @@ -0,0 +1,98 @@ +using IRaCIS.Core.Domain.Share; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + public class QCChallenge : Entity, IAuditAdd + { + [ForeignKey("CreateUserId")] + public User Creator { get; set; } + + //[ForeignKey("CreateUserId")] + //public User Replyer { get; set; } + + public Guid TrialId { get; set; } + public Guid SubjectVisitId { get; set; } + + public DateTime? DeadlineTime { get; set; } + + public string Note { get; set; } = string.Empty; + + //public bool NeedReUpload { get; set; } = false; + //public bool IsReuploaded { get; set; } = false; + + + public QCChanllengeReuploadEnum ReuploadEnum { get; set; } + + + public DateTime CreateTime { get; set; } = DateTime.Now; + public Guid CreateUserId { get; set; } = Guid.Empty; + + public string CreateUser { get; set; } = string.Empty; + public DateTime? ReUploadedTime { get; set; } + + public string ReUploader { get; set; } = string.Empty; + + public TrialQCProcess QCProcessEnum { get; set; } + + public CurrentQC CurrentQCEnum { get; set; } + + public DateTime? LatestMsgTime { get; set; } + + //public int ChallengeState { get; set; } + + public Guid? LatestReplyUserId { get; set; } + + [ForeignKey("LatestReplyUserId")] + public User LatestReplyUser { get; set; } + public int ChallengeCode { get; set; } + + + public bool IsClosed { get; set; } + + public DateTime? ClosedTime { get; set; } + + public string ClosedUser { get; set; } = string.Empty; + + public QCChallengeCloseEnum CloseResonEnum { get; set;} + + public string Content { get; set; } = string.Empty; + + public string ActionContent { get; set; } = string.Empty; + + public UserTypeEnum UserTypeEnum { get; set; } + + + public string ChallengeType { get; set; } = string.Empty; + + + + //导航属性 + [ForeignKey("SubjectVisitId")] + public SubjectVisit SubjectVisit { get; set; } + + public List DialogList { get; set; } = new List(); + + + + //public Guid QATrialTemplateId { get; set; } + //public QATrialTemplate TrialTemplate { get; set; } + + //public virtual ICollection QaTrialTemplateItemList { get; set; } + + //public virtual ICollection QARecordTemplateItemDetailList { get; set; } + + //public QAQuestion() + //{ + // //存放医生关联 Title、等各种多选项 + // QaTrialTemplateItemList = new HashSet(); + + // QARecordTemplateItemDetailList= new HashSet(); + //} + + + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain/QC/QCChallengeDialog.cs b/IRaCIS.Core.Domain/QC/QCChallengeDialog.cs new file mode 100644 index 00000000..7243019f --- /dev/null +++ b/IRaCIS.Core.Domain/QC/QCChallengeDialog.cs @@ -0,0 +1,25 @@ +using IRaCIS.Core.Domain.Share; +using System; + +namespace IRaCIS.Core.Domain.Models +{ + public class QCChallengeDialog : Entity, IAuditAddWithUserName + { + public QCChallenge QCChallenge { get; set; } + public string TalkContent { get; set; } = string.Empty; + + public Guid QCChallengeId { get; set; } + + public Guid SubjectVisitId { get; set; } + + + //public bool HasReply { get; set; } = false; + + public string CreateUser { get; set; } + + public DateTime CreateTime { get; set; } = DateTime.Now; + public Guid CreateUserId { get; set; } = Guid.Empty; + + public UserTypeEnum UserTypeEnum { get; set; } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain/QC/QCQuestion.cs b/IRaCIS.Core.Domain/QC/QCQuestion.cs new file mode 100644 index 00000000..564465b3 --- /dev/null +++ b/IRaCIS.Core.Domain/QC/QCQuestion.cs @@ -0,0 +1,85 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-11 11:19:10 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///QCQuestionConfigure + /// + [Table("QCQuestion")] + public class QCQuestion : Entity, IAuditUpdate, IAuditAdd + { + /// + /// QuestionName + /// + [Required] + public string QuestionName { get; set; } = string.Empty; + + /// + /// IsRequired + /// + [Required] + public bool IsRequired { get; set; } + + /// + /// IsEnable + /// + [Required] + public bool IsEnable { get; set; } + + /// + /// 下拉框、文本、单选、多选 + /// + [Required] + public string Type { get; set; } = string.Empty; + + /// + /// TypeValue + /// + [Required] + public string TypeValue { get; set; } = string.Empty; + + [ForeignKey("ParentId")] + public QCQuestion ParentQuestion { get; set; } + + public string ParentTriggerValue { get; set; } + public Guid? ParentId { get; set; } + + /// + /// ShowOrder + /// + [Required] + public int ShowOrder { get; set; } + + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// UpdateTime + /// + [Required] + public DateTime UpdateTime { get; set; } + + /// + /// UpdateUserId + /// + [Required] + public Guid UpdateUserId { get; set; } + + } + + +} diff --git a/IRaCIS.Core.Domain/QC/TrialQCQuestion.cs b/IRaCIS.Core.Domain/QC/TrialQCQuestion.cs new file mode 100644 index 00000000..0b934247 --- /dev/null +++ b/IRaCIS.Core.Domain/QC/TrialQCQuestion.cs @@ -0,0 +1,104 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-11 11:19:10 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///TrialQCQuestionConfigure + /// + [Table("TrialQCQuestion")] + public class TrialQCQuestion : Entity, IAuditUpdate, IAuditAdd + { + public Trial Trial { get; set; } + + /// + /// TrialId + /// + [Required] + public Guid TrialId { get; set; } + + /// + /// QuestionName + /// + [Required] + public string QuestionName { get; set; } = string.Empty; + + /// + /// IsRequired + /// + [Required] + public bool IsRequired { get; set; } + + /// + /// IsEnable + /// + [Required] + public bool IsEnable { get; set; } + + /// + /// 下拉框、文本、单选、多选 + /// + [Required] + public string Type { get; set; } = string.Empty; + + + public Guid? ParentId { get; set; } + + [ForeignKey("ParentId")] + public TrialQCQuestion ParentQCQuestion { get; set; } + + /// + /// TypeValue + /// + [Required] + public string TypeValue { get; set; } + + /// + /// ChildInvalidValue + /// + [Required] + public string ParentTriggerValue { get; set; } + + /// + /// ShowOrder + /// + [Required] + public int ShowOrder { get; set; } + + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// UpdateTime + /// + [Required] + public DateTime UpdateTime { get; set; } + + /// + /// UpdateUserId + /// + [Required] + public Guid UpdateUserId { get; set; } + + + public List TrialQCQuestionAnswerList { get; set; } + + + } + + +} diff --git a/IRaCIS.Core.Domain/QC/TrialQCQuestionAnswer.cs b/IRaCIS.Core.Domain/QC/TrialQCQuestionAnswer.cs new file mode 100644 index 00000000..58ec5c1e --- /dev/null +++ b/IRaCIS.Core.Domain/QC/TrialQCQuestionAnswer.cs @@ -0,0 +1,80 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-11 17:01:49 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using IRaCIS.Core.Domain.Share; +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///TrialQCQuestionRecord + /// + [Table("TrialQCQuestionAnswer")] + public class TrialQCQuestionAnswer : Entity, IAuditUpdate, IAuditAdd + { + + + /// + /// TrialId + /// + [Required] + public Guid TrialId { get; set; } + + /// + /// Answer + /// + [Required] + public string Answer { get; set; } + + /// + /// ChildAnswer + /// + //[Required] + //public string ChildAnswer { get; set; } + + + + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// UpdateTime + /// + [Required] + public DateTime UpdateTime { get; set; } + + /// + /// UpdateUserId + /// + [Required] + public Guid UpdateUserId { get; set; } + + public TrialQCProcess QCProcessEnum { get; set; } + + // 1代表第一个人QC数据 2 代表第二个人QC数据 + public CurrentQC CurrentQCEnum { get; set; } + + public Guid SubjectVisitId { get; set; } + + //public string Note { get; set; } + + public Guid TrialQCQuestionConfigureId { get; set; } + + public TrialQCQuestion TrialQCQuestionConfigure { get; set; } + + } + + +} diff --git a/IRaCIS.Core.Domain/Report/GlobalRS.cs b/IRaCIS.Core.Domain/Report/GlobalRS.cs new file mode 100644 index 00000000..a5eb5ca3 --- /dev/null +++ b/IRaCIS.Core.Domain/Report/GlobalRS.cs @@ -0,0 +1,15 @@ +using System; + +namespace IRaCIS.Core.Domain.Models +{ + public class GlobalRS : Entity + { + public Guid GlobalId { get; set; } + public string TpCode { get; set; } + public decimal VisitNum { get; set; } + public bool Agree { get; set; } + public string NewRS { get; set; } + public string Note { get; set; } + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain/Report/GlobalResult.cs b/IRaCIS.Core.Domain/Report/GlobalResult.cs new file mode 100644 index 00000000..1e6a2d7d --- /dev/null +++ b/IRaCIS.Core.Domain/Report/GlobalResult.cs @@ -0,0 +1,16 @@ +using System; + +namespace IRaCIS.Core.Domain.Models +{ + public class GlobalResult:Entity + { + public Guid GlobalId { get; set; } + public Guid SubjectId { get; set; } + public string SubjectCode { get; set; } + + public decimal VisitNum { get; set; } + public string SubjectNote { get; set; }=String.Empty; + public string Result { get; set; } = String.Empty; + + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain/Report/Report.cs b/IRaCIS.Core.Domain/Report/Report.cs new file mode 100644 index 00000000..8e59d146 --- /dev/null +++ b/IRaCIS.Core.Domain/Report/Report.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("Report")] + public class Report : Entity + { + public bool Qualified { get; set; } = true; + public string DiseaseProgression { get; set; } = string.Empty; + public string NotEvaluable { get; set; } = string.Empty; + public string Timepoint { get; set; } = string.Empty; + public string TrialCode { get; set; } = string.Empty; + public string SubjectCode { get; set; } = string.Empty; + public decimal? VisitNum { get; set; } + public string VisitName { get; set; } = string.Empty; + public int? DiseaseSituation { get; set; } + public string Comment { get; set; } + public Guid? TPId { get; set; } + public Guid? DicomStudyId { get; set; } + public string TpCode { get; set; } = string.Empty; + public bool AffectRead { get; set; } = false;//是否影响读片 + public string AffectReadNote { get; set; } = string.Empty;//备注说明 + } +} diff --git a/IRaCIS.Core.Domain/Report/TU_TR_RS.cs b/IRaCIS.Core.Domain/Report/TU_TR_RS.cs new file mode 100644 index 00000000..f0746125 --- /dev/null +++ b/IRaCIS.Core.Domain/Report/TU_TR_RS.cs @@ -0,0 +1,129 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("TU")] + public class TU : Entity + { + //病灶类型 1-非淋巴结靶病灶,2-淋巴结靶病灶,3-非靶病灶,,4-疑似新病灶,5-明确新病灶 + public int LesionType { get; set; } + public string STUDYID { get; set; } + public string DOMAIN { get; set; } = "TU"; + public string USUBJID { get; set; } + public int TUSEQ { get; set; } + public string TUGRPID { get; set; } = string.Empty; + public string TUREFID { get; set; } = string.Empty;//内部或外部的肿瘤/病灶标识。 例如:医学影像 ID + public string TUSPID { get; set; } = string.Empty; + public string TULNKID { get; set; } = string.Empty; + public string TUTESTCD { get; set; } = string.Empty; + public string TUTEST { get; set; } = string.Empty; + + //肿瘤标识的结果。 肿瘤标识的结果是对标识肿瘤的分类。 + //例如,当 TUTESTCD=TUMIDENT (肿瘤/病灶标识)时, + //TUORRES的值可能是 TARGET, NON-TARGET, NEW或者 BENIGN ABNORMALITY + public string TUORRES { get; set; } = string.Empty; + public string TUSTRESC { get; set; } = string.Empty; + public string TUNAM { get; set; } = string.Empty; + public string TULOC { get; set; } = string.Empty; + public string TULAT { get; set; } = string.Empty; + public string TUDIR { get; set; } = string.Empty; + public string TUPORTOT { get; set; } = string.Empty; + public string TUMETHOD { get; set; } = string.Empty; + public string TUEVAL { get; set; } = string.Empty; + public string TUEVALID { get; set; } = string.Empty; + public string TUACPTFL { get; set; } = string.Empty; + public decimal VISITNUM { get; set; } = 0; + public string VISIT { get; set; } = string.Empty; + public int VISITDY { get; set; } + public string EPOCH { get; set; } = string.Empty; + public string TUDTC { get; set; } = string.Empty; + public int TUDY { get; set; } = 0; + public string LocDescription { get; set; } = string.Empty; + + public string TpCode { get; set; } = string.Empty; + } + + [Table("TR")] + public class TR : Entity + { + public string STUDYID { get; set; } = string.Empty; + public string DOMAIN { get; set; } = "TR"; + public string USUBJID { get; set; } = string.Empty; + public int TRSEQ { get; set; } + public string TRGRPID { get; set; } = string.Empty; + public string TRREFID { get; set; } = string.Empty; + public string TRSPID { get; set; } = string.Empty; + public string TRLNKID { get; set; } = string.Empty; + public string TRLNKGRP { get; set; } = string.Empty; + public string TRTESTCD { get; set; } = string.Empty; + public string TRTEST { get; set; } = string.Empty; + public string TRORRES { get; set; } = string.Empty; + public double TRORRES_Double + { + get { + double temp = 0; + double.TryParse(TRORRES, out temp); + return temp; + } + } + public string TRORRESU { get; set; } = string.Empty; + public string TRSTRESC { get; set; } = string.Empty; + public double TRSTRESN { get; set; } = 0; + public string TRSTRESU { get; set; } = string.Empty; + public string TRSTAT { get; set; } = string.Empty; + public string TRREASND { get; set; } = string.Empty; + public string TRNAM { get; set; } = string.Empty; + public string TRMETHOD { get; set; } = string.Empty; + public string TREVAL { get; set; } = string.Empty; + public string TREVALID { get; set; } = string.Empty; + public string TRACPTFL { get; set; } = string.Empty; + public decimal VISITNUM { get; set; } = 0; + public string VISIT { get; set; } = string.Empty; + public int VISITDY { get; set; } = 0; + public string EPOCH { get; set; } = string.Empty; + public string TRDTC { get; set; } = string.Empty; + public int TRDY { get; set; } = 0; + public string Note { get; set; } // 备注 + public bool CoveredLesion { get; set; } = true;//本次扫描是否覆盖该病灶 + public string TpCode { get; set; } = string.Empty; + } + + [Table("RS")] + public class RS : Entity + { + public string STUDYID { get; set; } = string.Empty; + public string DOMAIN { get; set; } = "RS"; + public string USUBJID { get; set; } = string.Empty; + public int RSSEQ { get; set; } + public string RSGRPID { get; set; } = string.Empty; + public string RSREFID { get; set; } = string.Empty; + public string RSSPID { get; set; } = string.Empty; + public string RSLNKID { get; set; } = string.Empty; + public string RSLNKGRP { get; set; } = string.Empty; + public string RSTESTCD { get; set; } = string.Empty; + public string RSTEST { get; set; } = string.Empty; + public string RSCAT { get; set; } = string.Empty; + public string RSORRES { get; set; } = string.Empty; + public string RSSTRESC { get; set; } = string.Empty; + public string RSSTAT { get; set; } = string.Empty; + public string RSREASND { get; set; } = string.Empty; + public string RSNAM { get; set; } = string.Empty; + + public string RSEVAL { get; set; } = string.Empty; + public string RSEVALID { get; set; } = string.Empty; + public string RSACPTFL { get; set; } = string.Empty; + public decimal VISITNUM { get; set; } = 0; + public string VISIT { get; set; } = string.Empty; + public int VISITDY { get; set; } = 0; + public string EPOCH { get; set; } = string.Empty; + public string RSDTC { get; set; } = string.Empty; + public int RSDY { get; set; } = 0; + public string TpCode { get; set; } = string.Empty; + public Guid StudyGuid { get; set; } = Guid.Empty; + public Guid TrialGuid { get; set; } = Guid.Empty; + public Guid SubjectGuid { get; set; } = Guid.Empty; + public string Note { get; set; } = string.Empty; + + } +} diff --git a/IRaCIS.Core.Domain/SiteSurvey/TrialSiteEquipmentSurvey.cs b/IRaCIS.Core.Domain/SiteSurvey/TrialSiteEquipmentSurvey.cs new file mode 100644 index 00000000..b18094e5 --- /dev/null +++ b/IRaCIS.Core.Domain/SiteSurvey/TrialSiteEquipmentSurvey.cs @@ -0,0 +1,85 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-23 13:16:57 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using IRaCIS.Core.Domain.Share; +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///TrialSiteEquipmentSurvey + /// + [Table("TrialSiteEquipmentSurvey")] + public class TrialSiteEquipmentSurvey : Entity, IAuditUpdate, IAuditAdd + { + + + + [ForeignKey("TrialSiteSurveyId")] + public TrialSiteSurvey TrialSiteSurvey { get; set; } + + public Guid TrialSiteSurveyId { get; set; } + + + [ForeignKey("EquipmentTypeId")] + public Dictionary EquipmentType { get; set; } + + public Guid EquipmentTypeId { get; set; } + + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + /// + /// UpdateTime + /// + [Required] + public DateTime UpdateTime { get; set; } + + /// + /// UpdateUserId + /// + [Required] + public Guid UpdateUserId { get; set; } + + /// + /// Parameters + /// + [Required] + public string Parameters { get; set; } = string.Empty; + + /// + /// ManufacturerName + /// + [Required] + public string ManufacturerName { get; set; } = string.Empty; + + /// + /// ScannerType + /// + [Required] + public string ScannerType { get; set; } = string.Empty; + + /// + /// Note + /// + [Required] + public string Note { get; set; } = string.Empty; + + + + } + +} diff --git a/IRaCIS.Core.Domain/SiteSurvey/TrialSiteSurvey.cs b/IRaCIS.Core.Domain/SiteSurvey/TrialSiteSurvey.cs new file mode 100644 index 00000000..059716d5 --- /dev/null +++ b/IRaCIS.Core.Domain/SiteSurvey/TrialSiteSurvey.cs @@ -0,0 +1,129 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-23 13:16:57 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using IRaCIS.Core.Domain.Share; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///TrialSiteSurvey + /// + [Table("TrialSiteSurvey")] + public class TrialSiteSurvey : Entity, IAuditUpdate, IAuditAdd + { + //public bool IsLocked { get; set; }=false; + public TrialSiteSurveyEnum State { get; set; } = TrialSiteSurveyEnum.ToSubmit; + + + // 必须 { get; set; } 否则 翻译出错 + public List TrialSiteUserSurveyList { get; set; } =new List(); + + public List TrialSiteEquipmentSurveyList { get; set; } = new List(); + + + public TrialSite TrialSite { get; set; } + + public Trial Trial { get; set; } + + public Site Site { get; set; } + + public bool IsAbandon { get; set; } + + /// + /// TrialId + /// + [Required] + public Guid TrialId { get; set; } + + /// + /// SiteId + /// + [Required] + public Guid SiteId { get; set; } + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + /// + /// UpdateTime + /// + [Required] + public DateTime UpdateTime { get; set; } + + /// + /// UpdateUserId + /// + [Required] + public Guid UpdateUserId { get; set; } + + /// + /// UserName + /// + [Required] + public string UserName { get; set; } = string.Empty; + + /// + /// Phone + /// + [Required] + public string Phone { get; set; } = string.Empty; + + /// + /// Email + /// + [Required] + public string Email { get; set; } = string.Empty; + + /// + /// AverageEngravingCycle + /// + [Required] + public int AverageEngravingCycle { get; set; } + + /// + /// IsConfirmImagingTechnologist + /// + [Required] + public bool IsConfirmImagingTechnologist { get; set; } + + /// + /// NotConfirmReson + /// + [Required] + public string NotConfirmReson { get; set; } = string.Empty; + + /// + /// EfficacyEvaluatorType + /// + [Required] + public int EfficacyEvaluatorType { get; set; } + + /// + /// IsFollowStudyParameters + /// + [Required] + public bool IsFollowStudyParameters { get; set; } + + /// + /// NotFollowReson + /// + [Required] + public string NotFollowReson { get; set; } = string.Empty; + + } + +} diff --git a/IRaCIS.Core.Domain/SiteSurvey/TrialSiteUserSurvey.cs b/IRaCIS.Core.Domain/SiteSurvey/TrialSiteUserSurvey.cs new file mode 100644 index 00000000..b8b9ae46 --- /dev/null +++ b/IRaCIS.Core.Domain/SiteSurvey/TrialSiteUserSurvey.cs @@ -0,0 +1,124 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-23 13:16:57 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using IRaCIS.Core.Domain.Share; +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///TrialSiteUserSurvey + /// + [Table("TrialSiteUserSurvey")] + public class TrialSiteUserSurvey : Entity, IAuditUpdate, IAuditAdd + { + [ForeignKey("TrialSiteSurveyId")] + public TrialSiteSurvey TrialSiteSurvey { get; set; } + + public Guid TrialSiteSurveyId { get; set; } + + + //public TrialSiteSurveyEnum State { get; set; } = TrialSiteSurveyEnum.ToSubmit; + + public bool IsGenerateSuccess { get; set; } + + + [ForeignKey("UserTypeId")] + public UserType UserTypeRole { get; set; } + + public Guid? UserTypeId { get; set; } + + + public Guid? TrialRoleNameId { get; set; } + + public Dictionary TrialRoleName { get; set; } + + + + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + /// + /// UpdateTime + /// + [Required] + public DateTime UpdateTime { get; set; } + + /// + /// UpdateUserId + /// + [Required] + public Guid UpdateUserId { get; set; } + + public string UserName { get; set; } = string.Empty; + + /// + /// Phone + /// + [Required] + public string Phone { get; set; } = string.Empty; + + /// + /// Email + /// + [Required] + public string Email { get; set; } = string.Empty; + + /// + /// IsCorrect + /// + [Required] + public bool IsCorrect { get; set; } + + /// + /// IsGenerateAccount + /// + [Required] + public bool IsGenerateAccount { get; set; } + + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + + + + public string OrganizationName { get; set; } = string.Empty; + + public Guid? SystemUserId { get; set; } + + + public DateTime? ExpireTime { get; set; } + + public bool? IsJoin { get; set; } + + public DateTime? ConfirmTime { get; set; } + + public string RejectReason { get; set; } = string.Empty; + + + + + //public Guid GenerateUserId { get; set; } + + //public Guid TrialId { get; set; } + /// + /// IsDisable + /// + //[Required] + //public bool IsAddTrial { get; set; } + + } + +} diff --git a/IRaCIS.Core.Domain/Trial/DataInspection.cs b/IRaCIS.Core.Domain/Trial/DataInspection.cs new file mode 100644 index 00000000..b1254fcb --- /dev/null +++ b/IRaCIS.Core.Domain/Trial/DataInspection.cs @@ -0,0 +1,64 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-03-21 17:13:43 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using IRaCIS.Core.Domain.Share; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///DataInspection + /// + [Table("DataInspection")] + public class DataInspection : Entity, IAuditAdd + { + + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + + + [Required] + public int ModuleEnum { get; set; } + + + public string BlindName { get; set; } = string.Empty; + + + [Required] + public Guid TrialId { get; set; } + + + [Required] + public Guid SiteId { get; set; } + + + [Required] + public Guid SubjectId { get; set; } + + [Required] + public Guid SubjectVisitId { get; set; } + + + [Required] + public int OptEnum { get; set; } + + public string IP { get; set; } = string.Empty; + + + [Required] + public string Reason { get; set; } = string.Empty; + + + public bool IsSign { get; set; } + + + public Guid? SignId { get; set; } + + public string JsonDetail { get; set; } = string.Empty; + + } + +} diff --git a/IRaCIS.Core.Domain/Trial/Enroll.cs b/IRaCIS.Core.Domain/Trial/Enroll.cs new file mode 100644 index 00000000..8782f95e --- /dev/null +++ b/IRaCIS.Core.Domain/Trial/Enroll.cs @@ -0,0 +1,57 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("Enroll")] + public partial class Enroll : Entity,IAuditUpdate,IAuditAdd + { + + public virtual Trial Trial { get; set; } + + + public virtual Doctor Doctor { get; set; } + + //public TrialPaymentPrice TrialPaymentPrice { get; set; } + + public Guid DoctorId { get; set; } + public Guid TrialId { get; set; } + public Guid AttachmentId { get; set; } = Guid.Empty; + public int EnrollStatus { get; set; } + + public decimal? AdjustmentMultiple { get; set; } + public DateTime? EnrollTime { get; set; } + public DateTime? OutEnrollTime { get; set; } + + public string Memo { get; set; } = string.Empty; + + public int ReviewerReadingType { get; set; } = 0; + + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } = DateTime.Now; + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } + + public int? Training { get; set; } + + public int? RefresherTraining { get; set; } + + public int? Timepoint { get; set; } + + public int? Timepoint48H { get; set; } + + public int? Timepoint24H { get; set; } + + public int? Adjudication { get; set; } + + public int? Adjudication48H { get; set; } + + public int? Adjudication24H { get; set; } + + public int? Global { get; set; } + + + public int? Downtime { get; set; } + + } +} diff --git a/IRaCIS.Core.Domain/Trial/EnrollDetail.cs b/IRaCIS.Core.Domain/Trial/EnrollDetail.cs new file mode 100644 index 00000000..f866242c --- /dev/null +++ b/IRaCIS.Core.Domain/Trial/EnrollDetail.cs @@ -0,0 +1,28 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("EnrollDetail")] + public partial class EnrollDetail : Entity, IAuditAdd + { + public virtual TrialStatusDetail TrialDetail { get; set; } + + public Guid DoctorId { get; set; } + public Guid TrialId { get; set; } + public int EnrollStatus { get; set; } + public Guid? EnrollId { get; set; } + public string Memo { get; set; } = string.Empty; + public Guid CreateUserId { get; set; } + public int OptUserType { get; set; } + public DateTime CreateTime { get; set; } + + [ForeignKey("TrialDetail")] + public Guid TrialDetailId { get; set; } + + + [ForeignKey("CreateUserId")] + public User CreateUser { get; set; } + public Doctor Doctor { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Trial/Trial.cs b/IRaCIS.Core.Domain/Trial/Trial.cs new file mode 100644 index 00000000..ad39e4b5 --- /dev/null +++ b/IRaCIS.Core.Domain/Trial/Trial.cs @@ -0,0 +1,317 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using IRaCIS.Core.Domain.Share; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("Trial")] + public partial class Trial : Entity, IAuditUpdate, IAuditAdd,ISoftDelete + { + public Trial() + { + ClinicalTrialProjectDetails = new HashSet(); + TrialDicList = new List(); + } + + public List TrialDocumentList { get; set; } + public List EnrollList { get; set; } + public List WorkloadList { get; set; } + public List TrialUserList { get; set; } = new List(); + + public List SubjectList { get; set; } = new List(); + + public List StudyList { get; set; } = new List(); + + public List TrialSiteList { get; set; } = new List(); + + public List TrialSiteUserList { get; set; } = new List(); + + [ForeignKey("DeclarationTypeId")] + public Dictionary DeclarationType { get; set; } + + public Guid DeclarationTypeId { get; set; } = Guid.Empty; + + public Guid IndicationTypeId { get; set; } = Guid.Empty; + public Guid? PhaseId { get; set; } = Guid.Empty; + + + [ForeignKey("IndicationTypeId")] + public Dictionary IndicationType { get; set; } + + + [ForeignKey("PhaseId")] + public Dictionary Phase { get; set; } + + + [ForeignKey("SponsorId")] + public Sponsor Sponsor { get; set; } + + [ForeignKey("CROId")] + public CRO CRO { get; set; } + + [ForeignKey("ReviewModeId")] + public Dictionary ReviewMode { get; set; } + + + + + public virtual ICollection ClinicalTrialProjectDetails { get; set; } + public virtual ICollection TrialDicList { get; set; } + + [StringLength(100)] + public string TrialCode { get; set; } = string.Empty; + + public int Code { get; set; } + + [StringLength(512)] + public string Indication { get; set; } = string.Empty; + + //һ״̬ + [ConcurrencyCheck] + public int TrialEnrollStatus { get; set; } + + + //״̬ + [StringLength(500)] + public string TrialStatusStr { get; set; } = StaticData.TrialInitializing; + + + public List TrialStateChangeList { get; set; } = new List(); + + + + public Guid? CROId { get; set; } = Guid.Empty; + + public Guid? SponsorId { get; set; } = Guid.Empty; + public Guid? ReviewModeId { get; set; } = Guid.Empty; + + + [StringLength(500)] + public string TurnaroundTime { get; set; } = string.Empty; + + + public int ExpectedPatients { get; set; } + + public decimal TimePointsPerPatient { get; set; } + + public int GRRReviewers { get; set; } + + public int TotalReviewers { get; set; } + + [StringLength(500)] + public string ReviewProtocol { get; set; } = string.Empty; + + [StringLength(500)] + public string MessageFromClient { get; set; } = string.Empty; + + public string Note { get; set; } = string.Empty; + + + public string ReviewProtocolName { get; set; } = string.Empty; + public string MessageFromClientName { get; set; } = string.Empty; + public int Expedited { get; set; } + + public DateTime CreateTime { get; set; } + + public Guid CreateUserId { get; set; } + + public DateTime UpdateTime { get; set; } + + public Guid UpdateUserId { get; set; } + public int AttendedReviewerType { get; set; } = 0;//0ȫйҽ 1ҽ 2йҽҲҽ + + public bool VisitPlanConfirmed { get; set; } + + + + + /// + /// ߱ž + /// + public string SubjectCodeRule { get; set; } + /// + /// Ƿ ߱Ź + /// + public bool IsNoticeSubjectCodeRule { get; set; } = true; + + /// + /// Ƿ л׼ʱ䣨״θҩʱ䣩 + /// + public bool IsHaveFirstGiveMedicineDate { get; set; } = true; + + /// + /// Ƿ + /// + public bool IsHaveSubjectAge { get; set; } = false; + + + /// + /// Ƿ ȷ + /// + public bool IsEnrollementQualificationConfirm { get; set; } = false; + + + /// + /// ƻ + /// + public string OutEnrollmentVisitName { get; set; } = "EOT"; + + + + /// + /// Ƿ ֤Ƭ + /// + public bool IsVerifyVisitImageDate { get; set; } = false; + + + /// + /// ٴϢ 1ϵͳ¼ 2ϵͳ¼+PDF 0 + /// + public int ClinicalInformationTransmissionEnum { get; set; } = 1; + + /// + /// Ƿ ٴϢ + /// + public bool IsCRAAuditClinicalInformation { get; set; } = false; + + /// + /// QC 0 1 2˫ + /// + public TrialQCProcess QCProcessEnum { get; set; } = TrialQCProcess.DoubleAudit; + + /// + /// ӰһԺ˲ + /// + public bool IsImageConsistencyVerification { get; set; } = true; + + /// + /// Ӱ񵼳 + /// + public bool IsImageExport { get; set; } = false; + + + public bool IsSubjectSecondCodeView { get; set; } + + + /// + /// 1 Mint2 PACS + /// + + public int ImagePlatform { get; set; } = 1; + + //Ƭʽ + public int ReadingMode { get; set; } = 1; + + //Ƭ + public int ReadingType { get; set; } = 2; + + + public bool IsGlobalReading { get; set; } = true; + + public bool? IsArbitrationReading { get; set; } = true; + + public bool IsClinicalReading { get; set; } + + + public int ArbitrationRule { get; set; } = 2; + + + public int ChangeDefalutDays { get; set; } = 5; + + + /// + /// Ŀ + /// + public bool IsImageReplicationAcrossTrial { get; set; } = false; + + + + + public string BodyPartTypes { get; set; } = "ʲ|Բ||ز|/¸|ǻ|ȫ|"; + + + + public string Modalitys { get; set; } = "CT|MR|BoneScan|Photograph|PET|X-ray|US"; + + + public string PreliminaryAuditReuploadText { get; set; } = string.Empty; + + public string ReviewAuditReuploadText { get; set; } = string.Empty; + + + + //PD չǷʾ + public bool IsPDProgressView { get; set; } + + //о + public string ResearchProgramNo { get; set; } + + //ʵ + public string ExperimentName { get; set; } + + //еλ + public string MainResearchUnit { get; set; } + + // PI + public string HeadPI { get; set; } + + public bool IsUrgent { get; set; } + + /// + /// Ŀ 1 ʽĿ0 ʽĿ + /// time + public int TrialType { get; set; } + + //public string TempCode { get; set; } + + + public int PlanSiteCount { get; set; } + + public int PlanVisitCount { get; set; } + + + + public DateTime? TrialFinishedTime { get; set; } + + public bool IsSubjectSexView { get; set; } = false; + + public bool IsSubjectExpeditedView { get; set; } = false; + + + public bool IsTrialStart { get; set; } = false; + public bool IsDeleted { get; set; } + + + //QC + + public User QCQuestionConfirmedUser { get; set; } + public Guid? QCQuestionConfirmedUserId { get; set; } + public DateTime? QCQuestionConfirmedTime { get; set; } + + + public int? DigitPlaces { get; set; } = 2; + + + public bool IsTrialProcessConfirmed { get; set; } + public bool IsTrialBasicLogicConfirmed { get; set; } + public bool IsTrialUrgentConfirmed { get; set; } + + public bool IsQCQuestionConfirmed { get; set; } + + + + + //public Guid? ReviewTypeId { get; set; } = Guid.Empty; + + //[ForeignKey("ReviewTypeId")] + //public Dictionary ReviewType { get; set; } + + + //public Guid? QCSecondConfirmedUserId { get; set; } + //public DateTime? QCSecondConfirmedTime { get; set; } + + //public int QCQuestionConfirmState { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Trial/TrialDictionary.cs b/IRaCIS.Core.Domain/Trial/TrialDictionary.cs new file mode 100644 index 00000000..02df547c --- /dev/null +++ b/IRaCIS.Core.Domain/Trial/TrialDictionary.cs @@ -0,0 +1,22 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("TrialDictionary")] + public partial class TrialDictionary : Entity + { + //public Guid Id { get; set; } + + public virtual Trial Trial { get; set; } + public virtual Dictionary Dictionary { get; set; } + + [StringLength(50)] + public string KeyName { get; set; } = string.Empty; + + public Guid TrialId { get; set; } + + public Guid DictionaryId { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Trial/TrialSign.cs b/IRaCIS.Core.Domain/Trial/TrialSign.cs new file mode 100644 index 00000000..e318a5e9 --- /dev/null +++ b/IRaCIS.Core.Domain/Trial/TrialSign.cs @@ -0,0 +1,56 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-02-16 16:23:57 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using IRaCIS.Core.Domain.Share; +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///TrialSign + /// + [Table("TrialSign")] + public class TrialSign : Entity, IAuditAdd + { + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + /// + /// TrialId + /// + [Required] + public Guid TrialId { get; set; } + + /// + /// SubjectVisitId + /// + [Required] + public Guid SubjectVisitId { get; set; } + + + //关联基础数据 + public Guid SignCodeId { get; set; } + + public string SignCode { get; set; } + + public string SignText { get; set; } + + public bool IsCompleted { get; set; } + + } + + +} diff --git a/IRaCIS.Core.Domain/Trial/TrialStateChange.cs b/IRaCIS.Core.Domain/Trial/TrialStateChange.cs new file mode 100644 index 00000000..1bc58b74 --- /dev/null +++ b/IRaCIS.Core.Domain/Trial/TrialStateChange.cs @@ -0,0 +1,42 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-02-25 14:21:48 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///TrialStateChange + /// + [Table("TrialStateChange")] + public class TrialStateChange : Entity, IAuditAdd + { + [ForeignKey("TrialId")] + public Trial Trial { get; set; } + + public Guid TrialId { get; set; } + + + public string OriginState { get; set; } = String.Empty; + + + public string NowState { get; set; } = String.Empty; + + + public string Reason { get; set; }=String.Empty; + + + public DateTime CreateTime { get; set; } + + + public Guid CreateUserId { get; set; } + + + [ForeignKey("CreateUserId")] + public User User { get; set; } + + } + +} diff --git a/IRaCIS.Core.Domain/Trial/TrialStatusDetail.cs b/IRaCIS.Core.Domain/Trial/TrialStatusDetail.cs new file mode 100644 index 00000000..49aca071 --- /dev/null +++ b/IRaCIS.Core.Domain/Trial/TrialStatusDetail.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("TrialStatus")] + public partial class TrialStatusDetail : Entity, IAuditAdd + { + public virtual ICollection IntoGroupDetails { get; set; } + public TrialStatusDetail() + { + IntoGroupDetails = new HashSet(); + } + + public Trial Trial { get; set; } + + [ForeignKey("Trial")] + public Guid TrialId { get; set; } + + + + public int TrialStatus { get; set; } + + [StringLength(100)] + public string Memo { get; set; } = string.Empty; + + public int OptUserType { get; set; } + + public Guid CreateUserId { get; set; } + + public DateTime CreateTime { get; set; } + + + } +} diff --git a/IRaCIS.Core.Domain/Trial/WorkloadDistribution.cs b/IRaCIS.Core.Domain/Trial/WorkloadDistribution.cs new file mode 100644 index 00000000..d5972cc0 --- /dev/null +++ b/IRaCIS.Core.Domain/Trial/WorkloadDistribution.cs @@ -0,0 +1,67 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("WorkloadTP")] + public class WorkloadTP : Entity + { + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public Guid SubjectId { get; set; } + public Guid SubjectVisitId { get; set; } + public Guid StudyId { get; set; } + public string TimepointCode { get; set; } + public Guid ReviewerId { get; set; } + public int Status { get; set; } + public DateTime UpdateTime { get; set; } + } + + [Table("WorkloadGlobal")] + public class WorkloadGlobal : Entity + { + public Guid SiteId { get; set; } + public Guid VisitId { get; set; } + public string VisitName { get; set; } + + + // 项目Id,受试者Id,num 共同决定 Global 关联的所有study, + // 暂定设计成这样,后期如有需要,爱用中间表 关联。 + public Guid TrialId { get; set; } + public Guid SubjectId { get; set; } + public decimal VisitNum { get; set; } + + public string GlobalCode { get; set; } + public Guid ReviewerId { get; set; } + public int Status { get; set; } + public DateTime UpdateTime { get; set; } + } + + [Table("WorkloadAD")] + public class WorkloadAD : Entity + { + public Guid TrialId { get; set; } + public Guid SiteId { get; set; } + public Guid SubjectId { get; set; } + public string ADCode { get; set; } + public Guid ReviewerId { get; set; } + public int Status { get; set; } + public DateTime UpdateTime { get; set; } + public Guid Global1Id { get; set; } + public Guid Global2Id { get; set; } + + public Guid? SelectGlobalId { get; set; } + + public string AdNote { get; set; } + } + + [Table("WorkloadDetail")] + public class WorkloadDetail : Entity + { + public Guid WorkloadId { get; set; } + public string OptUserName { get; set; } + public DateTime OptTime { get; set; } = DateTime.Now; + public int Status { get; set; } + public Guid ReviewerId { get; set; } = Guid.Empty; + } +} diff --git a/IRaCIS.Core.Domain/TrialSiteUser/TrialAttachment.cs b/IRaCIS.Core.Domain/TrialSiteUser/TrialAttachment.cs new file mode 100644 index 00000000..9cbae133 --- /dev/null +++ b/IRaCIS.Core.Domain/TrialSiteUser/TrialAttachment.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("TrialAttachment")] + public class TrialAttachment : Entity, IAuditUpdate, IAuditAdd + { + public Guid TrialId { get; set; } + public string Type { get; set; } = String.Empty; + public string DocumentName { get; set; } = String.Empty; + public string DocumentPath { get; set; } = String.Empty; + + public string UserTypes { get; set; } = String.Empty; + + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid UpdateUserId { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/TrialSiteUser/TrialAudit.cs b/IRaCIS.Core.Domain/TrialSiteUser/TrialAudit.cs new file mode 100644 index 00000000..cba781e6 --- /dev/null +++ b/IRaCIS.Core.Domain/TrialSiteUser/TrialAudit.cs @@ -0,0 +1,29 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + public class TrialAudit:Entity + { + public int AuditType { get; set; } + + public Guid TrialId { get; set; } + + public Guid StudyId { get; set; } = Guid.Empty; + + public Guid? SubjectId { get; set; } + + public Guid OptUserId { get; set; } + + public string OptUser { get; set; } + + public DateTime OptTime { get; set; }=DateTime.Now; + public string Note { get; set; } + + public string Detail { get; set; } + + [ForeignKey("SubjectId")] + public Subject Subject { get; set; } + public Trial Trial { get; set; } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain/TrialSiteUser/TrialExternalUser.cs b/IRaCIS.Core.Domain/TrialSiteUser/TrialExternalUser.cs new file mode 100644 index 00000000..df9f172f --- /dev/null +++ b/IRaCIS.Core.Domain/TrialSiteUser/TrialExternalUser.cs @@ -0,0 +1,112 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-03-04 13:33:37 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using IRaCIS.Core.Domain.Share; +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///TrialExternalUser + /// + [Table("TrialExternalUser")] + public class TrialExternalUser : Entity, IAuditUpdate, IAuditAdd + { + + public Trial Trial { get; set; } + + /// + /// TrialId + /// + [Required] + public Guid TrialId { get; set; } + + + public Guid UserTypeId { get; set; } + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + /// + /// UpdateTime + /// + [Required] + public DateTime UpdateTime { get; set; } + + /// + /// UpdateUserId + /// + [Required] + public Guid UpdateUserId { get; set; } + + /// + /// Phone + /// + [Required] + public string Phone { get; set; } = String.Empty; + + /// + /// Email + /// + [Required] + public string Email { get; set; } + + /// + /// FirstName + /// + [Required] + public string FirstName { get; set; } + + /// + /// LastName + /// + [Required] + public string LastName { get; set; } + + /// + /// 邀请状态 + /// + [Required] + public TrialExternalUserStateEnum InviteState { get; set; } = TrialExternalUserStateEnum.WaitSent; + + ///// + ///// 是否存在系统用户表中 + ///// + //[Required] + //public bool IsExist { get; set; } + + + public string OrganizationName { get; set; }=String.Empty; + + + public bool IsSystemUser{ get; set; } + + + public Guid SystemUserId { get; set; } + + + + public DateTime? ExpireTime { get; set; } + + + public bool? IsJoin { get; set; } + + public DateTime? ConfirmTime { get; set; } + + public string RejectReason { get; set; } + + } + +} diff --git a/IRaCIS.Core.Domain/TrialSiteUser/TrialSite.cs b/IRaCIS.Core.Domain/TrialSiteUser/TrialSite.cs new file mode 100644 index 00000000..49dab166 --- /dev/null +++ b/IRaCIS.Core.Domain/TrialSiteUser/TrialSite.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + public class TrialSite : Entity, IAuditAdd, IAuditUpdate,ISoftDelete + { + + public Guid TrialId { get; set; } + + public Guid SiteId { get; set; } + + public string TrialSiteCode { get; set; } = String.Empty; + + public string TrialSiteAliasName { get; set; }=String.Empty; + + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + + public bool IsDeleted { get; set; } + + + //导航属性 + [ForeignKey("SiteId")] + public Site Site { get; set; } + + [ForeignKey("TrialId")] + public Trial Trial { get; set; } + + + + /// + /// Site 下面有多个访视记录 + /// + public List SubjectVisitList { get; set; } + + public List TrialSiteSurveyList { get; set; } + + + //Site 由多个人负责 + public List CRCUserList { get; set; } + + + public List SubjectList { get; set; } + + public List StudyList { get; set; } + + public List NoneDicomStudyList { get; set; } + + + public List StudyMonitorList { get; set; } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.Domain/TrialSiteUser/TrialSiteUser.cs b/IRaCIS.Core.Domain/TrialSiteUser/TrialSiteUser.cs new file mode 100644 index 00000000..ac0f8e2d --- /dev/null +++ b/IRaCIS.Core.Domain/TrialSiteUser/TrialSiteUser.cs @@ -0,0 +1,88 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-11-23 15:40:27 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using IRaCIS.Core.Domain.Share; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///UserTrialSite + /// + [Table("TrialUserSite")] + public class TrialSiteUser : Entity, IAuditUpdate, IAuditAdd,ISoftDelete + { + + [Required] + public Guid UserId { get; set; } + + + [Required] + public Guid TrialId { get; set; } + + + [Required] + public Guid SiteId { get; set; } + + public DateTime UpdateTime { get; set; } + + + public DateTime CreateTime { get; set; } + + public Guid CreateUserId { get; set; } + + public Guid UpdateUserId { get; set; } + + + public bool IsDeleted { get; set; } + + public DateTime? RemoveTime { get; set; } + + + + + //public Guid UserTypeId { get; set; } + //public string UserType { get; set; } = string.Empty; + //public string UserRealName { get; set; } = string.Empty; + //public UserType UserTypeEnum { get; set; } + + + + + + + + [ForeignKey("UserId")] + public User User { get; set; } + + [ForeignKey("TrialId")] + public Trial Trial { get; set; } + + [ForeignKey("SiteId")] + public Site Site { get; set; } + + + + + //public DicomStudy DicomStudy { get; set; } + + //public Subject Subject {get;set;} + + //TrialId 不管 其实就是 User和Site 的中间关系表 + [ForeignKey("SiteId")] + public TrialSite TrialSite { get; set; } + + [ForeignKey("UserId")] + public TrialUser TrialUser { get; set; } + [ForeignKey("UserId")] + public UserType UserTypeRole { get; set; } + + } + + + +} diff --git a/IRaCIS.Core.Domain/TrialSiteUser/TrialUser.cs b/IRaCIS.Core.Domain/TrialSiteUser/TrialUser.cs new file mode 100644 index 00000000..f5ab6f19 --- /dev/null +++ b/IRaCIS.Core.Domain/TrialSiteUser/TrialUser.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using IRaCIS.Core.Domain.Share; + +namespace IRaCIS.Core.Domain.Models +{ + /// + /// άԱĿϵ - ʵ + /// + [Table("TrialUser")] + public partial class TrialUser : Entity, IAuditUpdate, IAuditAdd ,ISoftDelete + { + public Guid UserId { get; set; } + public Guid TrialId { get; set; } + + public Trial Trial { get; set; } + + [ForeignKey("UserId")] + public User User { get; set; } + + + public List SiteList { get; set; } + + + // 0 ʾ 1 ʾͣ + //public int JoinState { get; set; } + + //Ѿȷϵ Ŀĵ + //public List TrialDocUserConfirmedList { get; set; } + //////һû ϶Ӧһû ͨûUserTypeId Ϊ ϵ ֶ + //public TrialDocNeedConfirmedUserType TrialDocNeedConfirmedUserType { get; set; } + //public List TrialDocumentList { get; set; } + //public Guid UserTypeId { get; set; } + //public string UserRealName { get; set; } + //public string UserType { get; set; } + //public UserType UserTypeEnum { get; set; } + + + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } + + public bool IsDeleted { get; set; } + + public DateTime? RemoveTime { get; set; } + + public DateTime? JoinTime { get; set; } + + + + } +} diff --git a/IRaCIS.Core.Domain/TrialSiteUser/TrialUserPreparation .cs b/IRaCIS.Core.Domain/TrialSiteUser/TrialUserPreparation .cs new file mode 100644 index 00000000..e9a2d66c --- /dev/null +++ b/IRaCIS.Core.Domain/TrialSiteUser/TrialUserPreparation .cs @@ -0,0 +1,74 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2022-03-24 13:22:08 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using IRaCIS.Core.Domain.Share; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///TrialUserPreparation + /// + [Table("TrialUserPreparation ")] + public class TrialUserPreparation : Entity, IAuditUpdate, IAuditAdd + { + + + + /// + /// UserId + /// + [Required] + public Guid UserId { get; set; } + + /// + /// TrialId + /// + [Required] + public Guid TrialId { get; set; } + + /// + /// UpdateTime + /// + [Required] + public DateTime UpdateTime { get; set; } + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + /// + /// UpdateUserId + /// + [Required] + public Guid UpdateUserId { get; set; } + + + public DateTime? ExpireTime { get; set; } + + + public bool? IsJoin { get; set; } + + public DateTime? JoinTime { get; set; } + + public string RejectReason { get; set; } + + + public User User { get; set; } + + public Trial Trial { get; set; } + + } + +} diff --git a/IRaCIS.Core.Domain/Visit/Subject.cs b/IRaCIS.Core.Domain/Visit/Subject.cs new file mode 100644 index 00000000..62402e24 --- /dev/null +++ b/IRaCIS.Core.Domain/Visit/Subject.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using IRaCIS.Core.Domain.Share; + +namespace IRaCIS.Core.Domain.Models +{ + /// + /// 受试者 + /// + [Table("Subject")] + public class Subject : Entity, IAuditAdd, IAuditUpdate + { + public List SubjectVisitList { get; set; } = new List(); + + //受试者有TrialId SiteId + //public List TrialSiteUserList { get; set; } = new List(); + + + //需要配置是两个键连接 + public TrialSite TrialSite { get; set; } + + [ForeignKey("TrialId")] + public Trial Trial { get; set; } + + [ForeignKey("SiteId")] + public Site Site { get; set; } + + public List StudyList { get; set; }=new List (); + + public string Code { get; set; } + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public int? Age { get; set; } + public string Sex { get; set; } = string.Empty; + public Guid SiteId { get; set; } = Guid.Empty; + + [ForeignKey("LatestSubjectVisitId")] + public SubjectVisit LatestSubjectVisit { get; set; } + public Guid? LatestSubjectVisitId { get; set; } = Guid.Empty; + + //public bool IsMissingImages { get; set; } = false; + + + public Guid TrialId { get; set; } = Guid.Empty; + public string MedicalNo { get; set; } = string.Empty; + + public SubjectStatus Status { get; set; } = SubjectStatus.OnVisit;//1 访视中,2 出组 3 访视结束 + public string Reason { get; set; } = string.Empty; + public bool IsEnrollment { get; set; } + + + public DateTime? OutEnrollmentTime { get; set; } + + public DateTime? VisitOverTime { get; set; } + + + + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime UpdateTime { get; set; } + public Guid UpdateUserId { get; set; } + + + public string ShortName { get; set; } = String.Empty; + + public string Height { get; set; } = String.Empty; + + public string Weight { get; set; } = String.Empty; + + public DateTime? BirthDate { get; set; } + public DateTime? SignDate { get; set; } + + public int StudyCount { get; set; } = 0; + public string Modalities { get; set; } = string.Empty; + + public DateTime? FirstGiveMedicineTime { get; set; } + + public bool IsUrgent { get; set; } + } +} diff --git a/IRaCIS.Core.Domain/Visit/SubjectVisit.cs b/IRaCIS.Core.Domain/Visit/SubjectVisit.cs new file mode 100644 index 00000000..0963d092 --- /dev/null +++ b/IRaCIS.Core.Domain/Visit/SubjectVisit.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using IRaCIS.Core.Domain.Share; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("SubjectVisit")] + public class SubjectVisit : Entity, IAuditUpdate, IAuditAdd + { + //一个访视 对应有对应Site的 TrialSiteCode 所以 fluentApi中配置 TrialSite 连表用TrialId SiteId 双字段 + public TrialSite TrialSite { get; set; } + public Guid TrialId { get; set; } + public Guid SubjectId { get; set; } + public Guid SiteId { get; set; } + + + public VisitStage VisitStage { get; set; } + public Guid? VisitStageId { get; set; } + public int VisitDay { get; set; } + public string VisitName { get; set; } = string.Empty; + public int VisitWindowLeft { get; set; } + public int VisitWindowRight { get; set; } + + [Column(TypeName = "decimal(18,1)")] + public decimal VisitNum { get; set; } + //public string AnonymousVisitName { get; set; } = string.Empty; + public string BlindName { get; set; } + + + + public string SVUPDES { get; set; } = string.Empty; + public DateTime? SVSTDTC { get; set; } + public DateTime? SVENDTC { get; set; } + public bool InPlan { get; set; } = true; + public bool IsBaseLine { get; set; } = false; + + + //0 未执行 1 执行了 2 不可用 + public VisitExecutedEnum VisitExecuted { get; set; } = VisitExecutedEnum.UnExecuted; + + public DateTime? EarliestScanDate { get; set; } + + public DateTime? LatestScanDate { get; set; } + + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + + //public SubjectVisitStateEnum VisitState { get; set; } + + //核查状态 + public CheckStateEnum CheckState { get; set; } + + //提交状态 + public SubmitStateEnum SubmitState { get; set; } + //审核状态 + public AuditStateEnum AuditState { get; set; } + public ForwardStateEnum ForwardState { get; set; } + public Guid? PreliminaryAuditUserId { get; set; } + + public Guid? ReviewAuditUserId { get; set; } + public DateTime? ReviewAuditTime { get; set; } + public DateTime? PreliminaryAuditTime { get; set; } + public Guid? ForwardUserId { get; set; } + public DateTime? ForwardTime { get; set; } + public Guid? CurrentActionUserId { get; set; } + public DateTime? CurrentActionUserExpireTime { get; set; } + + public DateTime? SubmitTime { get; set; } + public DateTime? CheckTime { get; set; } + + public bool IsUrgent { get; set; } + + public bool IsTake { get; set; } + + public bool IsFinalVisit { get; set; } + + public ChallengeStateEnum ChallengeState { get; set; } + + public string CheckResult { get; set; } = String.Empty; + + public bool? IsCheckBack { get; set; } + + public CheckChanllengeTypeEnum CheckChallengeState { get; set; } + public PDStateEnum PDState { get; set; } = PDStateEnum.None; + + //public bool IsOutEnromentVisit { get; set; } = false; + + public DateTime? CheckPassedTime { get; set; } + + public Guid? OutPlanPreviousVisitId { get; set; } + + //public Guid? ClinicalDataSignUserId { get; set; } + + //public DateTime? ClinicalDataSignTime { get; set; } + + [ForeignKey("ClinicalDataSignUserId")] + public User ClinicalDataSignUser { get; set; } + + + + [ForeignKey("PreliminaryAuditUserId")] + public User PreliminaryAuditUser { get; set; } + + [ForeignKey("ReviewAuditUserId")] + public User ReviewAuditUser { get; set; } + + [ForeignKey("CurrentActionUserId")] + public User CurrentActionUser { get; set; } + + public RequestBackStateEnum RequestBackState { get; set; } + + + public bool IsQCConfirmedReupload { get; set; } + + public bool IsLostVisit { get; set; } + + + //是否确认了、签名了 临床数据完整性 + public bool? IsConfirmedClinicalData { get; set; } + + + public bool IsEnrollmentConfirm { get; set; } + + + + //导航属性 + [ForeignKey("TrialId")] + public Trial Trial { get; set; } + + [ForeignKey("SiteId")] + public Site Site { get; set; } + + [ForeignKey("SubjectId")] + public Subject Subject { get; set; } + + + //// 一个访视可以被多个参与者 查看 + //public List TrialUsers { get; set; } + + ////一个访视 对应该Site下的多个CRC管理 必须加这个 不然生成的sql 会报 TrialSiteUserId 不存在该列名 + + //public List TrialSiteUserList { get; set; } + + public List PreviousHistoryList { get; set; } + + public List PreviousOtherList { get; set; } + + public List PreviousSurgeryList { get; set; } + + public List PreviousPDFList { get; set; } + + public List CheckChallengeDialogList { get; set; } = new List(); + + public List StudyList { get; set; } = new List(); + + public List NoneDicomStudyList { get; set; } = new List(); + + + public List QCChallengeList { get; set; } = new List(); + + public List QCChallengeDialogList { get; set; } = new List(); + + + } + + + +} diff --git a/IRaCIS.Core.Domain/Visit/VisitPlanInfluenceStat.cs b/IRaCIS.Core.Domain/Visit/VisitPlanInfluenceStat.cs new file mode 100644 index 00000000..5b744d40 --- /dev/null +++ b/IRaCIS.Core.Domain/Visit/VisitPlanInfluenceStat.cs @@ -0,0 +1,46 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-23 15:37:48 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///VisitPlanInfluenceStudystat + /// + [Table("VisitPlanInfluenceStat")] + public class VisitPlanInfluenceStat : Entity, IAuditAdd + { + + public List InfluenceStudyList { get; set; }=new List(); + + public Guid TrialId { get; set; } + + /// + /// CreateTime + /// + [Required] + public DateTime CreateTime { get; set; } + + /// + /// CreateUserId + /// + [Required] + public Guid CreateUserId { get; set; } + + /// + /// InfluenceCount + /// + [Required] + public int InconsistentCount { get; set; } + + [ForeignKey("CreateUserId")] + public User CreateUser { get; set; } + + } + +} diff --git a/IRaCIS.Core.Domain/Visit/VisitPlanInfluenceStudy.cs b/IRaCIS.Core.Domain/Visit/VisitPlanInfluenceStudy.cs new file mode 100644 index 00000000..329bb7f8 --- /dev/null +++ b/IRaCIS.Core.Domain/Visit/VisitPlanInfluenceStudy.cs @@ -0,0 +1,48 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2021-12-20 16:42:26 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///VisitPlanInfluenceSubjectVisit + /// + [Table("VisitPlanInfluenceStudy")] + public class VisitPlanInfluenceStudy : Entity, IAuditAdd + { + public SubjectVisit SubjectVisit { get; set; } + public Guid SubjectVisitId { get; set; } + + public Guid StudyId { get; set; } + + public Guid TrialId { get; set; } + + public bool IsDicomStudy { get; set; } + + public string Modality { get; set; } + + public bool IsOverWindowNowNotOverWindow { get; set; } + + public DateTime StudyTime { get; set; } + + public string HistoryWindow { get; set; } + + public string NowWindow { get; set; } + public DateTime CreateTime { get; set; } + + public Guid CreateUserId { get; set; } + + [ForeignKey("CreateUserId")] + public User CreateUser { get; set; } + + [ForeignKey("VisitPlanInfluenceStatId")] + public VisitPlanInfluenceStat VisitPlanInfluenceStat { get; set; } + + public Guid VisitPlanInfluenceStatId { get; set; } + + } + +} diff --git a/IRaCIS.Core.Domain/Visit/VisitStage.cs b/IRaCIS.Core.Domain/Visit/VisitStage.cs new file mode 100644 index 00000000..d8688cf5 --- /dev/null +++ b/IRaCIS.Core.Domain/Visit/VisitStage.cs @@ -0,0 +1,37 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("VisitStage")] + public class VisitStage : Entity, IAuditUpdate, IAuditAdd + { + public Trial Trial { get; set; } + public Guid TrialId { get; set; } + + public string BlindName { get; set; }=string.Empty; + + [Column(TypeName = "decimal(18,1)")] + public decimal VisitNum { get; set; } + public string VisitName { get; set; } = string.Empty; + public int VisitDay { get; set; } + public string Description { get; set; } = string.Empty; + + public bool IsConfirmed { get; set; }=false; + + public bool NeedGlobal { get; set; } = false; + + public bool IsBaseLine { get; set; } = false; + + //public string AnonymousVisitName { get; set; } = string.Empty; + + public int VisitWindowLeft { get; set; } + public int VisitWindowRight { get; set; } + + + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + } +} diff --git a/IRaCIS.Core.IdentityServer4.MVC/Config.cs b/IRaCIS.Core.IdentityServer4.MVC/Config.cs new file mode 100644 index 00000000..b1bd7787 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Config.cs @@ -0,0 +1,58 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using IdentityServer4.Models; +using System.Collections.Generic; + +namespace IRaCIS.Core.IdentityServer4.MVC +{ + public static class Config + { + public static IEnumerable IdentityResources => + new IdentityResource[] + { + new IdentityResources.OpenId(), + new IdentityResources.Profile(), + }; + + public static IEnumerable ApiScopes => + new ApiScope[] + { + new ApiScope("scope1"), + new ApiScope("scope2"), + }; + + public static IEnumerable Clients => + new Client[] + { + // m2m client credentials flow client + new Client + { + ClientId = "m2m.client", + ClientName = "Client Credentials Client", + + AllowedGrantTypes = GrantTypes.ClientCredentials, + ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) }, + + AllowedScopes = { "scope1" } + }, + + // interactive client using code flow + pkce + new Client + { + ClientId = "interactive", + ClientSecrets = { new Secret("49C1A7E1-0C79-4A89-A3D6-A37998FB86B0".Sha256()) }, + + AllowedGrantTypes = GrantTypes.Code, + + RedirectUris = { "https://localhost:44300/signin-oidc" }, + FrontChannelLogoutUri = "https://localhost:44300/signout-oidc", + PostLogoutRedirectUris = { "https://localhost:44300/signout-callback-oidc" }, + + AllowOfflineAccess = true, + AllowedScopes = { "openid", "profile", "scope2" } + }, + }; + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/IRaCIS.Core.IdentityServer4.MVC.csproj b/IRaCIS.Core.IdentityServer4.MVC/IRaCIS.Core.IdentityServer4.MVC.csproj new file mode 100644 index 00000000..4a280867 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/IRaCIS.Core.IdentityServer4.MVC.csproj @@ -0,0 +1,14 @@ + + + + netcoreapp3.1 + + + + + + + + + + \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Program.cs b/IRaCIS.Core.IdentityServer4.MVC/Program.cs new file mode 100644 index 00000000..19178278 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Program.cs @@ -0,0 +1,60 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using Serilog; +using Serilog.Events; +using Serilog.Sinks.SystemConsole.Themes; +using System; + +namespace IRaCIS.Core.IdentityServer4.MVC +{ + public class Program + { + public static int Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information) + .MinimumLevel.Override("System", LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Information) + .Enrich.FromLogContext() + // uncomment to write to Azure diagnostics stream + //.WriteTo.File( + // @"D:\home\LogFiles\Application\identityserver.txt", + // fileSizeLimitBytes: 1_000_000, + // rollOnFileSizeLimit: true, + // shared: true, + // flushToDiskInterval: TimeSpan.FromSeconds(1)) + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Code) + .CreateLogger(); + + try + { + Log.Information("Starting host..."); + CreateHostBuilder(args).Build().Run(); + return 0; + } + catch (Exception ex) + { + Log.Fatal(ex, "Host terminated unexpectedly."); + return 1; + } + finally + { + Log.CloseAndFlush(); + } + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSerilog() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Properties/launchSettings.json b/IRaCIS.Core.IdentityServer4.MVC/Properties/launchSettings.json new file mode 100644 index 00000000..f40feb3d --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "SelfHost": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001" + } + } +} diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/AccountController.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/AccountController.cs new file mode 100644 index 00000000..a6e63f54 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/AccountController.cs @@ -0,0 +1,368 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using IdentityModel; +using IdentityServer4; +using IdentityServer4.Events; +using IdentityServer4.Extensions; +using IdentityServer4.Models; +using IdentityServer4.Services; +using IdentityServer4.Stores; +using IdentityServer4.Test; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace IdentityServerHost.Quickstart.UI +{ + /// + /// This sample controller implements a typical login/logout/provision workflow for local and external accounts. + /// The login service encapsulates the interactions with the user data store. This data store is in-memory only and cannot be used for production! + /// The interaction service provides a way for the UI to communicate with identityserver for validation and context retrieval + /// + [SecurityHeaders] + [AllowAnonymous] + public class AccountController : Controller + { + private readonly TestUserStore _users; + private readonly IIdentityServerInteractionService _interaction; + private readonly IClientStore _clientStore; + private readonly IAuthenticationSchemeProvider _schemeProvider; + private readonly IEventService _events; + + public AccountController( + IIdentityServerInteractionService interaction, + IClientStore clientStore, + IAuthenticationSchemeProvider schemeProvider, + IEventService events, + TestUserStore users = null) + { + // if the TestUserStore is not in DI, then we'll just use the global users collection + // this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity) + _users = users ?? new TestUserStore(TestUsers.Users); + + _interaction = interaction; + _clientStore = clientStore; + _schemeProvider = schemeProvider; + _events = events; + } + + /// + /// Entry point into the login workflow + /// + [HttpGet] + public async Task Login(string returnUrl) + { + // build a model so we know what to show on the login page + var vm = await BuildLoginViewModelAsync(returnUrl); + + if (vm.IsExternalLoginOnly) + { + // we only have one option for logging in and it's an external provider + return RedirectToAction("Challenge", "External", new { scheme = vm.ExternalLoginScheme, returnUrl }); + } + + return View(vm); + } + + /// + /// Handle postback from username/password login + /// + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Login(LoginInputModel model, string button) + { + // check if we are in the context of an authorization request + var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); + + // the user clicked the "cancel" button + if (button != "login") + { + if (context != null) + { + // if the user cancels, send a result back into IdentityServer as if they + // denied the consent (even if this client does not require consent). + // this will send back an access denied OIDC error response to the client. + await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied); + + // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null + if (context.IsNativeClient()) + { + // The client is native, so this change in how to + // return the response is for better UX for the end user. + return this.LoadingPage("Redirect", model.ReturnUrl); + } + + return Redirect(model.ReturnUrl); + } + else + { + // since we don't have a valid context, then we just go back to the home page + return Redirect("~/"); + } + } + + if (ModelState.IsValid) + { + // validate username/password against in-memory store + if (_users.ValidateCredentials(model.Username, model.Password)) + { + var user = _users.FindByUsername(model.Username); + await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username, clientId: context?.Client.ClientId)); + + // only set explicit expiration here if user chooses "remember me". + // otherwise we rely upon expiration configured in cookie middleware. + AuthenticationProperties props = null; + if (AccountOptions.AllowRememberLogin && model.RememberLogin) + { + props = new AuthenticationProperties + { + IsPersistent = true, + ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) + }; + }; + + // issue authentication cookie with subject ID and username + var isuser = new IdentityServerUser(user.SubjectId) + { + DisplayName = user.Username + }; + + await HttpContext.SignInAsync(isuser, props); + + if (context != null) + { + if (context.IsNativeClient()) + { + // The client is native, so this change in how to + // return the response is for better UX for the end user. + return this.LoadingPage("Redirect", model.ReturnUrl); + } + + // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null + return Redirect(model.ReturnUrl); + } + + // request for a local page + if (Url.IsLocalUrl(model.ReturnUrl)) + { + return Redirect(model.ReturnUrl); + } + else if (string.IsNullOrEmpty(model.ReturnUrl)) + { + return Redirect("~/"); + } + else + { + // user might have clicked on a malicious link - should be logged + throw new Exception("invalid return URL"); + } + } + + await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId: context?.Client.ClientId)); + ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage); + } + + // something went wrong, show form with error + var vm = await BuildLoginViewModelAsync(model); + return View(vm); + } + + + /// + /// Show logout page + /// + [HttpGet] + public async Task Logout(string logoutId) + { + // build a model so the logout page knows what to display + var vm = await BuildLogoutViewModelAsync(logoutId); + + if (vm.ShowLogoutPrompt == false) + { + // if the request for logout was properly authenticated from IdentityServer, then + // we don't need to show the prompt and can just log the user out directly. + return await Logout(vm); + } + + return View(vm); + } + + /// + /// Handle logout page postback + /// + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Logout(LogoutInputModel model) + { + // build a model so the logged out page knows what to display + var vm = await BuildLoggedOutViewModelAsync(model.LogoutId); + + if (User?.Identity.IsAuthenticated == true) + { + // delete local authentication cookie + await HttpContext.SignOutAsync(); + + // raise the logout event + await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName())); + } + + // check if we need to trigger sign-out at an upstream identity provider + if (vm.TriggerExternalSignout) + { + // build a return URL so the upstream provider will redirect back + // to us after the user has logged out. this allows us to then + // complete our single sign-out processing. + string url = Url.Action("Logout", new { logoutId = vm.LogoutId }); + + // this triggers a redirect to the external provider for sign-out + return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme); + } + + return View("LoggedOut", vm); + } + + [HttpGet] + public IActionResult AccessDenied() + { + return View(); + } + + + /*****************************************/ + /* helper APIs for the AccountController */ + /*****************************************/ + private async Task BuildLoginViewModelAsync(string returnUrl) + { + var context = await _interaction.GetAuthorizationContextAsync(returnUrl); + if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null) + { + var local = context.IdP == IdentityServer4.IdentityServerConstants.LocalIdentityProvider; + + // this is meant to short circuit the UI and only trigger the one external IdP + var vm = new LoginViewModel + { + EnableLocalLogin = local, + ReturnUrl = returnUrl, + Username = context?.LoginHint, + }; + + if (!local) + { + vm.ExternalProviders = new[] { new ExternalProvider { AuthenticationScheme = context.IdP } }; + } + + return vm; + } + + var schemes = await _schemeProvider.GetAllSchemesAsync(); + + var providers = schemes + .Where(x => x.DisplayName != null) + .Select(x => new ExternalProvider + { + DisplayName = x.DisplayName ?? x.Name, + AuthenticationScheme = x.Name + }).ToList(); + + var allowLocal = true; + if (context?.Client.ClientId != null) + { + var client = await _clientStore.FindEnabledClientByIdAsync(context.Client.ClientId); + if (client != null) + { + allowLocal = client.EnableLocalLogin; + + if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any()) + { + providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList(); + } + } + } + + return new LoginViewModel + { + AllowRememberLogin = AccountOptions.AllowRememberLogin, + EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin, + ReturnUrl = returnUrl, + Username = context?.LoginHint, + ExternalProviders = providers.ToArray() + }; + } + + private async Task BuildLoginViewModelAsync(LoginInputModel model) + { + var vm = await BuildLoginViewModelAsync(model.ReturnUrl); + vm.Username = model.Username; + vm.RememberLogin = model.RememberLogin; + return vm; + } + + private async Task BuildLogoutViewModelAsync(string logoutId) + { + var vm = new LogoutViewModel { LogoutId = logoutId, ShowLogoutPrompt = AccountOptions.ShowLogoutPrompt }; + + if (User?.Identity.IsAuthenticated != true) + { + // if the user is not authenticated, then just show logged out page + vm.ShowLogoutPrompt = false; + return vm; + } + + var context = await _interaction.GetLogoutContextAsync(logoutId); + if (context?.ShowSignoutPrompt == false) + { + // it's safe to automatically sign-out + vm.ShowLogoutPrompt = false; + return vm; + } + + // show the logout prompt. this prevents attacks where the user + // is automatically signed out by another malicious web page. + return vm; + } + + private async Task BuildLoggedOutViewModelAsync(string logoutId) + { + // get context information (client name, post logout redirect URI and iframe for federated signout) + var logout = await _interaction.GetLogoutContextAsync(logoutId); + + var vm = new LoggedOutViewModel + { + AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut, + PostLogoutRedirectUri = logout?.PostLogoutRedirectUri, + ClientName = string.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName, + SignOutIframeUrl = logout?.SignOutIFrameUrl, + LogoutId = logoutId + }; + + if (User?.Identity.IsAuthenticated == true) + { + var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value; + if (idp != null && idp != IdentityServer4.IdentityServerConstants.LocalIdentityProvider) + { + var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(idp); + if (providerSupportsSignout) + { + if (vm.LogoutId == null) + { + // if there's no current logout context, we need to create one + // this captures necessary info from the current logged in user + // before we signout and redirect away to the external IdP for signout + vm.LogoutId = await _interaction.CreateLogoutContextAsync(); + } + + vm.ExternalAuthenticationScheme = idp; + } + } + } + + return vm; + } + } +} diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/AccountOptions.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/AccountOptions.cs new file mode 100644 index 00000000..552f1907 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/AccountOptions.cs @@ -0,0 +1,20 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using System; + +namespace IdentityServerHost.Quickstart.UI +{ + public class AccountOptions + { + public static bool AllowLocalLogin = true; + public static bool AllowRememberLogin = true; + public static TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30); + + public static bool ShowLogoutPrompt = true; + public static bool AutomaticRedirectAfterSignOut = false; + + public static string InvalidCredentialsErrorMessage = "Invalid username or password"; + } +} diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/ExternalController.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/ExternalController.cs new file mode 100644 index 00000000..9d267054 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/ExternalController.cs @@ -0,0 +1,196 @@ +using IdentityModel; +using IdentityServer4; +using IdentityServer4.Events; +using IdentityServer4.Services; +using IdentityServer4.Stores; +using IdentityServer4.Test; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace IdentityServerHost.Quickstart.UI +{ + [SecurityHeaders] + [AllowAnonymous] + public class ExternalController : Controller + { + private readonly TestUserStore _users; + private readonly IIdentityServerInteractionService _interaction; + private readonly IClientStore _clientStore; + private readonly ILogger _logger; + private readonly IEventService _events; + + public ExternalController( + IIdentityServerInteractionService interaction, + IClientStore clientStore, + IEventService events, + ILogger logger, + TestUserStore users = null) + { + // if the TestUserStore is not in DI, then we'll just use the global users collection + // this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity) + _users = users ?? new TestUserStore(TestUsers.Users); + + _interaction = interaction; + _clientStore = clientStore; + _logger = logger; + _events = events; + } + + /// + /// initiate roundtrip to external authentication provider + /// + [HttpGet] + public IActionResult Challenge(string scheme, string returnUrl) + { + if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/"; + + // validate returnUrl - either it is a valid OIDC URL or back to a local page + if (Url.IsLocalUrl(returnUrl) == false && _interaction.IsValidReturnUrl(returnUrl) == false) + { + // user might have clicked on a malicious link - should be logged + throw new Exception("invalid return URL"); + } + + // start challenge and roundtrip the return URL and scheme + var props = new AuthenticationProperties + { + RedirectUri = Url.Action(nameof(Callback)), + Items = + { + { "returnUrl", returnUrl }, + { "scheme", scheme }, + } + }; + + return Challenge(props, scheme); + + } + + /// + /// Post processing of external authentication + /// + [HttpGet] + public async Task Callback() + { + // read external identity from the temporary cookie + var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme); + if (result?.Succeeded != true) + { + throw new Exception("External authentication error"); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}"); + _logger.LogDebug("External claims: {@claims}", externalClaims); + } + + // lookup our user and external provider info + var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result); + if (user == null) + { + // this might be where you might initiate a custom workflow for user registration + // in this sample we don't show how that would be done, as our sample implementation + // simply auto-provisions new external user + user = AutoProvisionUser(provider, providerUserId, claims); + } + + // this allows us to collect any additional claims or properties + // for the specific protocols used and store them in the local auth cookie. + // this is typically used to store data needed for signout from those protocols. + var additionalLocalClaims = new List(); + var localSignInProps = new AuthenticationProperties(); + ProcessLoginCallback(result, additionalLocalClaims, localSignInProps); + + // issue authentication cookie for user + var isuser = new IdentityServerUser(user.SubjectId) + { + DisplayName = user.Username, + IdentityProvider = provider, + AdditionalClaims = additionalLocalClaims + }; + + await HttpContext.SignInAsync(isuser, localSignInProps); + + // delete temporary cookie used during external authentication + await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme); + + // retrieve return URL + var returnUrl = result.Properties.Items["returnUrl"] ?? "~/"; + + // check if external login is in the context of an OIDC request + var context = await _interaction.GetAuthorizationContextAsync(returnUrl); + await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, true, context?.Client.ClientId)); + + if (context != null) + { + if (context.IsNativeClient()) + { + // The client is native, so this change in how to + // return the response is for better UX for the end user. + return this.LoadingPage("Redirect", returnUrl); + } + } + + return Redirect(returnUrl); + } + + private (TestUser user, string provider, string providerUserId, IEnumerable claims) FindUserFromExternalProvider(AuthenticateResult result) + { + var externalUser = result.Principal; + + // try to determine the unique id of the external user (issued by the provider) + // the most common claim type for that are the sub claim and the NameIdentifier + // depending on the external provider, some other claim type might be used + var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ?? + externalUser.FindFirst(ClaimTypes.NameIdentifier) ?? + throw new Exception("Unknown userid"); + + // remove the user id claim so we don't include it as an extra claim if/when we provision the user + var claims = externalUser.Claims.ToList(); + claims.Remove(userIdClaim); + + var provider = result.Properties.Items["scheme"]; + var providerUserId = userIdClaim.Value; + + // find external user + var user = _users.FindByExternalProvider(provider, providerUserId); + + return (user, provider, providerUserId, claims); + } + + private TestUser AutoProvisionUser(string provider, string providerUserId, IEnumerable claims) + { + var user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList()); + return user; + } + + // if the external login is OIDC-based, there are certain things we need to preserve to make logout work + // this will be different for WS-Fed, SAML2p or other protocols + private void ProcessLoginCallback(AuthenticateResult externalResult, List localClaims, AuthenticationProperties localSignInProps) + { + // if the external system sent a session id claim, copy it over + // so we can use it for single sign-out + var sid = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId); + if (sid != null) + { + localClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value)); + } + + // if the external provider issued an id_token, we'll keep it for signout + var idToken = externalResult.Properties.GetTokenValue("id_token"); + if (idToken != null) + { + localSignInProps.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = idToken } }); + } + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/ExternalProvider.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/ExternalProvider.cs new file mode 100644 index 00000000..a1831137 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/ExternalProvider.cs @@ -0,0 +1,12 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +namespace IdentityServerHost.Quickstart.UI +{ + public class ExternalProvider + { + public string DisplayName { get; set; } + public string AuthenticationScheme { get; set; } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/LoggedOutViewModel.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/LoggedOutViewModel.cs new file mode 100644 index 00000000..63688328 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/LoggedOutViewModel.cs @@ -0,0 +1,19 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +namespace IdentityServerHost.Quickstart.UI +{ + public class LoggedOutViewModel + { + public string PostLogoutRedirectUri { get; set; } + public string ClientName { get; set; } + public string SignOutIframeUrl { get; set; } + + public bool AutomaticRedirectAfterSignOut { get; set; } + + public string LogoutId { get; set; } + public bool TriggerExternalSignout => ExternalAuthenticationScheme != null; + public string ExternalAuthenticationScheme { get; set; } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/LoginInputModel.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/LoginInputModel.cs new file mode 100644 index 00000000..bcb7853a --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/LoginInputModel.cs @@ -0,0 +1,18 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using System.ComponentModel.DataAnnotations; + +namespace IdentityServerHost.Quickstart.UI +{ + public class LoginInputModel + { + [Required] + public string Username { get; set; } + [Required] + public string Password { get; set; } + public bool RememberLogin { get; set; } + public string ReturnUrl { get; set; } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/LoginViewModel.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/LoginViewModel.cs new file mode 100644 index 00000000..9bf6c8f3 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/LoginViewModel.cs @@ -0,0 +1,22 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace IdentityServerHost.Quickstart.UI +{ + public class LoginViewModel : LoginInputModel + { + public bool AllowRememberLogin { get; set; } = true; + public bool EnableLocalLogin { get; set; } = true; + + public IEnumerable ExternalProviders { get; set; } = Enumerable.Empty(); + public IEnumerable VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName)); + + public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1; + public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null; + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/LogoutInputModel.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/LogoutInputModel.cs new file mode 100644 index 00000000..bb74202e --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/LogoutInputModel.cs @@ -0,0 +1,11 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +namespace IdentityServerHost.Quickstart.UI +{ + public class LogoutInputModel + { + public string LogoutId { get; set; } + } +} diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/LogoutViewModel.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/LogoutViewModel.cs new file mode 100644 index 00000000..236cd6c8 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/LogoutViewModel.cs @@ -0,0 +1,11 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +namespace IdentityServerHost.Quickstart.UI +{ + public class LogoutViewModel : LogoutInputModel + { + public bool ShowLogoutPrompt { get; set; } = true; + } +} diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/RedirectViewModel.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/RedirectViewModel.cs new file mode 100644 index 00000000..904ae206 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Account/RedirectViewModel.cs @@ -0,0 +1,12 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + + +namespace IdentityServerHost.Quickstart.UI +{ + public class RedirectViewModel + { + public string RedirectUrl { get; set; } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Consent/ConsentController.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Consent/ConsentController.cs new file mode 100644 index 00000000..cd19894d --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Consent/ConsentController.cs @@ -0,0 +1,262 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using IdentityServer4.Events; +using IdentityServer4.Extensions; +using IdentityServer4.Models; +using IdentityServer4.Services; +using IdentityServer4.Validation; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace IdentityServerHost.Quickstart.UI +{ + /// + /// This controller processes the consent UI + /// + [SecurityHeaders] + [Authorize] + public class ConsentController : Controller + { + private readonly IIdentityServerInteractionService _interaction; + private readonly IEventService _events; + private readonly ILogger _logger; + + public ConsentController( + IIdentityServerInteractionService interaction, + IEventService events, + ILogger logger) + { + _interaction = interaction; + _events = events; + _logger = logger; + } + + /// + /// Shows the consent screen + /// + /// + /// + [HttpGet] + public async Task Index(string returnUrl) + { + var vm = await BuildViewModelAsync(returnUrl); + if (vm != null) + { + return View("Index", vm); + } + + return View("Error"); + } + + /// + /// Handles the consent screen postback + /// + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Index(ConsentInputModel model) + { + var result = await ProcessConsent(model); + + if (result.IsRedirect) + { + var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); + if (context?.IsNativeClient() == true) + { + // The client is native, so this change in how to + // return the response is for better UX for the end user. + return this.LoadingPage("Redirect", result.RedirectUri); + } + + return Redirect(result.RedirectUri); + } + + if (result.HasValidationError) + { + ModelState.AddModelError(string.Empty, result.ValidationError); + } + + if (result.ShowView) + { + return View("Index", result.ViewModel); + } + + return View("Error"); + } + + /*****************************************/ + /* helper APIs for the ConsentController */ + /*****************************************/ + private async Task ProcessConsent(ConsentInputModel model) + { + var result = new ProcessConsentResult(); + + // validate return url is still valid + var request = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); + if (request == null) return result; + + ConsentResponse grantedConsent = null; + + // user clicked 'no' - send back the standard 'access_denied' response + if (model?.Button == "no") + { + grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied }; + + // emit event + await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues)); + } + // user clicked 'yes' - validate the data + else if (model?.Button == "yes") + { + // if the user consented to some scope, build the response model + if (model.ScopesConsented != null && model.ScopesConsented.Any()) + { + var scopes = model.ScopesConsented; + if (ConsentOptions.EnableOfflineAccess == false) + { + scopes = scopes.Where(x => x != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess); + } + + grantedConsent = new ConsentResponse + { + RememberConsent = model.RememberConsent, + ScopesValuesConsented = scopes.ToArray(), + Description = model.Description + }; + + // emit event + await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent)); + } + else + { + result.ValidationError = ConsentOptions.MustChooseOneErrorMessage; + } + } + else + { + result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage; + } + + if (grantedConsent != null) + { + // communicate outcome of consent back to identityserver + await _interaction.GrantConsentAsync(request, grantedConsent); + + // indicate that's it ok to redirect back to authorization endpoint + result.RedirectUri = model.ReturnUrl; + result.Client = request.Client; + } + else + { + // we need to redisplay the consent UI + result.ViewModel = await BuildViewModelAsync(model.ReturnUrl, model); + } + + return result; + } + + private async Task BuildViewModelAsync(string returnUrl, ConsentInputModel model = null) + { + var request = await _interaction.GetAuthorizationContextAsync(returnUrl); + if (request != null) + { + return CreateConsentViewModel(model, returnUrl, request); + } + else + { + _logger.LogError("No consent request matching request: {0}", returnUrl); + } + + return null; + } + + private ConsentViewModel CreateConsentViewModel( + ConsentInputModel model, string returnUrl, + AuthorizationRequest request) + { + var vm = new ConsentViewModel + { + RememberConsent = model?.RememberConsent ?? true, + ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty(), + Description = model?.Description, + + ReturnUrl = returnUrl, + + ClientName = request.Client.ClientName ?? request.Client.ClientId, + ClientUrl = request.Client.ClientUri, + ClientLogoUrl = request.Client.LogoUri, + AllowRememberConsent = request.Client.AllowRememberConsent + }; + + vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray(); + + var apiScopes = new List(); + foreach (var parsedScope in request.ValidatedResources.ParsedScopes) + { + var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName); + if (apiScope != null) + { + var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.RawValue) || model == null); + apiScopes.Add(scopeVm); + } + } + if (ConsentOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess) + { + apiScopes.Add(GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null)); + } + vm.ApiScopes = apiScopes; + + return vm; + } + + private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check) + { + return new ScopeViewModel + { + Value = identity.Name, + DisplayName = identity.DisplayName ?? identity.Name, + Description = identity.Description, + Emphasize = identity.Emphasize, + Required = identity.Required, + Checked = check || identity.Required + }; + } + + public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check) + { + var displayName = apiScope.DisplayName ?? apiScope.Name; + if (!String.IsNullOrWhiteSpace(parsedScopeValue.ParsedParameter)) + { + displayName += ":" + parsedScopeValue.ParsedParameter; + } + + return new ScopeViewModel + { + Value = parsedScopeValue.RawValue, + DisplayName = displayName, + Description = apiScope.Description, + Emphasize = apiScope.Emphasize, + Required = apiScope.Required, + Checked = check || apiScope.Required + }; + } + + private ScopeViewModel GetOfflineAccessScope(bool check) + { + return new ScopeViewModel + { + Value = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess, + DisplayName = ConsentOptions.OfflineAccessDisplayName, + Description = ConsentOptions.OfflineAccessDescription, + Emphasize = true, + Checked = check + }; + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Consent/ConsentInputModel.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Consent/ConsentInputModel.cs new file mode 100644 index 00000000..f608fe3b --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Consent/ConsentInputModel.cs @@ -0,0 +1,17 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using System.Collections.Generic; + +namespace IdentityServerHost.Quickstart.UI +{ + public class ConsentInputModel + { + public string Button { get; set; } + public IEnumerable ScopesConsented { get; set; } + public bool RememberConsent { get; set; } + public string ReturnUrl { get; set; } + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Consent/ConsentOptions.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Consent/ConsentOptions.cs new file mode 100644 index 00000000..998c51dc --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Consent/ConsentOptions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +namespace IdentityServerHost.Quickstart.UI +{ + public class ConsentOptions + { + public static bool EnableOfflineAccess = true; + public static string OfflineAccessDisplayName = "Offline Access"; + public static string OfflineAccessDescription = "Access to your applications and resources, even when you are offline"; + + public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission"; + public static readonly string InvalidSelectionErrorMessage = "Invalid selection"; + } +} diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Consent/ConsentViewModel.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Consent/ConsentViewModel.cs new file mode 100644 index 00000000..af4b9c5c --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Consent/ConsentViewModel.cs @@ -0,0 +1,19 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using System.Collections.Generic; + +namespace IdentityServerHost.Quickstart.UI +{ + public class ConsentViewModel : ConsentInputModel + { + public string ClientName { get; set; } + public string ClientUrl { get; set; } + public string ClientLogoUrl { get; set; } + public bool AllowRememberConsent { get; set; } + + public IEnumerable IdentityScopes { get; set; } + public IEnumerable ApiScopes { get; set; } + } +} diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Consent/ProcessConsentResult.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Consent/ProcessConsentResult.cs new file mode 100644 index 00000000..1d331df0 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Consent/ProcessConsentResult.cs @@ -0,0 +1,21 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using IdentityServer4.Models; + +namespace IdentityServerHost.Quickstart.UI +{ + public class ProcessConsentResult + { + public bool IsRedirect => RedirectUri != null; + public string RedirectUri { get; set; } + public Client Client { get; set; } + + public bool ShowView => ViewModel != null; + public ConsentViewModel ViewModel { get; set; } + + public bool HasValidationError => ValidationError != null; + public string ValidationError { get; set; } + } +} diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Consent/ScopeViewModel.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Consent/ScopeViewModel.cs new file mode 100644 index 00000000..532d1b1a --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Consent/ScopeViewModel.cs @@ -0,0 +1,16 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +namespace IdentityServerHost.Quickstart.UI +{ + public class ScopeViewModel + { + public string Value { get; set; } + public string DisplayName { get; set; } + public string Description { get; set; } + public bool Emphasize { get; set; } + public bool Required { get; set; } + public bool Checked { get; set; } + } +} diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Device/DeviceAuthorizationInputModel.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Device/DeviceAuthorizationInputModel.cs new file mode 100644 index 00000000..a221181e --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Device/DeviceAuthorizationInputModel.cs @@ -0,0 +1,11 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +namespace IdentityServerHost.Quickstart.UI +{ + public class DeviceAuthorizationInputModel : ConsentInputModel + { + public string UserCode { get; set; } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Device/DeviceAuthorizationViewModel.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Device/DeviceAuthorizationViewModel.cs new file mode 100644 index 00000000..3e8857f6 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Device/DeviceAuthorizationViewModel.cs @@ -0,0 +1,12 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +namespace IdentityServerHost.Quickstart.UI +{ + public class DeviceAuthorizationViewModel : ConsentViewModel + { + public string UserCode { get; set; } + public bool ConfirmUserCode { get; set; } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Device/DeviceController.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Device/DeviceController.cs new file mode 100644 index 00000000..4d385e36 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Device/DeviceController.cs @@ -0,0 +1,232 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using IdentityServer4.Configuration; +using IdentityServer4.Events; +using IdentityServer4.Extensions; +using IdentityServer4.Models; +using IdentityServer4.Services; +using IdentityServer4.Validation; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace IdentityServerHost.Quickstart.UI +{ + [Authorize] + [SecurityHeaders] + public class DeviceController : Controller + { + private readonly IDeviceFlowInteractionService _interaction; + private readonly IEventService _events; + private readonly IOptions _options; + private readonly ILogger _logger; + + public DeviceController( + IDeviceFlowInteractionService interaction, + IEventService eventService, + IOptions options, + ILogger logger) + { + _interaction = interaction; + _events = eventService; + _options = options; + _logger = logger; + } + + [HttpGet] + public async Task Index() + { + string userCodeParamName = _options.Value.UserInteraction.DeviceVerificationUserCodeParameter; + string userCode = Request.Query[userCodeParamName]; + if (string.IsNullOrWhiteSpace(userCode)) return View("UserCodeCapture"); + + var vm = await BuildViewModelAsync(userCode); + if (vm == null) return View("Error"); + + vm.ConfirmUserCode = true; + return View("UserCodeConfirmation", vm); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task UserCodeCapture(string userCode) + { + var vm = await BuildViewModelAsync(userCode); + if (vm == null) return View("Error"); + + return View("UserCodeConfirmation", vm); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Callback(DeviceAuthorizationInputModel model) + { + if (model == null) throw new ArgumentNullException(nameof(model)); + + var result = await ProcessConsent(model); + if (result.HasValidationError) return View("Error"); + + return View("Success"); + } + + private async Task ProcessConsent(DeviceAuthorizationInputModel model) + { + var result = new ProcessConsentResult(); + + var request = await _interaction.GetAuthorizationContextAsync(model.UserCode); + if (request == null) return result; + + ConsentResponse grantedConsent = null; + + // user clicked 'no' - send back the standard 'access_denied' response + if (model.Button == "no") + { + grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied }; + + // emit event + await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues)); + } + // user clicked 'yes' - validate the data + else if (model.Button == "yes") + { + // if the user consented to some scope, build the response model + if (model.ScopesConsented != null && model.ScopesConsented.Any()) + { + var scopes = model.ScopesConsented; + if (ConsentOptions.EnableOfflineAccess == false) + { + scopes = scopes.Where(x => x != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess); + } + + grantedConsent = new ConsentResponse + { + RememberConsent = model.RememberConsent, + ScopesValuesConsented = scopes.ToArray(), + Description = model.Description + }; + + // emit event + await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent)); + } + else + { + result.ValidationError = ConsentOptions.MustChooseOneErrorMessage; + } + } + else + { + result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage; + } + + if (grantedConsent != null) + { + // communicate outcome of consent back to identityserver + await _interaction.HandleRequestAsync(model.UserCode, grantedConsent); + + // indicate that's it ok to redirect back to authorization endpoint + result.RedirectUri = model.ReturnUrl; + result.Client = request.Client; + } + else + { + // we need to redisplay the consent UI + result.ViewModel = await BuildViewModelAsync(model.UserCode, model); + } + + return result; + } + + private async Task BuildViewModelAsync(string userCode, DeviceAuthorizationInputModel model = null) + { + var request = await _interaction.GetAuthorizationContextAsync(userCode); + if (request != null) + { + return CreateConsentViewModel(userCode, model, request); + } + + return null; + } + + private DeviceAuthorizationViewModel CreateConsentViewModel(string userCode, DeviceAuthorizationInputModel model, DeviceFlowAuthorizationRequest request) + { + var vm = new DeviceAuthorizationViewModel + { + UserCode = userCode, + Description = model?.Description, + + RememberConsent = model?.RememberConsent ?? true, + ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty(), + + ClientName = request.Client.ClientName ?? request.Client.ClientId, + ClientUrl = request.Client.ClientUri, + ClientLogoUrl = request.Client.LogoUri, + AllowRememberConsent = request.Client.AllowRememberConsent + }; + + vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray(); + + var apiScopes = new List(); + foreach (var parsedScope in request.ValidatedResources.ParsedScopes) + { + var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName); + if (apiScope != null) + { + var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.RawValue) || model == null); + apiScopes.Add(scopeVm); + } + } + if (ConsentOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess) + { + apiScopes.Add(GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null)); + } + vm.ApiScopes = apiScopes; + + return vm; + } + + private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check) + { + return new ScopeViewModel + { + Value = identity.Name, + DisplayName = identity.DisplayName ?? identity.Name, + Description = identity.Description, + Emphasize = identity.Emphasize, + Required = identity.Required, + Checked = check || identity.Required + }; + } + + public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check) + { + return new ScopeViewModel + { + Value = parsedScopeValue.RawValue, + // todo: use the parsed scope value in the display? + DisplayName = apiScope.DisplayName ?? apiScope.Name, + Description = apiScope.Description, + Emphasize = apiScope.Emphasize, + Required = apiScope.Required, + Checked = check || apiScope.Required + }; + } + private ScopeViewModel GetOfflineAccessScope(bool check) + { + return new ScopeViewModel + { + Value = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess, + DisplayName = ConsentOptions.OfflineAccessDisplayName, + Description = ConsentOptions.OfflineAccessDescription, + Emphasize = true, + Checked = check + }; + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Diagnostics/DiagnosticsController.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Diagnostics/DiagnosticsController.cs new file mode 100644 index 00000000..7c8d2b75 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Diagnostics/DiagnosticsController.cs @@ -0,0 +1,29 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Linq; +using System.Threading.Tasks; + +namespace IdentityServerHost.Quickstart.UI +{ + [SecurityHeaders] + [Authorize] + public class DiagnosticsController : Controller + { + public async Task Index() + { + var localAddresses = new string[] { "127.0.0.1", "::1", HttpContext.Connection.LocalIpAddress.ToString() }; + if (!localAddresses.Contains(HttpContext.Connection.RemoteIpAddress.ToString())) + { + return NotFound(); + } + + var model = new DiagnosticsViewModel(await HttpContext.AuthenticateAsync()); + return View(model); + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Diagnostics/DiagnosticsViewModel.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Diagnostics/DiagnosticsViewModel.cs new file mode 100644 index 00000000..f43c7685 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Diagnostics/DiagnosticsViewModel.cs @@ -0,0 +1,32 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using IdentityModel; +using Microsoft.AspNetCore.Authentication; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Text; + +namespace IdentityServerHost.Quickstart.UI +{ + public class DiagnosticsViewModel + { + public DiagnosticsViewModel(AuthenticateResult result) + { + AuthenticateResult = result; + + if (result.Properties.Items.ContainsKey("client_list")) + { + var encoded = result.Properties.Items["client_list"]; + var bytes = Base64Url.Decode(encoded); + var value = Encoding.UTF8.GetString(bytes); + + Clients = JsonConvert.DeserializeObject(value); + } + } + + public AuthenticateResult AuthenticateResult { get; } + public IEnumerable Clients { get; } = new List(); + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Extensions.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Extensions.cs new file mode 100644 index 00000000..2fc35fd7 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Extensions.cs @@ -0,0 +1,27 @@ +using IdentityServer4.Models; +using Microsoft.AspNetCore.Mvc; +using System; + +namespace IdentityServerHost.Quickstart.UI +{ + public static class Extensions + { + /// + /// Checks if the redirect URI is for a native client. + /// + /// + public static bool IsNativeClient(this AuthorizationRequest context) + { + return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal) + && !context.RedirectUri.StartsWith("http", StringComparison.Ordinal); + } + + public static IActionResult LoadingPage(this Controller controller, string viewName, string redirectUri) + { + controller.HttpContext.Response.StatusCode = 200; + controller.HttpContext.Response.Headers["Location"] = ""; + + return controller.View(viewName, new RedirectViewModel { RedirectUrl = redirectUri }); + } + } +} diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Grants/GrantsController.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Grants/GrantsController.cs new file mode 100644 index 00000000..991c3fa4 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Grants/GrantsController.cs @@ -0,0 +1,97 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using IdentityServer4.Events; +using IdentityServer4.Extensions; +using IdentityServer4.Services; +using IdentityServer4.Stores; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace IdentityServerHost.Quickstart.UI +{ + /// + /// This sample controller allows a user to revoke grants given to clients + /// + [SecurityHeaders] + [Authorize] + public class GrantsController : Controller + { + private readonly IIdentityServerInteractionService _interaction; + private readonly IClientStore _clients; + private readonly IResourceStore _resources; + private readonly IEventService _events; + + public GrantsController(IIdentityServerInteractionService interaction, + IClientStore clients, + IResourceStore resources, + IEventService events) + { + _interaction = interaction; + _clients = clients; + _resources = resources; + _events = events; + } + + /// + /// Show list of grants + /// + [HttpGet] + public async Task Index() + { + return View("Index", await BuildViewModelAsync()); + } + + /// + /// Handle postback to revoke a client + /// + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Revoke(string clientId) + { + await _interaction.RevokeUserConsentAsync(clientId); + await _events.RaiseAsync(new GrantsRevokedEvent(User.GetSubjectId(), clientId)); + + return RedirectToAction("Index"); + } + + private async Task BuildViewModelAsync() + { + var grants = await _interaction.GetAllUserGrantsAsync(); + + var list = new List(); + foreach (var grant in grants) + { + var client = await _clients.FindClientByIdAsync(grant.ClientId); + if (client != null) + { + var resources = await _resources.FindResourcesByScopeAsync(grant.Scopes); + + var item = new GrantViewModel() + { + ClientId = client.ClientId, + ClientName = client.ClientName ?? client.ClientId, + ClientLogoUrl = client.LogoUri, + ClientUrl = client.ClientUri, + Description = grant.Description, + Created = grant.CreationTime, + Expires = grant.Expiration, + IdentityGrantNames = resources.IdentityResources.Select(x => x.DisplayName ?? x.Name).ToArray(), + ApiGrantNames = resources.ApiScopes.Select(x => x.DisplayName ?? x.Name).ToArray() + }; + + list.Add(item); + } + } + + return new GrantsViewModel + { + Grants = list + }; + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Grants/GrantsViewModel.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Grants/GrantsViewModel.cs new file mode 100644 index 00000000..2114deeb --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Grants/GrantsViewModel.cs @@ -0,0 +1,27 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using System; +using System.Collections.Generic; + +namespace IdentityServerHost.Quickstart.UI +{ + public class GrantsViewModel + { + public IEnumerable Grants { get; set; } + } + + public class GrantViewModel + { + public string ClientId { get; set; } + public string ClientName { get; set; } + public string ClientUrl { get; set; } + public string ClientLogoUrl { get; set; } + public string Description { get; set; } + public DateTime Created { get; set; } + public DateTime? Expires { get; set; } + public IEnumerable IdentityGrantNames { get; set; } + public IEnumerable ApiGrantNames { get; set; } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Home/ErrorViewModel.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Home/ErrorViewModel.cs new file mode 100644 index 00000000..a29a21a9 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Home/ErrorViewModel.cs @@ -0,0 +1,22 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using IdentityServer4.Models; + +namespace IdentityServerHost.Quickstart.UI +{ + public class ErrorViewModel + { + public ErrorViewModel() + { + } + + public ErrorViewModel(string error) + { + Error = new ErrorMessage { Error = error }; + } + + public ErrorMessage Error { get; set; } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Home/HomeController.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Home/HomeController.cs new file mode 100644 index 00000000..9cf06789 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/Home/HomeController.cs @@ -0,0 +1,65 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using IdentityServer4.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; + +namespace IdentityServerHost.Quickstart.UI +{ + [SecurityHeaders] + [AllowAnonymous] + public class HomeController : Controller + { + private readonly IIdentityServerInteractionService _interaction; + private readonly IWebHostEnvironment _environment; + private readonly ILogger _logger; + + public HomeController(IIdentityServerInteractionService interaction, IWebHostEnvironment environment, ILogger logger) + { + _interaction = interaction; + _environment = environment; + _logger = logger; + } + + public IActionResult Index() + { + if (_environment.IsDevelopment()) + { + // only show in development + return View(); + } + + _logger.LogInformation("Homepage is disabled in production. Returning 404."); + return NotFound(); + } + + /// + /// Shows the error page + /// + public async Task Error(string errorId) + { + var vm = new ErrorViewModel(); + + // retrieve error details from identityserver + var message = await _interaction.GetErrorContextAsync(errorId); + if (message != null) + { + vm.Error = message; + + if (!_environment.IsDevelopment()) + { + // only show in development + message.ErrorDescription = null; + } + } + + return View("Error", vm); + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/SecurityHeadersAttribute.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/SecurityHeadersAttribute.cs new file mode 100644 index 00000000..93ca67ff --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/SecurityHeadersAttribute.cs @@ -0,0 +1,56 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace IdentityServerHost.Quickstart.UI +{ + public class SecurityHeadersAttribute : ActionFilterAttribute + { + public override void OnResultExecuting(ResultExecutingContext context) + { + var result = context.Result; + if (result is ViewResult) + { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options + if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Type-Options")) + { + context.HttpContext.Response.Headers.Add("X-Content-Type-Options", "nosniff"); + } + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options + if (!context.HttpContext.Response.Headers.ContainsKey("X-Frame-Options")) + { + context.HttpContext.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN"); + } + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + var csp = "default-src 'self'; object-src 'none'; frame-ancestors 'none'; sandbox allow-forms allow-same-origin allow-scripts; base-uri 'self';"; + // also consider adding upgrade-insecure-requests once you have HTTPS in place for production + //csp += "upgrade-insecure-requests;"; + // also an example if you need client images to be displayed from twitter + // csp += "img-src 'self' https://pbs.twimg.com;"; + + // once for standards compliant browsers + if (!context.HttpContext.Response.Headers.ContainsKey("Content-Security-Policy")) + { + context.HttpContext.Response.Headers.Add("Content-Security-Policy", csp); + } + // and once again for IE + if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Security-Policy")) + { + context.HttpContext.Response.Headers.Add("X-Content-Security-Policy", csp); + } + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy + var referrer_policy = "no-referrer"; + if (!context.HttpContext.Response.Headers.ContainsKey("Referrer-Policy")) + { + context.HttpContext.Response.Headers.Add("Referrer-Policy", referrer_policy); + } + } + } + } +} diff --git a/IRaCIS.Core.IdentityServer4.MVC/Quickstart/TestUsers.cs b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/TestUsers.cs new file mode 100644 index 00000000..39b96c8a --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Quickstart/TestUsers.cs @@ -0,0 +1,66 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using IdentityModel; +using IdentityServer4; +using IdentityServer4.Test; +using System.Collections.Generic; +using System.Security.Claims; +using System.Text.Json; + +namespace IdentityServerHost.Quickstart.UI +{ + public class TestUsers + { + public static List Users + { + get + { + var address = new + { + street_address = "One Hacker Way", + locality = "Heidelberg", + postal_code = 69118, + country = "Germany" + }; + + return new List + { + new TestUser + { + SubjectId = "818727", + Username = "alice", + Password = "alice", + Claims = + { + new Claim(JwtClaimTypes.Name, "Alice Smith"), + new Claim(JwtClaimTypes.GivenName, "Alice"), + new Claim(JwtClaimTypes.FamilyName, "Smith"), + new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"), + new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), + new Claim(JwtClaimTypes.WebSite, "http://alice.com"), + new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json) + } + }, + new TestUser + { + SubjectId = "88421113", + Username = "bob", + Password = "bob", + Claims = + { + new Claim(JwtClaimTypes.Name, "Bob Smith"), + new Claim(JwtClaimTypes.GivenName, "Bob"), + new Claim(JwtClaimTypes.FamilyName, "Smith"), + new Claim(JwtClaimTypes.Email, "BobSmith@email.com"), + new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), + new Claim(JwtClaimTypes.WebSite, "http://bob.com"), + new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json) + } + } + }; + } + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Startup.cs b/IRaCIS.Core.IdentityServer4.MVC/Startup.cs new file mode 100644 index 00000000..61388ff1 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Startup.cs @@ -0,0 +1,81 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using IdentityServer4; +using IdentityServerHost.Quickstart.UI; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace IRaCIS.Core.IdentityServer4.MVC +{ + public class Startup + { + public IWebHostEnvironment Environment { get; } + public IConfiguration Configuration { get; } + + public Startup(IWebHostEnvironment environment, IConfiguration configuration) + { + Environment = environment; + Configuration = configuration; + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddControllersWithViews(); + + var builder = services.AddIdentityServer(options => + { + options.Events.RaiseErrorEvents = true; + options.Events.RaiseInformationEvents = true; + options.Events.RaiseFailureEvents = true; + options.Events.RaiseSuccessEvents = true; + + // see https://identityserver4.readthedocs.io/en/latest/topics/resources.html + options.EmitStaticAudienceClaim = true; + }) + .AddTestUsers(TestUsers.Users); + + // in-memory, code config + builder.AddInMemoryIdentityResources(Config.IdentityResources); + builder.AddInMemoryApiScopes(Config.ApiScopes); + builder.AddInMemoryClients(Config.Clients); + + // not recommended for production - you need to store your key material somewhere secure + builder.AddDeveloperSigningCredential(); + + services.AddAuthentication() + .AddGoogle(options => + { + options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; + + // register your IdentityServer with Google at https://console.developers.google.com + // enable the Google+ API + // set the redirect URI to https://localhost:5001/signin-google + options.ClientId = "copy client ID from Google here"; + options.ClientSecret = "copy client secret from Google here"; + }); + } + + public void Configure(IApplicationBuilder app) + { + if (Environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseStaticFiles(); + + app.UseRouting(); + app.UseIdentityServer(); + app.UseAuthorization(); + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + }); + } + } +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Views/Account/AccessDenied.cshtml b/IRaCIS.Core.IdentityServer4.MVC/Views/Account/AccessDenied.cshtml new file mode 100644 index 00000000..0745189e --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Views/Account/AccessDenied.cshtml @@ -0,0 +1,7 @@ + +
+
+

Access Denied

+

You do not have access to that resource.

+
+
\ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Views/Account/LoggedOut.cshtml b/IRaCIS.Core.IdentityServer4.MVC/Views/Account/LoggedOut.cshtml new file mode 100644 index 00000000..dc9fbf7a --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Views/Account/LoggedOut.cshtml @@ -0,0 +1,34 @@ +@model LoggedOutViewModel + +@{ + // set this so the layout rendering sees an anonymous user + ViewData["signed-out"] = true; +} + +
+

+ Logout + You are now logged out +

+ + @if (Model.PostLogoutRedirectUri != null) + { +
+ Click here to return to the + @Model.ClientName application. +
+ } + + @if (Model.SignOutIframeUrl != null) + { + + } +
+ +@section scripts +{ + @if (Model.AutomaticRedirectAfterSignOut) + { + + } +} diff --git a/IRaCIS.Core.IdentityServer4.MVC/Views/Account/Login.cshtml b/IRaCIS.Core.IdentityServer4.MVC/Views/Account/Login.cshtml new file mode 100644 index 00000000..e4ccb1d8 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Views/Account/Login.cshtml @@ -0,0 +1,87 @@ +@model LoginViewModel + + \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Views/Account/Logout.cshtml b/IRaCIS.Core.IdentityServer4.MVC/Views/Account/Logout.cshtml new file mode 100644 index 00000000..b49dee09 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Views/Account/Logout.cshtml @@ -0,0 +1,15 @@ +@model LogoutViewModel + +
+
+

Logout

+

Would you like to logut of IdentityServer?

+
+ +
+ +
+ +
+
+
diff --git a/IRaCIS.Core.IdentityServer4.MVC/Views/Consent/Index.cshtml b/IRaCIS.Core.IdentityServer4.MVC/Views/Consent/Index.cshtml new file mode 100644 index 00000000..f8aa10d6 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Views/Consent/Index.cshtml @@ -0,0 +1,104 @@ +@model ConsentViewModel + + diff --git a/IRaCIS.Core.IdentityServer4.MVC/Views/Device/Success.cshtml b/IRaCIS.Core.IdentityServer4.MVC/Views/Device/Success.cshtml new file mode 100644 index 00000000..050dd91c --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Views/Device/Success.cshtml @@ -0,0 +1,7 @@ + +
+
+

Success

+

You have successfully authorized the device

+
+
diff --git a/IRaCIS.Core.IdentityServer4.MVC/Views/Device/UserCodeCapture.cshtml b/IRaCIS.Core.IdentityServer4.MVC/Views/Device/UserCodeCapture.cshtml new file mode 100644 index 00000000..6d412613 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Views/Device/UserCodeCapture.cshtml @@ -0,0 +1,23 @@ +@model string + +
+
+

User Code

+

Please enter the code displayed on your device.

+
+ + + +
+
+
+
+ + +
+ + +
+
+
+
diff --git a/IRaCIS.Core.IdentityServer4.MVC/Views/Device/UserCodeConfirmation.cshtml b/IRaCIS.Core.IdentityServer4.MVC/Views/Device/UserCodeConfirmation.cshtml new file mode 100644 index 00000000..e1d3b196 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Views/Device/UserCodeConfirmation.cshtml @@ -0,0 +1,108 @@ +@model DeviceAuthorizationViewModel + +
+
+ @if (Model.ClientLogoUrl != null) + { + + } +

+ @Model.ClientName + is requesting your permission +

+ @if (Model.ConfirmUserCode) + { +

Please confirm that the authorization request quotes the code: @Model.UserCode.

+ } +

Uncheck the permissions you do not wish to grant.

+
+ +
+
+ +
+
+ +
+ +
+
+ @if (Model.IdentityScopes.Any()) + { +
+
+
+ + Personal Information +
+
    + @foreach (var scope in Model.IdentityScopes) + { + + } +
+
+
+ } + + @if (Model.ApiScopes.Any()) + { +
+
+
+ + Application Access +
+
    + @foreach (var scope in Model.ApiScopes) + { + + } +
+
+
+ } + +
+
+
+ + Description +
+
+ +
+
+
+ + @if (Model.AllowRememberConsent) + { +
+
+ + +
+
+ } +
+
+ +
+
+ + +
+
+ @if (Model.ClientUrl != null) + { + + + @Model.ClientName + + } +
+
+
+
diff --git a/IRaCIS.Core.IdentityServer4.MVC/Views/Diagnostics/Index.cshtml b/IRaCIS.Core.IdentityServer4.MVC/Views/Diagnostics/Index.cshtml new file mode 100644 index 00000000..e939c0d8 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Views/Diagnostics/Index.cshtml @@ -0,0 +1,64 @@ +@model DiagnosticsViewModel + +
+
+

Authentication Cookie

+
+ +
+
+
+
+

Claims

+
+
+
+ @foreach (var claim in Model.AuthenticateResult.Principal.Claims) + { +
@claim.Type
+
@claim.Value
+ } +
+
+
+
+ +
+
+
+

Properties

+
+
+
+ @foreach (var prop in Model.AuthenticateResult.Properties.Items) + { +
@prop.Key
+
@prop.Value
+ } + @if (Model.Clients.Any()) + { +
Clients
+
+ @{ + var clients = Model.Clients.ToArray(); + for(var i = 0; i < clients.Length; i++) + { + @clients[i] + if (i < clients.Length - 1) + { + , + } + } + } +
+ } +
+
+
+
+
+
+ + + + diff --git a/IRaCIS.Core.IdentityServer4.MVC/Views/Grants/Index.cshtml b/IRaCIS.Core.IdentityServer4.MVC/Views/Grants/Index.cshtml new file mode 100644 index 00000000..a60f2261 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Views/Grants/Index.cshtml @@ -0,0 +1,87 @@ +@model GrantsViewModel + +
+
+

Client Application Permissions

+

Below is the list of applications you have given permission to and the resources they have access to.

+
+ + @if (Model.Grants.Any() == false) + { +
+
+
+ You have not given access to any applications +
+
+
+ } + else + { + foreach (var grant in Model.Grants) + { +
+
+
+
+ @if (grant.ClientLogoUrl != null) + { + + } + @grant.ClientName +
+ +
+
+ + +
+
+
+
+ +
    + @if (grant.Description != null) + { +
  • + @grant.Description +
  • + } +
  • + @grant.Created.ToString("yyyy-MM-dd") +
  • + @if (grant.Expires.HasValue) + { +
  • + @grant.Expires.Value.ToString("yyyy-MM-dd") +
  • + } + @if (grant.IdentityGrantNames.Any()) + { +
  • + +
      + @foreach (var name in grant.IdentityGrantNames) + { +
    • @name
    • + } +
    +
  • + } + @if (grant.ApiGrantNames.Any()) + { +
  • + +
      + @foreach (var name in grant.ApiGrantNames) + { +
    • @name
    • + } +
    +
  • + } +
+
+ } + } +
\ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Views/Home/Index.cshtml b/IRaCIS.Core.IdentityServer4.MVC/Views/Home/Index.cshtml new file mode 100644 index 00000000..36b2bfc3 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Views/Home/Index.cshtml @@ -0,0 +1,32 @@ +@using System.Diagnostics + +@{ + var version = FileVersionInfo.GetVersionInfo(typeof(IdentityServer4.Hosting.IdentityServerMiddleware).Assembly.Location).ProductVersion.Split('+').First(); +} + +
+

+ + Welcome to IdentityServer4 + (version @version) +

+ + +
diff --git a/IRaCIS.Core.IdentityServer4.MVC/Views/Shared/Error.cshtml b/IRaCIS.Core.IdentityServer4.MVC/Views/Shared/Error.cshtml new file mode 100644 index 00000000..4c746e77 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Views/Shared/Error.cshtml @@ -0,0 +1,40 @@ +@model ErrorViewModel + +@{ + var error = Model?.Error?.Error; + var errorDescription = Model?.Error?.ErrorDescription; + var request_id = Model?.Error?.RequestId; +} + +
+
+

Error

+
+ +
+
+
+ Sorry, there was an error + + @if (error != null) + { + + + : @error + + + + if (errorDescription != null) + { +
@errorDescription
+ } + } +
+ + @if (request_id != null) + { +
Request Id: @request_id
+ } +
+
+
diff --git a/IRaCIS.Core.IdentityServer4.MVC/Views/Shared/Redirect.cshtml b/IRaCIS.Core.IdentityServer4.MVC/Views/Shared/Redirect.cshtml new file mode 100644 index 00000000..ecc31c10 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Views/Shared/Redirect.cshtml @@ -0,0 +1,11 @@ +@model RedirectViewModel + +
+
+

You are now being returned to the application

+

Once complete, you may close this tab.

+
+
+ + + diff --git a/IRaCIS.Core.IdentityServer4.MVC/Views/Shared/_Layout.cshtml b/IRaCIS.Core.IdentityServer4.MVC/Views/Shared/_Layout.cshtml new file mode 100644 index 00000000..35605f44 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Views/Shared/_Layout.cshtml @@ -0,0 +1,28 @@ + + + + + + + + IdentityServer4 + + + + + + + + + + +
+ @RenderBody() +
+ + + + + @RenderSection("scripts", required: false) + + diff --git a/IRaCIS.Core.IdentityServer4.MVC/Views/Shared/_Nav.cshtml b/IRaCIS.Core.IdentityServer4.MVC/Views/Shared/_Nav.cshtml new file mode 100644 index 00000000..6ab6b702 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Views/Shared/_Nav.cshtml @@ -0,0 +1,33 @@ +@using IdentityServer4.Extensions + +@{ + string name = null; + if (!true.Equals(ViewData["signed-out"])) + { + name = Context.User?.GetDisplayName(); + } +} + + diff --git a/IRaCIS.Core.IdentityServer4.MVC/Views/Shared/_ScopeListItem.cshtml b/IRaCIS.Core.IdentityServer4.MVC/Views/Shared/_ScopeListItem.cshtml new file mode 100644 index 00000000..15eebcbd --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Views/Shared/_ScopeListItem.cshtml @@ -0,0 +1,34 @@ +@model ScopeViewModel + +
  • + + @if (Model.Required) + { + (required) + } + @if (Model.Description != null) + { + + } +
  • \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Views/Shared/_ValidationSummary.cshtml b/IRaCIS.Core.IdentityServer4.MVC/Views/Shared/_ValidationSummary.cshtml new file mode 100644 index 00000000..674d68d8 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Views/Shared/_ValidationSummary.cshtml @@ -0,0 +1,7 @@ +@if (ViewContext.ModelState.IsValid == false) +{ +
    + Error +
    +
    +} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/Views/_ViewImports.cshtml b/IRaCIS.Core.IdentityServer4.MVC/Views/_ViewImports.cshtml new file mode 100644 index 00000000..d0a0bc13 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Views/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@using IdentityServerHost.Quickstart.UI +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/IRaCIS.Core.IdentityServer4.MVC/Views/_ViewStart.cshtml b/IRaCIS.Core.IdentityServer4.MVC/Views/_ViewStart.cshtml new file mode 100644 index 00000000..a5f10045 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/IRaCIS.Core.IdentityServer4.MVC/updateUI.ps1 b/IRaCIS.Core.IdentityServer4.MVC/updateUI.ps1 new file mode 100644 index 00000000..52b63df5 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/updateUI.ps1 @@ -0,0 +1,172 @@ +iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/IdentityServer/IdentityServer4.Quickstart.UI/main/getmain.ps1')) +# SIG # Begin signature block +# MIIfngYJKoZIhvcNAQcCoIIfjzCCH4sCAQExDzANBglghkgBZQMEAgEFADB5Bgor +# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG +# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD+C49Q4fXWI1wP +# /GfU9LlK8JuQDQCq3vqITQmGwpJkQqCCDgcwggPFMIICraADAgECAhACrFwmagtA +# m48LefKuRiV3MA0GCSqGSIb3DQEBBQUAMGwxCzAJBgNVBAYTAlVTMRUwEwYDVQQK +# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xKzApBgNV +# BAMTIkRpZ2lDZXJ0IEhpZ2ggQXNzdXJhbmNlIEVWIFJvb3QgQ0EwHhcNMDYxMTEw +# MDAwMDAwWhcNMzExMTEwMDAwMDAwWjBsMQswCQYDVQQGEwJVUzEVMBMGA1UEChMM +# RGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSswKQYDVQQD +# EyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMIIBIjANBgkqhkiG +# 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxszlc+b71LvlLS0ypt/lgT/JzSVJtnEqw9WU +# NGeiChywX2mmQLHEt7KP0JikqUFZOtPclNY823Q4pErMTSWC90qlUxI47vNJbXGR +# fmO2q6Zfw6SE+E9iUb74xezbOJLjBuUIkQzEKEFV+8taiRV+ceg1v01yCT2+OjhQ +# W3cxG42zxyRFmqesbQAUWgS3uhPrUQqYQUEiTmVhh4FBUKZ5XIneGUpX1S7mXRxT +# LH6YzRoGFqRoc9A0BBNcoXHTWnxV215k4TeHMFYE5RG0KYAS8Xk5iKICEXwnZreI +# t3jyygqoOKsKZMK/Zl2VhMGhJR6HXRpQCyASzEG7bgtROLhLywIDAQABo2MwYTAO +# BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUsT7DaQP4 +# v0cB1JgmGggC72NkK8MwHwYDVR0jBBgwFoAUsT7DaQP4v0cB1JgmGggC72NkK8Mw +# DQYJKoZIhvcNAQEFBQADggEBABwaBpfc15yfPIhmBghXIdshR/gqZ6q/GDJ2QBBX +# wYrzetkRZY41+p78RbWe2UwxS7iR6EMsjrN4ztvjU3lx1uUhlAHaVYeaJGT2imbM +# 3pw3zag0sWmbI8ieeCIrcEPjVUcxYRnvWMWFL04w9qAxFiPI5+JlFjPLvxoboD34 +# yl6LMYtgCIktDAZcUrfE+QqY0RVfnxK+fDZjOL1EpH/kJisKxJdpDemM4sAQV7jI +# dhKRVfJIadi8KgJbD0TUIDHb9LpwJl2QYJ68SxcJL7TLHkNoyQcnwdJc9+ohuWgS +# nDycv578gFybY83sR6olJ2egN/MAgn1U16n46S4To3foH0owggSRMIIDeaADAgEC +# AhAHsEGNpR4UjDMbvN63E4MjMA0GCSqGSIb3DQEBCwUAMGwxCzAJBgNVBAYTAlVT +# MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +# b20xKzApBgNVBAMTIkRpZ2lDZXJ0IEhpZ2ggQXNzdXJhbmNlIEVWIFJvb3QgQ0Ew +# HhcNMTgwNDI3MTI0MTU5WhcNMjgwNDI3MTI0MTU5WjBaMQswCQYDVQQGEwJVUzEY +# MBYGA1UEChMPLk5FVCBGb3VuZGF0aW9uMTEwLwYDVQQDEyguTkVUIEZvdW5kYXRp +# b24gUHJvamVjdHMgQ29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +# AQ8AMIIBCgKCAQEAwQqv4aI0CI20XeYqTTZmyoxsSQgcCBGQnXnufbuDLhAB6GoT +# NB7HuEhNSS8ftV+6yq8GztBzYAJ0lALdBjWypMfL451/84AO5ZiZB3V7MB2uxgWo +# cV1ekDduU9bm1Q48jmR4SVkLItC+oQO/FIA2SBudVZUvYKeCJS5Ri9ibV7La4oo7 +# BJChFiP8uR+v3OU33dgm5BBhWmth4oTyq22zCfP3NO6gBWEIPFR5S+KcefUTYmn2 +# o7IvhvxzJsMCrNH1bxhwOyMl+DQcdWiVPuJBKDOO/hAKIxBG4i6ryQYBaKdhDgaA +# NSCik0UgZasz8Qgl8n0A73+dISPumD8L/4mdywIDAQABo4IBPzCCATswHQYDVR0O +# BBYEFMtck66Im/5Db1ZQUgJtePys4bFaMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSY +# JhoIAu9jZCvDMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzAS +# BgNVHRMBAf8ECDAGAQH/AgEAMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYY +# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEsGA1UdHwREMEIwQKA+oDyGOmh0dHA6 +# Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEhpZ2hBc3N1cmFuY2VFVlJvb3RD +# QS5jcmwwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8v +# d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwDQYJKoZIhvcNAQELBQADggEBALNGxKTz6gq6 +# clMF01GjC3RmJ/ZAoK1V7rwkqOkY3JDl++v1F4KrFWEzS8MbZsI/p4W31Eketazo +# Nxy23RT0zDsvJrwEC3R+/MRdkB7aTecsYmMeMHgtUrl3xEO3FubnQ0kKEU/HBCTd +# hR14GsQEccQQE6grFVlglrew+FzehWUu3SUQEp9t+iWpX/KfviDWx0H1azilMX15 +# lzJUxK7kCzmflrk5jCOCjKqhOdGJoQqstmwP+07qXO18bcCzEC908P+TYkh0z9gV +# rlj7tyW9K9zPVPJZsLRaBp/QjMcH65o9Y1hD1uWtFQYmbEYkT1K9tuXHtQYx1Rpf +# /dC8Nbl4iukwggWlMIIEjaADAgECAhAL5Ofkz0TFYBmonpg7pehYMA0GCSqGSIb3 +# DQEBCwUAMFoxCzAJBgNVBAYTAlVTMRgwFgYDVQQKEw8uTkVUIEZvdW5kYXRpb24x +# MTAvBgNVBAMTKC5ORVQgRm91bmRhdGlvbiBQcm9qZWN0cyBDb2RlIFNpZ25pbmcg +# Q0EwHhcNMTgwNjExMDAwMDAwWhcNMjEwNjE1MTIwMDAwWjCBoDEUMBIGA1UEBRML +# NjAzIDM4OSAwNjgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw +# DgYDVQQHEwdSZWRtb25kMSkwJwYDVQQKEyBJZGVudGl0eVNlcnZlciAoLk5FVCBG +# b3VuZGF0aW9uKTEpMCcGA1UEAxMgSWRlbnRpdHlTZXJ2ZXIgKC5ORVQgRm91bmRh +# dGlvbikwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwEhFpSIYi3hrr +# v/X9BtZkzufk7puhmTCVmQAPNm2R1eZZyMPhfxm5Sh/w/42CzlCG9LFgooha69z3 +# uoMMOKJEEQKZ6ByIV+r81o4lrHtSFbe4VlXavjQVFaVVjPSG6vWGykfHVCAeVpjx +# fVk/HH6tEX506lBiHgOrQGogoQrwdVnObc3c6RiVSIuvFeCoHvk2GgiqyzFER7iO +# R1055npVSAAAdxBvPA6KREcLb/qHukYCJZX4mY/SajBXwxupSnhRDbYhb+qHpFIL +# x7s/azxg7tVRpVh49oJimHA2uZ/jzh/KgUsUe9MFzT7KPduBK/pfX/fXED9Pt1NN +# 48VfPSuzAgMBAAGjggIeMIICGjAfBgNVHSMEGDAWgBTLXJOuiJv+Q29WUFICbXj8 +# rOGxWjAdBgNVHQ4EFgQU3CQnBPLvFkovKU/is0/LgQF/49swNAYDVR0RBC0wK6Ap +# BggrBgEFBQcIA6AdMBsMGVVTLVdBU0hJTkdUT04tNjAzIDM4OSAwNjgwDgYDVR0P +# AQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMIGZBgNVHR8EgZEwgY4wRaBD +# oEGGP2h0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9ORVRGb3VuZGF0aW9uUHJvamVj +# dHNDb2RlU2lnbmluZ0NBLmNybDBFoEOgQYY/aHR0cDovL2NybDQuZGlnaWNlcnQu +# Y29tL05FVEZvdW5kYXRpb25Qcm9qZWN0c0NvZGVTaWduaW5nQ0EuY3JsMEwGA1Ud +# IARFMEMwNwYJYIZIAYb9bAMBMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRp +# Z2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQQBMIGEBggrBgEFBQcBAQR4MHYwJAYIKwYB +# BQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBOBggrBgEFBQcwAoZCaHR0 +# cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL05FVEZvdW5kYXRpb25Qcm9qZWN0c0Nv +# ZGVTaWduaW5nQ0EuY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggEB +# AH0FqXX4p5RlC27jX6tcTEf2HT4mAiqosnYU/napWgQE2U9m73IZO+2MuiQWkUQi +# 2PLjbQQcOwfMwkt0SDaSAlfC1zhjZkZb2NcpJRg0cUAjcDzqh6hTzXRVJPD/UrW2 +# a5qBhYnSQDSWbYnVwfAQFFvnQcR5i/xnoOxq7+3LIvHoJafpsxcAFS57Vdsuw91u +# keB6uasOfvdd06Mpl9BLWZHpyEdnPIKMv6ALibTdw9lNzCQ+EmdT5Fwky8wHE8BH +# hhAdjSuGiyd+AzR3IuL96Q41h34c7pL827atOHwkkjUx+QTVkXbYoal6wwBKhi6I +# QJEhT0s/yyFFM7BrLhQpRSsxghDtMIIQ6QIBATBuMFoxCzAJBgNVBAYTAlVTMRgw +# FgYDVQQKEw8uTkVUIEZvdW5kYXRpb24xMTAvBgNVBAMTKC5ORVQgRm91bmRhdGlv +# biBQcm9qZWN0cyBDb2RlIFNpZ25pbmcgQ0ECEAvk5+TPRMVgGaiemDul6FgwDQYJ +# YIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZBgkqhkiG +# 9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIB +# FTAvBgkqhkiG9w0BCQQxIgQgxZ90qFa9Z0nsSHphbnc0TyUZkl09r0tzK+QnIENC +# +iwwDQYJKoZIhvcNAQEBBQAEggEAqmnB5uF5K9sZ8eadDpQhii/NQ4Io1XQwCGGG +# uVFi0y0DYy3gtvqH3efjYibrJnYmGtZQadT53o4W+4AwzcDxnW3gFftv+Pu3Oaah +# 0VuC8slxprLfVHs1r3qTekUj63RaaW/UqIx0n4GZ/kAkIz0mNtd29ZwRf+Z23uEz +# 4UrHxBC7CT11tQ45RhMXXzYph9GFGci70QiAKoSwy0f+CQr8z+fJPeXf8vGl2wCZ +# 5VT7wVsQHVxEyRcd9x4EHOxkyETHZGsP0bqd8tEdxFwZTL8BL2IDY8GmJx2N2/Db +# N9mBse/W2l0elly7F2soFRD/n3LMRGeNE7diTzf8PH0ISKL0T6GCDskwgg7FBgor +# BgEEAYI3AwMBMYIOtTCCDrEGCSqGSIb3DQEHAqCCDqIwgg6eAgEDMQ8wDQYJYIZI +# AWUDBAIBBQAweAYLKoZIhvcNAQkQAQSgaQRnMGUCAQEGCWCGSAGG/WwHATAxMA0G +# CWCGSAFlAwQCAQUABCCKjrx40y9swppgsYiBGuu+b7x4jH2wwWwEjUSrRhwoqQIR +# AP/CDohrjll0GQPHz35zqLgYDzIwMjAwNzAxMDczMjUwWqCCC7swggaCMIIFaqAD +# AgECAhAEzT+FaK52xhuw/nFgzKdtMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNVBAYT +# AlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2Vy +# dC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBUaW1lc3Rh +# bXBpbmcgQ0EwHhcNMTkxMDAxMDAwMDAwWhcNMzAxMDE3MDAwMDAwWjBMMQswCQYD +# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJDAiBgNVBAMTG1RJTUVT +# VEFNUC1TSEEyNTYtMjAxOS0xMC0xNTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +# AQoCggEBAOlkNZz6qZhlZBvkF9y4KTbMZwlYhU0w4Mn/5Ts8EShQrwcx4l0JGML2 +# iYxpCAQj4HctnRXluOihao7/1K7Sehbv+EG1HTl1wc8vp6xFfpRtrAMBmTxiPn56 +# /UWXMbT6t9lCPqdVm99aT1gCqDJpIhO+i4Itxpira5u0yfJlEQx0DbLwCJZ0xOiy +# SKKhFKX4+uGJcEQ7je/7pPTDub0ULOsMKCclgKsQSxYSYAtpIoxOzcbVsmVZIeB8 +# LBKNcA6Pisrg09ezOXdQ0EIsLnrOnGd6OHdUQP9PlQQg1OvIzocUCP4dgN3Q5yt4 +# 6r8fcMbuQhZTNkWbUxlJYp16ApuVFKMCAwEAAaOCAzgwggM0MA4GA1UdDwEB/wQE +# AwIHgDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMIIBvwYD +# VR0gBIIBtjCCAbIwggGhBglghkgBhv1sBwEwggGSMCgGCCsGAQUFBwIBFhxodHRw +# czovL3d3dy5kaWdpY2VydC5jb20vQ1BTMIIBZAYIKwYBBQUHAgIwggFWHoIBUgBB +# AG4AeQAgAHUAcwBlACAAbwBmACAAdABoAGkAcwAgAEMAZQByAHQAaQBmAGkAYwBh +# AHQAZQAgAGMAbwBuAHMAdABpAHQAdQB0AGUAcwAgAGEAYwBjAGUAcAB0AGEAbgBj +# AGUAIABvAGYAIAB0AGgAZQAgAEQAaQBnAGkAQwBlAHIAdAAgAEMAUAAvAEMAUABT +# ACAAYQBuAGQAIAB0AGgAZQAgAFIAZQBsAHkAaQBuAGcAIABQAGEAcgB0AHkAIABB +# AGcAcgBlAGUAbQBlAG4AdAAgAHcAaABpAGMAaAAgAGwAaQBtAGkAdAAgAGwAaQBh +# AGIAaQBsAGkAdAB5ACAAYQBuAGQAIABhAHIAZQAgAGkAbgBjAG8AcgBwAG8AcgBh +# AHQAZQBkACAAaABlAHIAZQBpAG4AIABiAHkAIAByAGUAZgBlAHIAZQBuAGMAZQAu +# MAsGCWCGSAGG/WwDFTAfBgNVHSMEGDAWgBT0tuEgHf4prtLkYaWyoiWyyBc1bjAd +# BgNVHQ4EFgQUVlMPwcYHp03X2G5XcoBQTOTsnsEwcQYDVR0fBGowaDAyoDCgLoYs +# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC10cy5jcmwwMqAw +# oC6GLGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtdHMuY3Js +# MIGFBggrBgEFBQcBAQR5MHcwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj +# ZXJ0LmNvbTBPBggrBgEFBQcwAoZDaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t +# L0RpZ2lDZXJ0U0hBMkFzc3VyZWRJRFRpbWVzdGFtcGluZ0NBLmNydDANBgkqhkiG +# 9w0BAQsFAAOCAQEALoOhRAVKBOO5MlL62YHwGrv4CY0juT3YkqHmRhxKL256PGNu +# NxejGr9YI7JDnJSDTjkJsCzox+HizO3LeWvO3iMBR+2VVIHggHsSsa8Chqk6c2r+ +# +J/BjdEhjOQpgsOKC2AAAp0fR8SftApoU39aEKb4Iub4U5IxX9iCgy1tE0Kug8EQ +# TqQk9Eec3g8icndcf0/pOZgrV5JE1+9uk9lDxwQzY1E3Vp5HBBHDo1hUIdjijlbX +# ST9X/AqfI1579JSN3Z0au996KqbSRaZVDI/2TIryls+JRtwxspGQo18zMGBV9fxr +# MKyh7eRHTjOeZ2ootU3C7VuXgvjLqQhsUwm09zCCBTEwggQZoAMCAQICEAqhJdbW +# Mht+QeQF2jaXwhUwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UEBhMCVVMxFTATBgNV +# BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIG +# A1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4XDTE2MDEwNzEyMDAw +# MFoXDTMxMDEwNzEyMDAwMFowcjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lD +# ZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGln +# aUNlcnQgU0hBMiBBc3N1cmVkIElEIFRpbWVzdGFtcGluZyBDQTCCASIwDQYJKoZI +# hvcNAQEBBQADggEPADCCAQoCggEBAL3QMu5LzY9/3am6gpnFOVQoV7YjSsQOB0Uz +# URB90Pl9TWh+57ag9I2ziOSXv2MhkJi/E7xX08PhfgjWahQAOPcuHjvuzKb2Mln+ +# X2U/4Jvr40ZHBhpVfgsnfsCi9aDg3iI/Dv9+lfvzo7oiPhisEeTwmQNtO4V8CdPu +# XciaC1TjqAlxa+DPIhAPdc9xck4Krd9AOly3UeGheRTGTSQjMF287DxgaqwvB8z9 +# 8OpH2YhQXv1mblZhJymJhFHmgudGUP2UKiyn5HU+upgPhH+fMRTWrdXyZMt7HgXQ +# hBlyF/EXBu89zdZN7wZC/aJTKk+FHcQdPK/P2qwQ9d2srOlW/5MCAwEAAaOCAc4w +# ggHKMB0GA1UdDgQWBBT0tuEgHf4prtLkYaWyoiWyyBc1bjAfBgNVHSMEGDAWgBRF +# 66Kv9JLLgjEtUYunpyGd823IDzASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB +# /wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDCDB5BggrBgEFBQcBAQRtMGswJAYI +# KwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3 +# aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v +# dENBLmNydDCBgQYDVR0fBHoweDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQu +# Y29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2Ny +# bDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBQBgNV +# HSAESTBHMDgGCmCGSAGG/WwAAgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cu +# ZGlnaWNlcnQuY29tL0NQUzALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggEB +# AHGVEulRh1Zpze/d2nyqY3qzeM8GN0CE70uEv8rPAwL9xafDDiBCLK938ysfDCFa +# KrcFNB1qrpn4J6JmvwmqYN92pDqTD/iy0dh8GWLoXoIlHsS6HHssIeLWWywUNUME +# aLLbdQLgcseY1jxk5R9IEBhfiThhTWJGJIdjjJFSLK8pieV4H9YLFKWA1xJHcLN1 +# 1ZOFk362kmf7U2GJqPVrlsD0WGkNfMgBsbkodbeZY4UijGHKeZR+WfyMD+NvtQEm +# tmyl7odRIeRYYJu6DC0rbaLEfrvEJStHAgh8Sa4TtuF8QkIoxhhWz0E0tmZdtnR7 +# 9VYzIi8iNrJLokqV2PWmjlIxggJNMIICSQIBATCBhjByMQswCQYDVQQGEwJVUzEV +# MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t +# MTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgVGltZXN0YW1waW5n +# IENBAhAEzT+FaK52xhuw/nFgzKdtMA0GCWCGSAFlAwQCAQUAoIGYMBoGCSqGSIb3 +# DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjAwNzAxMDczMjUw +# WjArBgsqhkiG9w0BCRACDDEcMBowGDAWBBQDJb1QXtqWMC3CL0+gHkwovig0xTAv +# BgkqhkiG9w0BCQQxIgQg1ROPiUz0oamrowd5hFiM4kXZA0f1AJpWU3aCalDNwy0w +# DQYJKoZIhvcNAQEBBQAEggEAQdCCEUOt8NJmW+UfehmMgjjGPSMOWiQc2R2qNfQd +# HebLcpcbzvFtNQTMek3w5i8N3rcaU7rKOaw+lLYI27F7ZRk4OOe1eaV0DRWPR8Kc +# +ipIKu0JX7oTiRoH2+AcyTr/XV8oN8Zl8QhWCCzkwEKmw1L+xekBwNBnqjd0doB1 +# FkG3n1ESnuTe6KtrSrR8bM0yvrwGKRNfhge78hPgQlyAGZMuRQgdTyHVA7IFKmYV +# ChbITr7swL+bZLAw99VFoxQOPLQ0bNZGuEvtzQmI4580lCzHbCipDVGAeydf0Sn/ +# nGiBc+5I8kG9tXFdQrNkyKI3fenSqZ5RXsw6rkn3ELb6TQ== +# SIG # End signature block diff --git a/IRaCIS.Core.IdentityServer4.MVC/wwwroot/css/site.css b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/css/site.css new file mode 100644 index 00000000..6dc1c4fc --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/css/site.css @@ -0,0 +1,24 @@ +.body-container { + margin-top: 60px; + padding-bottom: 40px; } + +.welcome-page li { + list-style: none; + padding: 4px; } + +.logged-out-page iframe { + display: none; + width: 0; + height: 0; } + +.grants-page .card { + margin-top: 20px; + border-bottom: 1px solid lightgray; } + .grants-page .card .card-title { + font-size: 120%; + font-weight: bold; } + .grants-page .card .card-title img { + width: 100px; + height: 100px; } + .grants-page .card label { + font-weight: bold; } diff --git a/IRaCIS.Core.IdentityServer4.MVC/wwwroot/css/site.min.css b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/css/site.min.css new file mode 100644 index 00000000..cc3fdade --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/css/site.min.css @@ -0,0 +1 @@ +.body-container{margin-top:60px;padding-bottom:40px;}.welcome-page li{list-style:none;padding:4px;}.logged-out-page iframe{display:none;width:0;height:0;}.grants-page .card{margin-top:20px;border-bottom:1px solid #d3d3d3;}.grants-page .card .card-title{font-size:120%;font-weight:bold;}.grants-page .card .card-title img{width:100px;height:100px;}.grants-page .card label{font-weight:bold;} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/wwwroot/css/site.scss b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/css/site.scss new file mode 100644 index 00000000..f7fa0964 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/css/site.scss @@ -0,0 +1,42 @@ +.body-container { + margin-top: 60px; + padding-bottom:40px; +} + +.welcome-page { + li { + list-style: none; + padding: 4px; + } +} + +.logged-out-page { + iframe { + display: none; + width: 0; + height: 0; + } +} + +.grants-page { + .card { + margin-top: 20px; + border-bottom: 1px solid lightgray; + + .card-title { + img { + width: 100px; + height: 100px; + } + + font-size: 120%; + font-weight: bold; + } + + label { + font-weight: bold; + } + } +} + + diff --git a/IRaCIS.Core.IdentityServer4.MVC/wwwroot/favicon.ico b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/favicon.ico new file mode 100644 index 00000000..ee470e42 Binary files /dev/null and b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/favicon.ico differ diff --git a/IRaCIS.Core.IdentityServer4.MVC/wwwroot/icon.jpg b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/icon.jpg new file mode 100644 index 00000000..e6525028 Binary files /dev/null and b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/icon.jpg differ diff --git a/IRaCIS.Core.IdentityServer4.MVC/wwwroot/icon.png b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/icon.png new file mode 100644 index 00000000..cd386d51 Binary files /dev/null and b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/icon.png differ diff --git a/IRaCIS.Core.IdentityServer4.MVC/wwwroot/js/signin-redirect.js b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/js/signin-redirect.js new file mode 100644 index 00000000..6ebc5691 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/js/signin-redirect.js @@ -0,0 +1 @@ +window.location.href = document.querySelector("meta[http-equiv=refresh]").getAttribute("data-url"); diff --git a/IRaCIS.Core.IdentityServer4.MVC/wwwroot/js/signout-redirect.js b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/js/signout-redirect.js new file mode 100644 index 00000000..cdfc5e78 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/js/signout-redirect.js @@ -0,0 +1,6 @@ +window.addEventListener("load", function () { + var a = document.querySelector("a.PostLogoutRedirectUri"); + if (a) { + window.location = a.href; + } +}); diff --git a/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/README.md b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/README.md new file mode 100644 index 00000000..d70069ee --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/README.md @@ -0,0 +1,209 @@ +

    + + Bootstrap logo + +

    + +

    Bootstrap

    + +

    + Sleek, intuitive, and powerful front-end framework for faster and easier web development. +
    + Explore Bootstrap docs » +
    +
    + Report bug + · + Request feature + · + Themes + · + Blog +

    + + +## Table of contents + +- [Quick start](#quick-start) +- [Status](#status) +- [What's included](#whats-included) +- [Bugs and feature requests](#bugs-and-feature-requests) +- [Documentation](#documentation) +- [Contributing](#contributing) +- [Community](#community) +- [Versioning](#versioning) +- [Creators](#creators) +- [Thanks](#thanks) +- [Copyright and license](#copyright-and-license) + + +## Quick start + +Several quick start options are available: + +- [Download the latest release.](https://github.com/twbs/bootstrap/archive/v4.4.1.zip) +- Clone the repo: `git clone https://github.com/twbs/bootstrap.git` +- Install with [npm](https://www.npmjs.com/): `npm install bootstrap` +- Install with [yarn](https://yarnpkg.com/): `yarn add bootstrap@4.4.1` +- Install with [Composer](https://getcomposer.org/): `composer require twbs/bootstrap:4.4.1` +- Install with [NuGet](https://www.nuget.org/): CSS: `Install-Package bootstrap` Sass: `Install-Package bootstrap.sass` + +Read the [Getting started page](https://getbootstrap.com/docs/4.4/getting-started/introduction/) for information on the framework contents, templates and examples, and more. + + +## Status + +[![Slack](https://bootstrap-slack.herokuapp.com/badge.svg)](https://bootstrap-slack.herokuapp.com/) +[![Build Status](https://github.com/twbs/bootstrap/workflows/Tests/badge.svg)](https://github.com/twbs/bootstrap/actions?workflow=Tests) +[![npm version](https://img.shields.io/npm/v/bootstrap.svg)](https://www.npmjs.com/package/bootstrap) +[![Gem version](https://img.shields.io/gem/v/bootstrap.svg)](https://rubygems.org/gems/bootstrap) +[![Meteor Atmosphere](https://img.shields.io/badge/meteor-twbs%3Abootstrap-blue.svg)](https://atmospherejs.com/twbs/bootstrap) +[![Packagist Prerelease](https://img.shields.io/packagist/vpre/twbs/bootstrap.svg)](https://packagist.org/packages/twbs/bootstrap) +[![NuGet](https://img.shields.io/nuget/vpre/bootstrap.svg)](https://www.nuget.org/packages/bootstrap/absoluteLatest) +[![peerDependencies Status](https://img.shields.io/david/peer/twbs/bootstrap.svg)](https://david-dm.org/twbs/bootstrap?type=peer) +[![devDependency Status](https://img.shields.io/david/dev/twbs/bootstrap.svg)](https://david-dm.org/twbs/bootstrap?type=dev) +[![Coverage Status](https://img.shields.io/coveralls/github/twbs/bootstrap/v4-dev.svg)](https://coveralls.io/github/twbs/bootstrap?branch=v4-dev) +[![CSS gzip size](https://img.badgesize.io/twbs/bootstrap/v4-dev/dist/css/bootstrap.min.css?compression=gzip&label=CSS+gzip+size)](https://github.com/twbs/bootstrap/tree/v4-dev/dist/css/bootstrap.min.css) +[![JS gzip size](https://img.badgesize.io/twbs/bootstrap/v4-dev/dist/js/bootstrap.min.js?compression=gzip&label=JS+gzip+size)](https://github.com/twbs/bootstrap/tree/v4-dev/dist/js/bootstrap.min.js) +[![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=SkxZcStBeExEdVJqQ2hWYnlWckpkNmNEY213SFp6WHFETWk2bGFuY3pCbz0tLXhqbHJsVlZhQnRBdEpod3NLSDMzaHc9PQ==--3d0b75245708616eb93113221beece33e680b229)](https://www.browserstack.com/automate/public-build/SkxZcStBeExEdVJqQ2hWYnlWckpkNmNEY213SFp6WHFETWk2bGFuY3pCbz0tLXhqbHJsVlZhQnRBdEpod3NLSDMzaHc9PQ==--3d0b75245708616eb93113221beece33e680b229) +[![Backers on Open Collective](https://opencollective.com/bootstrap/backers/badge.svg)](#backers) +[![Sponsors on Open Collective](https://opencollective.com/bootstrap/sponsors/badge.svg)](#sponsors) + + +## What's included + +Within the download you'll find the following directories and files, logically grouping common assets and providing both compiled and minified variations. You'll see something like this: + +```text +bootstrap/ +└── dist/ + ├── css/ + │ ├── bootstrap-grid.css + │ ├── bootstrap-grid.css.map + │ ├── bootstrap-grid.min.css + │ ├── bootstrap-grid.min.css.map + │ ├── bootstrap-reboot.css + │ ├── bootstrap-reboot.css.map + │ ├── bootstrap-reboot.min.css + │ ├── bootstrap-reboot.min.css.map + │ ├── bootstrap.css + │ ├── bootstrap.css.map + │ ├── bootstrap.min.css + │ └── bootstrap.min.css.map + └── js/ + ├── bootstrap.bundle.js + ├── bootstrap.bundle.js.map + ├── bootstrap.bundle.min.js + ├── bootstrap.bundle.min.js.map + ├── bootstrap.js + ├── bootstrap.js.map + ├── bootstrap.min.js + └── bootstrap.min.js.map +``` + +We provide compiled CSS and JS (`bootstrap.*`), as well as compiled and minified CSS and JS (`bootstrap.min.*`). [source maps](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps) (`bootstrap.*.map`) are available for use with certain browsers' developer tools. Bundled JS files (`bootstrap.bundle.js` and minified `bootstrap.bundle.min.js`) include [Popper](https://popper.js.org/), but not [jQuery](https://jquery.com/). + + +## Bugs and feature requests + +Have a bug or a feature request? Please first read the [issue guidelines](https://github.com/twbs/bootstrap/blob/master/CONTRIBUTING.md#using-the-issue-tracker) and search for existing and closed issues. If your problem or idea is not addressed yet, [please open a new issue](https://github.com/twbs/bootstrap/issues/new). + + +## Documentation + +Bootstrap's documentation, included in this repo in the root directory, is built with [Jekyll](https://jekyllrb.com/) and publicly hosted on GitHub Pages at . The docs may also be run locally. + +Documentation search is powered by [Algolia's DocSearch](https://community.algolia.com/docsearch/). Working on our search? Be sure to set `debug: true` in `site/docs/4.4/assets/js/src/search.js` file. + +### Running documentation locally + +1. Run through the [tooling setup](https://getbootstrap.com/docs/4.4/getting-started/build-tools/#tooling-setup) to install Jekyll (the site builder) and other Ruby dependencies with `bundle install`. +2. Run `npm install` to install Node.js dependencies. +3. Run `npm start` to compile CSS and JavaScript files, generate our docs, and watch for changes. +4. Open `http://localhost:9001` in your browser, and voilà. + +Learn more about using Jekyll by reading its [documentation](https://jekyllrb.com/docs/). + +### Documentation for previous releases + +You can find all our previous releases docs on . + +[Previous releases](https://github.com/twbs/bootstrap/releases) and their documentation are also available for download. + + +## Contributing + +Please read through our [contributing guidelines](https://github.com/twbs/bootstrap/blob/master/CONTRIBUTING.md). Included are directions for opening issues, coding standards, and notes on development. + +Moreover, if your pull request contains JavaScript patches or features, you must include [relevant unit tests](https://github.com/twbs/bootstrap/tree/master/js/tests). All HTML and CSS should conform to the [Code Guide](https://github.com/mdo/code-guide), maintained by [Mark Otto](https://github.com/mdo). + +Editor preferences are available in the [editor config](https://github.com/twbs/bootstrap/blob/master/.editorconfig) for easy use in common text editors. Read more and download plugins at . + + +## Community + +Get updates on Bootstrap's development and chat with the project maintainers and community members. + +- Follow [@getbootstrap on Twitter](https://twitter.com/getbootstrap). +- Read and subscribe to [The Official Bootstrap Blog](https://blog.getbootstrap.com/). +- Join [the official Slack room](https://bootstrap-slack.herokuapp.com/). +- Chat with fellow Bootstrappers in IRC. On the `irc.freenode.net` server, in the `##bootstrap` channel. +- Implementation help may be found at Stack Overflow (tagged [`bootstrap-4`](https://stackoverflow.com/questions/tagged/bootstrap-4)). +- Developers should use the keyword `bootstrap` on packages which modify or add to the functionality of Bootstrap when distributing through [npm](https://www.npmjs.com/browse/keyword/bootstrap) or similar delivery mechanisms for maximum discoverability. + + +## Versioning + +For transparency into our release cycle and in striving to maintain backward compatibility, Bootstrap is maintained under [the Semantic Versioning guidelines](https://semver.org/). Sometimes we screw up, but we adhere to those rules whenever possible. + +See [the Releases section of our GitHub project](https://github.com/twbs/bootstrap/releases) for changelogs for each release version of Bootstrap. Release announcement posts on [the official Bootstrap blog](https://blog.getbootstrap.com/) contain summaries of the most noteworthy changes made in each release. + + +## Creators + +**Mark Otto** + +- +- + +**Jacob Thornton** + +- +- + + +## Thanks + + + BrowserStack Logo + + +Thanks to [BrowserStack](https://www.browserstack.com/) for providing the infrastructure that allows us to test in real browsers! + + +## Backers + +Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/bootstrap#backer)] + +[![Bakers](https://opencollective.com/bootstrap/backers.svg?width=890)](https://opencollective.com/bootstrap#backers) + + +## Sponsors + +Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/bootstrap#sponsor)] + +[![](https://opencollective.com/bootstrap/sponsor/0/avatar.svg)](https://opencollective.com/bootstrap/sponsor/0/website) +[![](https://opencollective.com/bootstrap/sponsor/1/avatar.svg)](https://opencollective.com/bootstrap/sponsor/1/website) +[![](https://opencollective.com/bootstrap/sponsor/2/avatar.svg)](https://opencollective.com/bootstrap/sponsor/2/website) +[![](https://opencollective.com/bootstrap/sponsor/3/avatar.svg)](https://opencollective.com/bootstrap/sponsor/3/website) +[![](https://opencollective.com/bootstrap/sponsor/4/avatar.svg)](https://opencollective.com/bootstrap/sponsor/4/website) +[![](https://opencollective.com/bootstrap/sponsor/5/avatar.svg)](https://opencollective.com/bootstrap/sponsor/5/website) +[![](https://opencollective.com/bootstrap/sponsor/6/avatar.svg)](https://opencollective.com/bootstrap/sponsor/6/website) +[![](https://opencollective.com/bootstrap/sponsor/7/avatar.svg)](https://opencollective.com/bootstrap/sponsor/7/website) +[![](https://opencollective.com/bootstrap/sponsor/8/avatar.svg)](https://opencollective.com/bootstrap/sponsor/8/website) +[![](https://opencollective.com/bootstrap/sponsor/9/avatar.svg)](https://opencollective.com/bootstrap/sponsor/9/website) + + +## Copyright and license + +Code and documentation copyright 2011-2019 the [Bootstrap Authors](https://github.com/twbs/bootstrap/graphs/contributors) and [Twitter, Inc.](https://twitter.com) Code released under the [MIT License](https://github.com/twbs/bootstrap/blob/master/LICENSE). Docs released under [Creative Commons](https://creativecommons.org/licenses/by/3.0/). diff --git a/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css new file mode 100644 index 00000000..259a9e2c --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css @@ -0,0 +1,3899 @@ +/*! + * Bootstrap Grid v4.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +html { + box-sizing: border-box; + -ms-overflow-style: scrollbar; +} + +*, +*::before, +*::after { + box-sizing: inherit; +} + +.container { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1140px; + } +} + +.container-fluid, .container-sm, .container-md, .container-lg, .container-xl { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container, .container-sm { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container, .container-sm, .container-md { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container, .container-sm, .container-md, .container-lg { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container, .container-sm, .container-md, .container-lg, .container-xl { + max-width: 1140px; + } +} + +.row { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; +} + +.no-gutters { + margin-right: 0; + margin-left: 0; +} + +.no-gutters > .col, +.no-gutters > [class*="col-"] { + padding-right: 0; + padding-left: 0; +} + +.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, +.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm, +.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md, +.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg, +.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl, +.col-xl-auto { + position: relative; + width: 100%; + padding-right: 15px; + padding-left: 15px; +} + +.col { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; +} + +.row-cols-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; +} + +.row-cols-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; +} + +.row-cols-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; +} + +.row-cols-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; +} + +.row-cols-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; +} + +.row-cols-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; +} + +.col-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; +} + +.col-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; +} + +.col-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; +} + +.col-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; +} + +.col-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; +} + +.col-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; +} + +.col-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; +} + +.col-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; +} + +.col-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; +} + +.col-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; +} + +.col-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; +} + +.col-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; +} + +.col-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; +} + +.order-first { + -ms-flex-order: -1; + order: -1; +} + +.order-last { + -ms-flex-order: 13; + order: 13; +} + +.order-0 { + -ms-flex-order: 0; + order: 0; +} + +.order-1 { + -ms-flex-order: 1; + order: 1; +} + +.order-2 { + -ms-flex-order: 2; + order: 2; +} + +.order-3 { + -ms-flex-order: 3; + order: 3; +} + +.order-4 { + -ms-flex-order: 4; + order: 4; +} + +.order-5 { + -ms-flex-order: 5; + order: 5; +} + +.order-6 { + -ms-flex-order: 6; + order: 6; +} + +.order-7 { + -ms-flex-order: 7; + order: 7; +} + +.order-8 { + -ms-flex-order: 8; + order: 8; +} + +.order-9 { + -ms-flex-order: 9; + order: 9; +} + +.order-10 { + -ms-flex-order: 10; + order: 10; +} + +.order-11 { + -ms-flex-order: 11; + order: 11; +} + +.order-12 { + -ms-flex-order: 12; + order: 12; +} + +.offset-1 { + margin-left: 8.333333%; +} + +.offset-2 { + margin-left: 16.666667%; +} + +.offset-3 { + margin-left: 25%; +} + +.offset-4 { + margin-left: 33.333333%; +} + +.offset-5 { + margin-left: 41.666667%; +} + +.offset-6 { + margin-left: 50%; +} + +.offset-7 { + margin-left: 58.333333%; +} + +.offset-8 { + margin-left: 66.666667%; +} + +.offset-9 { + margin-left: 75%; +} + +.offset-10 { + margin-left: 83.333333%; +} + +.offset-11 { + margin-left: 91.666667%; +} + +@media (min-width: 576px) { + .col-sm { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-sm-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-sm-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-sm-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-sm-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-sm-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-sm-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-sm-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-sm-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-sm-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-sm-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-sm-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-sm-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-sm-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-sm-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-sm-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-sm-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-sm-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-sm-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-sm-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-sm-first { + -ms-flex-order: -1; + order: -1; + } + .order-sm-last { + -ms-flex-order: 13; + order: 13; + } + .order-sm-0 { + -ms-flex-order: 0; + order: 0; + } + .order-sm-1 { + -ms-flex-order: 1; + order: 1; + } + .order-sm-2 { + -ms-flex-order: 2; + order: 2; + } + .order-sm-3 { + -ms-flex-order: 3; + order: 3; + } + .order-sm-4 { + -ms-flex-order: 4; + order: 4; + } + .order-sm-5 { + -ms-flex-order: 5; + order: 5; + } + .order-sm-6 { + -ms-flex-order: 6; + order: 6; + } + .order-sm-7 { + -ms-flex-order: 7; + order: 7; + } + .order-sm-8 { + -ms-flex-order: 8; + order: 8; + } + .order-sm-9 { + -ms-flex-order: 9; + order: 9; + } + .order-sm-10 { + -ms-flex-order: 10; + order: 10; + } + .order-sm-11 { + -ms-flex-order: 11; + order: 11; + } + .order-sm-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-sm-0 { + margin-left: 0; + } + .offset-sm-1 { + margin-left: 8.333333%; + } + .offset-sm-2 { + margin-left: 16.666667%; + } + .offset-sm-3 { + margin-left: 25%; + } + .offset-sm-4 { + margin-left: 33.333333%; + } + .offset-sm-5 { + margin-left: 41.666667%; + } + .offset-sm-6 { + margin-left: 50%; + } + .offset-sm-7 { + margin-left: 58.333333%; + } + .offset-sm-8 { + margin-left: 66.666667%; + } + .offset-sm-9 { + margin-left: 75%; + } + .offset-sm-10 { + margin-left: 83.333333%; + } + .offset-sm-11 { + margin-left: 91.666667%; + } +} + +@media (min-width: 768px) { + .col-md { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-md-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-md-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-md-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-md-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-md-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-md-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-md-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-md-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-md-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-md-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-md-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-md-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-md-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-md-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-md-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-md-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-md-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-md-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-md-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-md-first { + -ms-flex-order: -1; + order: -1; + } + .order-md-last { + -ms-flex-order: 13; + order: 13; + } + .order-md-0 { + -ms-flex-order: 0; + order: 0; + } + .order-md-1 { + -ms-flex-order: 1; + order: 1; + } + .order-md-2 { + -ms-flex-order: 2; + order: 2; + } + .order-md-3 { + -ms-flex-order: 3; + order: 3; + } + .order-md-4 { + -ms-flex-order: 4; + order: 4; + } + .order-md-5 { + -ms-flex-order: 5; + order: 5; + } + .order-md-6 { + -ms-flex-order: 6; + order: 6; + } + .order-md-7 { + -ms-flex-order: 7; + order: 7; + } + .order-md-8 { + -ms-flex-order: 8; + order: 8; + } + .order-md-9 { + -ms-flex-order: 9; + order: 9; + } + .order-md-10 { + -ms-flex-order: 10; + order: 10; + } + .order-md-11 { + -ms-flex-order: 11; + order: 11; + } + .order-md-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-md-0 { + margin-left: 0; + } + .offset-md-1 { + margin-left: 8.333333%; + } + .offset-md-2 { + margin-left: 16.666667%; + } + .offset-md-3 { + margin-left: 25%; + } + .offset-md-4 { + margin-left: 33.333333%; + } + .offset-md-5 { + margin-left: 41.666667%; + } + .offset-md-6 { + margin-left: 50%; + } + .offset-md-7 { + margin-left: 58.333333%; + } + .offset-md-8 { + margin-left: 66.666667%; + } + .offset-md-9 { + margin-left: 75%; + } + .offset-md-10 { + margin-left: 83.333333%; + } + .offset-md-11 { + margin-left: 91.666667%; + } +} + +@media (min-width: 992px) { + .col-lg { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-lg-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-lg-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-lg-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-lg-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-lg-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-lg-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-lg-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-lg-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-lg-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-lg-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-lg-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-lg-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-lg-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-lg-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-lg-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-lg-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-lg-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-lg-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-lg-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-lg-first { + -ms-flex-order: -1; + order: -1; + } + .order-lg-last { + -ms-flex-order: 13; + order: 13; + } + .order-lg-0 { + -ms-flex-order: 0; + order: 0; + } + .order-lg-1 { + -ms-flex-order: 1; + order: 1; + } + .order-lg-2 { + -ms-flex-order: 2; + order: 2; + } + .order-lg-3 { + -ms-flex-order: 3; + order: 3; + } + .order-lg-4 { + -ms-flex-order: 4; + order: 4; + } + .order-lg-5 { + -ms-flex-order: 5; + order: 5; + } + .order-lg-6 { + -ms-flex-order: 6; + order: 6; + } + .order-lg-7 { + -ms-flex-order: 7; + order: 7; + } + .order-lg-8 { + -ms-flex-order: 8; + order: 8; + } + .order-lg-9 { + -ms-flex-order: 9; + order: 9; + } + .order-lg-10 { + -ms-flex-order: 10; + order: 10; + } + .order-lg-11 { + -ms-flex-order: 11; + order: 11; + } + .order-lg-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-lg-0 { + margin-left: 0; + } + .offset-lg-1 { + margin-left: 8.333333%; + } + .offset-lg-2 { + margin-left: 16.666667%; + } + .offset-lg-3 { + margin-left: 25%; + } + .offset-lg-4 { + margin-left: 33.333333%; + } + .offset-lg-5 { + margin-left: 41.666667%; + } + .offset-lg-6 { + margin-left: 50%; + } + .offset-lg-7 { + margin-left: 58.333333%; + } + .offset-lg-8 { + margin-left: 66.666667%; + } + .offset-lg-9 { + margin-left: 75%; + } + .offset-lg-10 { + margin-left: 83.333333%; + } + .offset-lg-11 { + margin-left: 91.666667%; + } +} + +@media (min-width: 1200px) { + .col-xl { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-xl-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-xl-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-xl-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-xl-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-xl-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-xl-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-xl-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-xl-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-xl-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-xl-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-xl-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-xl-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-xl-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-xl-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-xl-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-xl-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-xl-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-xl-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-xl-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-xl-first { + -ms-flex-order: -1; + order: -1; + } + .order-xl-last { + -ms-flex-order: 13; + order: 13; + } + .order-xl-0 { + -ms-flex-order: 0; + order: 0; + } + .order-xl-1 { + -ms-flex-order: 1; + order: 1; + } + .order-xl-2 { + -ms-flex-order: 2; + order: 2; + } + .order-xl-3 { + -ms-flex-order: 3; + order: 3; + } + .order-xl-4 { + -ms-flex-order: 4; + order: 4; + } + .order-xl-5 { + -ms-flex-order: 5; + order: 5; + } + .order-xl-6 { + -ms-flex-order: 6; + order: 6; + } + .order-xl-7 { + -ms-flex-order: 7; + order: 7; + } + .order-xl-8 { + -ms-flex-order: 8; + order: 8; + } + .order-xl-9 { + -ms-flex-order: 9; + order: 9; + } + .order-xl-10 { + -ms-flex-order: 10; + order: 10; + } + .order-xl-11 { + -ms-flex-order: 11; + order: 11; + } + .order-xl-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-xl-0 { + margin-left: 0; + } + .offset-xl-1 { + margin-left: 8.333333%; + } + .offset-xl-2 { + margin-left: 16.666667%; + } + .offset-xl-3 { + margin-left: 25%; + } + .offset-xl-4 { + margin-left: 33.333333%; + } + .offset-xl-5 { + margin-left: 41.666667%; + } + .offset-xl-6 { + margin-left: 50%; + } + .offset-xl-7 { + margin-left: 58.333333%; + } + .offset-xl-8 { + margin-left: 66.666667%; + } + .offset-xl-9 { + margin-left: 75%; + } + .offset-xl-10 { + margin-left: 83.333333%; + } + .offset-xl-11 { + margin-left: 91.666667%; + } +} + +.d-none { + display: none !important; +} + +.d-inline { + display: inline !important; +} + +.d-inline-block { + display: inline-block !important; +} + +.d-block { + display: block !important; +} + +.d-table { + display: table !important; +} + +.d-table-row { + display: table-row !important; +} + +.d-table-cell { + display: table-cell !important; +} + +.d-flex { + display: -ms-flexbox !important; + display: flex !important; +} + +.d-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; +} + +@media (min-width: 576px) { + .d-sm-none { + display: none !important; + } + .d-sm-inline { + display: inline !important; + } + .d-sm-inline-block { + display: inline-block !important; + } + .d-sm-block { + display: block !important; + } + .d-sm-table { + display: table !important; + } + .d-sm-table-row { + display: table-row !important; + } + .d-sm-table-cell { + display: table-cell !important; + } + .d-sm-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-sm-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 768px) { + .d-md-none { + display: none !important; + } + .d-md-inline { + display: inline !important; + } + .d-md-inline-block { + display: inline-block !important; + } + .d-md-block { + display: block !important; + } + .d-md-table { + display: table !important; + } + .d-md-table-row { + display: table-row !important; + } + .d-md-table-cell { + display: table-cell !important; + } + .d-md-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-md-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 992px) { + .d-lg-none { + display: none !important; + } + .d-lg-inline { + display: inline !important; + } + .d-lg-inline-block { + display: inline-block !important; + } + .d-lg-block { + display: block !important; + } + .d-lg-table { + display: table !important; + } + .d-lg-table-row { + display: table-row !important; + } + .d-lg-table-cell { + display: table-cell !important; + } + .d-lg-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-lg-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 1200px) { + .d-xl-none { + display: none !important; + } + .d-xl-inline { + display: inline !important; + } + .d-xl-inline-block { + display: inline-block !important; + } + .d-xl-block { + display: block !important; + } + .d-xl-table { + display: table !important; + } + .d-xl-table-row { + display: table-row !important; + } + .d-xl-table-cell { + display: table-cell !important; + } + .d-xl-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-xl-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media print { + .d-print-none { + display: none !important; + } + .d-print-inline { + display: inline !important; + } + .d-print-inline-block { + display: inline-block !important; + } + .d-print-block { + display: block !important; + } + .d-print-table { + display: table !important; + } + .d-print-table-row { + display: table-row !important; + } + .d-print-table-cell { + display: table-cell !important; + } + .d-print-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-print-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +.flex-row { + -ms-flex-direction: row !important; + flex-direction: row !important; +} + +.flex-column { + -ms-flex-direction: column !important; + flex-direction: column !important; +} + +.flex-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; +} + +.flex-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; +} + +.flex-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; +} + +.flex-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; +} + +.flex-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; +} + +.flex-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; +} + +.flex-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; +} + +.flex-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; +} + +.flex-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; +} + +.flex-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; +} + +.justify-content-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; +} + +.justify-content-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; +} + +.justify-content-center { + -ms-flex-pack: center !important; + justify-content: center !important; +} + +.justify-content-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; +} + +.justify-content-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; +} + +.align-items-start { + -ms-flex-align: start !important; + align-items: flex-start !important; +} + +.align-items-end { + -ms-flex-align: end !important; + align-items: flex-end !important; +} + +.align-items-center { + -ms-flex-align: center !important; + align-items: center !important; +} + +.align-items-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; +} + +.align-items-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; +} + +.align-content-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; +} + +.align-content-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; +} + +.align-content-center { + -ms-flex-line-pack: center !important; + align-content: center !important; +} + +.align-content-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; +} + +.align-content-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; +} + +.align-content-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; +} + +.align-self-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; +} + +.align-self-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; +} + +.align-self-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; +} + +.align-self-center { + -ms-flex-item-align: center !important; + align-self: center !important; +} + +.align-self-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; +} + +.align-self-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; +} + +@media (min-width: 576px) { + .flex-sm-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-sm-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-sm-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-sm-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-sm-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-sm-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-sm-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-sm-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-sm-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-sm-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-sm-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-sm-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-sm-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-sm-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-sm-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-sm-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-sm-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-sm-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-sm-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-sm-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-sm-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-sm-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-sm-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-sm-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-sm-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-sm-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-sm-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-sm-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-sm-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-sm-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-sm-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-sm-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-sm-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-sm-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 768px) { + .flex-md-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-md-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-md-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-md-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-md-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-md-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-md-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-md-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-md-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-md-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-md-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-md-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-md-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-md-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-md-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-md-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-md-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-md-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-md-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-md-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-md-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-md-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-md-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-md-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-md-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-md-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-md-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-md-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-md-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-md-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-md-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-md-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-md-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-md-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 992px) { + .flex-lg-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-lg-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-lg-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-lg-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-lg-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-lg-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-lg-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-lg-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-lg-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-lg-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-lg-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-lg-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-lg-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-lg-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-lg-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-lg-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-lg-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-lg-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-lg-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-lg-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-lg-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-lg-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-lg-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-lg-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-lg-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-lg-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-lg-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-lg-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-lg-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-lg-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-lg-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-lg-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-lg-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-lg-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 1200px) { + .flex-xl-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-xl-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-xl-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-xl-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-xl-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-xl-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-xl-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-xl-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-xl-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-xl-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-xl-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-xl-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-xl-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-xl-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-xl-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-xl-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-xl-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-xl-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-xl-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-xl-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-xl-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-xl-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-xl-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-xl-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-xl-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-xl-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-xl-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-xl-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-xl-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-xl-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-xl-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-xl-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-xl-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-xl-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +.m-0 { + margin: 0 !important; +} + +.mt-0, +.my-0 { + margin-top: 0 !important; +} + +.mr-0, +.mx-0 { + margin-right: 0 !important; +} + +.mb-0, +.my-0 { + margin-bottom: 0 !important; +} + +.ml-0, +.mx-0 { + margin-left: 0 !important; +} + +.m-1 { + margin: 0.25rem !important; +} + +.mt-1, +.my-1 { + margin-top: 0.25rem !important; +} + +.mr-1, +.mx-1 { + margin-right: 0.25rem !important; +} + +.mb-1, +.my-1 { + margin-bottom: 0.25rem !important; +} + +.ml-1, +.mx-1 { + margin-left: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.mt-2, +.my-2 { + margin-top: 0.5rem !important; +} + +.mr-2, +.mx-2 { + margin-right: 0.5rem !important; +} + +.mb-2, +.my-2 { + margin-bottom: 0.5rem !important; +} + +.ml-2, +.mx-2 { + margin-left: 0.5rem !important; +} + +.m-3 { + margin: 1rem !important; +} + +.mt-3, +.my-3 { + margin-top: 1rem !important; +} + +.mr-3, +.mx-3 { + margin-right: 1rem !important; +} + +.mb-3, +.my-3 { + margin-bottom: 1rem !important; +} + +.ml-3, +.mx-3 { + margin-left: 1rem !important; +} + +.m-4 { + margin: 1.5rem !important; +} + +.mt-4, +.my-4 { + margin-top: 1.5rem !important; +} + +.mr-4, +.mx-4 { + margin-right: 1.5rem !important; +} + +.mb-4, +.my-4 { + margin-bottom: 1.5rem !important; +} + +.ml-4, +.mx-4 { + margin-left: 1.5rem !important; +} + +.m-5 { + margin: 3rem !important; +} + +.mt-5, +.my-5 { + margin-top: 3rem !important; +} + +.mr-5, +.mx-5 { + margin-right: 3rem !important; +} + +.mb-5, +.my-5 { + margin-bottom: 3rem !important; +} + +.ml-5, +.mx-5 { + margin-left: 3rem !important; +} + +.p-0 { + padding: 0 !important; +} + +.pt-0, +.py-0 { + padding-top: 0 !important; +} + +.pr-0, +.px-0 { + padding-right: 0 !important; +} + +.pb-0, +.py-0 { + padding-bottom: 0 !important; +} + +.pl-0, +.px-0 { + padding-left: 0 !important; +} + +.p-1 { + padding: 0.25rem !important; +} + +.pt-1, +.py-1 { + padding-top: 0.25rem !important; +} + +.pr-1, +.px-1 { + padding-right: 0.25rem !important; +} + +.pb-1, +.py-1 { + padding-bottom: 0.25rem !important; +} + +.pl-1, +.px-1 { + padding-left: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.pt-2, +.py-2 { + padding-top: 0.5rem !important; +} + +.pr-2, +.px-2 { + padding-right: 0.5rem !important; +} + +.pb-2, +.py-2 { + padding-bottom: 0.5rem !important; +} + +.pl-2, +.px-2 { + padding-left: 0.5rem !important; +} + +.p-3 { + padding: 1rem !important; +} + +.pt-3, +.py-3 { + padding-top: 1rem !important; +} + +.pr-3, +.px-3 { + padding-right: 1rem !important; +} + +.pb-3, +.py-3 { + padding-bottom: 1rem !important; +} + +.pl-3, +.px-3 { + padding-left: 1rem !important; +} + +.p-4 { + padding: 1.5rem !important; +} + +.pt-4, +.py-4 { + padding-top: 1.5rem !important; +} + +.pr-4, +.px-4 { + padding-right: 1.5rem !important; +} + +.pb-4, +.py-4 { + padding-bottom: 1.5rem !important; +} + +.pl-4, +.px-4 { + padding-left: 1.5rem !important; +} + +.p-5 { + padding: 3rem !important; +} + +.pt-5, +.py-5 { + padding-top: 3rem !important; +} + +.pr-5, +.px-5 { + padding-right: 3rem !important; +} + +.pb-5, +.py-5 { + padding-bottom: 3rem !important; +} + +.pl-5, +.px-5 { + padding-left: 3rem !important; +} + +.m-n1 { + margin: -0.25rem !important; +} + +.mt-n1, +.my-n1 { + margin-top: -0.25rem !important; +} + +.mr-n1, +.mx-n1 { + margin-right: -0.25rem !important; +} + +.mb-n1, +.my-n1 { + margin-bottom: -0.25rem !important; +} + +.ml-n1, +.mx-n1 { + margin-left: -0.25rem !important; +} + +.m-n2 { + margin: -0.5rem !important; +} + +.mt-n2, +.my-n2 { + margin-top: -0.5rem !important; +} + +.mr-n2, +.mx-n2 { + margin-right: -0.5rem !important; +} + +.mb-n2, +.my-n2 { + margin-bottom: -0.5rem !important; +} + +.ml-n2, +.mx-n2 { + margin-left: -0.5rem !important; +} + +.m-n3 { + margin: -1rem !important; +} + +.mt-n3, +.my-n3 { + margin-top: -1rem !important; +} + +.mr-n3, +.mx-n3 { + margin-right: -1rem !important; +} + +.mb-n3, +.my-n3 { + margin-bottom: -1rem !important; +} + +.ml-n3, +.mx-n3 { + margin-left: -1rem !important; +} + +.m-n4 { + margin: -1.5rem !important; +} + +.mt-n4, +.my-n4 { + margin-top: -1.5rem !important; +} + +.mr-n4, +.mx-n4 { + margin-right: -1.5rem !important; +} + +.mb-n4, +.my-n4 { + margin-bottom: -1.5rem !important; +} + +.ml-n4, +.mx-n4 { + margin-left: -1.5rem !important; +} + +.m-n5 { + margin: -3rem !important; +} + +.mt-n5, +.my-n5 { + margin-top: -3rem !important; +} + +.mr-n5, +.mx-n5 { + margin-right: -3rem !important; +} + +.mb-n5, +.my-n5 { + margin-bottom: -3rem !important; +} + +.ml-n5, +.mx-n5 { + margin-left: -3rem !important; +} + +.m-auto { + margin: auto !important; +} + +.mt-auto, +.my-auto { + margin-top: auto !important; +} + +.mr-auto, +.mx-auto { + margin-right: auto !important; +} + +.mb-auto, +.my-auto { + margin-bottom: auto !important; +} + +.ml-auto, +.mx-auto { + margin-left: auto !important; +} + +@media (min-width: 576px) { + .m-sm-0 { + margin: 0 !important; + } + .mt-sm-0, + .my-sm-0 { + margin-top: 0 !important; + } + .mr-sm-0, + .mx-sm-0 { + margin-right: 0 !important; + } + .mb-sm-0, + .my-sm-0 { + margin-bottom: 0 !important; + } + .ml-sm-0, + .mx-sm-0 { + margin-left: 0 !important; + } + .m-sm-1 { + margin: 0.25rem !important; + } + .mt-sm-1, + .my-sm-1 { + margin-top: 0.25rem !important; + } + .mr-sm-1, + .mx-sm-1 { + margin-right: 0.25rem !important; + } + .mb-sm-1, + .my-sm-1 { + margin-bottom: 0.25rem !important; + } + .ml-sm-1, + .mx-sm-1 { + margin-left: 0.25rem !important; + } + .m-sm-2 { + margin: 0.5rem !important; + } + .mt-sm-2, + .my-sm-2 { + margin-top: 0.5rem !important; + } + .mr-sm-2, + .mx-sm-2 { + margin-right: 0.5rem !important; + } + .mb-sm-2, + .my-sm-2 { + margin-bottom: 0.5rem !important; + } + .ml-sm-2, + .mx-sm-2 { + margin-left: 0.5rem !important; + } + .m-sm-3 { + margin: 1rem !important; + } + .mt-sm-3, + .my-sm-3 { + margin-top: 1rem !important; + } + .mr-sm-3, + .mx-sm-3 { + margin-right: 1rem !important; + } + .mb-sm-3, + .my-sm-3 { + margin-bottom: 1rem !important; + } + .ml-sm-3, + .mx-sm-3 { + margin-left: 1rem !important; + } + .m-sm-4 { + margin: 1.5rem !important; + } + .mt-sm-4, + .my-sm-4 { + margin-top: 1.5rem !important; + } + .mr-sm-4, + .mx-sm-4 { + margin-right: 1.5rem !important; + } + .mb-sm-4, + .my-sm-4 { + margin-bottom: 1.5rem !important; + } + .ml-sm-4, + .mx-sm-4 { + margin-left: 1.5rem !important; + } + .m-sm-5 { + margin: 3rem !important; + } + .mt-sm-5, + .my-sm-5 { + margin-top: 3rem !important; + } + .mr-sm-5, + .mx-sm-5 { + margin-right: 3rem !important; + } + .mb-sm-5, + .my-sm-5 { + margin-bottom: 3rem !important; + } + .ml-sm-5, + .mx-sm-5 { + margin-left: 3rem !important; + } + .p-sm-0 { + padding: 0 !important; + } + .pt-sm-0, + .py-sm-0 { + padding-top: 0 !important; + } + .pr-sm-0, + .px-sm-0 { + padding-right: 0 !important; + } + .pb-sm-0, + .py-sm-0 { + padding-bottom: 0 !important; + } + .pl-sm-0, + .px-sm-0 { + padding-left: 0 !important; + } + .p-sm-1 { + padding: 0.25rem !important; + } + .pt-sm-1, + .py-sm-1 { + padding-top: 0.25rem !important; + } + .pr-sm-1, + .px-sm-1 { + padding-right: 0.25rem !important; + } + .pb-sm-1, + .py-sm-1 { + padding-bottom: 0.25rem !important; + } + .pl-sm-1, + .px-sm-1 { + padding-left: 0.25rem !important; + } + .p-sm-2 { + padding: 0.5rem !important; + } + .pt-sm-2, + .py-sm-2 { + padding-top: 0.5rem !important; + } + .pr-sm-2, + .px-sm-2 { + padding-right: 0.5rem !important; + } + .pb-sm-2, + .py-sm-2 { + padding-bottom: 0.5rem !important; + } + .pl-sm-2, + .px-sm-2 { + padding-left: 0.5rem !important; + } + .p-sm-3 { + padding: 1rem !important; + } + .pt-sm-3, + .py-sm-3 { + padding-top: 1rem !important; + } + .pr-sm-3, + .px-sm-3 { + padding-right: 1rem !important; + } + .pb-sm-3, + .py-sm-3 { + padding-bottom: 1rem !important; + } + .pl-sm-3, + .px-sm-3 { + padding-left: 1rem !important; + } + .p-sm-4 { + padding: 1.5rem !important; + } + .pt-sm-4, + .py-sm-4 { + padding-top: 1.5rem !important; + } + .pr-sm-4, + .px-sm-4 { + padding-right: 1.5rem !important; + } + .pb-sm-4, + .py-sm-4 { + padding-bottom: 1.5rem !important; + } + .pl-sm-4, + .px-sm-4 { + padding-left: 1.5rem !important; + } + .p-sm-5 { + padding: 3rem !important; + } + .pt-sm-5, + .py-sm-5 { + padding-top: 3rem !important; + } + .pr-sm-5, + .px-sm-5 { + padding-right: 3rem !important; + } + .pb-sm-5, + .py-sm-5 { + padding-bottom: 3rem !important; + } + .pl-sm-5, + .px-sm-5 { + padding-left: 3rem !important; + } + .m-sm-n1 { + margin: -0.25rem !important; + } + .mt-sm-n1, + .my-sm-n1 { + margin-top: -0.25rem !important; + } + .mr-sm-n1, + .mx-sm-n1 { + margin-right: -0.25rem !important; + } + .mb-sm-n1, + .my-sm-n1 { + margin-bottom: -0.25rem !important; + } + .ml-sm-n1, + .mx-sm-n1 { + margin-left: -0.25rem !important; + } + .m-sm-n2 { + margin: -0.5rem !important; + } + .mt-sm-n2, + .my-sm-n2 { + margin-top: -0.5rem !important; + } + .mr-sm-n2, + .mx-sm-n2 { + margin-right: -0.5rem !important; + } + .mb-sm-n2, + .my-sm-n2 { + margin-bottom: -0.5rem !important; + } + .ml-sm-n2, + .mx-sm-n2 { + margin-left: -0.5rem !important; + } + .m-sm-n3 { + margin: -1rem !important; + } + .mt-sm-n3, + .my-sm-n3 { + margin-top: -1rem !important; + } + .mr-sm-n3, + .mx-sm-n3 { + margin-right: -1rem !important; + } + .mb-sm-n3, + .my-sm-n3 { + margin-bottom: -1rem !important; + } + .ml-sm-n3, + .mx-sm-n3 { + margin-left: -1rem !important; + } + .m-sm-n4 { + margin: -1.5rem !important; + } + .mt-sm-n4, + .my-sm-n4 { + margin-top: -1.5rem !important; + } + .mr-sm-n4, + .mx-sm-n4 { + margin-right: -1.5rem !important; + } + .mb-sm-n4, + .my-sm-n4 { + margin-bottom: -1.5rem !important; + } + .ml-sm-n4, + .mx-sm-n4 { + margin-left: -1.5rem !important; + } + .m-sm-n5 { + margin: -3rem !important; + } + .mt-sm-n5, + .my-sm-n5 { + margin-top: -3rem !important; + } + .mr-sm-n5, + .mx-sm-n5 { + margin-right: -3rem !important; + } + .mb-sm-n5, + .my-sm-n5 { + margin-bottom: -3rem !important; + } + .ml-sm-n5, + .mx-sm-n5 { + margin-left: -3rem !important; + } + .m-sm-auto { + margin: auto !important; + } + .mt-sm-auto, + .my-sm-auto { + margin-top: auto !important; + } + .mr-sm-auto, + .mx-sm-auto { + margin-right: auto !important; + } + .mb-sm-auto, + .my-sm-auto { + margin-bottom: auto !important; + } + .ml-sm-auto, + .mx-sm-auto { + margin-left: auto !important; + } +} + +@media (min-width: 768px) { + .m-md-0 { + margin: 0 !important; + } + .mt-md-0, + .my-md-0 { + margin-top: 0 !important; + } + .mr-md-0, + .mx-md-0 { + margin-right: 0 !important; + } + .mb-md-0, + .my-md-0 { + margin-bottom: 0 !important; + } + .ml-md-0, + .mx-md-0 { + margin-left: 0 !important; + } + .m-md-1 { + margin: 0.25rem !important; + } + .mt-md-1, + .my-md-1 { + margin-top: 0.25rem !important; + } + .mr-md-1, + .mx-md-1 { + margin-right: 0.25rem !important; + } + .mb-md-1, + .my-md-1 { + margin-bottom: 0.25rem !important; + } + .ml-md-1, + .mx-md-1 { + margin-left: 0.25rem !important; + } + .m-md-2 { + margin: 0.5rem !important; + } + .mt-md-2, + .my-md-2 { + margin-top: 0.5rem !important; + } + .mr-md-2, + .mx-md-2 { + margin-right: 0.5rem !important; + } + .mb-md-2, + .my-md-2 { + margin-bottom: 0.5rem !important; + } + .ml-md-2, + .mx-md-2 { + margin-left: 0.5rem !important; + } + .m-md-3 { + margin: 1rem !important; + } + .mt-md-3, + .my-md-3 { + margin-top: 1rem !important; + } + .mr-md-3, + .mx-md-3 { + margin-right: 1rem !important; + } + .mb-md-3, + .my-md-3 { + margin-bottom: 1rem !important; + } + .ml-md-3, + .mx-md-3 { + margin-left: 1rem !important; + } + .m-md-4 { + margin: 1.5rem !important; + } + .mt-md-4, + .my-md-4 { + margin-top: 1.5rem !important; + } + .mr-md-4, + .mx-md-4 { + margin-right: 1.5rem !important; + } + .mb-md-4, + .my-md-4 { + margin-bottom: 1.5rem !important; + } + .ml-md-4, + .mx-md-4 { + margin-left: 1.5rem !important; + } + .m-md-5 { + margin: 3rem !important; + } + .mt-md-5, + .my-md-5 { + margin-top: 3rem !important; + } + .mr-md-5, + .mx-md-5 { + margin-right: 3rem !important; + } + .mb-md-5, + .my-md-5 { + margin-bottom: 3rem !important; + } + .ml-md-5, + .mx-md-5 { + margin-left: 3rem !important; + } + .p-md-0 { + padding: 0 !important; + } + .pt-md-0, + .py-md-0 { + padding-top: 0 !important; + } + .pr-md-0, + .px-md-0 { + padding-right: 0 !important; + } + .pb-md-0, + .py-md-0 { + padding-bottom: 0 !important; + } + .pl-md-0, + .px-md-0 { + padding-left: 0 !important; + } + .p-md-1 { + padding: 0.25rem !important; + } + .pt-md-1, + .py-md-1 { + padding-top: 0.25rem !important; + } + .pr-md-1, + .px-md-1 { + padding-right: 0.25rem !important; + } + .pb-md-1, + .py-md-1 { + padding-bottom: 0.25rem !important; + } + .pl-md-1, + .px-md-1 { + padding-left: 0.25rem !important; + } + .p-md-2 { + padding: 0.5rem !important; + } + .pt-md-2, + .py-md-2 { + padding-top: 0.5rem !important; + } + .pr-md-2, + .px-md-2 { + padding-right: 0.5rem !important; + } + .pb-md-2, + .py-md-2 { + padding-bottom: 0.5rem !important; + } + .pl-md-2, + .px-md-2 { + padding-left: 0.5rem !important; + } + .p-md-3 { + padding: 1rem !important; + } + .pt-md-3, + .py-md-3 { + padding-top: 1rem !important; + } + .pr-md-3, + .px-md-3 { + padding-right: 1rem !important; + } + .pb-md-3, + .py-md-3 { + padding-bottom: 1rem !important; + } + .pl-md-3, + .px-md-3 { + padding-left: 1rem !important; + } + .p-md-4 { + padding: 1.5rem !important; + } + .pt-md-4, + .py-md-4 { + padding-top: 1.5rem !important; + } + .pr-md-4, + .px-md-4 { + padding-right: 1.5rem !important; + } + .pb-md-4, + .py-md-4 { + padding-bottom: 1.5rem !important; + } + .pl-md-4, + .px-md-4 { + padding-left: 1.5rem !important; + } + .p-md-5 { + padding: 3rem !important; + } + .pt-md-5, + .py-md-5 { + padding-top: 3rem !important; + } + .pr-md-5, + .px-md-5 { + padding-right: 3rem !important; + } + .pb-md-5, + .py-md-5 { + padding-bottom: 3rem !important; + } + .pl-md-5, + .px-md-5 { + padding-left: 3rem !important; + } + .m-md-n1 { + margin: -0.25rem !important; + } + .mt-md-n1, + .my-md-n1 { + margin-top: -0.25rem !important; + } + .mr-md-n1, + .mx-md-n1 { + margin-right: -0.25rem !important; + } + .mb-md-n1, + .my-md-n1 { + margin-bottom: -0.25rem !important; + } + .ml-md-n1, + .mx-md-n1 { + margin-left: -0.25rem !important; + } + .m-md-n2 { + margin: -0.5rem !important; + } + .mt-md-n2, + .my-md-n2 { + margin-top: -0.5rem !important; + } + .mr-md-n2, + .mx-md-n2 { + margin-right: -0.5rem !important; + } + .mb-md-n2, + .my-md-n2 { + margin-bottom: -0.5rem !important; + } + .ml-md-n2, + .mx-md-n2 { + margin-left: -0.5rem !important; + } + .m-md-n3 { + margin: -1rem !important; + } + .mt-md-n3, + .my-md-n3 { + margin-top: -1rem !important; + } + .mr-md-n3, + .mx-md-n3 { + margin-right: -1rem !important; + } + .mb-md-n3, + .my-md-n3 { + margin-bottom: -1rem !important; + } + .ml-md-n3, + .mx-md-n3 { + margin-left: -1rem !important; + } + .m-md-n4 { + margin: -1.5rem !important; + } + .mt-md-n4, + .my-md-n4 { + margin-top: -1.5rem !important; + } + .mr-md-n4, + .mx-md-n4 { + margin-right: -1.5rem !important; + } + .mb-md-n4, + .my-md-n4 { + margin-bottom: -1.5rem !important; + } + .ml-md-n4, + .mx-md-n4 { + margin-left: -1.5rem !important; + } + .m-md-n5 { + margin: -3rem !important; + } + .mt-md-n5, + .my-md-n5 { + margin-top: -3rem !important; + } + .mr-md-n5, + .mx-md-n5 { + margin-right: -3rem !important; + } + .mb-md-n5, + .my-md-n5 { + margin-bottom: -3rem !important; + } + .ml-md-n5, + .mx-md-n5 { + margin-left: -3rem !important; + } + .m-md-auto { + margin: auto !important; + } + .mt-md-auto, + .my-md-auto { + margin-top: auto !important; + } + .mr-md-auto, + .mx-md-auto { + margin-right: auto !important; + } + .mb-md-auto, + .my-md-auto { + margin-bottom: auto !important; + } + .ml-md-auto, + .mx-md-auto { + margin-left: auto !important; + } +} + +@media (min-width: 992px) { + .m-lg-0 { + margin: 0 !important; + } + .mt-lg-0, + .my-lg-0 { + margin-top: 0 !important; + } + .mr-lg-0, + .mx-lg-0 { + margin-right: 0 !important; + } + .mb-lg-0, + .my-lg-0 { + margin-bottom: 0 !important; + } + .ml-lg-0, + .mx-lg-0 { + margin-left: 0 !important; + } + .m-lg-1 { + margin: 0.25rem !important; + } + .mt-lg-1, + .my-lg-1 { + margin-top: 0.25rem !important; + } + .mr-lg-1, + .mx-lg-1 { + margin-right: 0.25rem !important; + } + .mb-lg-1, + .my-lg-1 { + margin-bottom: 0.25rem !important; + } + .ml-lg-1, + .mx-lg-1 { + margin-left: 0.25rem !important; + } + .m-lg-2 { + margin: 0.5rem !important; + } + .mt-lg-2, + .my-lg-2 { + margin-top: 0.5rem !important; + } + .mr-lg-2, + .mx-lg-2 { + margin-right: 0.5rem !important; + } + .mb-lg-2, + .my-lg-2 { + margin-bottom: 0.5rem !important; + } + .ml-lg-2, + .mx-lg-2 { + margin-left: 0.5rem !important; + } + .m-lg-3 { + margin: 1rem !important; + } + .mt-lg-3, + .my-lg-3 { + margin-top: 1rem !important; + } + .mr-lg-3, + .mx-lg-3 { + margin-right: 1rem !important; + } + .mb-lg-3, + .my-lg-3 { + margin-bottom: 1rem !important; + } + .ml-lg-3, + .mx-lg-3 { + margin-left: 1rem !important; + } + .m-lg-4 { + margin: 1.5rem !important; + } + .mt-lg-4, + .my-lg-4 { + margin-top: 1.5rem !important; + } + .mr-lg-4, + .mx-lg-4 { + margin-right: 1.5rem !important; + } + .mb-lg-4, + .my-lg-4 { + margin-bottom: 1.5rem !important; + } + .ml-lg-4, + .mx-lg-4 { + margin-left: 1.5rem !important; + } + .m-lg-5 { + margin: 3rem !important; + } + .mt-lg-5, + .my-lg-5 { + margin-top: 3rem !important; + } + .mr-lg-5, + .mx-lg-5 { + margin-right: 3rem !important; + } + .mb-lg-5, + .my-lg-5 { + margin-bottom: 3rem !important; + } + .ml-lg-5, + .mx-lg-5 { + margin-left: 3rem !important; + } + .p-lg-0 { + padding: 0 !important; + } + .pt-lg-0, + .py-lg-0 { + padding-top: 0 !important; + } + .pr-lg-0, + .px-lg-0 { + padding-right: 0 !important; + } + .pb-lg-0, + .py-lg-0 { + padding-bottom: 0 !important; + } + .pl-lg-0, + .px-lg-0 { + padding-left: 0 !important; + } + .p-lg-1 { + padding: 0.25rem !important; + } + .pt-lg-1, + .py-lg-1 { + padding-top: 0.25rem !important; + } + .pr-lg-1, + .px-lg-1 { + padding-right: 0.25rem !important; + } + .pb-lg-1, + .py-lg-1 { + padding-bottom: 0.25rem !important; + } + .pl-lg-1, + .px-lg-1 { + padding-left: 0.25rem !important; + } + .p-lg-2 { + padding: 0.5rem !important; + } + .pt-lg-2, + .py-lg-2 { + padding-top: 0.5rem !important; + } + .pr-lg-2, + .px-lg-2 { + padding-right: 0.5rem !important; + } + .pb-lg-2, + .py-lg-2 { + padding-bottom: 0.5rem !important; + } + .pl-lg-2, + .px-lg-2 { + padding-left: 0.5rem !important; + } + .p-lg-3 { + padding: 1rem !important; + } + .pt-lg-3, + .py-lg-3 { + padding-top: 1rem !important; + } + .pr-lg-3, + .px-lg-3 { + padding-right: 1rem !important; + } + .pb-lg-3, + .py-lg-3 { + padding-bottom: 1rem !important; + } + .pl-lg-3, + .px-lg-3 { + padding-left: 1rem !important; + } + .p-lg-4 { + padding: 1.5rem !important; + } + .pt-lg-4, + .py-lg-4 { + padding-top: 1.5rem !important; + } + .pr-lg-4, + .px-lg-4 { + padding-right: 1.5rem !important; + } + .pb-lg-4, + .py-lg-4 { + padding-bottom: 1.5rem !important; + } + .pl-lg-4, + .px-lg-4 { + padding-left: 1.5rem !important; + } + .p-lg-5 { + padding: 3rem !important; + } + .pt-lg-5, + .py-lg-5 { + padding-top: 3rem !important; + } + .pr-lg-5, + .px-lg-5 { + padding-right: 3rem !important; + } + .pb-lg-5, + .py-lg-5 { + padding-bottom: 3rem !important; + } + .pl-lg-5, + .px-lg-5 { + padding-left: 3rem !important; + } + .m-lg-n1 { + margin: -0.25rem !important; + } + .mt-lg-n1, + .my-lg-n1 { + margin-top: -0.25rem !important; + } + .mr-lg-n1, + .mx-lg-n1 { + margin-right: -0.25rem !important; + } + .mb-lg-n1, + .my-lg-n1 { + margin-bottom: -0.25rem !important; + } + .ml-lg-n1, + .mx-lg-n1 { + margin-left: -0.25rem !important; + } + .m-lg-n2 { + margin: -0.5rem !important; + } + .mt-lg-n2, + .my-lg-n2 { + margin-top: -0.5rem !important; + } + .mr-lg-n2, + .mx-lg-n2 { + margin-right: -0.5rem !important; + } + .mb-lg-n2, + .my-lg-n2 { + margin-bottom: -0.5rem !important; + } + .ml-lg-n2, + .mx-lg-n2 { + margin-left: -0.5rem !important; + } + .m-lg-n3 { + margin: -1rem !important; + } + .mt-lg-n3, + .my-lg-n3 { + margin-top: -1rem !important; + } + .mr-lg-n3, + .mx-lg-n3 { + margin-right: -1rem !important; + } + .mb-lg-n3, + .my-lg-n3 { + margin-bottom: -1rem !important; + } + .ml-lg-n3, + .mx-lg-n3 { + margin-left: -1rem !important; + } + .m-lg-n4 { + margin: -1.5rem !important; + } + .mt-lg-n4, + .my-lg-n4 { + margin-top: -1.5rem !important; + } + .mr-lg-n4, + .mx-lg-n4 { + margin-right: -1.5rem !important; + } + .mb-lg-n4, + .my-lg-n4 { + margin-bottom: -1.5rem !important; + } + .ml-lg-n4, + .mx-lg-n4 { + margin-left: -1.5rem !important; + } + .m-lg-n5 { + margin: -3rem !important; + } + .mt-lg-n5, + .my-lg-n5 { + margin-top: -3rem !important; + } + .mr-lg-n5, + .mx-lg-n5 { + margin-right: -3rem !important; + } + .mb-lg-n5, + .my-lg-n5 { + margin-bottom: -3rem !important; + } + .ml-lg-n5, + .mx-lg-n5 { + margin-left: -3rem !important; + } + .m-lg-auto { + margin: auto !important; + } + .mt-lg-auto, + .my-lg-auto { + margin-top: auto !important; + } + .mr-lg-auto, + .mx-lg-auto { + margin-right: auto !important; + } + .mb-lg-auto, + .my-lg-auto { + margin-bottom: auto !important; + } + .ml-lg-auto, + .mx-lg-auto { + margin-left: auto !important; + } +} + +@media (min-width: 1200px) { + .m-xl-0 { + margin: 0 !important; + } + .mt-xl-0, + .my-xl-0 { + margin-top: 0 !important; + } + .mr-xl-0, + .mx-xl-0 { + margin-right: 0 !important; + } + .mb-xl-0, + .my-xl-0 { + margin-bottom: 0 !important; + } + .ml-xl-0, + .mx-xl-0 { + margin-left: 0 !important; + } + .m-xl-1 { + margin: 0.25rem !important; + } + .mt-xl-1, + .my-xl-1 { + margin-top: 0.25rem !important; + } + .mr-xl-1, + .mx-xl-1 { + margin-right: 0.25rem !important; + } + .mb-xl-1, + .my-xl-1 { + margin-bottom: 0.25rem !important; + } + .ml-xl-1, + .mx-xl-1 { + margin-left: 0.25rem !important; + } + .m-xl-2 { + margin: 0.5rem !important; + } + .mt-xl-2, + .my-xl-2 { + margin-top: 0.5rem !important; + } + .mr-xl-2, + .mx-xl-2 { + margin-right: 0.5rem !important; + } + .mb-xl-2, + .my-xl-2 { + margin-bottom: 0.5rem !important; + } + .ml-xl-2, + .mx-xl-2 { + margin-left: 0.5rem !important; + } + .m-xl-3 { + margin: 1rem !important; + } + .mt-xl-3, + .my-xl-3 { + margin-top: 1rem !important; + } + .mr-xl-3, + .mx-xl-3 { + margin-right: 1rem !important; + } + .mb-xl-3, + .my-xl-3 { + margin-bottom: 1rem !important; + } + .ml-xl-3, + .mx-xl-3 { + margin-left: 1rem !important; + } + .m-xl-4 { + margin: 1.5rem !important; + } + .mt-xl-4, + .my-xl-4 { + margin-top: 1.5rem !important; + } + .mr-xl-4, + .mx-xl-4 { + margin-right: 1.5rem !important; + } + .mb-xl-4, + .my-xl-4 { + margin-bottom: 1.5rem !important; + } + .ml-xl-4, + .mx-xl-4 { + margin-left: 1.5rem !important; + } + .m-xl-5 { + margin: 3rem !important; + } + .mt-xl-5, + .my-xl-5 { + margin-top: 3rem !important; + } + .mr-xl-5, + .mx-xl-5 { + margin-right: 3rem !important; + } + .mb-xl-5, + .my-xl-5 { + margin-bottom: 3rem !important; + } + .ml-xl-5, + .mx-xl-5 { + margin-left: 3rem !important; + } + .p-xl-0 { + padding: 0 !important; + } + .pt-xl-0, + .py-xl-0 { + padding-top: 0 !important; + } + .pr-xl-0, + .px-xl-0 { + padding-right: 0 !important; + } + .pb-xl-0, + .py-xl-0 { + padding-bottom: 0 !important; + } + .pl-xl-0, + .px-xl-0 { + padding-left: 0 !important; + } + .p-xl-1 { + padding: 0.25rem !important; + } + .pt-xl-1, + .py-xl-1 { + padding-top: 0.25rem !important; + } + .pr-xl-1, + .px-xl-1 { + padding-right: 0.25rem !important; + } + .pb-xl-1, + .py-xl-1 { + padding-bottom: 0.25rem !important; + } + .pl-xl-1, + .px-xl-1 { + padding-left: 0.25rem !important; + } + .p-xl-2 { + padding: 0.5rem !important; + } + .pt-xl-2, + .py-xl-2 { + padding-top: 0.5rem !important; + } + .pr-xl-2, + .px-xl-2 { + padding-right: 0.5rem !important; + } + .pb-xl-2, + .py-xl-2 { + padding-bottom: 0.5rem !important; + } + .pl-xl-2, + .px-xl-2 { + padding-left: 0.5rem !important; + } + .p-xl-3 { + padding: 1rem !important; + } + .pt-xl-3, + .py-xl-3 { + padding-top: 1rem !important; + } + .pr-xl-3, + .px-xl-3 { + padding-right: 1rem !important; + } + .pb-xl-3, + .py-xl-3 { + padding-bottom: 1rem !important; + } + .pl-xl-3, + .px-xl-3 { + padding-left: 1rem !important; + } + .p-xl-4 { + padding: 1.5rem !important; + } + .pt-xl-4, + .py-xl-4 { + padding-top: 1.5rem !important; + } + .pr-xl-4, + .px-xl-4 { + padding-right: 1.5rem !important; + } + .pb-xl-4, + .py-xl-4 { + padding-bottom: 1.5rem !important; + } + .pl-xl-4, + .px-xl-4 { + padding-left: 1.5rem !important; + } + .p-xl-5 { + padding: 3rem !important; + } + .pt-xl-5, + .py-xl-5 { + padding-top: 3rem !important; + } + .pr-xl-5, + .px-xl-5 { + padding-right: 3rem !important; + } + .pb-xl-5, + .py-xl-5 { + padding-bottom: 3rem !important; + } + .pl-xl-5, + .px-xl-5 { + padding-left: 3rem !important; + } + .m-xl-n1 { + margin: -0.25rem !important; + } + .mt-xl-n1, + .my-xl-n1 { + margin-top: -0.25rem !important; + } + .mr-xl-n1, + .mx-xl-n1 { + margin-right: -0.25rem !important; + } + .mb-xl-n1, + .my-xl-n1 { + margin-bottom: -0.25rem !important; + } + .ml-xl-n1, + .mx-xl-n1 { + margin-left: -0.25rem !important; + } + .m-xl-n2 { + margin: -0.5rem !important; + } + .mt-xl-n2, + .my-xl-n2 { + margin-top: -0.5rem !important; + } + .mr-xl-n2, + .mx-xl-n2 { + margin-right: -0.5rem !important; + } + .mb-xl-n2, + .my-xl-n2 { + margin-bottom: -0.5rem !important; + } + .ml-xl-n2, + .mx-xl-n2 { + margin-left: -0.5rem !important; + } + .m-xl-n3 { + margin: -1rem !important; + } + .mt-xl-n3, + .my-xl-n3 { + margin-top: -1rem !important; + } + .mr-xl-n3, + .mx-xl-n3 { + margin-right: -1rem !important; + } + .mb-xl-n3, + .my-xl-n3 { + margin-bottom: -1rem !important; + } + .ml-xl-n3, + .mx-xl-n3 { + margin-left: -1rem !important; + } + .m-xl-n4 { + margin: -1.5rem !important; + } + .mt-xl-n4, + .my-xl-n4 { + margin-top: -1.5rem !important; + } + .mr-xl-n4, + .mx-xl-n4 { + margin-right: -1.5rem !important; + } + .mb-xl-n4, + .my-xl-n4 { + margin-bottom: -1.5rem !important; + } + .ml-xl-n4, + .mx-xl-n4 { + margin-left: -1.5rem !important; + } + .m-xl-n5 { + margin: -3rem !important; + } + .mt-xl-n5, + .my-xl-n5 { + margin-top: -3rem !important; + } + .mr-xl-n5, + .mx-xl-n5 { + margin-right: -3rem !important; + } + .mb-xl-n5, + .my-xl-n5 { + margin-bottom: -3rem !important; + } + .ml-xl-n5, + .mx-xl-n5 { + margin-left: -3rem !important; + } + .m-xl-auto { + margin: auto !important; + } + .mt-xl-auto, + .my-xl-auto { + margin-top: auto !important; + } + .mr-xl-auto, + .mx-xl-auto { + margin-right: auto !important; + } + .mb-xl-auto, + .my-xl-auto { + margin-bottom: auto !important; + } + .ml-xl-auto, + .mx-xl-auto { + margin-left: auto !important; + } +} +/*# sourceMappingURL=bootstrap-grid.css.map */ \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map new file mode 100644 index 00000000..8661e3e3 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap-grid.scss","bootstrap-grid.css","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_breakpoints.scss","../../scss/_variables.scss","../../scss/mixins/_grid-framework.scss","../../scss/utilities/_display.scss","../../scss/utilities/_flex.scss","../../scss/utilities/_spacing.scss"],"names":[],"mappings":"AAAA;;;;;ECKE;ADEF;EACE,sBAAsB;EACtB,6BAA6B;ACA/B;;ADGA;;;EAGE,mBAAmB;ACArB;;ACTE;ECDA,WAAW;EACX,mBAA0B;EAC1B,kBAAyB;EACzB,kBAAkB;EAClB,iBAAiB;AFcnB;;AGqCI;EFtDF;ICWI,gBEqMK;EJ1LT;AACF;;AG+BI;EFtDF;ICWI,gBEsMK;EJrLT;AACF;;AGyBI;EFtDF;ICWI,gBEuMK;EJhLT;AACF;;AGmBI;EFtDF;ICWI,iBEwMM;EJ3KV;AACF;;ACnCE;ECPA,WAAW;EACX,mBAA0B;EAC1B,kBAAyB;EACzB,kBAAkB;EAClB,iBAAiB;AF8CnB;;AGKI;EFrCE;IACE,gBG8LG;EJ1JT;AACF;;AGDI;EFrCE;IACE,gBG+LG;EJrJT;AACF;;AGPI;EFrCE;IACE,gBGgMG;EJhJT;AACF;;AGbI;EFrCE;IACE,iBGiMI;EJ3IV;AACF;;ACnCE;ECrBA,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,mBAA0B;EAC1B,kBAAyB;AF4D3B;;ACpCE;EACE,eAAe;EACf,cAAc;ADuClB;;ACzCE;;EAMI,gBAAgB;EAChB,eAAe;ADwCrB;;AK1FE;;;;;;EACE,kBAAkB;EAClB,WAAW;EACX,mBAA0B;EAC1B,kBAAyB;ALkG7B;;AK/EM;EACE,0BAAa;EAAb,aAAa;EACb,oBAAY;EAAZ,YAAY;EACZ,eAAe;ALkFvB;;AK9EQ;EH4BJ,kBAAuB;EAAvB,cAAuB;EACvB,eAAwB;AFsD5B;;AKnFQ;EH4BJ,iBAAuB;EAAvB,aAAuB;EACvB,cAAwB;AF2D5B;;AKxFQ;EH4BJ,wBAAuB;EAAvB,oBAAuB;EACvB,qBAAwB;AFgE5B;;AK7FQ;EH4BJ,iBAAuB;EAAvB,aAAuB;EACvB,cAAwB;AFqE5B;;AKlGQ;EH4BJ,iBAAuB;EAAvB,aAAuB;EACvB,cAAwB;AF0E5B;;AKvGQ;EH4BJ,wBAAuB;EAAvB,oBAAuB;EACvB,qBAAwB;AF+E5B;;AKvGM;EHMJ,kBAAc;EAAd,cAAc;EACd,WAAW;EACX,eAAe;AFqGjB;;AKxGQ;EHPN,uBAAsC;EAAtC,mBAAsC;EAItC,oBAAuC;AFgHzC;;AK7GQ;EHPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AFqHzC;;AKlHQ;EHPN,iBAAsC;EAAtC,aAAsC;EAItC,cAAuC;AF0HzC;;AKvHQ;EHPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AF+HzC;;AK5HQ;EHPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AFoIzC;;AKjIQ;EHPN,iBAAsC;EAAtC,aAAsC;EAItC,cAAuC;AFyIzC;;AKtIQ;EHPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AF8IzC;;AK3IQ;EHPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AFmJzC;;AKhJQ;EHPN,iBAAsC;EAAtC,aAAsC;EAItC,cAAuC;AFwJzC;;AKrJQ;EHPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AF6JzC;;AK1JQ;EHPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AFkKzC;;AK/JQ;EHPN,kBAAsC;EAAtC,cAAsC;EAItC,eAAuC;AFuKzC;;AK/JM;EAAwB,kBAAS;EAAT,SAAS;ALmKvC;;AKjKM;EAAuB,kBD6KG;EC7KH,SD6KG;AJRhC;;AKlKQ;EAAwB,iBADZ;EACY,QADZ;ALuKpB;;AKtKQ;EAAwB,iBADZ;EACY,QADZ;AL2KpB;;AK1KQ;EAAwB,iBADZ;EACY,QADZ;AL+KpB;;AK9KQ;EAAwB,iBADZ;EACY,QADZ;ALmLpB;;AKlLQ;EAAwB,iBADZ;EACY,QADZ;ALuLpB;;AKtLQ;EAAwB,iBADZ;EACY,QADZ;AL2LpB;;AK1LQ;EAAwB,iBADZ;EACY,QADZ;AL+LpB;;AK9LQ;EAAwB,iBADZ;EACY,QADZ;ALmMpB;;AKlMQ;EAAwB,iBADZ;EACY,QADZ;ALuMpB;;AKtMQ;EAAwB,iBADZ;EACY,QADZ;AL2MpB;;AK1MQ;EAAwB,kBADZ;EACY,SADZ;AL+MpB;;AK9MQ;EAAwB,kBADZ;EACY,SADZ;ALmNpB;;AKlNQ;EAAwB,kBADZ;EACY,SADZ;ALuNpB;;AKhNU;EHRR,sBAA8C;AF4NhD;;AKpNU;EHRR,uBAA8C;AFgOhD;;AKxNU;EHRR,gBAA8C;AFoOhD;;AK5NU;EHRR,uBAA8C;AFwOhD;;AKhOU;EHRR,uBAA8C;AF4OhD;;AKpOU;EHRR,gBAA8C;AFgPhD;;AKxOU;EHRR,uBAA8C;AFoPhD;;AK5OU;EHRR,uBAA8C;AFwPhD;;AKhPU;EHRR,gBAA8C;AF4PhD;;AKpPU;EHRR,uBAA8C;AFgQhD;;AKxPU;EHRR,uBAA8C;AFoQhD;;AG/PI;EE9BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;ELiSrB;EK7RM;IH4BJ,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;EFoQ1B;EKjSM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFwQ1B;EKrSM;IH4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EF4Q1B;EKzSM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFgR1B;EK7SM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFoR1B;EKjTM;IH4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EFwR1B;EKhTI;IHMJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;EF6Sf;EKhTM;IHPN,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;EFuTvC;EKpTM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF2TvC;EKxTM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EF+TvC;EK5TM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFmUvC;EKhUM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFuUvC;EKpUM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EF2UvC;EKxUM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF+UvC;EK5UM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFmVvC;EKhVM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFuVvC;EKpVM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF2VvC;EKxVM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF+VvC;EK5VM;IHPN,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;EFmWvC;EK3VI;IAAwB,kBAAS;IAAT,SAAS;EL8VrC;EK5VI;IAAuB,kBD6KG;IC7KH,SD6KG;EJkL9B;EK5VM;IAAwB,iBADZ;IACY,QADZ;ELgWlB;EK/VM;IAAwB,iBADZ;IACY,QADZ;ELmWlB;EKlWM;IAAwB,iBADZ;IACY,QADZ;ELsWlB;EKrWM;IAAwB,iBADZ;IACY,QADZ;ELyWlB;EKxWM;IAAwB,iBADZ;IACY,QADZ;EL4WlB;EK3WM;IAAwB,iBADZ;IACY,QADZ;EL+WlB;EK9WM;IAAwB,iBADZ;IACY,QADZ;ELkXlB;EKjXM;IAAwB,iBADZ;IACY,QADZ;ELqXlB;EKpXM;IAAwB,iBADZ;IACY,QADZ;ELwXlB;EKvXM;IAAwB,iBADZ;IACY,QADZ;EL2XlB;EK1XM;IAAwB,kBADZ;IACY,SADZ;EL8XlB;EK7XM;IAAwB,kBADZ;IACY,SADZ;ELiYlB;EKhYM;IAAwB,kBADZ;IACY,SADZ;ELoYlB;EK7XQ;IHRR,cAA4B;EFwY5B;EKhYQ;IHRR,sBAA8C;EF2Y9C;EKnYQ;IHRR,uBAA8C;EF8Y9C;EKtYQ;IHRR,gBAA8C;EFiZ9C;EKzYQ;IHRR,uBAA8C;EFoZ9C;EK5YQ;IHRR,uBAA8C;EFuZ9C;EK/YQ;IHRR,gBAA8C;EF0Z9C;EKlZQ;IHRR,uBAA8C;EF6Z9C;EKrZQ;IHRR,uBAA8C;EFga9C;EKxZQ;IHRR,gBAA8C;EFma9C;EK3ZQ;IHRR,uBAA8C;EFsa9C;EK9ZQ;IHRR,uBAA8C;EFya9C;AACF;;AGraI;EE9BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;ELucrB;EKncM;IH4BJ,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;EF0a1B;EKvcM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EF8a1B;EK3cM;IH4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EFkb1B;EK/cM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFsb1B;EKndM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EF0b1B;EKvdM;IH4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EF8b1B;EKtdI;IHMJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;EFmdf;EKtdM;IHPN,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;EF6dvC;EK1dM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFievC;EK9dM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFqevC;EKleM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFyevC;EKteM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF6evC;EK1eM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFifvC;EK9eM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFqfvC;EKlfM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFyfvC;EKtfM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EF6fvC;EK1fM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFigBvC;EK9fM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFqgBvC;EKlgBM;IHPN,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;EFygBvC;EKjgBI;IAAwB,kBAAS;IAAT,SAAS;ELogBrC;EKlgBI;IAAuB,kBD6KG;IC7KH,SD6KG;EJwV9B;EKlgBM;IAAwB,iBADZ;IACY,QADZ;ELsgBlB;EKrgBM;IAAwB,iBADZ;IACY,QADZ;ELygBlB;EKxgBM;IAAwB,iBADZ;IACY,QADZ;EL4gBlB;EK3gBM;IAAwB,iBADZ;IACY,QADZ;EL+gBlB;EK9gBM;IAAwB,iBADZ;IACY,QADZ;ELkhBlB;EKjhBM;IAAwB,iBADZ;IACY,QADZ;ELqhBlB;EKphBM;IAAwB,iBADZ;IACY,QADZ;ELwhBlB;EKvhBM;IAAwB,iBADZ;IACY,QADZ;EL2hBlB;EK1hBM;IAAwB,iBADZ;IACY,QADZ;EL8hBlB;EK7hBM;IAAwB,iBADZ;IACY,QADZ;ELiiBlB;EKhiBM;IAAwB,kBADZ;IACY,SADZ;ELoiBlB;EKniBM;IAAwB,kBADZ;IACY,SADZ;ELuiBlB;EKtiBM;IAAwB,kBADZ;IACY,SADZ;EL0iBlB;EKniBQ;IHRR,cAA4B;EF8iB5B;EKtiBQ;IHRR,sBAA8C;EFijB9C;EKziBQ;IHRR,uBAA8C;EFojB9C;EK5iBQ;IHRR,gBAA8C;EFujB9C;EK/iBQ;IHRR,uBAA8C;EF0jB9C;EKljBQ;IHRR,uBAA8C;EF6jB9C;EKrjBQ;IHRR,gBAA8C;EFgkB9C;EKxjBQ;IHRR,uBAA8C;EFmkB9C;EK3jBQ;IHRR,uBAA8C;EFskB9C;EK9jBQ;IHRR,gBAA8C;EFykB9C;EKjkBQ;IHRR,uBAA8C;EF4kB9C;EKpkBQ;IHRR,uBAA8C;EF+kB9C;AACF;;AG3kBI;EE9BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;EL6mBrB;EKzmBM;IH4BJ,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;EFglB1B;EK7mBM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFolB1B;EKjnBM;IH4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EFwlB1B;EKrnBM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EF4lB1B;EKznBM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFgmB1B;EK7nBM;IH4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EFomB1B;EK5nBI;IHMJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;EFynBf;EK5nBM;IHPN,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;EFmoBvC;EKhoBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFuoBvC;EKpoBM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EF2oBvC;EKxoBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF+oBvC;EK5oBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFmpBvC;EKhpBM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFupBvC;EKppBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF2pBvC;EKxpBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF+pBvC;EK5pBM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFmqBvC;EKhqBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFuqBvC;EKpqBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF2qBvC;EKxqBM;IHPN,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;EF+qBvC;EKvqBI;IAAwB,kBAAS;IAAT,SAAS;EL0qBrC;EKxqBI;IAAuB,kBD6KG;IC7KH,SD6KG;EJ8f9B;EKxqBM;IAAwB,iBADZ;IACY,QADZ;EL4qBlB;EK3qBM;IAAwB,iBADZ;IACY,QADZ;EL+qBlB;EK9qBM;IAAwB,iBADZ;IACY,QADZ;ELkrBlB;EKjrBM;IAAwB,iBADZ;IACY,QADZ;ELqrBlB;EKprBM;IAAwB,iBADZ;IACY,QADZ;ELwrBlB;EKvrBM;IAAwB,iBADZ;IACY,QADZ;EL2rBlB;EK1rBM;IAAwB,iBADZ;IACY,QADZ;EL8rBlB;EK7rBM;IAAwB,iBADZ;IACY,QADZ;ELisBlB;EKhsBM;IAAwB,iBADZ;IACY,QADZ;ELosBlB;EKnsBM;IAAwB,iBADZ;IACY,QADZ;ELusBlB;EKtsBM;IAAwB,kBADZ;IACY,SADZ;EL0sBlB;EKzsBM;IAAwB,kBADZ;IACY,SADZ;EL6sBlB;EK5sBM;IAAwB,kBADZ;IACY,SADZ;ELgtBlB;EKzsBQ;IHRR,cAA4B;EFotB5B;EK5sBQ;IHRR,sBAA8C;EFutB9C;EK/sBQ;IHRR,uBAA8C;EF0tB9C;EKltBQ;IHRR,gBAA8C;EF6tB9C;EKrtBQ;IHRR,uBAA8C;EFguB9C;EKxtBQ;IHRR,uBAA8C;EFmuB9C;EK3tBQ;IHRR,gBAA8C;EFsuB9C;EK9tBQ;IHRR,uBAA8C;EFyuB9C;EKjuBQ;IHRR,uBAA8C;EF4uB9C;EKpuBQ;IHRR,gBAA8C;EF+uB9C;EKvuBQ;IHRR,uBAA8C;EFkvB9C;EK1uBQ;IHRR,uBAA8C;EFqvB9C;AACF;;AGjvBI;EE9BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;ELmxBrB;EK/wBM;IH4BJ,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;EFsvB1B;EKnxBM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EF0vB1B;EKvxBM;IH4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EF8vB1B;EK3xBM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFkwB1B;EK/xBM;IH4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFswB1B;EKnyBM;IH4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EF0wB1B;EKlyBI;IHMJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;EF+xBf;EKlyBM;IHPN,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;EFyyBvC;EKtyBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF6yBvC;EK1yBM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFizBvC;EK9yBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFqzBvC;EKlzBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFyzBvC;EKtzBM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EF6zBvC;EK1zBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFi0BvC;EK9zBM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFq0BvC;EKl0BM;IHPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFy0BvC;EKt0BM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF60BvC;EK10BM;IHPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFi1BvC;EK90BM;IHPN,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;EFq1BvC;EK70BI;IAAwB,kBAAS;IAAT,SAAS;ELg1BrC;EK90BI;IAAuB,kBD6KG;IC7KH,SD6KG;EJoqB9B;EK90BM;IAAwB,iBADZ;IACY,QADZ;ELk1BlB;EKj1BM;IAAwB,iBADZ;IACY,QADZ;ELq1BlB;EKp1BM;IAAwB,iBADZ;IACY,QADZ;ELw1BlB;EKv1BM;IAAwB,iBADZ;IACY,QADZ;EL21BlB;EK11BM;IAAwB,iBADZ;IACY,QADZ;EL81BlB;EK71BM;IAAwB,iBADZ;IACY,QADZ;ELi2BlB;EKh2BM;IAAwB,iBADZ;IACY,QADZ;ELo2BlB;EKn2BM;IAAwB,iBADZ;IACY,QADZ;ELu2BlB;EKt2BM;IAAwB,iBADZ;IACY,QADZ;EL02BlB;EKz2BM;IAAwB,iBADZ;IACY,QADZ;EL62BlB;EK52BM;IAAwB,kBADZ;IACY,SADZ;ELg3BlB;EK/2BM;IAAwB,kBADZ;IACY,SADZ;ELm3BlB;EKl3BM;IAAwB,kBADZ;IACY,SADZ;ELs3BlB;EK/2BQ;IHRR,cAA4B;EF03B5B;EKl3BQ;IHRR,sBAA8C;EF63B9C;EKr3BQ;IHRR,uBAA8C;EFg4B9C;EKx3BQ;IHRR,gBAA8C;EFm4B9C;EK33BQ;IHRR,uBAA8C;EFs4B9C;EK93BQ;IHRR,uBAA8C;EFy4B9C;EKj4BQ;IHRR,gBAA8C;EF44B9C;EKp4BQ;IHRR,uBAA8C;EF+4B9C;EKv4BQ;IHRR,uBAA8C;EFk5B9C;EK14BQ;IHRR,gBAA8C;EFq5B9C;EK74BQ;IHRR,uBAA8C;EFw5B9C;EKh5BQ;IHRR,uBAA8C;EF25B9C;AACF;;AMx8BM;EAAwB,wBAA0B;AN48BxD;;AM58BM;EAAwB,0BAA0B;ANg9BxD;;AMh9BM;EAAwB,gCAA0B;ANo9BxD;;AMp9BM;EAAwB,yBAA0B;ANw9BxD;;AMx9BM;EAAwB,yBAA0B;AN49BxD;;AM59BM;EAAwB,6BAA0B;ANg+BxD;;AMh+BM;EAAwB,8BAA0B;ANo+BxD;;AMp+BM;EAAwB,+BAA0B;EAA1B,wBAA0B;ANw+BxD;;AMx+BM;EAAwB,sCAA0B;EAA1B,+BAA0B;AN4+BxD;;AG37BI;EGjDE;IAAwB,wBAA0B;ENi/BtD;EMj/BI;IAAwB,0BAA0B;ENo/BtD;EMp/BI;IAAwB,gCAA0B;ENu/BtD;EMv/BI;IAAwB,yBAA0B;EN0/BtD;EM1/BI;IAAwB,yBAA0B;EN6/BtD;EM7/BI;IAAwB,6BAA0B;ENggCtD;EMhgCI;IAAwB,8BAA0B;ENmgCtD;EMngCI;IAAwB,+BAA0B;IAA1B,wBAA0B;ENsgCtD;EMtgCI;IAAwB,sCAA0B;IAA1B,+BAA0B;ENygCtD;AACF;;AGz9BI;EGjDE;IAAwB,wBAA0B;EN+gCtD;EM/gCI;IAAwB,0BAA0B;ENkhCtD;EMlhCI;IAAwB,gCAA0B;ENqhCtD;EMrhCI;IAAwB,yBAA0B;ENwhCtD;EMxhCI;IAAwB,yBAA0B;EN2hCtD;EM3hCI;IAAwB,6BAA0B;EN8hCtD;EM9hCI;IAAwB,8BAA0B;ENiiCtD;EMjiCI;IAAwB,+BAA0B;IAA1B,wBAA0B;ENoiCtD;EMpiCI;IAAwB,sCAA0B;IAA1B,+BAA0B;ENuiCtD;AACF;;AGv/BI;EGjDE;IAAwB,wBAA0B;EN6iCtD;EM7iCI;IAAwB,0BAA0B;ENgjCtD;EMhjCI;IAAwB,gCAA0B;ENmjCtD;EMnjCI;IAAwB,yBAA0B;ENsjCtD;EMtjCI;IAAwB,yBAA0B;ENyjCtD;EMzjCI;IAAwB,6BAA0B;EN4jCtD;EM5jCI;IAAwB,8BAA0B;EN+jCtD;EM/jCI;IAAwB,+BAA0B;IAA1B,wBAA0B;ENkkCtD;EMlkCI;IAAwB,sCAA0B;IAA1B,+BAA0B;ENqkCtD;AACF;;AGrhCI;EGjDE;IAAwB,wBAA0B;EN2kCtD;EM3kCI;IAAwB,0BAA0B;EN8kCtD;EM9kCI;IAAwB,gCAA0B;ENilCtD;EMjlCI;IAAwB,yBAA0B;ENolCtD;EMplCI;IAAwB,yBAA0B;ENulCtD;EMvlCI;IAAwB,6BAA0B;EN0lCtD;EM1lCI;IAAwB,8BAA0B;EN6lCtD;EM7lCI;IAAwB,+BAA0B;IAA1B,wBAA0B;ENgmCtD;EMhmCI;IAAwB,sCAA0B;IAA1B,+BAA0B;ENmmCtD;AACF;;AM1lCA;EAEI;IAAqB,wBAA0B;EN6lCjD;EM7lCE;IAAqB,0BAA0B;ENgmCjD;EMhmCE;IAAqB,gCAA0B;ENmmCjD;EMnmCE;IAAqB,yBAA0B;ENsmCjD;EMtmCE;IAAqB,yBAA0B;ENymCjD;EMzmCE;IAAqB,6BAA0B;EN4mCjD;EM5mCE;IAAqB,8BAA0B;EN+mCjD;EM/mCE;IAAqB,+BAA0B;IAA1B,wBAA0B;ENknCjD;EMlnCE;IAAqB,sCAA0B;IAA1B,+BAA0B;ENqnCjD;AACF;;AOnoCI;EAAgC,kCAA8B;EAA9B,8BAA8B;APuoClE;;AOtoCI;EAAgC,qCAAiC;EAAjC,iCAAiC;AP0oCrE;;AOzoCI;EAAgC,0CAAsC;EAAtC,sCAAsC;AP6oC1E;;AO5oCI;EAAgC,6CAAyC;EAAzC,yCAAyC;APgpC7E;;AO9oCI;EAA8B,8BAA0B;EAA1B,0BAA0B;APkpC5D;;AOjpCI;EAA8B,gCAA4B;EAA5B,4BAA4B;APqpC9D;;AOppCI;EAA8B,sCAAkC;EAAlC,kCAAkC;APwpCpE;;AOvpCI;EAA8B,6BAAyB;EAAzB,yBAAyB;AP2pC3D;;AO1pCI;EAA8B,+BAAuB;EAAvB,uBAAuB;AP8pCzD;;AO7pCI;EAA8B,+BAAuB;EAAvB,uBAAuB;APiqCzD;;AOhqCI;EAA8B,+BAAyB;EAAzB,yBAAyB;APoqC3D;;AOnqCI;EAA8B,+BAAyB;EAAzB,yBAAyB;APuqC3D;;AOrqCI;EAAoC,+BAAsC;EAAtC,sCAAsC;APyqC9E;;AOxqCI;EAAoC,6BAAoC;EAApC,oCAAoC;AP4qC5E;;AO3qCI;EAAoC,gCAAkC;EAAlC,kCAAkC;AP+qC1E;;AO9qCI;EAAoC,iCAAyC;EAAzC,yCAAyC;APkrCjF;;AOjrCI;EAAoC,oCAAwC;EAAxC,wCAAwC;APqrChF;;AOnrCI;EAAiC,gCAAkC;EAAlC,kCAAkC;APurCvE;;AOtrCI;EAAiC,8BAAgC;EAAhC,gCAAgC;AP0rCrE;;AOzrCI;EAAiC,iCAA8B;EAA9B,8BAA8B;AP6rCnE;;AO5rCI;EAAiC,mCAAgC;EAAhC,gCAAgC;APgsCrE;;AO/rCI;EAAiC,kCAA+B;EAA/B,+BAA+B;APmsCpE;;AOjsCI;EAAkC,oCAAoC;EAApC,oCAAoC;APqsC1E;;AOpsCI;EAAkC,kCAAkC;EAAlC,kCAAkC;APwsCxE;;AOvsCI;EAAkC,qCAAgC;EAAhC,gCAAgC;AP2sCtE;;AO1sCI;EAAkC,sCAAuC;EAAvC,uCAAuC;AP8sC7E;;AO7sCI;EAAkC,yCAAsC;EAAtC,sCAAsC;APitC5E;;AOhtCI;EAAkC,sCAAiC;EAAjC,iCAAiC;APotCvE;;AOltCI;EAAgC,oCAA2B;EAA3B,2BAA2B;APstC/D;;AOrtCI;EAAgC,qCAAiC;EAAjC,iCAAiC;APytCrE;;AOxtCI;EAAgC,mCAA+B;EAA/B,+BAA+B;AP4tCnE;;AO3tCI;EAAgC,sCAA6B;EAA7B,6BAA6B;AP+tCjE;;AO9tCI;EAAgC,wCAA+B;EAA/B,+BAA+B;APkuCnE;;AOjuCI;EAAgC,uCAA8B;EAA9B,8BAA8B;APquClE;;AGztCI;EIlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;EPgxChE;EO/wCE;IAAgC,qCAAiC;IAAjC,iCAAiC;EPkxCnE;EOjxCE;IAAgC,0CAAsC;IAAtC,sCAAsC;EPoxCxE;EOnxCE;IAAgC,6CAAyC;IAAzC,yCAAyC;EPsxC3E;EOpxCE;IAA8B,8BAA0B;IAA1B,0BAA0B;EPuxC1D;EOtxCE;IAA8B,gCAA4B;IAA5B,4BAA4B;EPyxC5D;EOxxCE;IAA8B,sCAAkC;IAAlC,kCAAkC;EP2xClE;EO1xCE;IAA8B,6BAAyB;IAAzB,yBAAyB;EP6xCzD;EO5xCE;IAA8B,+BAAuB;IAAvB,uBAAuB;EP+xCvD;EO9xCE;IAA8B,+BAAuB;IAAvB,uBAAuB;EPiyCvD;EOhyCE;IAA8B,+BAAyB;IAAzB,yBAAyB;EPmyCzD;EOlyCE;IAA8B,+BAAyB;IAAzB,yBAAyB;EPqyCzD;EOnyCE;IAAoC,+BAAsC;IAAtC,sCAAsC;EPsyC5E;EOryCE;IAAoC,6BAAoC;IAApC,oCAAoC;EPwyC1E;EOvyCE;IAAoC,gCAAkC;IAAlC,kCAAkC;EP0yCxE;EOzyCE;IAAoC,iCAAyC;IAAzC,yCAAyC;EP4yC/E;EO3yCE;IAAoC,oCAAwC;IAAxC,wCAAwC;EP8yC9E;EO5yCE;IAAiC,gCAAkC;IAAlC,kCAAkC;EP+yCrE;EO9yCE;IAAiC,8BAAgC;IAAhC,gCAAgC;EPizCnE;EOhzCE;IAAiC,iCAA8B;IAA9B,8BAA8B;EPmzCjE;EOlzCE;IAAiC,mCAAgC;IAAhC,gCAAgC;EPqzCnE;EOpzCE;IAAiC,kCAA+B;IAA/B,+BAA+B;EPuzClE;EOrzCE;IAAkC,oCAAoC;IAApC,oCAAoC;EPwzCxE;EOvzCE;IAAkC,kCAAkC;IAAlC,kCAAkC;EP0zCtE;EOzzCE;IAAkC,qCAAgC;IAAhC,gCAAgC;EP4zCpE;EO3zCE;IAAkC,sCAAuC;IAAvC,uCAAuC;EP8zC3E;EO7zCE;IAAkC,yCAAsC;IAAtC,sCAAsC;EPg0C1E;EO/zCE;IAAkC,sCAAiC;IAAjC,iCAAiC;EPk0CrE;EOh0CE;IAAgC,oCAA2B;IAA3B,2BAA2B;EPm0C7D;EOl0CE;IAAgC,qCAAiC;IAAjC,iCAAiC;EPq0CnE;EOp0CE;IAAgC,mCAA+B;IAA/B,+BAA+B;EPu0CjE;EOt0CE;IAAgC,sCAA6B;IAA7B,6BAA6B;EPy0C/D;EOx0CE;IAAgC,wCAA+B;IAA/B,+BAA+B;EP20CjE;EO10CE;IAAgC,uCAA8B;IAA9B,8BAA8B;EP60ChE;AACF;;AGl0CI;EIlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;EPy3ChE;EOx3CE;IAAgC,qCAAiC;IAAjC,iCAAiC;EP23CnE;EO13CE;IAAgC,0CAAsC;IAAtC,sCAAsC;EP63CxE;EO53CE;IAAgC,6CAAyC;IAAzC,yCAAyC;EP+3C3E;EO73CE;IAA8B,8BAA0B;IAA1B,0BAA0B;EPg4C1D;EO/3CE;IAA8B,gCAA4B;IAA5B,4BAA4B;EPk4C5D;EOj4CE;IAA8B,sCAAkC;IAAlC,kCAAkC;EPo4ClE;EOn4CE;IAA8B,6BAAyB;IAAzB,yBAAyB;EPs4CzD;EOr4CE;IAA8B,+BAAuB;IAAvB,uBAAuB;EPw4CvD;EOv4CE;IAA8B,+BAAuB;IAAvB,uBAAuB;EP04CvD;EOz4CE;IAA8B,+BAAyB;IAAzB,yBAAyB;EP44CzD;EO34CE;IAA8B,+BAAyB;IAAzB,yBAAyB;EP84CzD;EO54CE;IAAoC,+BAAsC;IAAtC,sCAAsC;EP+4C5E;EO94CE;IAAoC,6BAAoC;IAApC,oCAAoC;EPi5C1E;EOh5CE;IAAoC,gCAAkC;IAAlC,kCAAkC;EPm5CxE;EOl5CE;IAAoC,iCAAyC;IAAzC,yCAAyC;EPq5C/E;EOp5CE;IAAoC,oCAAwC;IAAxC,wCAAwC;EPu5C9E;EOr5CE;IAAiC,gCAAkC;IAAlC,kCAAkC;EPw5CrE;EOv5CE;IAAiC,8BAAgC;IAAhC,gCAAgC;EP05CnE;EOz5CE;IAAiC,iCAA8B;IAA9B,8BAA8B;EP45CjE;EO35CE;IAAiC,mCAAgC;IAAhC,gCAAgC;EP85CnE;EO75CE;IAAiC,kCAA+B;IAA/B,+BAA+B;EPg6ClE;EO95CE;IAAkC,oCAAoC;IAApC,oCAAoC;EPi6CxE;EOh6CE;IAAkC,kCAAkC;IAAlC,kCAAkC;EPm6CtE;EOl6CE;IAAkC,qCAAgC;IAAhC,gCAAgC;EPq6CpE;EOp6CE;IAAkC,sCAAuC;IAAvC,uCAAuC;EPu6C3E;EOt6CE;IAAkC,yCAAsC;IAAtC,sCAAsC;EPy6C1E;EOx6CE;IAAkC,sCAAiC;IAAjC,iCAAiC;EP26CrE;EOz6CE;IAAgC,oCAA2B;IAA3B,2BAA2B;EP46C7D;EO36CE;IAAgC,qCAAiC;IAAjC,iCAAiC;EP86CnE;EO76CE;IAAgC,mCAA+B;IAA/B,+BAA+B;EPg7CjE;EO/6CE;IAAgC,sCAA6B;IAA7B,6BAA6B;EPk7C/D;EOj7CE;IAAgC,wCAA+B;IAA/B,+BAA+B;EPo7CjE;EOn7CE;IAAgC,uCAA8B;IAA9B,8BAA8B;EPs7ChE;AACF;;AG36CI;EIlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;EPk+ChE;EOj+CE;IAAgC,qCAAiC;IAAjC,iCAAiC;EPo+CnE;EOn+CE;IAAgC,0CAAsC;IAAtC,sCAAsC;EPs+CxE;EOr+CE;IAAgC,6CAAyC;IAAzC,yCAAyC;EPw+C3E;EOt+CE;IAA8B,8BAA0B;IAA1B,0BAA0B;EPy+C1D;EOx+CE;IAA8B,gCAA4B;IAA5B,4BAA4B;EP2+C5D;EO1+CE;IAA8B,sCAAkC;IAAlC,kCAAkC;EP6+ClE;EO5+CE;IAA8B,6BAAyB;IAAzB,yBAAyB;EP++CzD;EO9+CE;IAA8B,+BAAuB;IAAvB,uBAAuB;EPi/CvD;EOh/CE;IAA8B,+BAAuB;IAAvB,uBAAuB;EPm/CvD;EOl/CE;IAA8B,+BAAyB;IAAzB,yBAAyB;EPq/CzD;EOp/CE;IAA8B,+BAAyB;IAAzB,yBAAyB;EPu/CzD;EOr/CE;IAAoC,+BAAsC;IAAtC,sCAAsC;EPw/C5E;EOv/CE;IAAoC,6BAAoC;IAApC,oCAAoC;EP0/C1E;EOz/CE;IAAoC,gCAAkC;IAAlC,kCAAkC;EP4/CxE;EO3/CE;IAAoC,iCAAyC;IAAzC,yCAAyC;EP8/C/E;EO7/CE;IAAoC,oCAAwC;IAAxC,wCAAwC;EPggD9E;EO9/CE;IAAiC,gCAAkC;IAAlC,kCAAkC;EPigDrE;EOhgDE;IAAiC,8BAAgC;IAAhC,gCAAgC;EPmgDnE;EOlgDE;IAAiC,iCAA8B;IAA9B,8BAA8B;EPqgDjE;EOpgDE;IAAiC,mCAAgC;IAAhC,gCAAgC;EPugDnE;EOtgDE;IAAiC,kCAA+B;IAA/B,+BAA+B;EPygDlE;EOvgDE;IAAkC,oCAAoC;IAApC,oCAAoC;EP0gDxE;EOzgDE;IAAkC,kCAAkC;IAAlC,kCAAkC;EP4gDtE;EO3gDE;IAAkC,qCAAgC;IAAhC,gCAAgC;EP8gDpE;EO7gDE;IAAkC,sCAAuC;IAAvC,uCAAuC;EPghD3E;EO/gDE;IAAkC,yCAAsC;IAAtC,sCAAsC;EPkhD1E;EOjhDE;IAAkC,sCAAiC;IAAjC,iCAAiC;EPohDrE;EOlhDE;IAAgC,oCAA2B;IAA3B,2BAA2B;EPqhD7D;EOphDE;IAAgC,qCAAiC;IAAjC,iCAAiC;EPuhDnE;EOthDE;IAAgC,mCAA+B;IAA/B,+BAA+B;EPyhDjE;EOxhDE;IAAgC,sCAA6B;IAA7B,6BAA6B;EP2hD/D;EO1hDE;IAAgC,wCAA+B;IAA/B,+BAA+B;EP6hDjE;EO5hDE;IAAgC,uCAA8B;IAA9B,8BAA8B;EP+hDhE;AACF;;AGphDI;EIlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;EP2kDhE;EO1kDE;IAAgC,qCAAiC;IAAjC,iCAAiC;EP6kDnE;EO5kDE;IAAgC,0CAAsC;IAAtC,sCAAsC;EP+kDxE;EO9kDE;IAAgC,6CAAyC;IAAzC,yCAAyC;EPilD3E;EO/kDE;IAA8B,8BAA0B;IAA1B,0BAA0B;EPklD1D;EOjlDE;IAA8B,gCAA4B;IAA5B,4BAA4B;EPolD5D;EOnlDE;IAA8B,sCAAkC;IAAlC,kCAAkC;EPslDlE;EOrlDE;IAA8B,6BAAyB;IAAzB,yBAAyB;EPwlDzD;EOvlDE;IAA8B,+BAAuB;IAAvB,uBAAuB;EP0lDvD;EOzlDE;IAA8B,+BAAuB;IAAvB,uBAAuB;EP4lDvD;EO3lDE;IAA8B,+BAAyB;IAAzB,yBAAyB;EP8lDzD;EO7lDE;IAA8B,+BAAyB;IAAzB,yBAAyB;EPgmDzD;EO9lDE;IAAoC,+BAAsC;IAAtC,sCAAsC;EPimD5E;EOhmDE;IAAoC,6BAAoC;IAApC,oCAAoC;EPmmD1E;EOlmDE;IAAoC,gCAAkC;IAAlC,kCAAkC;EPqmDxE;EOpmDE;IAAoC,iCAAyC;IAAzC,yCAAyC;EPumD/E;EOtmDE;IAAoC,oCAAwC;IAAxC,wCAAwC;EPymD9E;EOvmDE;IAAiC,gCAAkC;IAAlC,kCAAkC;EP0mDrE;EOzmDE;IAAiC,8BAAgC;IAAhC,gCAAgC;EP4mDnE;EO3mDE;IAAiC,iCAA8B;IAA9B,8BAA8B;EP8mDjE;EO7mDE;IAAiC,mCAAgC;IAAhC,gCAAgC;EPgnDnE;EO/mDE;IAAiC,kCAA+B;IAA/B,+BAA+B;EPknDlE;EOhnDE;IAAkC,oCAAoC;IAApC,oCAAoC;EPmnDxE;EOlnDE;IAAkC,kCAAkC;IAAlC,kCAAkC;EPqnDtE;EOpnDE;IAAkC,qCAAgC;IAAhC,gCAAgC;EPunDpE;EOtnDE;IAAkC,sCAAuC;IAAvC,uCAAuC;EPynD3E;EOxnDE;IAAkC,yCAAsC;IAAtC,sCAAsC;EP2nD1E;EO1nDE;IAAkC,sCAAiC;IAAjC,iCAAiC;EP6nDrE;EO3nDE;IAAgC,oCAA2B;IAA3B,2BAA2B;EP8nD7D;EO7nDE;IAAgC,qCAAiC;IAAjC,iCAAiC;EPgoDnE;EO/nDE;IAAgC,mCAA+B;IAA/B,+BAA+B;EPkoDjE;EOjoDE;IAAgC,sCAA6B;IAA7B,6BAA6B;EPooD/D;EOnoDE;IAAgC,wCAA+B;IAA/B,+BAA+B;EPsoDjE;EOroDE;IAAgC,uCAA8B;IAA9B,8BAA8B;EPwoDhE;AACF;;AQ/qDQ;EAAgC,oBAA4B;ARmrDpE;;AQlrDQ;;EAEE,wBAAoC;ARqrD9C;;AQnrDQ;;EAEE,0BAAwC;ARsrDlD;;AQprDQ;;EAEE,2BAA0C;ARurDpD;;AQrrDQ;;EAEE,yBAAsC;ARwrDhD;;AQvsDQ;EAAgC,0BAA4B;AR2sDpE;;AQ1sDQ;;EAEE,8BAAoC;AR6sD9C;;AQ3sDQ;;EAEE,gCAAwC;AR8sDlD;;AQ5sDQ;;EAEE,iCAA0C;AR+sDpD;;AQ7sDQ;;EAEE,+BAAsC;ARgtDhD;;AQ/tDQ;EAAgC,yBAA4B;ARmuDpE;;AQluDQ;;EAEE,6BAAoC;ARquD9C;;AQnuDQ;;EAEE,+BAAwC;ARsuDlD;;AQpuDQ;;EAEE,gCAA0C;ARuuDpD;;AQruDQ;;EAEE,8BAAsC;ARwuDhD;;AQvvDQ;EAAgC,uBAA4B;AR2vDpE;;AQ1vDQ;;EAEE,2BAAoC;AR6vD9C;;AQ3vDQ;;EAEE,6BAAwC;AR8vDlD;;AQ5vDQ;;EAEE,8BAA0C;AR+vDpD;;AQ7vDQ;;EAEE,4BAAsC;ARgwDhD;;AQ/wDQ;EAAgC,yBAA4B;ARmxDpE;;AQlxDQ;;EAEE,6BAAoC;ARqxD9C;;AQnxDQ;;EAEE,+BAAwC;ARsxDlD;;AQpxDQ;;EAEE,gCAA0C;ARuxDpD;;AQrxDQ;;EAEE,8BAAsC;ARwxDhD;;AQvyDQ;EAAgC,uBAA4B;AR2yDpE;;AQ1yDQ;;EAEE,2BAAoC;AR6yD9C;;AQ3yDQ;;EAEE,6BAAwC;AR8yDlD;;AQ5yDQ;;EAEE,8BAA0C;AR+yDpD;;AQ7yDQ;;EAEE,4BAAsC;ARgzDhD;;AQ/zDQ;EAAgC,qBAA4B;ARm0DpE;;AQl0DQ;;EAEE,yBAAoC;ARq0D9C;;AQn0DQ;;EAEE,2BAAwC;ARs0DlD;;AQp0DQ;;EAEE,4BAA0C;ARu0DpD;;AQr0DQ;;EAEE,0BAAsC;ARw0DhD;;AQv1DQ;EAAgC,2BAA4B;AR21DpE;;AQ11DQ;;EAEE,+BAAoC;AR61D9C;;AQ31DQ;;EAEE,iCAAwC;AR81DlD;;AQ51DQ;;EAEE,kCAA0C;AR+1DpD;;AQ71DQ;;EAEE,gCAAsC;ARg2DhD;;AQ/2DQ;EAAgC,0BAA4B;ARm3DpE;;AQl3DQ;;EAEE,8BAAoC;ARq3D9C;;AQn3DQ;;EAEE,gCAAwC;ARs3DlD;;AQp3DQ;;EAEE,iCAA0C;ARu3DpD;;AQr3DQ;;EAEE,+BAAsC;ARw3DhD;;AQv4DQ;EAAgC,wBAA4B;AR24DpE;;AQ14DQ;;EAEE,4BAAoC;AR64D9C;;AQ34DQ;;EAEE,8BAAwC;AR84DlD;;AQ54DQ;;EAEE,+BAA0C;AR+4DpD;;AQ74DQ;;EAEE,6BAAsC;ARg5DhD;;AQ/5DQ;EAAgC,0BAA4B;ARm6DpE;;AQl6DQ;;EAEE,8BAAoC;ARq6D9C;;AQn6DQ;;EAEE,gCAAwC;ARs6DlD;;AQp6DQ;;EAEE,iCAA0C;ARu6DpD;;AQr6DQ;;EAEE,+BAAsC;ARw6DhD;;AQv7DQ;EAAgC,wBAA4B;AR27DpE;;AQ17DQ;;EAEE,4BAAoC;AR67D9C;;AQ37DQ;;EAEE,8BAAwC;AR87DlD;;AQ57DQ;;EAEE,+BAA0C;AR+7DpD;;AQ77DQ;;EAEE,6BAAsC;ARg8DhD;;AQx7DQ;EAAwB,2BAA2B;AR47D3D;;AQ37DQ;;EAEE,+BAA+B;AR87DzC;;AQ57DQ;;EAEE,iCAAiC;AR+7D3C;;AQ77DQ;;EAEE,kCAAkC;ARg8D5C;;AQ97DQ;;EAEE,gCAAgC;ARi8D1C;;AQh9DQ;EAAwB,0BAA2B;ARo9D3D;;AQn9DQ;;EAEE,8BAA+B;ARs9DzC;;AQp9DQ;;EAEE,gCAAiC;ARu9D3C;;AQr9DQ;;EAEE,iCAAkC;ARw9D5C;;AQt9DQ;;EAEE,+BAAgC;ARy9D1C;;AQx+DQ;EAAwB,wBAA2B;AR4+D3D;;AQ3+DQ;;EAEE,4BAA+B;AR8+DzC;;AQ5+DQ;;EAEE,8BAAiC;AR++D3C;;AQ7+DQ;;EAEE,+BAAkC;ARg/D5C;;AQ9+DQ;;EAEE,6BAAgC;ARi/D1C;;AQhgEQ;EAAwB,0BAA2B;ARogE3D;;AQngEQ;;EAEE,8BAA+B;ARsgEzC;;AQpgEQ;;EAEE,gCAAiC;ARugE3C;;AQrgEQ;;EAEE,iCAAkC;ARwgE5C;;AQtgEQ;;EAEE,+BAAgC;ARygE1C;;AQxhEQ;EAAwB,wBAA2B;AR4hE3D;;AQ3hEQ;;EAEE,4BAA+B;AR8hEzC;;AQ5hEQ;;EAEE,8BAAiC;AR+hE3C;;AQ7hEQ;;EAEE,+BAAkC;ARgiE5C;;AQ9hEQ;;EAEE,6BAAgC;ARiiE1C;;AQ3hEI;EAAmB,uBAAuB;AR+hE9C;;AQ9hEI;;EAEE,2BAA2B;ARiiEjC;;AQ/hEI;;EAEE,6BAA6B;ARkiEnC;;AQhiEI;;EAEE,8BAA8B;ARmiEpC;;AQjiEI;;EAEE,4BAA4B;ARoiElC;;AG7iEI;EKlDI;IAAgC,oBAA4B;ERomElE;EQnmEM;;IAEE,wBAAoC;ERqmE5C;EQnmEM;;IAEE,0BAAwC;ERqmEhD;EQnmEM;;IAEE,2BAA0C;ERqmElD;EQnmEM;;IAEE,yBAAsC;ERqmE9C;EQpnEM;IAAgC,0BAA4B;ERunElE;EQtnEM;;IAEE,8BAAoC;ERwnE5C;EQtnEM;;IAEE,gCAAwC;ERwnEhD;EQtnEM;;IAEE,iCAA0C;ERwnElD;EQtnEM;;IAEE,+BAAsC;ERwnE9C;EQvoEM;IAAgC,yBAA4B;ER0oElE;EQzoEM;;IAEE,6BAAoC;ER2oE5C;EQzoEM;;IAEE,+BAAwC;ER2oEhD;EQzoEM;;IAEE,gCAA0C;ER2oElD;EQzoEM;;IAEE,8BAAsC;ER2oE9C;EQ1pEM;IAAgC,uBAA4B;ER6pElE;EQ5pEM;;IAEE,2BAAoC;ER8pE5C;EQ5pEM;;IAEE,6BAAwC;ER8pEhD;EQ5pEM;;IAEE,8BAA0C;ER8pElD;EQ5pEM;;IAEE,4BAAsC;ER8pE9C;EQ7qEM;IAAgC,yBAA4B;ERgrElE;EQ/qEM;;IAEE,6BAAoC;ERirE5C;EQ/qEM;;IAEE,+BAAwC;ERirEhD;EQ/qEM;;IAEE,gCAA0C;ERirElD;EQ/qEM;;IAEE,8BAAsC;ERirE9C;EQhsEM;IAAgC,uBAA4B;ERmsElE;EQlsEM;;IAEE,2BAAoC;ERosE5C;EQlsEM;;IAEE,6BAAwC;ERosEhD;EQlsEM;;IAEE,8BAA0C;ERosElD;EQlsEM;;IAEE,4BAAsC;ERosE9C;EQntEM;IAAgC,qBAA4B;ERstElE;EQrtEM;;IAEE,yBAAoC;ERutE5C;EQrtEM;;IAEE,2BAAwC;ERutEhD;EQrtEM;;IAEE,4BAA0C;ERutElD;EQrtEM;;IAEE,0BAAsC;ERutE9C;EQtuEM;IAAgC,2BAA4B;ERyuElE;EQxuEM;;IAEE,+BAAoC;ER0uE5C;EQxuEM;;IAEE,iCAAwC;ER0uEhD;EQxuEM;;IAEE,kCAA0C;ER0uElD;EQxuEM;;IAEE,gCAAsC;ER0uE9C;EQzvEM;IAAgC,0BAA4B;ER4vElE;EQ3vEM;;IAEE,8BAAoC;ER6vE5C;EQ3vEM;;IAEE,gCAAwC;ER6vEhD;EQ3vEM;;IAEE,iCAA0C;ER6vElD;EQ3vEM;;IAEE,+BAAsC;ER6vE9C;EQ5wEM;IAAgC,wBAA4B;ER+wElE;EQ9wEM;;IAEE,4BAAoC;ERgxE5C;EQ9wEM;;IAEE,8BAAwC;ERgxEhD;EQ9wEM;;IAEE,+BAA0C;ERgxElD;EQ9wEM;;IAEE,6BAAsC;ERgxE9C;EQ/xEM;IAAgC,0BAA4B;ERkyElE;EQjyEM;;IAEE,8BAAoC;ERmyE5C;EQjyEM;;IAEE,gCAAwC;ERmyEhD;EQjyEM;;IAEE,iCAA0C;ERmyElD;EQjyEM;;IAEE,+BAAsC;ERmyE9C;EQlzEM;IAAgC,wBAA4B;ERqzElE;EQpzEM;;IAEE,4BAAoC;ERszE5C;EQpzEM;;IAEE,8BAAwC;ERszEhD;EQpzEM;;IAEE,+BAA0C;ERszElD;EQpzEM;;IAEE,6BAAsC;ERszE9C;EQ9yEM;IAAwB,2BAA2B;ERizEzD;EQhzEM;;IAEE,+BAA+B;ERkzEvC;EQhzEM;;IAEE,iCAAiC;ERkzEzC;EQhzEM;;IAEE,kCAAkC;ERkzE1C;EQhzEM;;IAEE,gCAAgC;ERkzExC;EQj0EM;IAAwB,0BAA2B;ERo0EzD;EQn0EM;;IAEE,8BAA+B;ERq0EvC;EQn0EM;;IAEE,gCAAiC;ERq0EzC;EQn0EM;;IAEE,iCAAkC;ERq0E1C;EQn0EM;;IAEE,+BAAgC;ERq0ExC;EQp1EM;IAAwB,wBAA2B;ERu1EzD;EQt1EM;;IAEE,4BAA+B;ERw1EvC;EQt1EM;;IAEE,8BAAiC;ERw1EzC;EQt1EM;;IAEE,+BAAkC;ERw1E1C;EQt1EM;;IAEE,6BAAgC;ERw1ExC;EQv2EM;IAAwB,0BAA2B;ER02EzD;EQz2EM;;IAEE,8BAA+B;ER22EvC;EQz2EM;;IAEE,gCAAiC;ER22EzC;EQz2EM;;IAEE,iCAAkC;ER22E1C;EQz2EM;;IAEE,+BAAgC;ER22ExC;EQ13EM;IAAwB,wBAA2B;ER63EzD;EQ53EM;;IAEE,4BAA+B;ER83EvC;EQ53EM;;IAEE,8BAAiC;ER83EzC;EQ53EM;;IAEE,+BAAkC;ER83E1C;EQ53EM;;IAEE,6BAAgC;ER83ExC;EQx3EE;IAAmB,uBAAuB;ER23E5C;EQ13EE;;IAEE,2BAA2B;ER43E/B;EQ13EE;;IAEE,6BAA6B;ER43EjC;EQ13EE;;IAEE,8BAA8B;ER43ElC;EQ13EE;;IAEE,4BAA4B;ER43EhC;AACF;;AGt4EI;EKlDI;IAAgC,oBAA4B;ER67ElE;EQ57EM;;IAEE,wBAAoC;ER87E5C;EQ57EM;;IAEE,0BAAwC;ER87EhD;EQ57EM;;IAEE,2BAA0C;ER87ElD;EQ57EM;;IAEE,yBAAsC;ER87E9C;EQ78EM;IAAgC,0BAA4B;ERg9ElE;EQ/8EM;;IAEE,8BAAoC;ERi9E5C;EQ/8EM;;IAEE,gCAAwC;ERi9EhD;EQ/8EM;;IAEE,iCAA0C;ERi9ElD;EQ/8EM;;IAEE,+BAAsC;ERi9E9C;EQh+EM;IAAgC,yBAA4B;ERm+ElE;EQl+EM;;IAEE,6BAAoC;ERo+E5C;EQl+EM;;IAEE,+BAAwC;ERo+EhD;EQl+EM;;IAEE,gCAA0C;ERo+ElD;EQl+EM;;IAEE,8BAAsC;ERo+E9C;EQn/EM;IAAgC,uBAA4B;ERs/ElE;EQr/EM;;IAEE,2BAAoC;ERu/E5C;EQr/EM;;IAEE,6BAAwC;ERu/EhD;EQr/EM;;IAEE,8BAA0C;ERu/ElD;EQr/EM;;IAEE,4BAAsC;ERu/E9C;EQtgFM;IAAgC,yBAA4B;ERygFlE;EQxgFM;;IAEE,6BAAoC;ER0gF5C;EQxgFM;;IAEE,+BAAwC;ER0gFhD;EQxgFM;;IAEE,gCAA0C;ER0gFlD;EQxgFM;;IAEE,8BAAsC;ER0gF9C;EQzhFM;IAAgC,uBAA4B;ER4hFlE;EQ3hFM;;IAEE,2BAAoC;ER6hF5C;EQ3hFM;;IAEE,6BAAwC;ER6hFhD;EQ3hFM;;IAEE,8BAA0C;ER6hFlD;EQ3hFM;;IAEE,4BAAsC;ER6hF9C;EQ5iFM;IAAgC,qBAA4B;ER+iFlE;EQ9iFM;;IAEE,yBAAoC;ERgjF5C;EQ9iFM;;IAEE,2BAAwC;ERgjFhD;EQ9iFM;;IAEE,4BAA0C;ERgjFlD;EQ9iFM;;IAEE,0BAAsC;ERgjF9C;EQ/jFM;IAAgC,2BAA4B;ERkkFlE;EQjkFM;;IAEE,+BAAoC;ERmkF5C;EQjkFM;;IAEE,iCAAwC;ERmkFhD;EQjkFM;;IAEE,kCAA0C;ERmkFlD;EQjkFM;;IAEE,gCAAsC;ERmkF9C;EQllFM;IAAgC,0BAA4B;ERqlFlE;EQplFM;;IAEE,8BAAoC;ERslF5C;EQplFM;;IAEE,gCAAwC;ERslFhD;EQplFM;;IAEE,iCAA0C;ERslFlD;EQplFM;;IAEE,+BAAsC;ERslF9C;EQrmFM;IAAgC,wBAA4B;ERwmFlE;EQvmFM;;IAEE,4BAAoC;ERymF5C;EQvmFM;;IAEE,8BAAwC;ERymFhD;EQvmFM;;IAEE,+BAA0C;ERymFlD;EQvmFM;;IAEE,6BAAsC;ERymF9C;EQxnFM;IAAgC,0BAA4B;ER2nFlE;EQ1nFM;;IAEE,8BAAoC;ER4nF5C;EQ1nFM;;IAEE,gCAAwC;ER4nFhD;EQ1nFM;;IAEE,iCAA0C;ER4nFlD;EQ1nFM;;IAEE,+BAAsC;ER4nF9C;EQ3oFM;IAAgC,wBAA4B;ER8oFlE;EQ7oFM;;IAEE,4BAAoC;ER+oF5C;EQ7oFM;;IAEE,8BAAwC;ER+oFhD;EQ7oFM;;IAEE,+BAA0C;ER+oFlD;EQ7oFM;;IAEE,6BAAsC;ER+oF9C;EQvoFM;IAAwB,2BAA2B;ER0oFzD;EQzoFM;;IAEE,+BAA+B;ER2oFvC;EQzoFM;;IAEE,iCAAiC;ER2oFzC;EQzoFM;;IAEE,kCAAkC;ER2oF1C;EQzoFM;;IAEE,gCAAgC;ER2oFxC;EQ1pFM;IAAwB,0BAA2B;ER6pFzD;EQ5pFM;;IAEE,8BAA+B;ER8pFvC;EQ5pFM;;IAEE,gCAAiC;ER8pFzC;EQ5pFM;;IAEE,iCAAkC;ER8pF1C;EQ5pFM;;IAEE,+BAAgC;ER8pFxC;EQ7qFM;IAAwB,wBAA2B;ERgrFzD;EQ/qFM;;IAEE,4BAA+B;ERirFvC;EQ/qFM;;IAEE,8BAAiC;ERirFzC;EQ/qFM;;IAEE,+BAAkC;ERirF1C;EQ/qFM;;IAEE,6BAAgC;ERirFxC;EQhsFM;IAAwB,0BAA2B;ERmsFzD;EQlsFM;;IAEE,8BAA+B;ERosFvC;EQlsFM;;IAEE,gCAAiC;ERosFzC;EQlsFM;;IAEE,iCAAkC;ERosF1C;EQlsFM;;IAEE,+BAAgC;ERosFxC;EQntFM;IAAwB,wBAA2B;ERstFzD;EQrtFM;;IAEE,4BAA+B;ERutFvC;EQrtFM;;IAEE,8BAAiC;ERutFzC;EQrtFM;;IAEE,+BAAkC;ERutF1C;EQrtFM;;IAEE,6BAAgC;ERutFxC;EQjtFE;IAAmB,uBAAuB;ERotF5C;EQntFE;;IAEE,2BAA2B;ERqtF/B;EQntFE;;IAEE,6BAA6B;ERqtFjC;EQntFE;;IAEE,8BAA8B;ERqtFlC;EQntFE;;IAEE,4BAA4B;ERqtFhC;AACF;;AG/tFI;EKlDI;IAAgC,oBAA4B;ERsxFlE;EQrxFM;;IAEE,wBAAoC;ERuxF5C;EQrxFM;;IAEE,0BAAwC;ERuxFhD;EQrxFM;;IAEE,2BAA0C;ERuxFlD;EQrxFM;;IAEE,yBAAsC;ERuxF9C;EQtyFM;IAAgC,0BAA4B;ERyyFlE;EQxyFM;;IAEE,8BAAoC;ER0yF5C;EQxyFM;;IAEE,gCAAwC;ER0yFhD;EQxyFM;;IAEE,iCAA0C;ER0yFlD;EQxyFM;;IAEE,+BAAsC;ER0yF9C;EQzzFM;IAAgC,yBAA4B;ER4zFlE;EQ3zFM;;IAEE,6BAAoC;ER6zF5C;EQ3zFM;;IAEE,+BAAwC;ER6zFhD;EQ3zFM;;IAEE,gCAA0C;ER6zFlD;EQ3zFM;;IAEE,8BAAsC;ER6zF9C;EQ50FM;IAAgC,uBAA4B;ER+0FlE;EQ90FM;;IAEE,2BAAoC;ERg1F5C;EQ90FM;;IAEE,6BAAwC;ERg1FhD;EQ90FM;;IAEE,8BAA0C;ERg1FlD;EQ90FM;;IAEE,4BAAsC;ERg1F9C;EQ/1FM;IAAgC,yBAA4B;ERk2FlE;EQj2FM;;IAEE,6BAAoC;ERm2F5C;EQj2FM;;IAEE,+BAAwC;ERm2FhD;EQj2FM;;IAEE,gCAA0C;ERm2FlD;EQj2FM;;IAEE,8BAAsC;ERm2F9C;EQl3FM;IAAgC,uBAA4B;ERq3FlE;EQp3FM;;IAEE,2BAAoC;ERs3F5C;EQp3FM;;IAEE,6BAAwC;ERs3FhD;EQp3FM;;IAEE,8BAA0C;ERs3FlD;EQp3FM;;IAEE,4BAAsC;ERs3F9C;EQr4FM;IAAgC,qBAA4B;ERw4FlE;EQv4FM;;IAEE,yBAAoC;ERy4F5C;EQv4FM;;IAEE,2BAAwC;ERy4FhD;EQv4FM;;IAEE,4BAA0C;ERy4FlD;EQv4FM;;IAEE,0BAAsC;ERy4F9C;EQx5FM;IAAgC,2BAA4B;ER25FlE;EQ15FM;;IAEE,+BAAoC;ER45F5C;EQ15FM;;IAEE,iCAAwC;ER45FhD;EQ15FM;;IAEE,kCAA0C;ER45FlD;EQ15FM;;IAEE,gCAAsC;ER45F9C;EQ36FM;IAAgC,0BAA4B;ER86FlE;EQ76FM;;IAEE,8BAAoC;ER+6F5C;EQ76FM;;IAEE,gCAAwC;ER+6FhD;EQ76FM;;IAEE,iCAA0C;ER+6FlD;EQ76FM;;IAEE,+BAAsC;ER+6F9C;EQ97FM;IAAgC,wBAA4B;ERi8FlE;EQh8FM;;IAEE,4BAAoC;ERk8F5C;EQh8FM;;IAEE,8BAAwC;ERk8FhD;EQh8FM;;IAEE,+BAA0C;ERk8FlD;EQh8FM;;IAEE,6BAAsC;ERk8F9C;EQj9FM;IAAgC,0BAA4B;ERo9FlE;EQn9FM;;IAEE,8BAAoC;ERq9F5C;EQn9FM;;IAEE,gCAAwC;ERq9FhD;EQn9FM;;IAEE,iCAA0C;ERq9FlD;EQn9FM;;IAEE,+BAAsC;ERq9F9C;EQp+FM;IAAgC,wBAA4B;ERu+FlE;EQt+FM;;IAEE,4BAAoC;ERw+F5C;EQt+FM;;IAEE,8BAAwC;ERw+FhD;EQt+FM;;IAEE,+BAA0C;ERw+FlD;EQt+FM;;IAEE,6BAAsC;ERw+F9C;EQh+FM;IAAwB,2BAA2B;ERm+FzD;EQl+FM;;IAEE,+BAA+B;ERo+FvC;EQl+FM;;IAEE,iCAAiC;ERo+FzC;EQl+FM;;IAEE,kCAAkC;ERo+F1C;EQl+FM;;IAEE,gCAAgC;ERo+FxC;EQn/FM;IAAwB,0BAA2B;ERs/FzD;EQr/FM;;IAEE,8BAA+B;ERu/FvC;EQr/FM;;IAEE,gCAAiC;ERu/FzC;EQr/FM;;IAEE,iCAAkC;ERu/F1C;EQr/FM;;IAEE,+BAAgC;ERu/FxC;EQtgGM;IAAwB,wBAA2B;ERygGzD;EQxgGM;;IAEE,4BAA+B;ER0gGvC;EQxgGM;;IAEE,8BAAiC;ER0gGzC;EQxgGM;;IAEE,+BAAkC;ER0gG1C;EQxgGM;;IAEE,6BAAgC;ER0gGxC;EQzhGM;IAAwB,0BAA2B;ER4hGzD;EQ3hGM;;IAEE,8BAA+B;ER6hGvC;EQ3hGM;;IAEE,gCAAiC;ER6hGzC;EQ3hGM;;IAEE,iCAAkC;ER6hG1C;EQ3hGM;;IAEE,+BAAgC;ER6hGxC;EQ5iGM;IAAwB,wBAA2B;ER+iGzD;EQ9iGM;;IAEE,4BAA+B;ERgjGvC;EQ9iGM;;IAEE,8BAAiC;ERgjGzC;EQ9iGM;;IAEE,+BAAkC;ERgjG1C;EQ9iGM;;IAEE,6BAAgC;ERgjGxC;EQ1iGE;IAAmB,uBAAuB;ER6iG5C;EQ5iGE;;IAEE,2BAA2B;ER8iG/B;EQ5iGE;;IAEE,6BAA6B;ER8iGjC;EQ5iGE;;IAEE,8BAA8B;ER8iGlC;EQ5iGE;;IAEE,4BAA4B;ER8iGhC;AACF;;AGxjGI;EKlDI;IAAgC,oBAA4B;ER+mGlE;EQ9mGM;;IAEE,wBAAoC;ERgnG5C;EQ9mGM;;IAEE,0BAAwC;ERgnGhD;EQ9mGM;;IAEE,2BAA0C;ERgnGlD;EQ9mGM;;IAEE,yBAAsC;ERgnG9C;EQ/nGM;IAAgC,0BAA4B;ERkoGlE;EQjoGM;;IAEE,8BAAoC;ERmoG5C;EQjoGM;;IAEE,gCAAwC;ERmoGhD;EQjoGM;;IAEE,iCAA0C;ERmoGlD;EQjoGM;;IAEE,+BAAsC;ERmoG9C;EQlpGM;IAAgC,yBAA4B;ERqpGlE;EQppGM;;IAEE,6BAAoC;ERspG5C;EQppGM;;IAEE,+BAAwC;ERspGhD;EQppGM;;IAEE,gCAA0C;ERspGlD;EQppGM;;IAEE,8BAAsC;ERspG9C;EQrqGM;IAAgC,uBAA4B;ERwqGlE;EQvqGM;;IAEE,2BAAoC;ERyqG5C;EQvqGM;;IAEE,6BAAwC;ERyqGhD;EQvqGM;;IAEE,8BAA0C;ERyqGlD;EQvqGM;;IAEE,4BAAsC;ERyqG9C;EQxrGM;IAAgC,yBAA4B;ER2rGlE;EQ1rGM;;IAEE,6BAAoC;ER4rG5C;EQ1rGM;;IAEE,+BAAwC;ER4rGhD;EQ1rGM;;IAEE,gCAA0C;ER4rGlD;EQ1rGM;;IAEE,8BAAsC;ER4rG9C;EQ3sGM;IAAgC,uBAA4B;ER8sGlE;EQ7sGM;;IAEE,2BAAoC;ER+sG5C;EQ7sGM;;IAEE,6BAAwC;ER+sGhD;EQ7sGM;;IAEE,8BAA0C;ER+sGlD;EQ7sGM;;IAEE,4BAAsC;ER+sG9C;EQ9tGM;IAAgC,qBAA4B;ERiuGlE;EQhuGM;;IAEE,yBAAoC;ERkuG5C;EQhuGM;;IAEE,2BAAwC;ERkuGhD;EQhuGM;;IAEE,4BAA0C;ERkuGlD;EQhuGM;;IAEE,0BAAsC;ERkuG9C;EQjvGM;IAAgC,2BAA4B;ERovGlE;EQnvGM;;IAEE,+BAAoC;ERqvG5C;EQnvGM;;IAEE,iCAAwC;ERqvGhD;EQnvGM;;IAEE,kCAA0C;ERqvGlD;EQnvGM;;IAEE,gCAAsC;ERqvG9C;EQpwGM;IAAgC,0BAA4B;ERuwGlE;EQtwGM;;IAEE,8BAAoC;ERwwG5C;EQtwGM;;IAEE,gCAAwC;ERwwGhD;EQtwGM;;IAEE,iCAA0C;ERwwGlD;EQtwGM;;IAEE,+BAAsC;ERwwG9C;EQvxGM;IAAgC,wBAA4B;ER0xGlE;EQzxGM;;IAEE,4BAAoC;ER2xG5C;EQzxGM;;IAEE,8BAAwC;ER2xGhD;EQzxGM;;IAEE,+BAA0C;ER2xGlD;EQzxGM;;IAEE,6BAAsC;ER2xG9C;EQ1yGM;IAAgC,0BAA4B;ER6yGlE;EQ5yGM;;IAEE,8BAAoC;ER8yG5C;EQ5yGM;;IAEE,gCAAwC;ER8yGhD;EQ5yGM;;IAEE,iCAA0C;ER8yGlD;EQ5yGM;;IAEE,+BAAsC;ER8yG9C;EQ7zGM;IAAgC,wBAA4B;ERg0GlE;EQ/zGM;;IAEE,4BAAoC;ERi0G5C;EQ/zGM;;IAEE,8BAAwC;ERi0GhD;EQ/zGM;;IAEE,+BAA0C;ERi0GlD;EQ/zGM;;IAEE,6BAAsC;ERi0G9C;EQzzGM;IAAwB,2BAA2B;ER4zGzD;EQ3zGM;;IAEE,+BAA+B;ER6zGvC;EQ3zGM;;IAEE,iCAAiC;ER6zGzC;EQ3zGM;;IAEE,kCAAkC;ER6zG1C;EQ3zGM;;IAEE,gCAAgC;ER6zGxC;EQ50GM;IAAwB,0BAA2B;ER+0GzD;EQ90GM;;IAEE,8BAA+B;ERg1GvC;EQ90GM;;IAEE,gCAAiC;ERg1GzC;EQ90GM;;IAEE,iCAAkC;ERg1G1C;EQ90GM;;IAEE,+BAAgC;ERg1GxC;EQ/1GM;IAAwB,wBAA2B;ERk2GzD;EQj2GM;;IAEE,4BAA+B;ERm2GvC;EQj2GM;;IAEE,8BAAiC;ERm2GzC;EQj2GM;;IAEE,+BAAkC;ERm2G1C;EQj2GM;;IAEE,6BAAgC;ERm2GxC;EQl3GM;IAAwB,0BAA2B;ERq3GzD;EQp3GM;;IAEE,8BAA+B;ERs3GvC;EQp3GM;;IAEE,gCAAiC;ERs3GzC;EQp3GM;;IAEE,iCAAkC;ERs3G1C;EQp3GM;;IAEE,+BAAgC;ERs3GxC;EQr4GM;IAAwB,wBAA2B;ERw4GzD;EQv4GM;;IAEE,4BAA+B;ERy4GvC;EQv4GM;;IAEE,8BAAiC;ERy4GzC;EQv4GM;;IAEE,+BAAkC;ERy4G1C;EQv4GM;;IAEE,6BAAgC;ERy4GxC;EQn4GE;IAAmB,uBAAuB;ERs4G5C;EQr4GE;;IAEE,2BAA2B;ERu4G/B;EQr4GE;;IAEE,6BAA6B;ERu4GjC;EQr4GE;;IAEE,8BAA8B;ERu4GlC;EQr4GE;;IAEE,4BAA4B;ERu4GhC;AACF","file":"bootstrap-grid.css","sourcesContent":["/*!\n * Bootstrap Grid v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n\nhtml {\n box-sizing: border-box;\n -ms-overflow-style: scrollbar;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: inherit;\n}\n\n@import \"functions\";\n@import \"variables\";\n\n@import \"mixins/breakpoints\";\n@import \"mixins/grid-framework\";\n@import \"mixins/grid\";\n\n@import \"grid\";\n@import \"utilities/display\";\n@import \"utilities/flex\";\n@import \"utilities/spacing\";\n","/*!\n * Bootstrap Grid v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\nhtml {\n box-sizing: border-box;\n -ms-overflow-style: scrollbar;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: inherit;\n}\n\n.container {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container {\n max-width: 1140px;\n }\n}\n\n.container-fluid, .container-sm, .container-md, .container-lg, .container-xl {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container, .container-sm {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container, .container-sm, .container-md {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container, .container-sm, .container-md, .container-lg {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container, .container-sm, .container-md, .container-lg, .container-xl {\n max-width: 1140px;\n }\n}\n\n.row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n\n.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,\n.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm,\n.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md,\n.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg,\n.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl,\n.col-xl-auto {\n position: relative;\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n}\n\n.col {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n}\n\n.row-cols-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n}\n\n.col-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n}\n\n.col-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-3 {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.col-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.col-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n}\n\n.col-6 {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.col-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n}\n\n.col-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n}\n\n.col-9 {\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.col-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n}\n\n.col-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n}\n\n.col-12 {\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.order-first {\n order: -1;\n}\n\n.order-last {\n order: 13;\n}\n\n.order-0 {\n order: 0;\n}\n\n.order-1 {\n order: 1;\n}\n\n.order-2 {\n order: 2;\n}\n\n.order-3 {\n order: 3;\n}\n\n.order-4 {\n order: 4;\n}\n\n.order-5 {\n order: 5;\n}\n\n.order-6 {\n order: 6;\n}\n\n.order-7 {\n order: 7;\n}\n\n.order-8 {\n order: 8;\n}\n\n.order-9 {\n order: 9;\n}\n\n.order-10 {\n order: 10;\n}\n\n.order-11 {\n order: 11;\n}\n\n.order-12 {\n order: 12;\n}\n\n.offset-1 {\n margin-left: 8.333333%;\n}\n\n.offset-2 {\n margin-left: 16.666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.333333%;\n}\n\n.offset-5 {\n margin-left: 41.666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.333333%;\n}\n\n.offset-8 {\n margin-left: 66.666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.333333%;\n}\n\n.offset-11 {\n margin-left: 91.666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-sm-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-sm-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-sm-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-sm-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-sm-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-sm-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-sm-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-sm-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-sm-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-sm-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-sm-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-sm-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-sm-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-sm-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-sm-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-sm-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-sm-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-sm-first {\n order: -1;\n }\n .order-sm-last {\n order: 13;\n }\n .order-sm-0 {\n order: 0;\n }\n .order-sm-1 {\n order: 1;\n }\n .order-sm-2 {\n order: 2;\n }\n .order-sm-3 {\n order: 3;\n }\n .order-sm-4 {\n order: 4;\n }\n .order-sm-5 {\n order: 5;\n }\n .order-sm-6 {\n order: 6;\n }\n .order-sm-7 {\n order: 7;\n }\n .order-sm-8 {\n order: 8;\n }\n .order-sm-9 {\n order: 9;\n }\n .order-sm-10 {\n order: 10;\n }\n .order-sm-11 {\n order: 11;\n }\n .order-sm-12 {\n order: 12;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.333333%;\n }\n .offset-sm-2 {\n margin-left: 16.666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.333333%;\n }\n .offset-sm-5 {\n margin-left: 41.666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.333333%;\n }\n .offset-sm-8 {\n margin-left: 66.666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.333333%;\n }\n .offset-sm-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 768px) {\n .col-md {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-md-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-md-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-md-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-md-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-md-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-md-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-md-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-md-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-md-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-md-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-md-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-md-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-md-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-md-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-md-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-md-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-md-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-md-first {\n order: -1;\n }\n .order-md-last {\n order: 13;\n }\n .order-md-0 {\n order: 0;\n }\n .order-md-1 {\n order: 1;\n }\n .order-md-2 {\n order: 2;\n }\n .order-md-3 {\n order: 3;\n }\n .order-md-4 {\n order: 4;\n }\n .order-md-5 {\n order: 5;\n }\n .order-md-6 {\n order: 6;\n }\n .order-md-7 {\n order: 7;\n }\n .order-md-8 {\n order: 8;\n }\n .order-md-9 {\n order: 9;\n }\n .order-md-10 {\n order: 10;\n }\n .order-md-11 {\n order: 11;\n }\n .order-md-12 {\n order: 12;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.333333%;\n }\n .offset-md-2 {\n margin-left: 16.666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.333333%;\n }\n .offset-md-5 {\n margin-left: 41.666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.333333%;\n }\n .offset-md-8 {\n margin-left: 66.666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.333333%;\n }\n .offset-md-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 992px) {\n .col-lg {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-lg-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-lg-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-lg-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-lg-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-lg-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-lg-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-lg-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-lg-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-lg-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-lg-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-lg-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-lg-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-lg-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-lg-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-lg-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-lg-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-lg-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-lg-first {\n order: -1;\n }\n .order-lg-last {\n order: 13;\n }\n .order-lg-0 {\n order: 0;\n }\n .order-lg-1 {\n order: 1;\n }\n .order-lg-2 {\n order: 2;\n }\n .order-lg-3 {\n order: 3;\n }\n .order-lg-4 {\n order: 4;\n }\n .order-lg-5 {\n order: 5;\n }\n .order-lg-6 {\n order: 6;\n }\n .order-lg-7 {\n order: 7;\n }\n .order-lg-8 {\n order: 8;\n }\n .order-lg-9 {\n order: 9;\n }\n .order-lg-10 {\n order: 10;\n }\n .order-lg-11 {\n order: 11;\n }\n .order-lg-12 {\n order: 12;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.333333%;\n }\n .offset-lg-2 {\n margin-left: 16.666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.333333%;\n }\n .offset-lg-5 {\n margin-left: 41.666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.333333%;\n }\n .offset-lg-8 {\n margin-left: 66.666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.333333%;\n }\n .offset-lg-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 1200px) {\n .col-xl {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-xl-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-xl-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-xl-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-xl-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-xl-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-xl-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-xl-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-xl-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-xl-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-xl-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-xl-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-xl-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-xl-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-xl-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-xl-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-xl-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-xl-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-xl-first {\n order: -1;\n }\n .order-xl-last {\n order: 13;\n }\n .order-xl-0 {\n order: 0;\n }\n .order-xl-1 {\n order: 1;\n }\n .order-xl-2 {\n order: 2;\n }\n .order-xl-3 {\n order: 3;\n }\n .order-xl-4 {\n order: 4;\n }\n .order-xl-5 {\n order: 5;\n }\n .order-xl-6 {\n order: 6;\n }\n .order-xl-7 {\n order: 7;\n }\n .order-xl-8 {\n order: 8;\n }\n .order-xl-9 {\n order: 9;\n }\n .order-xl-10 {\n order: 10;\n }\n .order-xl-11 {\n order: 11;\n }\n .order-xl-12 {\n order: 12;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.333333%;\n }\n .offset-xl-2 {\n margin-left: 16.666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.333333%;\n }\n .offset-xl-5 {\n margin-left: 41.666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.333333%;\n }\n .offset-xl-8 {\n margin-left: 66.666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.333333%;\n }\n .offset-xl-11 {\n margin-left: 91.666667%;\n }\n}\n\n.d-none {\n display: none !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-none {\n display: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 768px) {\n .d-md-none {\n display: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: flex !important;\n }\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 992px) {\n .d-lg-none {\n display: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 1200px) {\n .d-xl-none {\n display: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media print {\n .d-print-none {\n display: none !important;\n }\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: flex !important;\n }\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n .flex-sm-row {\n flex-direction: row !important;\n }\n .flex-sm-column {\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n justify-content: center !important;\n }\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n align-items: center !important;\n }\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n align-content: center !important;\n }\n .align-content-sm-between {\n align-content: space-between !important;\n }\n .align-content-sm-around {\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n align-self: auto !important;\n }\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n align-self: center !important;\n }\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 768px) {\n .flex-md-row {\n flex-direction: row !important;\n }\n .flex-md-column {\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n justify-content: center !important;\n }\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n .align-items-md-start {\n align-items: flex-start !important;\n }\n .align-items-md-end {\n align-items: flex-end !important;\n }\n .align-items-md-center {\n align-items: center !important;\n }\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n .align-content-md-start {\n align-content: flex-start !important;\n }\n .align-content-md-end {\n align-content: flex-end !important;\n }\n .align-content-md-center {\n align-content: center !important;\n }\n .align-content-md-between {\n align-content: space-between !important;\n }\n .align-content-md-around {\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n .align-self-md-auto {\n align-self: auto !important;\n }\n .align-self-md-start {\n align-self: flex-start !important;\n }\n .align-self-md-end {\n align-self: flex-end !important;\n }\n .align-self-md-center {\n align-self: center !important;\n }\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 992px) {\n .flex-lg-row {\n flex-direction: row !important;\n }\n .flex-lg-column {\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n justify-content: center !important;\n }\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n align-items: center !important;\n }\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n align-content: center !important;\n }\n .align-content-lg-between {\n align-content: space-between !important;\n }\n .align-content-lg-around {\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n align-self: auto !important;\n }\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n align-self: center !important;\n }\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 1200px) {\n .flex-xl-row {\n flex-direction: row !important;\n }\n .flex-xl-column {\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n justify-content: center !important;\n }\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n align-items: center !important;\n }\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n align-content: center !important;\n }\n .align-content-xl-between {\n align-content: space-between !important;\n }\n .align-content-xl-around {\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n align-self: auto !important;\n }\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n align-self: center !important;\n }\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n margin-left: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n margin-left: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n margin-left: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.mt-3,\n.my-3 {\n margin-top: 1rem !important;\n}\n\n.mr-3,\n.mx-3 {\n margin-right: 1rem !important;\n}\n\n.mb-3,\n.my-3 {\n margin-bottom: 1rem !important;\n}\n\n.ml-3,\n.mx-3 {\n margin-left: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.mt-4,\n.my-4 {\n margin-top: 1.5rem !important;\n}\n\n.mr-4,\n.mx-4 {\n margin-right: 1.5rem !important;\n}\n\n.mb-4,\n.my-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.ml-4,\n.mx-4 {\n margin-left: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.mt-5,\n.my-5 {\n margin-top: 3rem !important;\n}\n\n.mr-5,\n.mx-5 {\n margin-right: 3rem !important;\n}\n\n.mb-5,\n.my-5 {\n margin-bottom: 3rem !important;\n}\n\n.ml-5,\n.mx-5 {\n margin-left: 3rem !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n padding-left: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n padding-left: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n padding-left: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.pt-3,\n.py-3 {\n padding-top: 1rem !important;\n}\n\n.pr-3,\n.px-3 {\n padding-right: 1rem !important;\n}\n\n.pb-3,\n.py-3 {\n padding-bottom: 1rem !important;\n}\n\n.pl-3,\n.px-3 {\n padding-left: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.pt-4,\n.py-4 {\n padding-top: 1.5rem !important;\n}\n\n.pr-4,\n.px-4 {\n padding-right: 1.5rem !important;\n}\n\n.pb-4,\n.py-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pl-4,\n.px-4 {\n padding-left: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.pt-5,\n.py-5 {\n padding-top: 3rem !important;\n}\n\n.pr-5,\n.px-5 {\n padding-right: 3rem !important;\n}\n\n.pb-5,\n.py-5 {\n padding-bottom: 3rem !important;\n}\n\n.pl-5,\n.px-5 {\n padding-left: 3rem !important;\n}\n\n.m-n1 {\n margin: -0.25rem !important;\n}\n\n.mt-n1,\n.my-n1 {\n margin-top: -0.25rem !important;\n}\n\n.mr-n1,\n.mx-n1 {\n margin-right: -0.25rem !important;\n}\n\n.mb-n1,\n.my-n1 {\n margin-bottom: -0.25rem !important;\n}\n\n.ml-n1,\n.mx-n1 {\n margin-left: -0.25rem !important;\n}\n\n.m-n2 {\n margin: -0.5rem !important;\n}\n\n.mt-n2,\n.my-n2 {\n margin-top: -0.5rem !important;\n}\n\n.mr-n2,\n.mx-n2 {\n margin-right: -0.5rem !important;\n}\n\n.mb-n2,\n.my-n2 {\n margin-bottom: -0.5rem !important;\n}\n\n.ml-n2,\n.mx-n2 {\n margin-left: -0.5rem !important;\n}\n\n.m-n3 {\n margin: -1rem !important;\n}\n\n.mt-n3,\n.my-n3 {\n margin-top: -1rem !important;\n}\n\n.mr-n3,\n.mx-n3 {\n margin-right: -1rem !important;\n}\n\n.mb-n3,\n.my-n3 {\n margin-bottom: -1rem !important;\n}\n\n.ml-n3,\n.mx-n3 {\n margin-left: -1rem !important;\n}\n\n.m-n4 {\n margin: -1.5rem !important;\n}\n\n.mt-n4,\n.my-n4 {\n margin-top: -1.5rem !important;\n}\n\n.mr-n4,\n.mx-n4 {\n margin-right: -1.5rem !important;\n}\n\n.mb-n4,\n.my-n4 {\n margin-bottom: -1.5rem !important;\n}\n\n.ml-n4,\n.mx-n4 {\n margin-left: -1.5rem !important;\n}\n\n.m-n5 {\n margin: -3rem !important;\n}\n\n.mt-n5,\n.my-n5 {\n margin-top: -3rem !important;\n}\n\n.mr-n5,\n.mx-n5 {\n margin-right: -3rem !important;\n}\n\n.mb-n5,\n.my-n5 {\n margin-bottom: -3rem !important;\n}\n\n.ml-n5,\n.mx-n5 {\n margin-left: -3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n .m-sm-0 {\n margin: 0 !important;\n }\n .mt-sm-0,\n .my-sm-0 {\n margin-top: 0 !important;\n }\n .mr-sm-0,\n .mx-sm-0 {\n margin-right: 0 !important;\n }\n .mb-sm-0,\n .my-sm-0 {\n margin-bottom: 0 !important;\n }\n .ml-sm-0,\n .mx-sm-0 {\n margin-left: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .mt-sm-1,\n .my-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mr-sm-1,\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n }\n .mb-sm-1,\n .my-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-sm-1,\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .mt-sm-2,\n .my-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mr-sm-2,\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n }\n .mb-sm-2,\n .my-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-sm-2,\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .mt-sm-3,\n .my-sm-3 {\n margin-top: 1rem !important;\n }\n .mr-sm-3,\n .mx-sm-3 {\n margin-right: 1rem !important;\n }\n .mb-sm-3,\n .my-sm-3 {\n margin-bottom: 1rem !important;\n }\n .ml-sm-3,\n .mx-sm-3 {\n margin-left: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .mt-sm-4,\n .my-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mr-sm-4,\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n }\n .mb-sm-4,\n .my-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-sm-4,\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .mt-sm-5,\n .my-sm-5 {\n margin-top: 3rem !important;\n }\n .mr-sm-5,\n .mx-sm-5 {\n margin-right: 3rem !important;\n }\n .mb-sm-5,\n .my-sm-5 {\n margin-bottom: 3rem !important;\n }\n .ml-sm-5,\n .mx-sm-5 {\n margin-left: 3rem !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .pt-sm-0,\n .py-sm-0 {\n padding-top: 0 !important;\n }\n .pr-sm-0,\n .px-sm-0 {\n padding-right: 0 !important;\n }\n .pb-sm-0,\n .py-sm-0 {\n padding-bottom: 0 !important;\n }\n .pl-sm-0,\n .px-sm-0 {\n padding-left: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .pt-sm-1,\n .py-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pr-sm-1,\n .px-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pb-sm-1,\n .py-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-sm-1,\n .px-sm-1 {\n padding-left: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .pt-sm-2,\n .py-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pr-sm-2,\n .px-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pb-sm-2,\n .py-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-sm-2,\n .px-sm-2 {\n padding-left: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .pt-sm-3,\n .py-sm-3 {\n padding-top: 1rem !important;\n }\n .pr-sm-3,\n .px-sm-3 {\n padding-right: 1rem !important;\n }\n .pb-sm-3,\n .py-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pl-sm-3,\n .px-sm-3 {\n padding-left: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .pt-sm-4,\n .py-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pr-sm-4,\n .px-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pb-sm-4,\n .py-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-sm-4,\n .px-sm-4 {\n padding-left: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .pt-sm-5,\n .py-sm-5 {\n padding-top: 3rem !important;\n }\n .pr-sm-5,\n .px-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-5,\n .py-sm-5 {\n padding-bottom: 3rem !important;\n }\n .pl-sm-5,\n .px-sm-5 {\n padding-left: 3rem !important;\n }\n .m-sm-n1 {\n margin: -0.25rem !important;\n }\n .mt-sm-n1,\n .my-sm-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-sm-n1,\n .mx-sm-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-sm-n1,\n .my-sm-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-sm-n1,\n .mx-sm-n1 {\n margin-left: -0.25rem !important;\n }\n .m-sm-n2 {\n margin: -0.5rem !important;\n }\n .mt-sm-n2,\n .my-sm-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-sm-n2,\n .mx-sm-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-sm-n2,\n .my-sm-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-sm-n2,\n .mx-sm-n2 {\n margin-left: -0.5rem !important;\n }\n .m-sm-n3 {\n margin: -1rem !important;\n }\n .mt-sm-n3,\n .my-sm-n3 {\n margin-top: -1rem !important;\n }\n .mr-sm-n3,\n .mx-sm-n3 {\n margin-right: -1rem !important;\n }\n .mb-sm-n3,\n .my-sm-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-sm-n3,\n .mx-sm-n3 {\n margin-left: -1rem !important;\n }\n .m-sm-n4 {\n margin: -1.5rem !important;\n }\n .mt-sm-n4,\n .my-sm-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-sm-n4,\n .mx-sm-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-sm-n4,\n .my-sm-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-sm-n4,\n .mx-sm-n4 {\n margin-left: -1.5rem !important;\n }\n .m-sm-n5 {\n margin: -3rem !important;\n }\n .mt-sm-n5,\n .my-sm-n5 {\n margin-top: -3rem !important;\n }\n .mr-sm-n5,\n .mx-sm-n5 {\n margin-right: -3rem !important;\n }\n .mb-sm-n5,\n .my-sm-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-sm-n5,\n .mx-sm-n5 {\n margin-left: -3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mt-sm-auto,\n .my-sm-auto {\n margin-top: auto !important;\n }\n .mr-sm-auto,\n .mx-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-auto,\n .my-sm-auto {\n margin-bottom: auto !important;\n }\n .ml-sm-auto,\n .mx-sm-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 768px) {\n .m-md-0 {\n margin: 0 !important;\n }\n .mt-md-0,\n .my-md-0 {\n margin-top: 0 !important;\n }\n .mr-md-0,\n .mx-md-0 {\n margin-right: 0 !important;\n }\n .mb-md-0,\n .my-md-0 {\n margin-bottom: 0 !important;\n }\n .ml-md-0,\n .mx-md-0 {\n margin-left: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .mt-md-1,\n .my-md-1 {\n margin-top: 0.25rem !important;\n }\n .mr-md-1,\n .mx-md-1 {\n margin-right: 0.25rem !important;\n }\n .mb-md-1,\n .my-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-md-1,\n .mx-md-1 {\n margin-left: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .mt-md-2,\n .my-md-2 {\n margin-top: 0.5rem !important;\n }\n .mr-md-2,\n .mx-md-2 {\n margin-right: 0.5rem !important;\n }\n .mb-md-2,\n .my-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-md-2,\n .mx-md-2 {\n margin-left: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .mt-md-3,\n .my-md-3 {\n margin-top: 1rem !important;\n }\n .mr-md-3,\n .mx-md-3 {\n margin-right: 1rem !important;\n }\n .mb-md-3,\n .my-md-3 {\n margin-bottom: 1rem !important;\n }\n .ml-md-3,\n .mx-md-3 {\n margin-left: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .mt-md-4,\n .my-md-4 {\n margin-top: 1.5rem !important;\n }\n .mr-md-4,\n .mx-md-4 {\n margin-right: 1.5rem !important;\n }\n .mb-md-4,\n .my-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-md-4,\n .mx-md-4 {\n margin-left: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .mt-md-5,\n .my-md-5 {\n margin-top: 3rem !important;\n }\n .mr-md-5,\n .mx-md-5 {\n margin-right: 3rem !important;\n }\n .mb-md-5,\n .my-md-5 {\n margin-bottom: 3rem !important;\n }\n .ml-md-5,\n .mx-md-5 {\n margin-left: 3rem !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .pt-md-0,\n .py-md-0 {\n padding-top: 0 !important;\n }\n .pr-md-0,\n .px-md-0 {\n padding-right: 0 !important;\n }\n .pb-md-0,\n .py-md-0 {\n padding-bottom: 0 !important;\n }\n .pl-md-0,\n .px-md-0 {\n padding-left: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .pt-md-1,\n .py-md-1 {\n padding-top: 0.25rem !important;\n }\n .pr-md-1,\n .px-md-1 {\n padding-right: 0.25rem !important;\n }\n .pb-md-1,\n .py-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-md-1,\n .px-md-1 {\n padding-left: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .pt-md-2,\n .py-md-2 {\n padding-top: 0.5rem !important;\n }\n .pr-md-2,\n .px-md-2 {\n padding-right: 0.5rem !important;\n }\n .pb-md-2,\n .py-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-md-2,\n .px-md-2 {\n padding-left: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .pt-md-3,\n .py-md-3 {\n padding-top: 1rem !important;\n }\n .pr-md-3,\n .px-md-3 {\n padding-right: 1rem !important;\n }\n .pb-md-3,\n .py-md-3 {\n padding-bottom: 1rem !important;\n }\n .pl-md-3,\n .px-md-3 {\n padding-left: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .pt-md-4,\n .py-md-4 {\n padding-top: 1.5rem !important;\n }\n .pr-md-4,\n .px-md-4 {\n padding-right: 1.5rem !important;\n }\n .pb-md-4,\n .py-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-md-4,\n .px-md-4 {\n padding-left: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .pt-md-5,\n .py-md-5 {\n padding-top: 3rem !important;\n }\n .pr-md-5,\n .px-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-5,\n .py-md-5 {\n padding-bottom: 3rem !important;\n }\n .pl-md-5,\n .px-md-5 {\n padding-left: 3rem !important;\n }\n .m-md-n1 {\n margin: -0.25rem !important;\n }\n .mt-md-n1,\n .my-md-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-md-n1,\n .mx-md-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-md-n1,\n .my-md-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-md-n1,\n .mx-md-n1 {\n margin-left: -0.25rem !important;\n }\n .m-md-n2 {\n margin: -0.5rem !important;\n }\n .mt-md-n2,\n .my-md-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-md-n2,\n .mx-md-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-md-n2,\n .my-md-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-md-n2,\n .mx-md-n2 {\n margin-left: -0.5rem !important;\n }\n .m-md-n3 {\n margin: -1rem !important;\n }\n .mt-md-n3,\n .my-md-n3 {\n margin-top: -1rem !important;\n }\n .mr-md-n3,\n .mx-md-n3 {\n margin-right: -1rem !important;\n }\n .mb-md-n3,\n .my-md-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-md-n3,\n .mx-md-n3 {\n margin-left: -1rem !important;\n }\n .m-md-n4 {\n margin: -1.5rem !important;\n }\n .mt-md-n4,\n .my-md-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-md-n4,\n .mx-md-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-md-n4,\n .my-md-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-md-n4,\n .mx-md-n4 {\n margin-left: -1.5rem !important;\n }\n .m-md-n5 {\n margin: -3rem !important;\n }\n .mt-md-n5,\n .my-md-n5 {\n margin-top: -3rem !important;\n }\n .mr-md-n5,\n .mx-md-n5 {\n margin-right: -3rem !important;\n }\n .mb-md-n5,\n .my-md-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-md-n5,\n .mx-md-n5 {\n margin-left: -3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mt-md-auto,\n .my-md-auto {\n margin-top: auto !important;\n }\n .mr-md-auto,\n .mx-md-auto {\n margin-right: auto !important;\n }\n .mb-md-auto,\n .my-md-auto {\n margin-bottom: auto !important;\n }\n .ml-md-auto,\n .mx-md-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 992px) {\n .m-lg-0 {\n margin: 0 !important;\n }\n .mt-lg-0,\n .my-lg-0 {\n margin-top: 0 !important;\n }\n .mr-lg-0,\n .mx-lg-0 {\n margin-right: 0 !important;\n }\n .mb-lg-0,\n .my-lg-0 {\n margin-bottom: 0 !important;\n }\n .ml-lg-0,\n .mx-lg-0 {\n margin-left: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .mt-lg-1,\n .my-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mr-lg-1,\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n }\n .mb-lg-1,\n .my-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-lg-1,\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .mt-lg-2,\n .my-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mr-lg-2,\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n }\n .mb-lg-2,\n .my-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-lg-2,\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .mt-lg-3,\n .my-lg-3 {\n margin-top: 1rem !important;\n }\n .mr-lg-3,\n .mx-lg-3 {\n margin-right: 1rem !important;\n }\n .mb-lg-3,\n .my-lg-3 {\n margin-bottom: 1rem !important;\n }\n .ml-lg-3,\n .mx-lg-3 {\n margin-left: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .mt-lg-4,\n .my-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mr-lg-4,\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n }\n .mb-lg-4,\n .my-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-lg-4,\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .mt-lg-5,\n .my-lg-5 {\n margin-top: 3rem !important;\n }\n .mr-lg-5,\n .mx-lg-5 {\n margin-right: 3rem !important;\n }\n .mb-lg-5,\n .my-lg-5 {\n margin-bottom: 3rem !important;\n }\n .ml-lg-5,\n .mx-lg-5 {\n margin-left: 3rem !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .pt-lg-0,\n .py-lg-0 {\n padding-top: 0 !important;\n }\n .pr-lg-0,\n .px-lg-0 {\n padding-right: 0 !important;\n }\n .pb-lg-0,\n .py-lg-0 {\n padding-bottom: 0 !important;\n }\n .pl-lg-0,\n .px-lg-0 {\n padding-left: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .pt-lg-1,\n .py-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pr-lg-1,\n .px-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pb-lg-1,\n .py-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-lg-1,\n .px-lg-1 {\n padding-left: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .pt-lg-2,\n .py-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pr-lg-2,\n .px-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pb-lg-2,\n .py-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-lg-2,\n .px-lg-2 {\n padding-left: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .pt-lg-3,\n .py-lg-3 {\n padding-top: 1rem !important;\n }\n .pr-lg-3,\n .px-lg-3 {\n padding-right: 1rem !important;\n }\n .pb-lg-3,\n .py-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pl-lg-3,\n .px-lg-3 {\n padding-left: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .pt-lg-4,\n .py-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pr-lg-4,\n .px-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pb-lg-4,\n .py-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-lg-4,\n .px-lg-4 {\n padding-left: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .pt-lg-5,\n .py-lg-5 {\n padding-top: 3rem !important;\n }\n .pr-lg-5,\n .px-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-5,\n .py-lg-5 {\n padding-bottom: 3rem !important;\n }\n .pl-lg-5,\n .px-lg-5 {\n padding-left: 3rem !important;\n }\n .m-lg-n1 {\n margin: -0.25rem !important;\n }\n .mt-lg-n1,\n .my-lg-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-lg-n1,\n .mx-lg-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-lg-n1,\n .my-lg-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-lg-n1,\n .mx-lg-n1 {\n margin-left: -0.25rem !important;\n }\n .m-lg-n2 {\n margin: -0.5rem !important;\n }\n .mt-lg-n2,\n .my-lg-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-lg-n2,\n .mx-lg-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-lg-n2,\n .my-lg-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-lg-n2,\n .mx-lg-n2 {\n margin-left: -0.5rem !important;\n }\n .m-lg-n3 {\n margin: -1rem !important;\n }\n .mt-lg-n3,\n .my-lg-n3 {\n margin-top: -1rem !important;\n }\n .mr-lg-n3,\n .mx-lg-n3 {\n margin-right: -1rem !important;\n }\n .mb-lg-n3,\n .my-lg-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-lg-n3,\n .mx-lg-n3 {\n margin-left: -1rem !important;\n }\n .m-lg-n4 {\n margin: -1.5rem !important;\n }\n .mt-lg-n4,\n .my-lg-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-lg-n4,\n .mx-lg-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-lg-n4,\n .my-lg-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-lg-n4,\n .mx-lg-n4 {\n margin-left: -1.5rem !important;\n }\n .m-lg-n5 {\n margin: -3rem !important;\n }\n .mt-lg-n5,\n .my-lg-n5 {\n margin-top: -3rem !important;\n }\n .mr-lg-n5,\n .mx-lg-n5 {\n margin-right: -3rem !important;\n }\n .mb-lg-n5,\n .my-lg-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-lg-n5,\n .mx-lg-n5 {\n margin-left: -3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mt-lg-auto,\n .my-lg-auto {\n margin-top: auto !important;\n }\n .mr-lg-auto,\n .mx-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-auto,\n .my-lg-auto {\n margin-bottom: auto !important;\n }\n .ml-lg-auto,\n .mx-lg-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 1200px) {\n .m-xl-0 {\n margin: 0 !important;\n }\n .mt-xl-0,\n .my-xl-0 {\n margin-top: 0 !important;\n }\n .mr-xl-0,\n .mx-xl-0 {\n margin-right: 0 !important;\n }\n .mb-xl-0,\n .my-xl-0 {\n margin-bottom: 0 !important;\n }\n .ml-xl-0,\n .mx-xl-0 {\n margin-left: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .mt-xl-1,\n .my-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mr-xl-1,\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n }\n .mb-xl-1,\n .my-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-xl-1,\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .mt-xl-2,\n .my-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mr-xl-2,\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n }\n .mb-xl-2,\n .my-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-xl-2,\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .mt-xl-3,\n .my-xl-3 {\n margin-top: 1rem !important;\n }\n .mr-xl-3,\n .mx-xl-3 {\n margin-right: 1rem !important;\n }\n .mb-xl-3,\n .my-xl-3 {\n margin-bottom: 1rem !important;\n }\n .ml-xl-3,\n .mx-xl-3 {\n margin-left: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .mt-xl-4,\n .my-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mr-xl-4,\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n }\n .mb-xl-4,\n .my-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-xl-4,\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .mt-xl-5,\n .my-xl-5 {\n margin-top: 3rem !important;\n }\n .mr-xl-5,\n .mx-xl-5 {\n margin-right: 3rem !important;\n }\n .mb-xl-5,\n .my-xl-5 {\n margin-bottom: 3rem !important;\n }\n .ml-xl-5,\n .mx-xl-5 {\n margin-left: 3rem !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .pt-xl-0,\n .py-xl-0 {\n padding-top: 0 !important;\n }\n .pr-xl-0,\n .px-xl-0 {\n padding-right: 0 !important;\n }\n .pb-xl-0,\n .py-xl-0 {\n padding-bottom: 0 !important;\n }\n .pl-xl-0,\n .px-xl-0 {\n padding-left: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .pt-xl-1,\n .py-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pr-xl-1,\n .px-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pb-xl-1,\n .py-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-xl-1,\n .px-xl-1 {\n padding-left: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .pt-xl-2,\n .py-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pr-xl-2,\n .px-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pb-xl-2,\n .py-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-xl-2,\n .px-xl-2 {\n padding-left: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .pt-xl-3,\n .py-xl-3 {\n padding-top: 1rem !important;\n }\n .pr-xl-3,\n .px-xl-3 {\n padding-right: 1rem !important;\n }\n .pb-xl-3,\n .py-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pl-xl-3,\n .px-xl-3 {\n padding-left: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .pt-xl-4,\n .py-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pr-xl-4,\n .px-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pb-xl-4,\n .py-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-xl-4,\n .px-xl-4 {\n padding-left: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .pt-xl-5,\n .py-xl-5 {\n padding-top: 3rem !important;\n }\n .pr-xl-5,\n .px-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-5,\n .py-xl-5 {\n padding-bottom: 3rem !important;\n }\n .pl-xl-5,\n .px-xl-5 {\n padding-left: 3rem !important;\n }\n .m-xl-n1 {\n margin: -0.25rem !important;\n }\n .mt-xl-n1,\n .my-xl-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-xl-n1,\n .mx-xl-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-xl-n1,\n .my-xl-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-xl-n1,\n .mx-xl-n1 {\n margin-left: -0.25rem !important;\n }\n .m-xl-n2 {\n margin: -0.5rem !important;\n }\n .mt-xl-n2,\n .my-xl-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-xl-n2,\n .mx-xl-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-xl-n2,\n .my-xl-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-xl-n2,\n .mx-xl-n2 {\n margin-left: -0.5rem !important;\n }\n .m-xl-n3 {\n margin: -1rem !important;\n }\n .mt-xl-n3,\n .my-xl-n3 {\n margin-top: -1rem !important;\n }\n .mr-xl-n3,\n .mx-xl-n3 {\n margin-right: -1rem !important;\n }\n .mb-xl-n3,\n .my-xl-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-xl-n3,\n .mx-xl-n3 {\n margin-left: -1rem !important;\n }\n .m-xl-n4 {\n margin: -1.5rem !important;\n }\n .mt-xl-n4,\n .my-xl-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-xl-n4,\n .mx-xl-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-xl-n4,\n .my-xl-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-xl-n4,\n .mx-xl-n4 {\n margin-left: -1.5rem !important;\n }\n .m-xl-n5 {\n margin: -3rem !important;\n }\n .mt-xl-n5,\n .my-xl-n5 {\n margin-top: -3rem !important;\n }\n .mr-xl-n5,\n .mx-xl-n5 {\n margin-right: -3rem !important;\n }\n .mb-xl-n5,\n .my-xl-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-xl-n5,\n .mx-xl-n5 {\n margin-left: -3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mt-xl-auto,\n .my-xl-auto {\n margin-top: auto !important;\n }\n .mr-xl-auto,\n .mx-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-auto,\n .my-xl-auto {\n margin-bottom: auto !important;\n }\n .ml-xl-auto,\n .mx-xl-auto {\n margin-left: auto !important;\n }\n}\n\n/*# sourceMappingURL=bootstrap-grid.css.map */","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n // Single container class with breakpoint max-widths\n .container {\n @include make-container();\n @include make-container-max-widths();\n }\n\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n @each $name, $width in $grid-breakpoints {\n @if ($container-max-width > $width or $breakpoint == $name) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n }\n }\n }\n }\n}\n\n\n// Row\n//\n// Rows contain your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n }\n\n // Remove the negative margin from default .row, then the horizontal padding\n // from all immediate children columns (to prevent runaway style inheritance).\n .no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n > .col,\n > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n }\n}\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","/// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-container($gutter: $grid-gutter-width) {\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n margin-right: auto;\n margin-left: auto;\n}\n\n\n// For each breakpoint, define the maximum width of the container in a media query\n@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {\n @each $breakpoint, $container-max-width in $max-widths {\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n max-width: $container-max-width;\n }\n }\n}\n\n@mixin make-row($gutter: $grid-gutter-width) {\n display: flex;\n flex-wrap: wrap;\n margin-right: -$gutter / 2;\n margin-left: -$gutter / 2;\n}\n\n@mixin make-col-ready($gutter: $grid-gutter-width) {\n position: relative;\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we use `flex` values\n // later on to override this initial width.\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n}\n\n@mixin make-col($size, $columns: $grid-columns) {\n flex: 0 0 percentage($size / $columns);\n // Add a `max-width` to ensure content within each column does not blow out\n // the width of the column. Applies to IE10+ and Firefox. Chrome and Safari\n // do not appear to require this.\n max-width: percentage($size / $columns);\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%; // Reset earlier grid tiers\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: $size / $columns;\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// numberof columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n & > * {\n flex: 0 0 100% / $count;\n max-width: 100% / $count;\n }\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @return if($n != null and $n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width. Null for the largest (last) breakpoint.\n// The maximum value is calculated as the minimum of the next one less 0.02px\n// to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $next: breakpoint-next($name, $breakpoints);\n @return if($next, breakpoint-min($next, $breakpoints) - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $max: breakpoint-max($name, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($name, $breakpoints) {\n @content;\n }\n }\n}\n","// Variables\n//\n// Variables should follow the `$component-state-property-size` formula for\n// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.\n\n// Color system\n\n$white: #fff !default;\n$gray-100: #f8f9fa !default;\n$gray-200: #e9ecef !default;\n$gray-300: #dee2e6 !default;\n$gray-400: #ced4da !default;\n$gray-500: #adb5bd !default;\n$gray-600: #6c757d !default;\n$gray-700: #495057 !default;\n$gray-800: #343a40 !default;\n$gray-900: #212529 !default;\n$black: #000 !default;\n\n$grays: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$grays: map-merge(\n (\n \"100\": $gray-100,\n \"200\": $gray-200,\n \"300\": $gray-300,\n \"400\": $gray-400,\n \"500\": $gray-500,\n \"600\": $gray-600,\n \"700\": $gray-700,\n \"800\": $gray-800,\n \"900\": $gray-900\n ),\n $grays\n);\n\n$blue: #007bff !default;\n$indigo: #6610f2 !default;\n$purple: #6f42c1 !default;\n$pink: #e83e8c !default;\n$red: #dc3545 !default;\n$orange: #fd7e14 !default;\n$yellow: #ffc107 !default;\n$green: #28a745 !default;\n$teal: #20c997 !default;\n$cyan: #17a2b8 !default;\n\n$colors: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$colors: map-merge(\n (\n \"blue\": $blue,\n \"indigo\": $indigo,\n \"purple\": $purple,\n \"pink\": $pink,\n \"red\": $red,\n \"orange\": $orange,\n \"yellow\": $yellow,\n \"green\": $green,\n \"teal\": $teal,\n \"cyan\": $cyan,\n \"white\": $white,\n \"gray\": $gray-600,\n \"gray-dark\": $gray-800\n ),\n $colors\n);\n\n$primary: $blue !default;\n$secondary: $gray-600 !default;\n$success: $green !default;\n$info: $cyan !default;\n$warning: $yellow !default;\n$danger: $red !default;\n$light: $gray-100 !default;\n$dark: $gray-800 !default;\n\n$theme-colors: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$theme-colors: map-merge(\n (\n \"primary\": $primary,\n \"secondary\": $secondary,\n \"success\": $success,\n \"info\": $info,\n \"warning\": $warning,\n \"danger\": $danger,\n \"light\": $light,\n \"dark\": $dark\n ),\n $theme-colors\n);\n\n// Set a specific jump point for requesting color jumps\n$theme-color-interval: 8% !default;\n\n// The yiq lightness value that determines when the lightness of color changes from \"dark\" to \"light\". Acceptable values are between 0 and 255.\n$yiq-contrasted-threshold: 150 !default;\n\n// Customize the light and dark text colors for use in our YIQ color contrast function.\n$yiq-text-dark: $gray-900 !default;\n$yiq-text-light: $white !default;\n\n// Characters which are escaped by the escape-svg function\n$escaped-characters: (\n (\"<\",\"%3c\"),\n (\">\",\"%3e\"),\n (\"#\",\"%23\"),\n) !default;\n\n\n// Options\n//\n// Quickly modify global styling by enabling or disabling optional features.\n\n$enable-caret: true !default;\n$enable-rounded: true !default;\n$enable-shadows: false !default;\n$enable-gradients: false !default;\n$enable-transitions: true !default;\n$enable-prefers-reduced-motion-media-query: true !default;\n$enable-hover-media-query: false !default; // Deprecated, no longer affects any compiled CSS\n$enable-grid-classes: true !default;\n$enable-pointer-cursor-for-buttons: true !default;\n$enable-print-styles: true !default;\n$enable-responsive-font-sizes: false !default;\n$enable-validation-icons: true !default;\n$enable-deprecation-messages: true !default;\n\n\n// Spacing\n//\n// Control the default styling of most Bootstrap elements by modifying these\n// variables. Mostly focused on spacing.\n// You can add more entries to the $spacers map, should you need more variation.\n\n$spacer: 1rem !default;\n$spacers: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$spacers: map-merge(\n (\n 0: 0,\n 1: ($spacer * .25),\n 2: ($spacer * .5),\n 3: $spacer,\n 4: ($spacer * 1.5),\n 5: ($spacer * 3)\n ),\n $spacers\n);\n\n// This variable affects the `.h-*` and `.w-*` classes.\n$sizes: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$sizes: map-merge(\n (\n 25: 25%,\n 50: 50%,\n 75: 75%,\n 100: 100%,\n auto: auto\n ),\n $sizes\n);\n\n\n// Body\n//\n// Settings for the `` element.\n\n$body-bg: $white !default;\n$body-color: $gray-900 !default;\n\n\n// Links\n//\n// Style anchor elements.\n\n$link-color: theme-color(\"primary\") !default;\n$link-decoration: none !default;\n$link-hover-color: darken($link-color, 15%) !default;\n$link-hover-decoration: underline !default;\n// Darken percentage for links with `.text-*` class (e.g. `.text-success`)\n$emphasized-link-hover-darken-percentage: 15% !default;\n\n// Paragraphs\n//\n// Style p element.\n\n$paragraph-margin-bottom: 1rem !default;\n\n\n// Grid breakpoints\n//\n// Define the minimum dimensions at which your layout will change,\n// adapting to different screen sizes, for use in media queries.\n\n$grid-breakpoints: (\n xs: 0,\n sm: 576px,\n md: 768px,\n lg: 992px,\n xl: 1200px\n) !default;\n\n@include _assert-ascending($grid-breakpoints, \"$grid-breakpoints\");\n@include _assert-starts-at-zero($grid-breakpoints, \"$grid-breakpoints\");\n\n\n// Grid containers\n//\n// Define the maximum width of `.container` for different screen sizes.\n\n$container-max-widths: (\n sm: 540px,\n md: 720px,\n lg: 960px,\n xl: 1140px\n) !default;\n\n@include _assert-ascending($container-max-widths, \"$container-max-widths\");\n\n\n// Grid columns\n//\n// Set the number of columns and specify the width of the gutters.\n\n$grid-columns: 12 !default;\n$grid-gutter-width: 30px !default;\n$grid-row-columns: 6 !default;\n\n\n// Components\n//\n// Define common padding and border radius sizes and more.\n\n$line-height-lg: 1.5 !default;\n$line-height-sm: 1.5 !default;\n\n$border-width: 1px !default;\n$border-color: $gray-300 !default;\n\n$border-radius: .25rem !default;\n$border-radius-lg: .3rem !default;\n$border-radius-sm: .2rem !default;\n\n$rounded-pill: 50rem !default;\n\n$box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default;\n$box-shadow: 0 .5rem 1rem rgba($black, .15) !default;\n$box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default;\n\n$component-active-color: $white !default;\n$component-active-bg: theme-color(\"primary\") !default;\n\n$caret-width: .3em !default;\n$caret-vertical-align: $caret-width * .85 !default;\n$caret-spacing: $caret-width * .85 !default;\n\n$transition-base: all .2s ease-in-out !default;\n$transition-fade: opacity .15s linear !default;\n$transition-collapse: height .35s ease !default;\n\n$embed-responsive-aspect-ratios: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$embed-responsive-aspect-ratios: join(\n (\n (21 9),\n (16 9),\n (4 3),\n (1 1),\n ),\n $embed-responsive-aspect-ratios\n);\n\n// Typography\n//\n// Font, line-height, and color for body text, headings, and more.\n\n// stylelint-disable value-keyword-case\n$font-family-sans-serif: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\" !default;\n$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !default;\n$font-family-base: $font-family-sans-serif !default;\n// stylelint-enable value-keyword-case\n\n$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`\n$font-size-lg: $font-size-base * 1.25 !default;\n$font-size-sm: $font-size-base * .875 !default;\n\n$font-weight-lighter: lighter !default;\n$font-weight-light: 300 !default;\n$font-weight-normal: 400 !default;\n$font-weight-bold: 700 !default;\n$font-weight-bolder: bolder !default;\n\n$font-weight-base: $font-weight-normal !default;\n$line-height-base: 1.5 !default;\n\n$h1-font-size: $font-size-base * 2.5 !default;\n$h2-font-size: $font-size-base * 2 !default;\n$h3-font-size: $font-size-base * 1.75 !default;\n$h4-font-size: $font-size-base * 1.5 !default;\n$h5-font-size: $font-size-base * 1.25 !default;\n$h6-font-size: $font-size-base !default;\n\n$headings-margin-bottom: $spacer / 2 !default;\n$headings-font-family: null !default;\n$headings-font-weight: 500 !default;\n$headings-line-height: 1.2 !default;\n$headings-color: null !default;\n\n$display1-size: 6rem !default;\n$display2-size: 5.5rem !default;\n$display3-size: 4.5rem !default;\n$display4-size: 3.5rem !default;\n\n$display1-weight: 300 !default;\n$display2-weight: 300 !default;\n$display3-weight: 300 !default;\n$display4-weight: 300 !default;\n$display-line-height: $headings-line-height !default;\n\n$lead-font-size: $font-size-base * 1.25 !default;\n$lead-font-weight: 300 !default;\n\n$small-font-size: 80% !default;\n\n$text-muted: $gray-600 !default;\n\n$blockquote-small-color: $gray-600 !default;\n$blockquote-small-font-size: $small-font-size !default;\n$blockquote-font-size: $font-size-base * 1.25 !default;\n\n$hr-border-color: rgba($black, .1) !default;\n$hr-border-width: $border-width !default;\n\n$mark-padding: .2em !default;\n\n$dt-font-weight: $font-weight-bold !default;\n\n$kbd-box-shadow: inset 0 -.1rem 0 rgba($black, .25) !default;\n$nested-kbd-font-weight: $font-weight-bold !default;\n\n$list-inline-padding: .5rem !default;\n\n$mark-bg: #fcf8e3 !default;\n\n$hr-margin-y: $spacer !default;\n\n\n// Tables\n//\n// Customizes the `.table` component with basic values, each used across all table variations.\n\n$table-cell-padding: .75rem !default;\n$table-cell-padding-sm: .3rem !default;\n\n$table-color: $body-color !default;\n$table-bg: null !default;\n$table-accent-bg: rgba($black, .05) !default;\n$table-hover-color: $table-color !default;\n$table-hover-bg: rgba($black, .075) !default;\n$table-active-bg: $table-hover-bg !default;\n\n$table-border-width: $border-width !default;\n$table-border-color: $border-color !default;\n\n$table-head-bg: $gray-200 !default;\n$table-head-color: $gray-700 !default;\n\n$table-dark-color: $white !default;\n$table-dark-bg: $gray-800 !default;\n$table-dark-accent-bg: rgba($white, .05) !default;\n$table-dark-hover-color: $table-dark-color !default;\n$table-dark-hover-bg: rgba($white, .075) !default;\n$table-dark-border-color: lighten($table-dark-bg, 7.5%) !default;\n\n$table-striped-order: odd !default;\n\n$table-caption-color: $text-muted !default;\n\n$table-bg-level: -9 !default;\n$table-border-level: -6 !default;\n\n\n// Buttons + Forms\n//\n// Shared variables that are reassigned to `$input-` and `$btn-` specific variables.\n\n$input-btn-padding-y: .375rem !default;\n$input-btn-padding-x: .75rem !default;\n$input-btn-font-family: null !default;\n$input-btn-font-size: $font-size-base !default;\n$input-btn-line-height: $line-height-base !default;\n\n$input-btn-focus-width: .2rem !default;\n$input-btn-focus-color: rgba($component-active-bg, .25) !default;\n$input-btn-focus-box-shadow: 0 0 0 $input-btn-focus-width $input-btn-focus-color !default;\n\n$input-btn-padding-y-sm: .25rem !default;\n$input-btn-padding-x-sm: .5rem !default;\n$input-btn-font-size-sm: $font-size-sm !default;\n$input-btn-line-height-sm: $line-height-sm !default;\n\n$input-btn-padding-y-lg: .5rem !default;\n$input-btn-padding-x-lg: 1rem !default;\n$input-btn-font-size-lg: $font-size-lg !default;\n$input-btn-line-height-lg: $line-height-lg !default;\n\n$input-btn-border-width: $border-width !default;\n\n\n// Buttons\n//\n// For each of Bootstrap's buttons, define text, background, and border color.\n\n$btn-padding-y: $input-btn-padding-y !default;\n$btn-padding-x: $input-btn-padding-x !default;\n$btn-font-family: $input-btn-font-family !default;\n$btn-font-size: $input-btn-font-size !default;\n$btn-line-height: $input-btn-line-height !default;\n$btn-white-space: null !default; // Set to `nowrap` to prevent text wrapping\n\n$btn-padding-y-sm: $input-btn-padding-y-sm !default;\n$btn-padding-x-sm: $input-btn-padding-x-sm !default;\n$btn-font-size-sm: $input-btn-font-size-sm !default;\n$btn-line-height-sm: $input-btn-line-height-sm !default;\n\n$btn-padding-y-lg: $input-btn-padding-y-lg !default;\n$btn-padding-x-lg: $input-btn-padding-x-lg !default;\n$btn-font-size-lg: $input-btn-font-size-lg !default;\n$btn-line-height-lg: $input-btn-line-height-lg !default;\n\n$btn-border-width: $input-btn-border-width !default;\n\n$btn-font-weight: $font-weight-normal !default;\n$btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default;\n$btn-focus-width: $input-btn-focus-width !default;\n$btn-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$btn-disabled-opacity: .65 !default;\n$btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default;\n\n$btn-link-disabled-color: $gray-600 !default;\n\n$btn-block-spacing-y: .5rem !default;\n\n// Allows for customizing button radius independently from global border radius\n$btn-border-radius: $border-radius !default;\n$btn-border-radius-lg: $border-radius-lg !default;\n$btn-border-radius-sm: $border-radius-sm !default;\n\n$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n\n// Forms\n\n$label-margin-bottom: .5rem !default;\n\n$input-padding-y: $input-btn-padding-y !default;\n$input-padding-x: $input-btn-padding-x !default;\n$input-font-family: $input-btn-font-family !default;\n$input-font-size: $input-btn-font-size !default;\n$input-font-weight: $font-weight-base !default;\n$input-line-height: $input-btn-line-height !default;\n\n$input-padding-y-sm: $input-btn-padding-y-sm !default;\n$input-padding-x-sm: $input-btn-padding-x-sm !default;\n$input-font-size-sm: $input-btn-font-size-sm !default;\n$input-line-height-sm: $input-btn-line-height-sm !default;\n\n$input-padding-y-lg: $input-btn-padding-y-lg !default;\n$input-padding-x-lg: $input-btn-padding-x-lg !default;\n$input-font-size-lg: $input-btn-font-size-lg !default;\n$input-line-height-lg: $input-btn-line-height-lg !default;\n\n$input-bg: $white !default;\n$input-disabled-bg: $gray-200 !default;\n\n$input-color: $gray-700 !default;\n$input-border-color: $gray-400 !default;\n$input-border-width: $input-btn-border-width !default;\n$input-box-shadow: inset 0 1px 1px rgba($black, .075) !default;\n\n$input-border-radius: $border-radius !default;\n$input-border-radius-lg: $border-radius-lg !default;\n$input-border-radius-sm: $border-radius-sm !default;\n\n$input-focus-bg: $input-bg !default;\n$input-focus-border-color: lighten($component-active-bg, 25%) !default;\n$input-focus-color: $input-color !default;\n$input-focus-width: $input-btn-focus-width !default;\n$input-focus-box-shadow: $input-btn-focus-box-shadow !default;\n\n$input-placeholder-color: $gray-600 !default;\n$input-plaintext-color: $body-color !default;\n\n$input-height-border: $input-border-width * 2 !default;\n\n$input-height-inner: add($input-line-height * 1em, $input-padding-y * 2) !default;\n$input-height-inner-half: add($input-line-height * .5em, $input-padding-y) !default;\n$input-height-inner-quarter: add($input-line-height * .25em, $input-padding-y / 2) !default;\n\n$input-height: add($input-line-height * 1em, add($input-padding-y * 2, $input-height-border, false)) !default;\n$input-height-sm: add($input-line-height-sm * 1em, add($input-padding-y-sm * 2, $input-height-border, false)) !default;\n$input-height-lg: add($input-line-height-lg * 1em, add($input-padding-y-lg * 2, $input-height-border, false)) !default;\n\n$input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$form-text-margin-top: .25rem !default;\n\n$form-check-input-gutter: 1.25rem !default;\n$form-check-input-margin-y: .3rem !default;\n$form-check-input-margin-x: .25rem !default;\n\n$form-check-inline-margin-x: .75rem !default;\n$form-check-inline-input-margin-x: .3125rem !default;\n\n$form-grid-gutter-width: 10px !default;\n$form-group-margin-bottom: 1rem !default;\n\n$input-group-addon-color: $input-color !default;\n$input-group-addon-bg: $gray-200 !default;\n$input-group-addon-border-color: $input-border-color !default;\n\n$custom-forms-transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$custom-control-gutter: .5rem !default;\n$custom-control-spacer-x: 1rem !default;\n$custom-control-cursor: null !default;\n\n$custom-control-indicator-size: 1rem !default;\n$custom-control-indicator-bg: $input-bg !default;\n\n$custom-control-indicator-bg-size: 50% 50% !default;\n$custom-control-indicator-box-shadow: $input-box-shadow !default;\n$custom-control-indicator-border-color: $gray-500 !default;\n$custom-control-indicator-border-width: $input-border-width !default;\n\n$custom-control-label-color: null !default;\n\n$custom-control-indicator-disabled-bg: $input-disabled-bg !default;\n$custom-control-label-disabled-color: $gray-600 !default;\n\n$custom-control-indicator-checked-color: $component-active-color !default;\n$custom-control-indicator-checked-bg: $component-active-bg !default;\n$custom-control-indicator-checked-disabled-bg: rgba(theme-color(\"primary\"), .5) !default;\n$custom-control-indicator-checked-box-shadow: none !default;\n$custom-control-indicator-checked-border-color: $custom-control-indicator-checked-bg !default;\n\n$custom-control-indicator-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-control-indicator-focus-border-color: $input-focus-border-color !default;\n\n$custom-control-indicator-active-color: $component-active-color !default;\n$custom-control-indicator-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-control-indicator-active-box-shadow: none !default;\n$custom-control-indicator-active-border-color: $custom-control-indicator-active-bg !default;\n\n$custom-checkbox-indicator-border-radius: $border-radius !default;\n$custom-checkbox-indicator-icon-checked: url(\"data:image/svg+xml,\") !default;\n\n$custom-checkbox-indicator-indeterminate-bg: $component-active-bg !default;\n$custom-checkbox-indicator-indeterminate-color: $custom-control-indicator-checked-color !default;\n$custom-checkbox-indicator-icon-indeterminate: url(\"data:image/svg+xml,\") !default;\n$custom-checkbox-indicator-indeterminate-box-shadow: none !default;\n$custom-checkbox-indicator-indeterminate-border-color: $custom-checkbox-indicator-indeterminate-bg !default;\n\n$custom-radio-indicator-border-radius: 50% !default;\n$custom-radio-indicator-icon-checked: url(\"data:image/svg+xml,\") !default;\n\n$custom-switch-width: $custom-control-indicator-size * 1.75 !default;\n$custom-switch-indicator-border-radius: $custom-control-indicator-size / 2 !default;\n$custom-switch-indicator-size: subtract($custom-control-indicator-size, $custom-control-indicator-border-width * 4) !default;\n\n$custom-select-padding-y: $input-padding-y !default;\n$custom-select-padding-x: $input-padding-x !default;\n$custom-select-font-family: $input-font-family !default;\n$custom-select-font-size: $input-font-size !default;\n$custom-select-height: $input-height !default;\n$custom-select-indicator-padding: 1rem !default; // Extra padding to account for the presence of the background-image based indicator\n$custom-select-font-weight: $input-font-weight !default;\n$custom-select-line-height: $input-line-height !default;\n$custom-select-color: $input-color !default;\n$custom-select-disabled-color: $gray-600 !default;\n$custom-select-bg: $input-bg !default;\n$custom-select-disabled-bg: $gray-200 !default;\n$custom-select-bg-size: 8px 10px !default; // In pixels because image dimensions\n$custom-select-indicator-color: $gray-800 !default;\n$custom-select-indicator: url(\"data:image/svg+xml,\") !default;\n$custom-select-background: escape-svg($custom-select-indicator) no-repeat right $custom-select-padding-x center / $custom-select-bg-size !default; // Used so we can have multiple background elements (e.g., arrow and feedback icon)\n\n$custom-select-feedback-icon-padding-right: add(1em * .75, (2 * $custom-select-padding-y * .75) + $custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-position: center right ($custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-size: $input-height-inner-half $input-height-inner-half !default;\n\n$custom-select-border-width: $input-border-width !default;\n$custom-select-border-color: $input-border-color !default;\n$custom-select-border-radius: $border-radius !default;\n$custom-select-box-shadow: inset 0 1px 2px rgba($black, .075) !default;\n\n$custom-select-focus-border-color: $input-focus-border-color !default;\n$custom-select-focus-width: $input-focus-width !default;\n$custom-select-focus-box-shadow: 0 0 0 $custom-select-focus-width $input-btn-focus-color !default;\n\n$custom-select-padding-y-sm: $input-padding-y-sm !default;\n$custom-select-padding-x-sm: $input-padding-x-sm !default;\n$custom-select-font-size-sm: $input-font-size-sm !default;\n$custom-select-height-sm: $input-height-sm !default;\n\n$custom-select-padding-y-lg: $input-padding-y-lg !default;\n$custom-select-padding-x-lg: $input-padding-x-lg !default;\n$custom-select-font-size-lg: $input-font-size-lg !default;\n$custom-select-height-lg: $input-height-lg !default;\n\n$custom-range-track-width: 100% !default;\n$custom-range-track-height: .5rem !default;\n$custom-range-track-cursor: pointer !default;\n$custom-range-track-bg: $gray-300 !default;\n$custom-range-track-border-radius: 1rem !default;\n$custom-range-track-box-shadow: inset 0 .25rem .25rem rgba($black, .1) !default;\n\n$custom-range-thumb-width: 1rem !default;\n$custom-range-thumb-height: $custom-range-thumb-width !default;\n$custom-range-thumb-bg: $component-active-bg !default;\n$custom-range-thumb-border: 0 !default;\n$custom-range-thumb-border-radius: 1rem !default;\n$custom-range-thumb-box-shadow: 0 .1rem .25rem rgba($black, .1) !default;\n$custom-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow !default;\n$custom-range-thumb-focus-box-shadow-width: $input-focus-width !default; // For focus box shadow issue in IE/Edge\n$custom-range-thumb-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-range-thumb-disabled-bg: $gray-500 !default;\n\n$custom-file-height: $input-height !default;\n$custom-file-height-inner: $input-height-inner !default;\n$custom-file-focus-border-color: $input-focus-border-color !default;\n$custom-file-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-file-disabled-bg: $input-disabled-bg !default;\n\n$custom-file-padding-y: $input-padding-y !default;\n$custom-file-padding-x: $input-padding-x !default;\n$custom-file-line-height: $input-line-height !default;\n$custom-file-font-family: $input-font-family !default;\n$custom-file-font-weight: $input-font-weight !default;\n$custom-file-color: $input-color !default;\n$custom-file-bg: $input-bg !default;\n$custom-file-border-width: $input-border-width !default;\n$custom-file-border-color: $input-border-color !default;\n$custom-file-border-radius: $input-border-radius !default;\n$custom-file-box-shadow: $input-box-shadow !default;\n$custom-file-button-color: $custom-file-color !default;\n$custom-file-button-bg: $input-group-addon-bg !default;\n$custom-file-text: (\n en: \"Browse\"\n) !default;\n\n\n// Form validation\n\n$form-feedback-margin-top: $form-text-margin-top !default;\n$form-feedback-font-size: $small-font-size !default;\n$form-feedback-valid-color: theme-color(\"success\") !default;\n$form-feedback-invalid-color: theme-color(\"danger\") !default;\n\n$form-feedback-icon-valid-color: $form-feedback-valid-color !default;\n$form-feedback-icon-valid: url(\"data:image/svg+xml,\") !default;\n$form-feedback-icon-invalid-color: $form-feedback-invalid-color !default;\n$form-feedback-icon-invalid: url(\"data:image/svg+xml,\") !default;\n\n$form-validation-states: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$form-validation-states: map-merge(\n (\n \"valid\": (\n \"color\": $form-feedback-valid-color,\n \"icon\": $form-feedback-icon-valid\n ),\n \"invalid\": (\n \"color\": $form-feedback-invalid-color,\n \"icon\": $form-feedback-icon-invalid\n ),\n ),\n $form-validation-states\n);\n\n// Z-index master list\n//\n// Warning: Avoid customizing these values. They're used for a bird's eye view\n// of components dependent on the z-axis and are designed to all work together.\n\n$zindex-dropdown: 1000 !default;\n$zindex-sticky: 1020 !default;\n$zindex-fixed: 1030 !default;\n$zindex-modal-backdrop: 1040 !default;\n$zindex-modal: 1050 !default;\n$zindex-popover: 1060 !default;\n$zindex-tooltip: 1070 !default;\n\n\n// Navs\n\n$nav-link-padding-y: .5rem !default;\n$nav-link-padding-x: 1rem !default;\n$nav-link-disabled-color: $gray-600 !default;\n\n$nav-tabs-border-color: $gray-300 !default;\n$nav-tabs-border-width: $border-width !default;\n$nav-tabs-border-radius: $border-radius !default;\n$nav-tabs-link-hover-border-color: $gray-200 $gray-200 $nav-tabs-border-color !default;\n$nav-tabs-link-active-color: $gray-700 !default;\n$nav-tabs-link-active-bg: $body-bg !default;\n$nav-tabs-link-active-border-color: $gray-300 $gray-300 $nav-tabs-link-active-bg !default;\n\n$nav-pills-border-radius: $border-radius !default;\n$nav-pills-link-active-color: $component-active-color !default;\n$nav-pills-link-active-bg: $component-active-bg !default;\n\n$nav-divider-color: $gray-200 !default;\n$nav-divider-margin-y: $spacer / 2 !default;\n\n\n// Navbar\n\n$navbar-padding-y: $spacer / 2 !default;\n$navbar-padding-x: $spacer !default;\n\n$navbar-nav-link-padding-x: .5rem !default;\n\n$navbar-brand-font-size: $font-size-lg !default;\n// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link\n$nav-link-height: $font-size-base * $line-height-base + $nav-link-padding-y * 2 !default;\n$navbar-brand-height: $navbar-brand-font-size * $line-height-base !default;\n$navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) / 2 !default;\n\n$navbar-toggler-padding-y: .25rem !default;\n$navbar-toggler-padding-x: .75rem !default;\n$navbar-toggler-font-size: $font-size-lg !default;\n$navbar-toggler-border-radius: $btn-border-radius !default;\n\n$navbar-dark-color: rgba($white, .5) !default;\n$navbar-dark-hover-color: rgba($white, .75) !default;\n$navbar-dark-active-color: $white !default;\n$navbar-dark-disabled-color: rgba($white, .25) !default;\n$navbar-dark-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-dark-toggler-border-color: rgba($white, .1) !default;\n\n$navbar-light-color: rgba($black, .5) !default;\n$navbar-light-hover-color: rgba($black, .7) !default;\n$navbar-light-active-color: rgba($black, .9) !default;\n$navbar-light-disabled-color: rgba($black, .3) !default;\n$navbar-light-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-light-toggler-border-color: rgba($black, .1) !default;\n\n$navbar-light-brand-color: $navbar-light-active-color !default;\n$navbar-light-brand-hover-color: $navbar-light-active-color !default;\n$navbar-dark-brand-color: $navbar-dark-active-color !default;\n$navbar-dark-brand-hover-color: $navbar-dark-active-color !default;\n\n\n// Dropdowns\n//\n// Dropdown menu container and contents.\n\n$dropdown-min-width: 10rem !default;\n$dropdown-padding-y: .5rem !default;\n$dropdown-spacer: .125rem !default;\n$dropdown-font-size: $font-size-base !default;\n$dropdown-color: $body-color !default;\n$dropdown-bg: $white !default;\n$dropdown-border-color: rgba($black, .15) !default;\n$dropdown-border-radius: $border-radius !default;\n$dropdown-border-width: $border-width !default;\n$dropdown-inner-border-radius: subtract($dropdown-border-radius, $dropdown-border-width) !default;\n$dropdown-divider-bg: $gray-200 !default;\n$dropdown-divider-margin-y: $nav-divider-margin-y !default;\n$dropdown-box-shadow: 0 .5rem 1rem rgba($black, .175) !default;\n\n$dropdown-link-color: $gray-900 !default;\n$dropdown-link-hover-color: darken($gray-900, 5%) !default;\n$dropdown-link-hover-bg: $gray-100 !default;\n\n$dropdown-link-active-color: $component-active-color !default;\n$dropdown-link-active-bg: $component-active-bg !default;\n\n$dropdown-link-disabled-color: $gray-600 !default;\n\n$dropdown-item-padding-y: .25rem !default;\n$dropdown-item-padding-x: 1.5rem !default;\n\n$dropdown-header-color: $gray-600 !default;\n\n\n// Pagination\n\n$pagination-padding-y: .5rem !default;\n$pagination-padding-x: .75rem !default;\n$pagination-padding-y-sm: .25rem !default;\n$pagination-padding-x-sm: .5rem !default;\n$pagination-padding-y-lg: .75rem !default;\n$pagination-padding-x-lg: 1.5rem !default;\n$pagination-line-height: 1.25 !default;\n\n$pagination-color: $link-color !default;\n$pagination-bg: $white !default;\n$pagination-border-width: $border-width !default;\n$pagination-border-color: $gray-300 !default;\n\n$pagination-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$pagination-focus-outline: 0 !default;\n\n$pagination-hover-color: $link-hover-color !default;\n$pagination-hover-bg: $gray-200 !default;\n$pagination-hover-border-color: $gray-300 !default;\n\n$pagination-active-color: $component-active-color !default;\n$pagination-active-bg: $component-active-bg !default;\n$pagination-active-border-color: $pagination-active-bg !default;\n\n$pagination-disabled-color: $gray-600 !default;\n$pagination-disabled-bg: $white !default;\n$pagination-disabled-border-color: $gray-300 !default;\n\n\n// Jumbotron\n\n$jumbotron-padding: 2rem !default;\n$jumbotron-color: null !default;\n$jumbotron-bg: $gray-200 !default;\n\n\n// Cards\n\n$card-spacer-y: .75rem !default;\n$card-spacer-x: 1.25rem !default;\n$card-border-width: $border-width !default;\n$card-border-radius: $border-radius !default;\n$card-border-color: rgba($black, .125) !default;\n$card-inner-border-radius: subtract($card-border-radius, $card-border-width) !default;\n$card-cap-bg: rgba($black, .03) !default;\n$card-cap-color: null !default;\n$card-height: null !default;\n$card-color: null !default;\n$card-bg: $white !default;\n\n$card-img-overlay-padding: 1.25rem !default;\n\n$card-group-margin: $grid-gutter-width / 2 !default;\n$card-deck-margin: $card-group-margin !default;\n\n$card-columns-count: 3 !default;\n$card-columns-gap: 1.25rem !default;\n$card-columns-margin: $card-spacer-y !default;\n\n\n// Tooltips\n\n$tooltip-font-size: $font-size-sm !default;\n$tooltip-max-width: 200px !default;\n$tooltip-color: $white !default;\n$tooltip-bg: $black !default;\n$tooltip-border-radius: $border-radius !default;\n$tooltip-opacity: .9 !default;\n$tooltip-padding-y: .25rem !default;\n$tooltip-padding-x: .5rem !default;\n$tooltip-margin: 0 !default;\n\n$tooltip-arrow-width: .8rem !default;\n$tooltip-arrow-height: .4rem !default;\n$tooltip-arrow-color: $tooltip-bg !default;\n\n// Form tooltips must come after regular tooltips\n$form-feedback-tooltip-padding-y: $tooltip-padding-y !default;\n$form-feedback-tooltip-padding-x: $tooltip-padding-x !default;\n$form-feedback-tooltip-font-size: $tooltip-font-size !default;\n$form-feedback-tooltip-line-height: $line-height-base !default;\n$form-feedback-tooltip-opacity: $tooltip-opacity !default;\n$form-feedback-tooltip-border-radius: $tooltip-border-radius !default;\n\n\n// Popovers\n\n$popover-font-size: $font-size-sm !default;\n$popover-bg: $white !default;\n$popover-max-width: 276px !default;\n$popover-border-width: $border-width !default;\n$popover-border-color: rgba($black, .2) !default;\n$popover-border-radius: $border-radius-lg !default;\n$popover-inner-border-radius: subtract($popover-border-radius, $popover-border-width) !default;\n$popover-box-shadow: 0 .25rem .5rem rgba($black, .2) !default;\n\n$popover-header-bg: darken($popover-bg, 3%) !default;\n$popover-header-color: $headings-color !default;\n$popover-header-padding-y: .5rem !default;\n$popover-header-padding-x: .75rem !default;\n\n$popover-body-color: $body-color !default;\n$popover-body-padding-y: $popover-header-padding-y !default;\n$popover-body-padding-x: $popover-header-padding-x !default;\n\n$popover-arrow-width: 1rem !default;\n$popover-arrow-height: .5rem !default;\n$popover-arrow-color: $popover-bg !default;\n\n$popover-arrow-outer-color: fade-in($popover-border-color, .05) !default;\n\n\n// Toasts\n\n$toast-max-width: 350px !default;\n$toast-padding-x: .75rem !default;\n$toast-padding-y: .25rem !default;\n$toast-font-size: .875rem !default;\n$toast-color: null !default;\n$toast-background-color: rgba($white, .85) !default;\n$toast-border-width: 1px !default;\n$toast-border-color: rgba(0, 0, 0, .1) !default;\n$toast-border-radius: .25rem !default;\n$toast-box-shadow: 0 .25rem .75rem rgba($black, .1) !default;\n\n$toast-header-color: $gray-600 !default;\n$toast-header-background-color: rgba($white, .85) !default;\n$toast-header-border-color: rgba(0, 0, 0, .05) !default;\n\n\n// Badges\n\n$badge-font-size: 75% !default;\n$badge-font-weight: $font-weight-bold !default;\n$badge-padding-y: .25em !default;\n$badge-padding-x: .4em !default;\n$badge-border-radius: $border-radius !default;\n\n$badge-transition: $btn-transition !default;\n$badge-focus-width: $input-btn-focus-width !default;\n\n$badge-pill-padding-x: .6em !default;\n// Use a higher than normal value to ensure completely rounded edges when\n// customizing padding or font-size on labels.\n$badge-pill-border-radius: 10rem !default;\n\n\n// Modals\n\n// Padding applied to the modal body\n$modal-inner-padding: 1rem !default;\n\n// Margin between elements in footer, must be lower than or equal to 2 * $modal-inner-padding\n$modal-footer-margin-between: .5rem !default;\n\n$modal-dialog-margin: .5rem !default;\n$modal-dialog-margin-y-sm-up: 1.75rem !default;\n\n$modal-title-line-height: $line-height-base !default;\n\n$modal-content-color: null !default;\n$modal-content-bg: $white !default;\n$modal-content-border-color: rgba($black, .2) !default;\n$modal-content-border-width: $border-width !default;\n$modal-content-border-radius: $border-radius-lg !default;\n$modal-content-inner-border-radius: subtract($modal-content-border-radius, $modal-content-border-width) !default;\n$modal-content-box-shadow-xs: 0 .25rem .5rem rgba($black, .5) !default;\n$modal-content-box-shadow-sm-up: 0 .5rem 1rem rgba($black, .5) !default;\n\n$modal-backdrop-bg: $black !default;\n$modal-backdrop-opacity: .5 !default;\n$modal-header-border-color: $border-color !default;\n$modal-footer-border-color: $modal-header-border-color !default;\n$modal-header-border-width: $modal-content-border-width !default;\n$modal-footer-border-width: $modal-header-border-width !default;\n$modal-header-padding-y: 1rem !default;\n$modal-header-padding-x: 1rem !default;\n$modal-header-padding: $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility\n\n$modal-xl: 1140px !default;\n$modal-lg: 800px !default;\n$modal-md: 500px !default;\n$modal-sm: 300px !default;\n\n$modal-fade-transform: translate(0, -50px) !default;\n$modal-show-transform: none !default;\n$modal-transition: transform .3s ease-out !default;\n$modal-scale-transform: scale(1.02) !default;\n\n\n// Alerts\n//\n// Define alert colors, border radius, and padding.\n\n$alert-padding-y: .75rem !default;\n$alert-padding-x: 1.25rem !default;\n$alert-margin-bottom: 1rem !default;\n$alert-border-radius: $border-radius !default;\n$alert-link-font-weight: $font-weight-bold !default;\n$alert-border-width: $border-width !default;\n\n$alert-bg-level: -10 !default;\n$alert-border-level: -9 !default;\n$alert-color-level: 6 !default;\n\n\n// Progress bars\n\n$progress-height: 1rem !default;\n$progress-font-size: $font-size-base * .75 !default;\n$progress-bg: $gray-200 !default;\n$progress-border-radius: $border-radius !default;\n$progress-box-shadow: inset 0 .1rem .1rem rgba($black, .1) !default;\n$progress-bar-color: $white !default;\n$progress-bar-bg: theme-color(\"primary\") !default;\n$progress-bar-animation-timing: 1s linear infinite !default;\n$progress-bar-transition: width .6s ease !default;\n\n\n// List group\n\n$list-group-color: null !default;\n$list-group-bg: $white !default;\n$list-group-border-color: rgba($black, .125) !default;\n$list-group-border-width: $border-width !default;\n$list-group-border-radius: $border-radius !default;\n\n$list-group-item-padding-y: .75rem !default;\n$list-group-item-padding-x: 1.25rem !default;\n\n$list-group-hover-bg: $gray-100 !default;\n$list-group-active-color: $component-active-color !default;\n$list-group-active-bg: $component-active-bg !default;\n$list-group-active-border-color: $list-group-active-bg !default;\n\n$list-group-disabled-color: $gray-600 !default;\n$list-group-disabled-bg: $list-group-bg !default;\n\n$list-group-action-color: $gray-700 !default;\n$list-group-action-hover-color: $list-group-action-color !default;\n\n$list-group-action-active-color: $body-color !default;\n$list-group-action-active-bg: $gray-200 !default;\n\n\n// Image thumbnails\n\n$thumbnail-padding: .25rem !default;\n$thumbnail-bg: $body-bg !default;\n$thumbnail-border-width: $border-width !default;\n$thumbnail-border-color: $gray-300 !default;\n$thumbnail-border-radius: $border-radius !default;\n$thumbnail-box-shadow: 0 1px 2px rgba($black, .075) !default;\n\n\n// Figures\n\n$figure-caption-font-size: 90% !default;\n$figure-caption-color: $gray-600 !default;\n\n\n// Breadcrumbs\n\n$breadcrumb-font-size: null !default;\n\n$breadcrumb-padding-y: .75rem !default;\n$breadcrumb-padding-x: 1rem !default;\n$breadcrumb-item-padding: .5rem !default;\n\n$breadcrumb-margin-bottom: 1rem !default;\n\n$breadcrumb-bg: $gray-200 !default;\n$breadcrumb-divider-color: $gray-600 !default;\n$breadcrumb-active-color: $gray-600 !default;\n$breadcrumb-divider: quote(\"/\") !default;\n\n$breadcrumb-border-radius: $border-radius !default;\n\n\n// Carousel\n\n$carousel-control-color: $white !default;\n$carousel-control-width: 15% !default;\n$carousel-control-opacity: .5 !default;\n$carousel-control-hover-opacity: .9 !default;\n$carousel-control-transition: opacity .15s ease !default;\n\n$carousel-indicator-width: 30px !default;\n$carousel-indicator-height: 3px !default;\n$carousel-indicator-hit-area-height: 10px !default;\n$carousel-indicator-spacer: 3px !default;\n$carousel-indicator-active-bg: $white !default;\n$carousel-indicator-transition: opacity .6s ease !default;\n\n$carousel-caption-width: 70% !default;\n$carousel-caption-color: $white !default;\n\n$carousel-control-icon-width: 20px !default;\n\n$carousel-control-prev-icon-bg: url(\"data:image/svg+xml,\") !default;\n$carousel-control-next-icon-bg: url(\"data:image/svg+xml,\") !default;\n\n$carousel-transition-duration: .6s !default;\n$carousel-transition: transform $carousel-transition-duration ease-in-out !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`)\n\n\n// Spinners\n\n$spinner-width: 2rem !default;\n$spinner-height: $spinner-width !default;\n$spinner-border-width: .25em !default;\n\n$spinner-width-sm: 1rem !default;\n$spinner-height-sm: $spinner-width-sm !default;\n$spinner-border-width-sm: .2em !default;\n\n\n// Close\n\n$close-font-size: $font-size-base * 1.5 !default;\n$close-font-weight: $font-weight-bold !default;\n$close-color: $black !default;\n$close-text-shadow: 0 1px 0 $white !default;\n\n\n// Code\n\n$code-font-size: 87.5% !default;\n$code-color: $pink !default;\n\n$kbd-padding-y: .2rem !default;\n$kbd-padding-x: .4rem !default;\n$kbd-font-size: $code-font-size !default;\n$kbd-color: $white !default;\n$kbd-bg: $gray-900 !default;\n\n$pre-color: $gray-900 !default;\n$pre-scrollable-max-height: 340px !default;\n\n\n// Utilities\n\n$displays: none, inline, inline-block, block, table, table-row, table-cell, flex, inline-flex !default;\n$overflows: auto, hidden !default;\n$positions: static, relative, absolute, fixed, sticky !default;\n\n\n// Printing\n\n$print-page-size: a3 !default;\n$print-body-min-width: map-get($grid-breakpoints, \"lg\") !default;\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n // Common properties for all breakpoints\n %grid-column {\n position: relative;\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n }\n\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n // Allow columns to stretch full width below their breakpoints\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @extend %grid-column;\n }\n }\n .col#{$infix},\n .col#{$infix}-auto {\n @extend %grid-column;\n }\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n\n .order#{$infix}-first { order: -1; }\n\n .order#{$infix}-last { order: $columns + 1; }\n\n @for $i from 0 through $columns {\n .order#{$infix}-#{$i} { order: $i; }\n }\n\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n }\n}\n","// stylelint-disable declaration-no-important\n\n//\n// Utilities for common `display` values\n//\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $value in $displays {\n .d#{$infix}-#{$value} { display: $value !important; }\n }\n }\n}\n\n\n//\n// Utilities for toggling `display` in print\n//\n\n@media print {\n @each $value in $displays {\n .d-print-#{$value} { display: $value !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n// Flex variation\n//\n// Custom styles for additional flex alignment options.\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .flex#{$infix}-row { flex-direction: row !important; }\n .flex#{$infix}-column { flex-direction: column !important; }\n .flex#{$infix}-row-reverse { flex-direction: row-reverse !important; }\n .flex#{$infix}-column-reverse { flex-direction: column-reverse !important; }\n\n .flex#{$infix}-wrap { flex-wrap: wrap !important; }\n .flex#{$infix}-nowrap { flex-wrap: nowrap !important; }\n .flex#{$infix}-wrap-reverse { flex-wrap: wrap-reverse !important; }\n .flex#{$infix}-fill { flex: 1 1 auto !important; }\n .flex#{$infix}-grow-0 { flex-grow: 0 !important; }\n .flex#{$infix}-grow-1 { flex-grow: 1 !important; }\n .flex#{$infix}-shrink-0 { flex-shrink: 0 !important; }\n .flex#{$infix}-shrink-1 { flex-shrink: 1 !important; }\n\n .justify-content#{$infix}-start { justify-content: flex-start !important; }\n .justify-content#{$infix}-end { justify-content: flex-end !important; }\n .justify-content#{$infix}-center { justify-content: center !important; }\n .justify-content#{$infix}-between { justify-content: space-between !important; }\n .justify-content#{$infix}-around { justify-content: space-around !important; }\n\n .align-items#{$infix}-start { align-items: flex-start !important; }\n .align-items#{$infix}-end { align-items: flex-end !important; }\n .align-items#{$infix}-center { align-items: center !important; }\n .align-items#{$infix}-baseline { align-items: baseline !important; }\n .align-items#{$infix}-stretch { align-items: stretch !important; }\n\n .align-content#{$infix}-start { align-content: flex-start !important; }\n .align-content#{$infix}-end { align-content: flex-end !important; }\n .align-content#{$infix}-center { align-content: center !important; }\n .align-content#{$infix}-between { align-content: space-between !important; }\n .align-content#{$infix}-around { align-content: space-around !important; }\n .align-content#{$infix}-stretch { align-content: stretch !important; }\n\n .align-self#{$infix}-auto { align-self: auto !important; }\n .align-self#{$infix}-start { align-self: flex-start !important; }\n .align-self#{$infix}-end { align-self: flex-end !important; }\n .align-self#{$infix}-center { align-self: center !important; }\n .align-self#{$infix}-baseline { align-self: baseline !important; }\n .align-self#{$infix}-stretch { align-self: stretch !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n// Margin and Padding\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $prop, $abbrev in (margin: m, padding: p) {\n @each $size, $length in $spacers {\n .#{$abbrev}#{$infix}-#{$size} { #{$prop}: $length !important; }\n .#{$abbrev}t#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-top: $length !important;\n }\n .#{$abbrev}r#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-right: $length !important;\n }\n .#{$abbrev}b#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-bottom: $length !important;\n }\n .#{$abbrev}l#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-left: $length !important;\n }\n }\n }\n\n // Negative margins (e.g., where `.mb-n1` is negative version of `.mb-1`)\n @each $size, $length in $spacers {\n @if $size != 0 {\n .m#{$infix}-n#{$size} { margin: -$length !important; }\n .mt#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-top: -$length !important;\n }\n .mr#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-right: -$length !important;\n }\n .mb#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-bottom: -$length !important;\n }\n .ml#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-left: -$length !important;\n }\n }\n }\n\n // Some special margin utils\n .m#{$infix}-auto { margin: auto !important; }\n .mt#{$infix}-auto,\n .my#{$infix}-auto {\n margin-top: auto !important;\n }\n .mr#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-right: auto !important;\n }\n .mb#{$infix}-auto,\n .my#{$infix}-auto {\n margin-bottom: auto !important;\n }\n .ml#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-left: auto !important;\n }\n }\n}\n"]} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css new file mode 100644 index 00000000..6533f319 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap Grid v4.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */html{box-sizing:border-box;-ms-overflow-style:scrollbar}*,::after,::before{box-sizing:inherit}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-sm-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-md-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-lg-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-xl-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}} +/*# sourceMappingURL=bootstrap-grid.min.css.map */ \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map new file mode 100644 index 00000000..1b393db3 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap-grid.scss","dist/css/bootstrap-grid.css","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_breakpoints.scss","../../scss/mixins/_grid-framework.scss","../../scss/utilities/_display.scss","../../scss/utilities/_flex.scss","../../scss/utilities/_spacing.scss"],"names":[],"mappings":"AAAA;;;;;AAOA,KACE,WAAA,WACA,mBAAA,UAGF,ECCA,QADA,SDGE,WAAA,QETA,WCDA,MAAA,KACA,cAAA,KACA,aAAA,KACA,aAAA,KACA,YAAA,KCmDE,yBFtDF,WCWI,UAAA,OC2CF,yBFtDF,WCWI,UAAA,OC2CF,yBFtDF,WCWI,UAAA,OC2CF,0BFtDF,WCWI,UAAA,QDLJ,iBAAA,cAAA,cAAA,cAAA,cCPA,MAAA,KACA,cAAA,KACA,aAAA,KACA,aAAA,KACA,YAAA,KCmDE,yBFrCE,WAAA,cACE,UAAA,OEoCJ,yBFrCE,WAAA,cAAA,cACE,UAAA,OEoCJ,yBFrCE,WAAA,cAAA,cAAA,cACE,UAAA,OEoCJ,0BFrCE,WAAA,cAAA,cAAA,cAAA,cACE,UAAA,QAoBN,KCrBA,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,aAAA,MACA,YAAA,MDwBA,YACE,aAAA,EACA,YAAA,EAFF,iBD8CF,0BCxCM,cAAA,EACA,aAAA,EGlDJ,KAAA,OAAA,QAAA,QAAA,QAAA,OAAA,OAAA,OAAA,OAAA,OAAA,OAAA,OAAA,OJ+FF,UAEqJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACtG,aAFqJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACtG,aAFkJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACnG,aAEqJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACtG,aIlGI,SAAA,SACA,MAAA,KACA,cAAA,KACA,aAAA,KAmBE,KACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAIA,cF4BJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KE7BI,cF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,cF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WE7BI,cF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,cF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,cF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WExBE,UFMJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEHM,OFPN,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UEGM,OFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,OFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,OFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,OFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,OFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,OFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,OFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,OFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,QFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,QFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,QFPN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEQI,aAAwB,eAAA,GAAA,MAAA,GAExB,YAAuB,eAAA,GAAA,MAAA,GAGrB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,UAAwB,eAAA,GAAA,MAAA,GAAxB,UAAwB,eAAA,GAAA,MAAA,GAAxB,UAAwB,eAAA,GAAA,MAAA,GAMtB,UFRR,YAAA,UEQQ,UFRR,YAAA,WEQQ,UFRR,YAAA,IEQQ,UFRR,YAAA,WEQQ,UFRR,YAAA,WEQQ,UFRR,YAAA,IEQQ,UFRR,YAAA,WEQQ,UFRR,YAAA,WEQQ,UFRR,YAAA,IEQQ,WFRR,YAAA,WEQQ,WFRR,YAAA,WCKE,yBC9BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAIA,iBF4BJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WExBE,aFMJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEHM,UFPN,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,WFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,WFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,WFPN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEQI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAMtB,aFRR,YAAA,EEQQ,aFRR,YAAA,UEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,cFRR,YAAA,WEQQ,cFRR,YAAA,YCKE,yBC9BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAIA,iBF4BJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WExBE,aFMJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEHM,UFPN,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,WFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,WFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,WFPN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEQI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAMtB,aFRR,YAAA,EEQQ,aFRR,YAAA,UEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,cFRR,YAAA,WEQQ,cFRR,YAAA,YCKE,yBC9BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAIA,iBF4BJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WExBE,aFMJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEHM,UFPN,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,WFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,WFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,WFPN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEQI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAMtB,aFRR,YAAA,EEQQ,aFRR,YAAA,UEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,cFRR,YAAA,WEQQ,cFRR,YAAA,YCKE,0BC9BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAIA,iBF4BJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IE7BI,iBF4BJ,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WExBE,aFMJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEHM,UFPN,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,UFPN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEGM,WFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,WFPN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEGM,WFPN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEQI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAMtB,aFRR,YAAA,EEQQ,aFRR,YAAA,UEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,WEQQ,aFRR,YAAA,IEQQ,cFRR,YAAA,WEQQ,cFRR,YAAA,YG5CI,QAAwB,QAAA,eAAxB,UAAwB,QAAA,iBAAxB,gBAAwB,QAAA,uBAAxB,SAAwB,QAAA,gBAAxB,SAAwB,QAAA,gBAAxB,aAAwB,QAAA,oBAAxB,cAAwB,QAAA,qBAAxB,QAAwB,QAAA,sBAAA,QAAA,eAAxB,eAAwB,QAAA,6BAAA,QAAA,sBFiD1B,yBEjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uBFiD1B,yBEjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uBFiD1B,yBEjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uBFiD1B,0BEjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uBAU9B,aAEI,cAAqB,QAAA,eAArB,gBAAqB,QAAA,iBAArB,sBAAqB,QAAA,uBAArB,eAAqB,QAAA,gBAArB,eAAqB,QAAA,gBAArB,mBAAqB,QAAA,oBAArB,oBAAqB,QAAA,qBAArB,cAAqB,QAAA,sBAAA,QAAA,eAArB,qBAAqB,QAAA,6BAAA,QAAA,uBCbrB,UAAgC,mBAAA,cAAA,eAAA,cAChC,aAAgC,mBAAA,iBAAA,eAAA,iBAChC,kBAAgC,mBAAA,sBAAA,eAAA,sBAChC,qBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,WAA8B,cAAA,eAAA,UAAA,eAC9B,aAA8B,cAAA,iBAAA,UAAA,iBAC9B,mBAA8B,cAAA,uBAAA,UAAA,uBAC9B,WAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,aAA8B,kBAAA,YAAA,UAAA,YAC9B,aAA8B,kBAAA,YAAA,UAAA,YAC9B,eAA8B,kBAAA,YAAA,YAAA,YAC9B,eAA8B,kBAAA,YAAA,YAAA,YAE9B,uBAAoC,cAAA,gBAAA,gBAAA,qBACpC,qBAAoC,cAAA,cAAA,gBAAA,mBACpC,wBAAoC,cAAA,iBAAA,gBAAA,iBACpC,yBAAoC,cAAA,kBAAA,gBAAA,wBACpC,wBAAoC,cAAA,qBAAA,gBAAA,uBAEpC,mBAAiC,eAAA,gBAAA,YAAA,qBACjC,iBAAiC,eAAA,cAAA,YAAA,mBACjC,oBAAiC,eAAA,iBAAA,YAAA,iBACjC,sBAAiC,eAAA,mBAAA,YAAA,mBACjC,qBAAiC,eAAA,kBAAA,YAAA,kBAEjC,qBAAkC,mBAAA,gBAAA,cAAA,qBAClC,mBAAkC,mBAAA,cAAA,cAAA,mBAClC,sBAAkC,mBAAA,iBAAA,cAAA,iBAClC,uBAAkC,mBAAA,kBAAA,cAAA,wBAClC,sBAAkC,mBAAA,qBAAA,cAAA,uBAClC,uBAAkC,mBAAA,kBAAA,cAAA,kBAElC,iBAAgC,oBAAA,eAAA,WAAA,eAChC,kBAAgC,oBAAA,gBAAA,WAAA,qBAChC,gBAAgC,oBAAA,cAAA,WAAA,mBAChC,mBAAgC,oBAAA,iBAAA,WAAA,iBAChC,qBAAgC,oBAAA,mBAAA,WAAA,mBAChC,oBAAgC,oBAAA,kBAAA,WAAA,kBHYhC,yBGlDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mBHYhC,yBGlDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mBHYhC,yBGlDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mBHYhC,0BGlDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mBCtC5B,KAAgC,OAAA,YAChC,MPiiER,MO/hEU,WAAA,YAEF,MPkiER,MOhiEU,aAAA,YAEF,MPmiER,MOjiEU,cAAA,YAEF,MPoiER,MOliEU,YAAA,YAfF,KAAgC,OAAA,iBAChC,MPyjER,MOvjEU,WAAA,iBAEF,MP0jER,MOxjEU,aAAA,iBAEF,MP2jER,MOzjEU,cAAA,iBAEF,MP4jER,MO1jEU,YAAA,iBAfF,KAAgC,OAAA,gBAChC,MPilER,MO/kEU,WAAA,gBAEF,MPklER,MOhlEU,aAAA,gBAEF,MPmlER,MOjlEU,cAAA,gBAEF,MPolER,MOllEU,YAAA,gBAfF,KAAgC,OAAA,eAChC,MPymER,MOvmEU,WAAA,eAEF,MP0mER,MOxmEU,aAAA,eAEF,MP2mER,MOzmEU,cAAA,eAEF,MP4mER,MO1mEU,YAAA,eAfF,KAAgC,OAAA,iBAChC,MPioER,MO/nEU,WAAA,iBAEF,MPkoER,MOhoEU,aAAA,iBAEF,MPmoER,MOjoEU,cAAA,iBAEF,MPooER,MOloEU,YAAA,iBAfF,KAAgC,OAAA,eAChC,MPypER,MOvpEU,WAAA,eAEF,MP0pER,MOxpEU,aAAA,eAEF,MP2pER,MOzpEU,cAAA,eAEF,MP4pER,MO1pEU,YAAA,eAfF,KAAgC,QAAA,YAChC,MPirER,MO/qEU,YAAA,YAEF,MPkrER,MOhrEU,cAAA,YAEF,MPmrER,MOjrEU,eAAA,YAEF,MPorER,MOlrEU,aAAA,YAfF,KAAgC,QAAA,iBAChC,MPysER,MOvsEU,YAAA,iBAEF,MP0sER,MOxsEU,cAAA,iBAEF,MP2sER,MOzsEU,eAAA,iBAEF,MP4sER,MO1sEU,aAAA,iBAfF,KAAgC,QAAA,gBAChC,MPiuER,MO/tEU,YAAA,gBAEF,MPkuER,MOhuEU,cAAA,gBAEF,MPmuER,MOjuEU,eAAA,gBAEF,MPouER,MOluEU,aAAA,gBAfF,KAAgC,QAAA,eAChC,MPyvER,MOvvEU,YAAA,eAEF,MP0vER,MOxvEU,cAAA,eAEF,MP2vER,MOzvEU,eAAA,eAEF,MP4vER,MO1vEU,aAAA,eAfF,KAAgC,QAAA,iBAChC,MPixER,MO/wEU,YAAA,iBAEF,MPkxER,MOhxEU,cAAA,iBAEF,MPmxER,MOjxEU,eAAA,iBAEF,MPoxER,MOlxEU,aAAA,iBAfF,KAAgC,QAAA,eAChC,MPyyER,MOvyEU,YAAA,eAEF,MP0yER,MOxyEU,cAAA,eAEF,MP2yER,MOzyEU,eAAA,eAEF,MP4yER,MO1yEU,aAAA,eAQF,MAAwB,OAAA,kBACxB,OP0yER,OOxyEU,WAAA,kBAEF,OP2yER,OOzyEU,aAAA,kBAEF,OP4yER,OO1yEU,cAAA,kBAEF,OP6yER,OO3yEU,YAAA,kBAfF,MAAwB,OAAA,iBACxB,OPk0ER,OOh0EU,WAAA,iBAEF,OPm0ER,OOj0EU,aAAA,iBAEF,OPo0ER,OOl0EU,cAAA,iBAEF,OPq0ER,OOn0EU,YAAA,iBAfF,MAAwB,OAAA,gBACxB,OP01ER,OOx1EU,WAAA,gBAEF,OP21ER,OOz1EU,aAAA,gBAEF,OP41ER,OO11EU,cAAA,gBAEF,OP61ER,OO31EU,YAAA,gBAfF,MAAwB,OAAA,kBACxB,OPk3ER,OOh3EU,WAAA,kBAEF,OPm3ER,OOj3EU,aAAA,kBAEF,OPo3ER,OOl3EU,cAAA,kBAEF,OPq3ER,OOn3EU,YAAA,kBAfF,MAAwB,OAAA,gBACxB,OP04ER,OOx4EU,WAAA,gBAEF,OP24ER,OOz4EU,aAAA,gBAEF,OP44ER,OO14EU,cAAA,gBAEF,OP64ER,OO34EU,YAAA,gBAMN,QAAmB,OAAA,eACnB,SP64EJ,SO34EM,WAAA,eAEF,SP84EJ,SO54EM,aAAA,eAEF,SP+4EJ,SO74EM,cAAA,eAEF,SPg5EJ,SO94EM,YAAA,eJTF,yBIlDI,QAAgC,OAAA,YAChC,SPi9EN,SO/8EQ,WAAA,YAEF,SPi9EN,SO/8EQ,aAAA,YAEF,SPi9EN,SO/8EQ,cAAA,YAEF,SPi9EN,SO/8EQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SPo+EN,SOl+EQ,WAAA,iBAEF,SPo+EN,SOl+EQ,aAAA,iBAEF,SPo+EN,SOl+EQ,cAAA,iBAEF,SPo+EN,SOl+EQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SPu/EN,SOr/EQ,WAAA,gBAEF,SPu/EN,SOr/EQ,aAAA,gBAEF,SPu/EN,SOr/EQ,cAAA,gBAEF,SPu/EN,SOr/EQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SP0gFN,SOxgFQ,WAAA,eAEF,SP0gFN,SOxgFQ,aAAA,eAEF,SP0gFN,SOxgFQ,cAAA,eAEF,SP0gFN,SOxgFQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SP6hFN,SO3hFQ,WAAA,iBAEF,SP6hFN,SO3hFQ,aAAA,iBAEF,SP6hFN,SO3hFQ,cAAA,iBAEF,SP6hFN,SO3hFQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SPgjFN,SO9iFQ,WAAA,eAEF,SPgjFN,SO9iFQ,aAAA,eAEF,SPgjFN,SO9iFQ,cAAA,eAEF,SPgjFN,SO9iFQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SPmkFN,SOjkFQ,YAAA,YAEF,SPmkFN,SOjkFQ,cAAA,YAEF,SPmkFN,SOjkFQ,eAAA,YAEF,SPmkFN,SOjkFQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SPslFN,SOplFQ,YAAA,iBAEF,SPslFN,SOplFQ,cAAA,iBAEF,SPslFN,SOplFQ,eAAA,iBAEF,SPslFN,SOplFQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SPymFN,SOvmFQ,YAAA,gBAEF,SPymFN,SOvmFQ,cAAA,gBAEF,SPymFN,SOvmFQ,eAAA,gBAEF,SPymFN,SOvmFQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SP4nFN,SO1nFQ,YAAA,eAEF,SP4nFN,SO1nFQ,cAAA,eAEF,SP4nFN,SO1nFQ,eAAA,eAEF,SP4nFN,SO1nFQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SP+oFN,SO7oFQ,YAAA,iBAEF,SP+oFN,SO7oFQ,cAAA,iBAEF,SP+oFN,SO7oFQ,eAAA,iBAEF,SP+oFN,SO7oFQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SPkqFN,SOhqFQ,YAAA,eAEF,SPkqFN,SOhqFQ,cAAA,eAEF,SPkqFN,SOhqFQ,eAAA,eAEF,SPkqFN,SOhqFQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UP8pFN,UO5pFQ,WAAA,kBAEF,UP8pFN,UO5pFQ,aAAA,kBAEF,UP8pFN,UO5pFQ,cAAA,kBAEF,UP8pFN,UO5pFQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UPirFN,UO/qFQ,WAAA,iBAEF,UPirFN,UO/qFQ,aAAA,iBAEF,UPirFN,UO/qFQ,cAAA,iBAEF,UPirFN,UO/qFQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UPosFN,UOlsFQ,WAAA,gBAEF,UPosFN,UOlsFQ,aAAA,gBAEF,UPosFN,UOlsFQ,cAAA,gBAEF,UPosFN,UOlsFQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UPutFN,UOrtFQ,WAAA,kBAEF,UPutFN,UOrtFQ,aAAA,kBAEF,UPutFN,UOrtFQ,cAAA,kBAEF,UPutFN,UOrtFQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UP0uFN,UOxuFQ,WAAA,gBAEF,UP0uFN,UOxuFQ,aAAA,gBAEF,UP0uFN,UOxuFQ,cAAA,gBAEF,UP0uFN,UOxuFQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YPwuFF,YOtuFI,WAAA,eAEF,YPwuFF,YOtuFI,aAAA,eAEF,YPwuFF,YOtuFI,cAAA,eAEF,YPwuFF,YOtuFI,YAAA,gBJTF,yBIlDI,QAAgC,OAAA,YAChC,SP0yFN,SOxyFQ,WAAA,YAEF,SP0yFN,SOxyFQ,aAAA,YAEF,SP0yFN,SOxyFQ,cAAA,YAEF,SP0yFN,SOxyFQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SP6zFN,SO3zFQ,WAAA,iBAEF,SP6zFN,SO3zFQ,aAAA,iBAEF,SP6zFN,SO3zFQ,cAAA,iBAEF,SP6zFN,SO3zFQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SPg1FN,SO90FQ,WAAA,gBAEF,SPg1FN,SO90FQ,aAAA,gBAEF,SPg1FN,SO90FQ,cAAA,gBAEF,SPg1FN,SO90FQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SPm2FN,SOj2FQ,WAAA,eAEF,SPm2FN,SOj2FQ,aAAA,eAEF,SPm2FN,SOj2FQ,cAAA,eAEF,SPm2FN,SOj2FQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SPs3FN,SOp3FQ,WAAA,iBAEF,SPs3FN,SOp3FQ,aAAA,iBAEF,SPs3FN,SOp3FQ,cAAA,iBAEF,SPs3FN,SOp3FQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SPy4FN,SOv4FQ,WAAA,eAEF,SPy4FN,SOv4FQ,aAAA,eAEF,SPy4FN,SOv4FQ,cAAA,eAEF,SPy4FN,SOv4FQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SP45FN,SO15FQ,YAAA,YAEF,SP45FN,SO15FQ,cAAA,YAEF,SP45FN,SO15FQ,eAAA,YAEF,SP45FN,SO15FQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SP+6FN,SO76FQ,YAAA,iBAEF,SP+6FN,SO76FQ,cAAA,iBAEF,SP+6FN,SO76FQ,eAAA,iBAEF,SP+6FN,SO76FQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SPk8FN,SOh8FQ,YAAA,gBAEF,SPk8FN,SOh8FQ,cAAA,gBAEF,SPk8FN,SOh8FQ,eAAA,gBAEF,SPk8FN,SOh8FQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SPq9FN,SOn9FQ,YAAA,eAEF,SPq9FN,SOn9FQ,cAAA,eAEF,SPq9FN,SOn9FQ,eAAA,eAEF,SPq9FN,SOn9FQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SPw+FN,SOt+FQ,YAAA,iBAEF,SPw+FN,SOt+FQ,cAAA,iBAEF,SPw+FN,SOt+FQ,eAAA,iBAEF,SPw+FN,SOt+FQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SP2/FN,SOz/FQ,YAAA,eAEF,SP2/FN,SOz/FQ,cAAA,eAEF,SP2/FN,SOz/FQ,eAAA,eAEF,SP2/FN,SOz/FQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UPu/FN,UOr/FQ,WAAA,kBAEF,UPu/FN,UOr/FQ,aAAA,kBAEF,UPu/FN,UOr/FQ,cAAA,kBAEF,UPu/FN,UOr/FQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UP0gGN,UOxgGQ,WAAA,iBAEF,UP0gGN,UOxgGQ,aAAA,iBAEF,UP0gGN,UOxgGQ,cAAA,iBAEF,UP0gGN,UOxgGQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UP6hGN,UO3hGQ,WAAA,gBAEF,UP6hGN,UO3hGQ,aAAA,gBAEF,UP6hGN,UO3hGQ,cAAA,gBAEF,UP6hGN,UO3hGQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UPgjGN,UO9iGQ,WAAA,kBAEF,UPgjGN,UO9iGQ,aAAA,kBAEF,UPgjGN,UO9iGQ,cAAA,kBAEF,UPgjGN,UO9iGQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UPmkGN,UOjkGQ,WAAA,gBAEF,UPmkGN,UOjkGQ,aAAA,gBAEF,UPmkGN,UOjkGQ,cAAA,gBAEF,UPmkGN,UOjkGQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YPikGF,YO/jGI,WAAA,eAEF,YPikGF,YO/jGI,aAAA,eAEF,YPikGF,YO/jGI,cAAA,eAEF,YPikGF,YO/jGI,YAAA,gBJTF,yBIlDI,QAAgC,OAAA,YAChC,SPmoGN,SOjoGQ,WAAA,YAEF,SPmoGN,SOjoGQ,aAAA,YAEF,SPmoGN,SOjoGQ,cAAA,YAEF,SPmoGN,SOjoGQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SPspGN,SOppGQ,WAAA,iBAEF,SPspGN,SOppGQ,aAAA,iBAEF,SPspGN,SOppGQ,cAAA,iBAEF,SPspGN,SOppGQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SPyqGN,SOvqGQ,WAAA,gBAEF,SPyqGN,SOvqGQ,aAAA,gBAEF,SPyqGN,SOvqGQ,cAAA,gBAEF,SPyqGN,SOvqGQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SP4rGN,SO1rGQ,WAAA,eAEF,SP4rGN,SO1rGQ,aAAA,eAEF,SP4rGN,SO1rGQ,cAAA,eAEF,SP4rGN,SO1rGQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SP+sGN,SO7sGQ,WAAA,iBAEF,SP+sGN,SO7sGQ,aAAA,iBAEF,SP+sGN,SO7sGQ,cAAA,iBAEF,SP+sGN,SO7sGQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SPkuGN,SOhuGQ,WAAA,eAEF,SPkuGN,SOhuGQ,aAAA,eAEF,SPkuGN,SOhuGQ,cAAA,eAEF,SPkuGN,SOhuGQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SPqvGN,SOnvGQ,YAAA,YAEF,SPqvGN,SOnvGQ,cAAA,YAEF,SPqvGN,SOnvGQ,eAAA,YAEF,SPqvGN,SOnvGQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SPwwGN,SOtwGQ,YAAA,iBAEF,SPwwGN,SOtwGQ,cAAA,iBAEF,SPwwGN,SOtwGQ,eAAA,iBAEF,SPwwGN,SOtwGQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SP2xGN,SOzxGQ,YAAA,gBAEF,SP2xGN,SOzxGQ,cAAA,gBAEF,SP2xGN,SOzxGQ,eAAA,gBAEF,SP2xGN,SOzxGQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SP8yGN,SO5yGQ,YAAA,eAEF,SP8yGN,SO5yGQ,cAAA,eAEF,SP8yGN,SO5yGQ,eAAA,eAEF,SP8yGN,SO5yGQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SPi0GN,SO/zGQ,YAAA,iBAEF,SPi0GN,SO/zGQ,cAAA,iBAEF,SPi0GN,SO/zGQ,eAAA,iBAEF,SPi0GN,SO/zGQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SPo1GN,SOl1GQ,YAAA,eAEF,SPo1GN,SOl1GQ,cAAA,eAEF,SPo1GN,SOl1GQ,eAAA,eAEF,SPo1GN,SOl1GQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UPg1GN,UO90GQ,WAAA,kBAEF,UPg1GN,UO90GQ,aAAA,kBAEF,UPg1GN,UO90GQ,cAAA,kBAEF,UPg1GN,UO90GQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UPm2GN,UOj2GQ,WAAA,iBAEF,UPm2GN,UOj2GQ,aAAA,iBAEF,UPm2GN,UOj2GQ,cAAA,iBAEF,UPm2GN,UOj2GQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UPs3GN,UOp3GQ,WAAA,gBAEF,UPs3GN,UOp3GQ,aAAA,gBAEF,UPs3GN,UOp3GQ,cAAA,gBAEF,UPs3GN,UOp3GQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UPy4GN,UOv4GQ,WAAA,kBAEF,UPy4GN,UOv4GQ,aAAA,kBAEF,UPy4GN,UOv4GQ,cAAA,kBAEF,UPy4GN,UOv4GQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UP45GN,UO15GQ,WAAA,gBAEF,UP45GN,UO15GQ,aAAA,gBAEF,UP45GN,UO15GQ,cAAA,gBAEF,UP45GN,UO15GQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YP05GF,YOx5GI,WAAA,eAEF,YP05GF,YOx5GI,aAAA,eAEF,YP05GF,YOx5GI,cAAA,eAEF,YP05GF,YOx5GI,YAAA,gBJTF,0BIlDI,QAAgC,OAAA,YAChC,SP49GN,SO19GQ,WAAA,YAEF,SP49GN,SO19GQ,aAAA,YAEF,SP49GN,SO19GQ,cAAA,YAEF,SP49GN,SO19GQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SP++GN,SO7+GQ,WAAA,iBAEF,SP++GN,SO7+GQ,aAAA,iBAEF,SP++GN,SO7+GQ,cAAA,iBAEF,SP++GN,SO7+GQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SPkgHN,SOhgHQ,WAAA,gBAEF,SPkgHN,SOhgHQ,aAAA,gBAEF,SPkgHN,SOhgHQ,cAAA,gBAEF,SPkgHN,SOhgHQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SPqhHN,SOnhHQ,WAAA,eAEF,SPqhHN,SOnhHQ,aAAA,eAEF,SPqhHN,SOnhHQ,cAAA,eAEF,SPqhHN,SOnhHQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SPwiHN,SOtiHQ,WAAA,iBAEF,SPwiHN,SOtiHQ,aAAA,iBAEF,SPwiHN,SOtiHQ,cAAA,iBAEF,SPwiHN,SOtiHQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SP2jHN,SOzjHQ,WAAA,eAEF,SP2jHN,SOzjHQ,aAAA,eAEF,SP2jHN,SOzjHQ,cAAA,eAEF,SP2jHN,SOzjHQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SP8kHN,SO5kHQ,YAAA,YAEF,SP8kHN,SO5kHQ,cAAA,YAEF,SP8kHN,SO5kHQ,eAAA,YAEF,SP8kHN,SO5kHQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SPimHN,SO/lHQ,YAAA,iBAEF,SPimHN,SO/lHQ,cAAA,iBAEF,SPimHN,SO/lHQ,eAAA,iBAEF,SPimHN,SO/lHQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SPonHN,SOlnHQ,YAAA,gBAEF,SPonHN,SOlnHQ,cAAA,gBAEF,SPonHN,SOlnHQ,eAAA,gBAEF,SPonHN,SOlnHQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SPuoHN,SOroHQ,YAAA,eAEF,SPuoHN,SOroHQ,cAAA,eAEF,SPuoHN,SOroHQ,eAAA,eAEF,SPuoHN,SOroHQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SP0pHN,SOxpHQ,YAAA,iBAEF,SP0pHN,SOxpHQ,cAAA,iBAEF,SP0pHN,SOxpHQ,eAAA,iBAEF,SP0pHN,SOxpHQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SP6qHN,SO3qHQ,YAAA,eAEF,SP6qHN,SO3qHQ,cAAA,eAEF,SP6qHN,SO3qHQ,eAAA,eAEF,SP6qHN,SO3qHQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UPyqHN,UOvqHQ,WAAA,kBAEF,UPyqHN,UOvqHQ,aAAA,kBAEF,UPyqHN,UOvqHQ,cAAA,kBAEF,UPyqHN,UOvqHQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UP4rHN,UO1rHQ,WAAA,iBAEF,UP4rHN,UO1rHQ,aAAA,iBAEF,UP4rHN,UO1rHQ,cAAA,iBAEF,UP4rHN,UO1rHQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UP+sHN,UO7sHQ,WAAA,gBAEF,UP+sHN,UO7sHQ,aAAA,gBAEF,UP+sHN,UO7sHQ,cAAA,gBAEF,UP+sHN,UO7sHQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UPkuHN,UOhuHQ,WAAA,kBAEF,UPkuHN,UOhuHQ,aAAA,kBAEF,UPkuHN,UOhuHQ,cAAA,kBAEF,UPkuHN,UOhuHQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UPqvHN,UOnvHQ,WAAA,gBAEF,UPqvHN,UOnvHQ,aAAA,gBAEF,UPqvHN,UOnvHQ,cAAA,gBAEF,UPqvHN,UOnvHQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YPmvHF,YOjvHI,WAAA,eAEF,YPmvHF,YOjvHI,aAAA,eAEF,YPmvHF,YOjvHI,cAAA,eAEF,YPmvHF,YOjvHI,YAAA","sourcesContent":["/*!\n * Bootstrap Grid v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n\nhtml {\n box-sizing: border-box;\n -ms-overflow-style: scrollbar;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: inherit;\n}\n\n@import \"functions\";\n@import \"variables\";\n\n@import \"mixins/breakpoints\";\n@import \"mixins/grid-framework\";\n@import \"mixins/grid\";\n\n@import \"grid\";\n@import \"utilities/display\";\n@import \"utilities/flex\";\n@import \"utilities/spacing\";\n","/*!\n * Bootstrap Grid v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\nhtml {\n box-sizing: border-box;\n -ms-overflow-style: scrollbar;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: inherit;\n}\n\n.container {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container {\n max-width: 1140px;\n }\n}\n\n.container-fluid, .container-sm, .container-md, .container-lg, .container-xl {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container, .container-sm {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container, .container-sm, .container-md {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container, .container-sm, .container-md, .container-lg {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container, .container-sm, .container-md, .container-lg, .container-xl {\n max-width: 1140px;\n }\n}\n\n.row {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n\n.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,\n.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm,\n.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md,\n.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg,\n.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl,\n.col-xl-auto {\n position: relative;\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n}\n\n.col {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n}\n\n.row-cols-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.row-cols-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.row-cols-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.row-cols-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.row-cols-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n}\n\n.row-cols-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n}\n\n.col-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n}\n\n.col-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.col-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.col-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n}\n\n.col-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.col-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n}\n\n.col-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n}\n\n.col-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.col-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n}\n\n.col-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n}\n\n.col-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.order-first {\n -ms-flex-order: -1;\n order: -1;\n}\n\n.order-last {\n -ms-flex-order: 13;\n order: 13;\n}\n\n.order-0 {\n -ms-flex-order: 0;\n order: 0;\n}\n\n.order-1 {\n -ms-flex-order: 1;\n order: 1;\n}\n\n.order-2 {\n -ms-flex-order: 2;\n order: 2;\n}\n\n.order-3 {\n -ms-flex-order: 3;\n order: 3;\n}\n\n.order-4 {\n -ms-flex-order: 4;\n order: 4;\n}\n\n.order-5 {\n -ms-flex-order: 5;\n order: 5;\n}\n\n.order-6 {\n -ms-flex-order: 6;\n order: 6;\n}\n\n.order-7 {\n -ms-flex-order: 7;\n order: 7;\n}\n\n.order-8 {\n -ms-flex-order: 8;\n order: 8;\n}\n\n.order-9 {\n -ms-flex-order: 9;\n order: 9;\n}\n\n.order-10 {\n -ms-flex-order: 10;\n order: 10;\n}\n\n.order-11 {\n -ms-flex-order: 11;\n order: 11;\n}\n\n.order-12 {\n -ms-flex-order: 12;\n order: 12;\n}\n\n.offset-1 {\n margin-left: 8.333333%;\n}\n\n.offset-2 {\n margin-left: 16.666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.333333%;\n}\n\n.offset-5 {\n margin-left: 41.666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.333333%;\n}\n\n.offset-8 {\n margin-left: 66.666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.333333%;\n}\n\n.offset-11 {\n margin-left: 91.666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-sm-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-sm-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-sm-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-sm-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-sm-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-sm-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-sm-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-sm-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-sm-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-sm-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-sm-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-sm-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-sm-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-sm-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-sm-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-sm-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-sm-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-sm-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-sm-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-sm-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-sm-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-sm-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-sm-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-sm-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-sm-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-sm-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-sm-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-sm-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-sm-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-sm-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-sm-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-sm-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.333333%;\n }\n .offset-sm-2 {\n margin-left: 16.666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.333333%;\n }\n .offset-sm-5 {\n margin-left: 41.666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.333333%;\n }\n .offset-sm-8 {\n margin-left: 66.666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.333333%;\n }\n .offset-sm-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 768px) {\n .col-md {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-md-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-md-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-md-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-md-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-md-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-md-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-md-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-md-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-md-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-md-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-md-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-md-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-md-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-md-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-md-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-md-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-md-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-md-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-md-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-md-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-md-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-md-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-md-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-md-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-md-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-md-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-md-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-md-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-md-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-md-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-md-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-md-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.333333%;\n }\n .offset-md-2 {\n margin-left: 16.666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.333333%;\n }\n .offset-md-5 {\n margin-left: 41.666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.333333%;\n }\n .offset-md-8 {\n margin-left: 66.666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.333333%;\n }\n .offset-md-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 992px) {\n .col-lg {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-lg-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-lg-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-lg-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-lg-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-lg-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-lg-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-lg-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-lg-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-lg-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-lg-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-lg-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-lg-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-lg-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-lg-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-lg-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-lg-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-lg-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-lg-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-lg-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-lg-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-lg-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-lg-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-lg-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-lg-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-lg-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-lg-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-lg-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-lg-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-lg-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-lg-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-lg-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-lg-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.333333%;\n }\n .offset-lg-2 {\n margin-left: 16.666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.333333%;\n }\n .offset-lg-5 {\n margin-left: 41.666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.333333%;\n }\n .offset-lg-8 {\n margin-left: 66.666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.333333%;\n }\n .offset-lg-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 1200px) {\n .col-xl {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-xl-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-xl-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-xl-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-xl-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-xl-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-xl-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-xl-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-xl-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-xl-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-xl-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-xl-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-xl-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-xl-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-xl-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-xl-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-xl-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-xl-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-xl-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-xl-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-xl-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-xl-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-xl-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-xl-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-xl-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-xl-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-xl-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-xl-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-xl-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-xl-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-xl-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-xl-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-xl-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.333333%;\n }\n .offset-xl-2 {\n margin-left: 16.666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.333333%;\n }\n .offset-xl-5 {\n margin-left: 41.666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.333333%;\n }\n .offset-xl-8 {\n margin-left: 66.666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.333333%;\n }\n .offset-xl-11 {\n margin-left: 91.666667%;\n }\n}\n\n.d-none {\n display: none !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n}\n\n.d-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-none {\n display: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 768px) {\n .d-md-none {\n display: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-md-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 992px) {\n .d-lg-none {\n display: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 1200px) {\n .d-xl-none {\n display: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media print {\n .d-print-none {\n display: none !important;\n }\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-print-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n.flex-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n}\n\n.flex-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n}\n\n.flex-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n}\n\n.flex-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n}\n\n.justify-content-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n}\n\n.justify-content-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n}\n\n.align-items-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n}\n\n.align-items-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n}\n\n.align-items-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n}\n\n.align-items-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n}\n\n.align-content-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n}\n\n.align-content-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n}\n\n.align-content-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n}\n\n.align-content-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n}\n\n.align-content-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n}\n\n.align-self-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n}\n\n.align-self-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n}\n\n.align-self-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n}\n\n.align-self-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n}\n\n.align-self-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n .flex-sm-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-sm-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-sm-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-sm-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-sm-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-sm-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-sm-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-sm-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-sm-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-sm-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-sm-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-sm-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-sm-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-sm-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 768px) {\n .flex-md-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-md-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-md-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-md-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-md-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-md-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-md-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-md-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-md-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-md-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-md-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-md-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-md-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-md-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-md-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-md-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-md-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-md-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-md-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-md-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-md-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 992px) {\n .flex-lg-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-lg-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-lg-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-lg-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-lg-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-lg-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-lg-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-lg-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-lg-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-lg-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-lg-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-lg-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-lg-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-lg-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 1200px) {\n .flex-xl-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-xl-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-xl-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-xl-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-xl-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-xl-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-xl-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-xl-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-xl-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-xl-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-xl-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-xl-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-xl-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-xl-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n margin-left: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n margin-left: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n margin-left: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.mt-3,\n.my-3 {\n margin-top: 1rem !important;\n}\n\n.mr-3,\n.mx-3 {\n margin-right: 1rem !important;\n}\n\n.mb-3,\n.my-3 {\n margin-bottom: 1rem !important;\n}\n\n.ml-3,\n.mx-3 {\n margin-left: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.mt-4,\n.my-4 {\n margin-top: 1.5rem !important;\n}\n\n.mr-4,\n.mx-4 {\n margin-right: 1.5rem !important;\n}\n\n.mb-4,\n.my-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.ml-4,\n.mx-4 {\n margin-left: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.mt-5,\n.my-5 {\n margin-top: 3rem !important;\n}\n\n.mr-5,\n.mx-5 {\n margin-right: 3rem !important;\n}\n\n.mb-5,\n.my-5 {\n margin-bottom: 3rem !important;\n}\n\n.ml-5,\n.mx-5 {\n margin-left: 3rem !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n padding-left: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n padding-left: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n padding-left: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.pt-3,\n.py-3 {\n padding-top: 1rem !important;\n}\n\n.pr-3,\n.px-3 {\n padding-right: 1rem !important;\n}\n\n.pb-3,\n.py-3 {\n padding-bottom: 1rem !important;\n}\n\n.pl-3,\n.px-3 {\n padding-left: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.pt-4,\n.py-4 {\n padding-top: 1.5rem !important;\n}\n\n.pr-4,\n.px-4 {\n padding-right: 1.5rem !important;\n}\n\n.pb-4,\n.py-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pl-4,\n.px-4 {\n padding-left: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.pt-5,\n.py-5 {\n padding-top: 3rem !important;\n}\n\n.pr-5,\n.px-5 {\n padding-right: 3rem !important;\n}\n\n.pb-5,\n.py-5 {\n padding-bottom: 3rem !important;\n}\n\n.pl-5,\n.px-5 {\n padding-left: 3rem !important;\n}\n\n.m-n1 {\n margin: -0.25rem !important;\n}\n\n.mt-n1,\n.my-n1 {\n margin-top: -0.25rem !important;\n}\n\n.mr-n1,\n.mx-n1 {\n margin-right: -0.25rem !important;\n}\n\n.mb-n1,\n.my-n1 {\n margin-bottom: -0.25rem !important;\n}\n\n.ml-n1,\n.mx-n1 {\n margin-left: -0.25rem !important;\n}\n\n.m-n2 {\n margin: -0.5rem !important;\n}\n\n.mt-n2,\n.my-n2 {\n margin-top: -0.5rem !important;\n}\n\n.mr-n2,\n.mx-n2 {\n margin-right: -0.5rem !important;\n}\n\n.mb-n2,\n.my-n2 {\n margin-bottom: -0.5rem !important;\n}\n\n.ml-n2,\n.mx-n2 {\n margin-left: -0.5rem !important;\n}\n\n.m-n3 {\n margin: -1rem !important;\n}\n\n.mt-n3,\n.my-n3 {\n margin-top: -1rem !important;\n}\n\n.mr-n3,\n.mx-n3 {\n margin-right: -1rem !important;\n}\n\n.mb-n3,\n.my-n3 {\n margin-bottom: -1rem !important;\n}\n\n.ml-n3,\n.mx-n3 {\n margin-left: -1rem !important;\n}\n\n.m-n4 {\n margin: -1.5rem !important;\n}\n\n.mt-n4,\n.my-n4 {\n margin-top: -1.5rem !important;\n}\n\n.mr-n4,\n.mx-n4 {\n margin-right: -1.5rem !important;\n}\n\n.mb-n4,\n.my-n4 {\n margin-bottom: -1.5rem !important;\n}\n\n.ml-n4,\n.mx-n4 {\n margin-left: -1.5rem !important;\n}\n\n.m-n5 {\n margin: -3rem !important;\n}\n\n.mt-n5,\n.my-n5 {\n margin-top: -3rem !important;\n}\n\n.mr-n5,\n.mx-n5 {\n margin-right: -3rem !important;\n}\n\n.mb-n5,\n.my-n5 {\n margin-bottom: -3rem !important;\n}\n\n.ml-n5,\n.mx-n5 {\n margin-left: -3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n .m-sm-0 {\n margin: 0 !important;\n }\n .mt-sm-0,\n .my-sm-0 {\n margin-top: 0 !important;\n }\n .mr-sm-0,\n .mx-sm-0 {\n margin-right: 0 !important;\n }\n .mb-sm-0,\n .my-sm-0 {\n margin-bottom: 0 !important;\n }\n .ml-sm-0,\n .mx-sm-0 {\n margin-left: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .mt-sm-1,\n .my-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mr-sm-1,\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n }\n .mb-sm-1,\n .my-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-sm-1,\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .mt-sm-2,\n .my-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mr-sm-2,\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n }\n .mb-sm-2,\n .my-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-sm-2,\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .mt-sm-3,\n .my-sm-3 {\n margin-top: 1rem !important;\n }\n .mr-sm-3,\n .mx-sm-3 {\n margin-right: 1rem !important;\n }\n .mb-sm-3,\n .my-sm-3 {\n margin-bottom: 1rem !important;\n }\n .ml-sm-3,\n .mx-sm-3 {\n margin-left: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .mt-sm-4,\n .my-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mr-sm-4,\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n }\n .mb-sm-4,\n .my-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-sm-4,\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .mt-sm-5,\n .my-sm-5 {\n margin-top: 3rem !important;\n }\n .mr-sm-5,\n .mx-sm-5 {\n margin-right: 3rem !important;\n }\n .mb-sm-5,\n .my-sm-5 {\n margin-bottom: 3rem !important;\n }\n .ml-sm-5,\n .mx-sm-5 {\n margin-left: 3rem !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .pt-sm-0,\n .py-sm-0 {\n padding-top: 0 !important;\n }\n .pr-sm-0,\n .px-sm-0 {\n padding-right: 0 !important;\n }\n .pb-sm-0,\n .py-sm-0 {\n padding-bottom: 0 !important;\n }\n .pl-sm-0,\n .px-sm-0 {\n padding-left: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .pt-sm-1,\n .py-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pr-sm-1,\n .px-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pb-sm-1,\n .py-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-sm-1,\n .px-sm-1 {\n padding-left: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .pt-sm-2,\n .py-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pr-sm-2,\n .px-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pb-sm-2,\n .py-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-sm-2,\n .px-sm-2 {\n padding-left: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .pt-sm-3,\n .py-sm-3 {\n padding-top: 1rem !important;\n }\n .pr-sm-3,\n .px-sm-3 {\n padding-right: 1rem !important;\n }\n .pb-sm-3,\n .py-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pl-sm-3,\n .px-sm-3 {\n padding-left: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .pt-sm-4,\n .py-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pr-sm-4,\n .px-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pb-sm-4,\n .py-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-sm-4,\n .px-sm-4 {\n padding-left: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .pt-sm-5,\n .py-sm-5 {\n padding-top: 3rem !important;\n }\n .pr-sm-5,\n .px-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-5,\n .py-sm-5 {\n padding-bottom: 3rem !important;\n }\n .pl-sm-5,\n .px-sm-5 {\n padding-left: 3rem !important;\n }\n .m-sm-n1 {\n margin: -0.25rem !important;\n }\n .mt-sm-n1,\n .my-sm-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-sm-n1,\n .mx-sm-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-sm-n1,\n .my-sm-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-sm-n1,\n .mx-sm-n1 {\n margin-left: -0.25rem !important;\n }\n .m-sm-n2 {\n margin: -0.5rem !important;\n }\n .mt-sm-n2,\n .my-sm-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-sm-n2,\n .mx-sm-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-sm-n2,\n .my-sm-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-sm-n2,\n .mx-sm-n2 {\n margin-left: -0.5rem !important;\n }\n .m-sm-n3 {\n margin: -1rem !important;\n }\n .mt-sm-n3,\n .my-sm-n3 {\n margin-top: -1rem !important;\n }\n .mr-sm-n3,\n .mx-sm-n3 {\n margin-right: -1rem !important;\n }\n .mb-sm-n3,\n .my-sm-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-sm-n3,\n .mx-sm-n3 {\n margin-left: -1rem !important;\n }\n .m-sm-n4 {\n margin: -1.5rem !important;\n }\n .mt-sm-n4,\n .my-sm-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-sm-n4,\n .mx-sm-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-sm-n4,\n .my-sm-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-sm-n4,\n .mx-sm-n4 {\n margin-left: -1.5rem !important;\n }\n .m-sm-n5 {\n margin: -3rem !important;\n }\n .mt-sm-n5,\n .my-sm-n5 {\n margin-top: -3rem !important;\n }\n .mr-sm-n5,\n .mx-sm-n5 {\n margin-right: -3rem !important;\n }\n .mb-sm-n5,\n .my-sm-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-sm-n5,\n .mx-sm-n5 {\n margin-left: -3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mt-sm-auto,\n .my-sm-auto {\n margin-top: auto !important;\n }\n .mr-sm-auto,\n .mx-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-auto,\n .my-sm-auto {\n margin-bottom: auto !important;\n }\n .ml-sm-auto,\n .mx-sm-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 768px) {\n .m-md-0 {\n margin: 0 !important;\n }\n .mt-md-0,\n .my-md-0 {\n margin-top: 0 !important;\n }\n .mr-md-0,\n .mx-md-0 {\n margin-right: 0 !important;\n }\n .mb-md-0,\n .my-md-0 {\n margin-bottom: 0 !important;\n }\n .ml-md-0,\n .mx-md-0 {\n margin-left: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .mt-md-1,\n .my-md-1 {\n margin-top: 0.25rem !important;\n }\n .mr-md-1,\n .mx-md-1 {\n margin-right: 0.25rem !important;\n }\n .mb-md-1,\n .my-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-md-1,\n .mx-md-1 {\n margin-left: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .mt-md-2,\n .my-md-2 {\n margin-top: 0.5rem !important;\n }\n .mr-md-2,\n .mx-md-2 {\n margin-right: 0.5rem !important;\n }\n .mb-md-2,\n .my-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-md-2,\n .mx-md-2 {\n margin-left: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .mt-md-3,\n .my-md-3 {\n margin-top: 1rem !important;\n }\n .mr-md-3,\n .mx-md-3 {\n margin-right: 1rem !important;\n }\n .mb-md-3,\n .my-md-3 {\n margin-bottom: 1rem !important;\n }\n .ml-md-3,\n .mx-md-3 {\n margin-left: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .mt-md-4,\n .my-md-4 {\n margin-top: 1.5rem !important;\n }\n .mr-md-4,\n .mx-md-4 {\n margin-right: 1.5rem !important;\n }\n .mb-md-4,\n .my-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-md-4,\n .mx-md-4 {\n margin-left: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .mt-md-5,\n .my-md-5 {\n margin-top: 3rem !important;\n }\n .mr-md-5,\n .mx-md-5 {\n margin-right: 3rem !important;\n }\n .mb-md-5,\n .my-md-5 {\n margin-bottom: 3rem !important;\n }\n .ml-md-5,\n .mx-md-5 {\n margin-left: 3rem !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .pt-md-0,\n .py-md-0 {\n padding-top: 0 !important;\n }\n .pr-md-0,\n .px-md-0 {\n padding-right: 0 !important;\n }\n .pb-md-0,\n .py-md-0 {\n padding-bottom: 0 !important;\n }\n .pl-md-0,\n .px-md-0 {\n padding-left: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .pt-md-1,\n .py-md-1 {\n padding-top: 0.25rem !important;\n }\n .pr-md-1,\n .px-md-1 {\n padding-right: 0.25rem !important;\n }\n .pb-md-1,\n .py-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-md-1,\n .px-md-1 {\n padding-left: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .pt-md-2,\n .py-md-2 {\n padding-top: 0.5rem !important;\n }\n .pr-md-2,\n .px-md-2 {\n padding-right: 0.5rem !important;\n }\n .pb-md-2,\n .py-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-md-2,\n .px-md-2 {\n padding-left: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .pt-md-3,\n .py-md-3 {\n padding-top: 1rem !important;\n }\n .pr-md-3,\n .px-md-3 {\n padding-right: 1rem !important;\n }\n .pb-md-3,\n .py-md-3 {\n padding-bottom: 1rem !important;\n }\n .pl-md-3,\n .px-md-3 {\n padding-left: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .pt-md-4,\n .py-md-4 {\n padding-top: 1.5rem !important;\n }\n .pr-md-4,\n .px-md-4 {\n padding-right: 1.5rem !important;\n }\n .pb-md-4,\n .py-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-md-4,\n .px-md-4 {\n padding-left: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .pt-md-5,\n .py-md-5 {\n padding-top: 3rem !important;\n }\n .pr-md-5,\n .px-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-5,\n .py-md-5 {\n padding-bottom: 3rem !important;\n }\n .pl-md-5,\n .px-md-5 {\n padding-left: 3rem !important;\n }\n .m-md-n1 {\n margin: -0.25rem !important;\n }\n .mt-md-n1,\n .my-md-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-md-n1,\n .mx-md-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-md-n1,\n .my-md-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-md-n1,\n .mx-md-n1 {\n margin-left: -0.25rem !important;\n }\n .m-md-n2 {\n margin: -0.5rem !important;\n }\n .mt-md-n2,\n .my-md-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-md-n2,\n .mx-md-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-md-n2,\n .my-md-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-md-n2,\n .mx-md-n2 {\n margin-left: -0.5rem !important;\n }\n .m-md-n3 {\n margin: -1rem !important;\n }\n .mt-md-n3,\n .my-md-n3 {\n margin-top: -1rem !important;\n }\n .mr-md-n3,\n .mx-md-n3 {\n margin-right: -1rem !important;\n }\n .mb-md-n3,\n .my-md-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-md-n3,\n .mx-md-n3 {\n margin-left: -1rem !important;\n }\n .m-md-n4 {\n margin: -1.5rem !important;\n }\n .mt-md-n4,\n .my-md-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-md-n4,\n .mx-md-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-md-n4,\n .my-md-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-md-n4,\n .mx-md-n4 {\n margin-left: -1.5rem !important;\n }\n .m-md-n5 {\n margin: -3rem !important;\n }\n .mt-md-n5,\n .my-md-n5 {\n margin-top: -3rem !important;\n }\n .mr-md-n5,\n .mx-md-n5 {\n margin-right: -3rem !important;\n }\n .mb-md-n5,\n .my-md-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-md-n5,\n .mx-md-n5 {\n margin-left: -3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mt-md-auto,\n .my-md-auto {\n margin-top: auto !important;\n }\n .mr-md-auto,\n .mx-md-auto {\n margin-right: auto !important;\n }\n .mb-md-auto,\n .my-md-auto {\n margin-bottom: auto !important;\n }\n .ml-md-auto,\n .mx-md-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 992px) {\n .m-lg-0 {\n margin: 0 !important;\n }\n .mt-lg-0,\n .my-lg-0 {\n margin-top: 0 !important;\n }\n .mr-lg-0,\n .mx-lg-0 {\n margin-right: 0 !important;\n }\n .mb-lg-0,\n .my-lg-0 {\n margin-bottom: 0 !important;\n }\n .ml-lg-0,\n .mx-lg-0 {\n margin-left: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .mt-lg-1,\n .my-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mr-lg-1,\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n }\n .mb-lg-1,\n .my-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-lg-1,\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .mt-lg-2,\n .my-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mr-lg-2,\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n }\n .mb-lg-2,\n .my-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-lg-2,\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .mt-lg-3,\n .my-lg-3 {\n margin-top: 1rem !important;\n }\n .mr-lg-3,\n .mx-lg-3 {\n margin-right: 1rem !important;\n }\n .mb-lg-3,\n .my-lg-3 {\n margin-bottom: 1rem !important;\n }\n .ml-lg-3,\n .mx-lg-3 {\n margin-left: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .mt-lg-4,\n .my-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mr-lg-4,\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n }\n .mb-lg-4,\n .my-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-lg-4,\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .mt-lg-5,\n .my-lg-5 {\n margin-top: 3rem !important;\n }\n .mr-lg-5,\n .mx-lg-5 {\n margin-right: 3rem !important;\n }\n .mb-lg-5,\n .my-lg-5 {\n margin-bottom: 3rem !important;\n }\n .ml-lg-5,\n .mx-lg-5 {\n margin-left: 3rem !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .pt-lg-0,\n .py-lg-0 {\n padding-top: 0 !important;\n }\n .pr-lg-0,\n .px-lg-0 {\n padding-right: 0 !important;\n }\n .pb-lg-0,\n .py-lg-0 {\n padding-bottom: 0 !important;\n }\n .pl-lg-0,\n .px-lg-0 {\n padding-left: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .pt-lg-1,\n .py-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pr-lg-1,\n .px-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pb-lg-1,\n .py-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-lg-1,\n .px-lg-1 {\n padding-left: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .pt-lg-2,\n .py-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pr-lg-2,\n .px-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pb-lg-2,\n .py-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-lg-2,\n .px-lg-2 {\n padding-left: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .pt-lg-3,\n .py-lg-3 {\n padding-top: 1rem !important;\n }\n .pr-lg-3,\n .px-lg-3 {\n padding-right: 1rem !important;\n }\n .pb-lg-3,\n .py-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pl-lg-3,\n .px-lg-3 {\n padding-left: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .pt-lg-4,\n .py-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pr-lg-4,\n .px-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pb-lg-4,\n .py-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-lg-4,\n .px-lg-4 {\n padding-left: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .pt-lg-5,\n .py-lg-5 {\n padding-top: 3rem !important;\n }\n .pr-lg-5,\n .px-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-5,\n .py-lg-5 {\n padding-bottom: 3rem !important;\n }\n .pl-lg-5,\n .px-lg-5 {\n padding-left: 3rem !important;\n }\n .m-lg-n1 {\n margin: -0.25rem !important;\n }\n .mt-lg-n1,\n .my-lg-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-lg-n1,\n .mx-lg-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-lg-n1,\n .my-lg-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-lg-n1,\n .mx-lg-n1 {\n margin-left: -0.25rem !important;\n }\n .m-lg-n2 {\n margin: -0.5rem !important;\n }\n .mt-lg-n2,\n .my-lg-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-lg-n2,\n .mx-lg-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-lg-n2,\n .my-lg-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-lg-n2,\n .mx-lg-n2 {\n margin-left: -0.5rem !important;\n }\n .m-lg-n3 {\n margin: -1rem !important;\n }\n .mt-lg-n3,\n .my-lg-n3 {\n margin-top: -1rem !important;\n }\n .mr-lg-n3,\n .mx-lg-n3 {\n margin-right: -1rem !important;\n }\n .mb-lg-n3,\n .my-lg-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-lg-n3,\n .mx-lg-n3 {\n margin-left: -1rem !important;\n }\n .m-lg-n4 {\n margin: -1.5rem !important;\n }\n .mt-lg-n4,\n .my-lg-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-lg-n4,\n .mx-lg-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-lg-n4,\n .my-lg-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-lg-n4,\n .mx-lg-n4 {\n margin-left: -1.5rem !important;\n }\n .m-lg-n5 {\n margin: -3rem !important;\n }\n .mt-lg-n5,\n .my-lg-n5 {\n margin-top: -3rem !important;\n }\n .mr-lg-n5,\n .mx-lg-n5 {\n margin-right: -3rem !important;\n }\n .mb-lg-n5,\n .my-lg-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-lg-n5,\n .mx-lg-n5 {\n margin-left: -3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mt-lg-auto,\n .my-lg-auto {\n margin-top: auto !important;\n }\n .mr-lg-auto,\n .mx-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-auto,\n .my-lg-auto {\n margin-bottom: auto !important;\n }\n .ml-lg-auto,\n .mx-lg-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 1200px) {\n .m-xl-0 {\n margin: 0 !important;\n }\n .mt-xl-0,\n .my-xl-0 {\n margin-top: 0 !important;\n }\n .mr-xl-0,\n .mx-xl-0 {\n margin-right: 0 !important;\n }\n .mb-xl-0,\n .my-xl-0 {\n margin-bottom: 0 !important;\n }\n .ml-xl-0,\n .mx-xl-0 {\n margin-left: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .mt-xl-1,\n .my-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mr-xl-1,\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n }\n .mb-xl-1,\n .my-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-xl-1,\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .mt-xl-2,\n .my-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mr-xl-2,\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n }\n .mb-xl-2,\n .my-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-xl-2,\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .mt-xl-3,\n .my-xl-3 {\n margin-top: 1rem !important;\n }\n .mr-xl-3,\n .mx-xl-3 {\n margin-right: 1rem !important;\n }\n .mb-xl-3,\n .my-xl-3 {\n margin-bottom: 1rem !important;\n }\n .ml-xl-3,\n .mx-xl-3 {\n margin-left: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .mt-xl-4,\n .my-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mr-xl-4,\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n }\n .mb-xl-4,\n .my-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-xl-4,\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .mt-xl-5,\n .my-xl-5 {\n margin-top: 3rem !important;\n }\n .mr-xl-5,\n .mx-xl-5 {\n margin-right: 3rem !important;\n }\n .mb-xl-5,\n .my-xl-5 {\n margin-bottom: 3rem !important;\n }\n .ml-xl-5,\n .mx-xl-5 {\n margin-left: 3rem !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .pt-xl-0,\n .py-xl-0 {\n padding-top: 0 !important;\n }\n .pr-xl-0,\n .px-xl-0 {\n padding-right: 0 !important;\n }\n .pb-xl-0,\n .py-xl-0 {\n padding-bottom: 0 !important;\n }\n .pl-xl-0,\n .px-xl-0 {\n padding-left: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .pt-xl-1,\n .py-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pr-xl-1,\n .px-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pb-xl-1,\n .py-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-xl-1,\n .px-xl-1 {\n padding-left: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .pt-xl-2,\n .py-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pr-xl-2,\n .px-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pb-xl-2,\n .py-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-xl-2,\n .px-xl-2 {\n padding-left: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .pt-xl-3,\n .py-xl-3 {\n padding-top: 1rem !important;\n }\n .pr-xl-3,\n .px-xl-3 {\n padding-right: 1rem !important;\n }\n .pb-xl-3,\n .py-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pl-xl-3,\n .px-xl-3 {\n padding-left: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .pt-xl-4,\n .py-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pr-xl-4,\n .px-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pb-xl-4,\n .py-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-xl-4,\n .px-xl-4 {\n padding-left: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .pt-xl-5,\n .py-xl-5 {\n padding-top: 3rem !important;\n }\n .pr-xl-5,\n .px-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-5,\n .py-xl-5 {\n padding-bottom: 3rem !important;\n }\n .pl-xl-5,\n .px-xl-5 {\n padding-left: 3rem !important;\n }\n .m-xl-n1 {\n margin: -0.25rem !important;\n }\n .mt-xl-n1,\n .my-xl-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-xl-n1,\n .mx-xl-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-xl-n1,\n .my-xl-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-xl-n1,\n .mx-xl-n1 {\n margin-left: -0.25rem !important;\n }\n .m-xl-n2 {\n margin: -0.5rem !important;\n }\n .mt-xl-n2,\n .my-xl-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-xl-n2,\n .mx-xl-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-xl-n2,\n .my-xl-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-xl-n2,\n .mx-xl-n2 {\n margin-left: -0.5rem !important;\n }\n .m-xl-n3 {\n margin: -1rem !important;\n }\n .mt-xl-n3,\n .my-xl-n3 {\n margin-top: -1rem !important;\n }\n .mr-xl-n3,\n .mx-xl-n3 {\n margin-right: -1rem !important;\n }\n .mb-xl-n3,\n .my-xl-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-xl-n3,\n .mx-xl-n3 {\n margin-left: -1rem !important;\n }\n .m-xl-n4 {\n margin: -1.5rem !important;\n }\n .mt-xl-n4,\n .my-xl-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-xl-n4,\n .mx-xl-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-xl-n4,\n .my-xl-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-xl-n4,\n .mx-xl-n4 {\n margin-left: -1.5rem !important;\n }\n .m-xl-n5 {\n margin: -3rem !important;\n }\n .mt-xl-n5,\n .my-xl-n5 {\n margin-top: -3rem !important;\n }\n .mr-xl-n5,\n .mx-xl-n5 {\n margin-right: -3rem !important;\n }\n .mb-xl-n5,\n .my-xl-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-xl-n5,\n .mx-xl-n5 {\n margin-left: -3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mt-xl-auto,\n .my-xl-auto {\n margin-top: auto !important;\n }\n .mr-xl-auto,\n .mx-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-auto,\n .my-xl-auto {\n margin-bottom: auto !important;\n }\n .ml-xl-auto,\n .mx-xl-auto {\n margin-left: auto !important;\n }\n}\n/*# sourceMappingURL=bootstrap-grid.css.map */","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n // Single container class with breakpoint max-widths\n .container {\n @include make-container();\n @include make-container-max-widths();\n }\n\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n @each $name, $width in $grid-breakpoints {\n @if ($container-max-width > $width or $breakpoint == $name) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n }\n }\n }\n }\n}\n\n\n// Row\n//\n// Rows contain your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n }\n\n // Remove the negative margin from default .row, then the horizontal padding\n // from all immediate children columns (to prevent runaway style inheritance).\n .no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n > .col,\n > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n }\n}\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","/// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-container($gutter: $grid-gutter-width) {\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n margin-right: auto;\n margin-left: auto;\n}\n\n\n// For each breakpoint, define the maximum width of the container in a media query\n@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {\n @each $breakpoint, $container-max-width in $max-widths {\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n max-width: $container-max-width;\n }\n }\n}\n\n@mixin make-row($gutter: $grid-gutter-width) {\n display: flex;\n flex-wrap: wrap;\n margin-right: -$gutter / 2;\n margin-left: -$gutter / 2;\n}\n\n@mixin make-col-ready($gutter: $grid-gutter-width) {\n position: relative;\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we use `flex` values\n // later on to override this initial width.\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n}\n\n@mixin make-col($size, $columns: $grid-columns) {\n flex: 0 0 percentage($size / $columns);\n // Add a `max-width` to ensure content within each column does not blow out\n // the width of the column. Applies to IE10+ and Firefox. Chrome and Safari\n // do not appear to require this.\n max-width: percentage($size / $columns);\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%; // Reset earlier grid tiers\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: $size / $columns;\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// numberof columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n & > * {\n flex: 0 0 100% / $count;\n max-width: 100% / $count;\n }\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @return if($n != null and $n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width. Null for the largest (last) breakpoint.\n// The maximum value is calculated as the minimum of the next one less 0.02px\n// to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $next: breakpoint-next($name, $breakpoints);\n @return if($next, breakpoint-min($next, $breakpoints) - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $max: breakpoint-max($name, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($name, $breakpoints) {\n @content;\n }\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n // Common properties for all breakpoints\n %grid-column {\n position: relative;\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n }\n\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n // Allow columns to stretch full width below their breakpoints\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @extend %grid-column;\n }\n }\n .col#{$infix},\n .col#{$infix}-auto {\n @extend %grid-column;\n }\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n\n .order#{$infix}-first { order: -1; }\n\n .order#{$infix}-last { order: $columns + 1; }\n\n @for $i from 0 through $columns {\n .order#{$infix}-#{$i} { order: $i; }\n }\n\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n }\n}\n","// stylelint-disable declaration-no-important\n\n//\n// Utilities for common `display` values\n//\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $value in $displays {\n .d#{$infix}-#{$value} { display: $value !important; }\n }\n }\n}\n\n\n//\n// Utilities for toggling `display` in print\n//\n\n@media print {\n @each $value in $displays {\n .d-print-#{$value} { display: $value !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n// Flex variation\n//\n// Custom styles for additional flex alignment options.\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .flex#{$infix}-row { flex-direction: row !important; }\n .flex#{$infix}-column { flex-direction: column !important; }\n .flex#{$infix}-row-reverse { flex-direction: row-reverse !important; }\n .flex#{$infix}-column-reverse { flex-direction: column-reverse !important; }\n\n .flex#{$infix}-wrap { flex-wrap: wrap !important; }\n .flex#{$infix}-nowrap { flex-wrap: nowrap !important; }\n .flex#{$infix}-wrap-reverse { flex-wrap: wrap-reverse !important; }\n .flex#{$infix}-fill { flex: 1 1 auto !important; }\n .flex#{$infix}-grow-0 { flex-grow: 0 !important; }\n .flex#{$infix}-grow-1 { flex-grow: 1 !important; }\n .flex#{$infix}-shrink-0 { flex-shrink: 0 !important; }\n .flex#{$infix}-shrink-1 { flex-shrink: 1 !important; }\n\n .justify-content#{$infix}-start { justify-content: flex-start !important; }\n .justify-content#{$infix}-end { justify-content: flex-end !important; }\n .justify-content#{$infix}-center { justify-content: center !important; }\n .justify-content#{$infix}-between { justify-content: space-between !important; }\n .justify-content#{$infix}-around { justify-content: space-around !important; }\n\n .align-items#{$infix}-start { align-items: flex-start !important; }\n .align-items#{$infix}-end { align-items: flex-end !important; }\n .align-items#{$infix}-center { align-items: center !important; }\n .align-items#{$infix}-baseline { align-items: baseline !important; }\n .align-items#{$infix}-stretch { align-items: stretch !important; }\n\n .align-content#{$infix}-start { align-content: flex-start !important; }\n .align-content#{$infix}-end { align-content: flex-end !important; }\n .align-content#{$infix}-center { align-content: center !important; }\n .align-content#{$infix}-between { align-content: space-between !important; }\n .align-content#{$infix}-around { align-content: space-around !important; }\n .align-content#{$infix}-stretch { align-content: stretch !important; }\n\n .align-self#{$infix}-auto { align-self: auto !important; }\n .align-self#{$infix}-start { align-self: flex-start !important; }\n .align-self#{$infix}-end { align-self: flex-end !important; }\n .align-self#{$infix}-center { align-self: center !important; }\n .align-self#{$infix}-baseline { align-self: baseline !important; }\n .align-self#{$infix}-stretch { align-self: stretch !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n// Margin and Padding\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $prop, $abbrev in (margin: m, padding: p) {\n @each $size, $length in $spacers {\n .#{$abbrev}#{$infix}-#{$size} { #{$prop}: $length !important; }\n .#{$abbrev}t#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-top: $length !important;\n }\n .#{$abbrev}r#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-right: $length !important;\n }\n .#{$abbrev}b#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-bottom: $length !important;\n }\n .#{$abbrev}l#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-left: $length !important;\n }\n }\n }\n\n // Negative margins (e.g., where `.mb-n1` is negative version of `.mb-1`)\n @each $size, $length in $spacers {\n @if $size != 0 {\n .m#{$infix}-n#{$size} { margin: -$length !important; }\n .mt#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-top: -$length !important;\n }\n .mr#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-right: -$length !important;\n }\n .mb#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-bottom: -$length !important;\n }\n .ml#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-left: -$length !important;\n }\n }\n }\n\n // Some special margin utils\n .m#{$infix}-auto { margin: auto !important; }\n .mt#{$infix}-auto,\n .my#{$infix}-auto {\n margin-top: auto !important;\n }\n .mr#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-right: auto !important;\n }\n .mb#{$infix}-auto,\n .my#{$infix}-auto {\n margin-bottom: auto !important;\n }\n .ml#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-left: auto !important;\n }\n }\n}\n"]} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css new file mode 100644 index 00000000..91b0fc4c --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css @@ -0,0 +1,327 @@ +/*! + * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) + */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + line-height: 1.15; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { + display: block; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #212529; + text-align: left; + background-color: #fff; +} + +[tabindex="-1"]:focus:not(:focus-visible) { + outline: 0 !important; +} + +hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} + +h1, h2, h3, h4, h5, h6 { + margin-top: 0; + margin-bottom: 0.5rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title], +abbr[data-original-title] { + text-decoration: underline; + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + cursor: help; + border-bottom: 0; + -webkit-text-decoration-skip-ink: none; + text-decoration-skip-ink: none; +} + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; +} + +dt { + font-weight: 700; +} + +dd { + margin-bottom: .5rem; + margin-left: 0; +} + +blockquote { + margin: 0 0 1rem; +} + +b, +strong { + font-weight: bolder; +} + +small { + font-size: 80%; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sub { + bottom: -.25em; +} + +sup { + top: -.5em; +} + +a { + color: #007bff; + text-decoration: none; + background-color: transparent; +} + +a:hover { + color: #0056b3; + text-decoration: underline; +} + +a:not([href]) { + color: inherit; + text-decoration: none; +} + +a:not([href]):hover { + color: inherit; + text-decoration: none; +} + +pre, +code, +kbd, +samp { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 1em; +} + +pre { + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; +} + +figure { + margin: 0 0 1rem; +} + +img { + vertical-align: middle; + border-style: none; +} + +svg { + overflow: hidden; + vertical-align: middle; +} + +table { + border-collapse: collapse; +} + +caption { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + color: #6c757d; + text-align: left; + caption-side: bottom; +} + +th { + text-align: inherit; +} + +label { + display: inline-block; + margin-bottom: 0.5rem; +} + +button { + border-radius: 0; +} + +button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} + +input, +button, +select, +optgroup, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +input { + overflow: visible; +} + +button, +select { + text-transform: none; +} + +select { + word-wrap: normal; +} + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +button:not(:disabled), +[type="button"]:not(:disabled), +[type="reset"]:not(:disabled), +[type="submit"]:not(:disabled) { + cursor: pointer; +} + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + padding: 0; + border-style: none; +} + +input[type="radio"], +input[type="checkbox"] { + box-sizing: border-box; + padding: 0; +} + +input[type="date"], +input[type="time"], +input[type="datetime-local"], +input[type="month"] { + -webkit-appearance: listbox; +} + +textarea { + overflow: auto; + resize: vertical; +} + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + max-width: 100%; + padding: 0; + margin-bottom: .5rem; + font-size: 1.5rem; + line-height: inherit; + color: inherit; + white-space: normal; +} + +progress { + vertical-align: baseline; +} + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +[type="search"] { + outline-offset: -2px; + -webkit-appearance: none; +} + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-file-upload-button { + font: inherit; + -webkit-appearance: button; +} + +output { + display: inline-block; +} + +summary { + display: list-item; + cursor: pointer; +} + +template { + display: none; +} + +[hidden] { + display: none !important; +} +/*# sourceMappingURL=bootstrap-reboot.css.map */ \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map new file mode 100644 index 00000000..701f6715 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap-reboot.scss","bootstrap-reboot.css","../../scss/_reboot.scss","../../scss/_variables.scss","../../scss/vendor/_rfs.scss","../../scss/mixins/_hover.scss"],"names":[],"mappings":"AAAA;;;;;;ECME;ACYF;;;EAGE,sBAAsB;ADVxB;;ACaA;EACE,uBAAuB;EACvB,iBAAiB;EACjB,8BAA8B;EAC9B,6CCXa;AFCf;;ACgBA;EACE,cAAc;ADbhB;;ACuBA;EACE,SAAS;EACT,kMCyOiN;ECzJ7M,eAtCY;EFxChB,gBCkP+B;EDjP/B,gBCsP+B;EDrP/B,cCnCgB;EDoChB,gBAAgB;EAChB,sBC9Ca;AF0Bf;;AAEA;EC+BE,qBAAqB;AD7BvB;;ACsCA;EACE,uBAAuB;EACvB,SAAS;EACT,iBAAiB;ADnCnB;;ACgDA;EACE,aAAa;EACb,qBCoNuC;AFjQzC;;ACoDA;EACE,aAAa;EACb,mBCuF8B;AFxIhC;;AC4DA;;EAEE,0BAA0B;EAC1B,yCAAiC;EAAjC,iCAAiC;EACjC,YAAY;EACZ,gBAAgB;EAChB,sCAA8B;EAA9B,8BAA8B;ADzDhC;;AC4DA;EACE,mBAAmB;EACnB,kBAAkB;EAClB,oBAAoB;ADzDtB;;AC4DA;;;EAGE,aAAa;EACb,mBAAmB;ADzDrB;;AC4DA;;;;EAIE,gBAAgB;ADzDlB;;AC4DA;EACE,gBCqJ+B;AF9MjC;;AC4DA;EACE,oBAAoB;EACpB,cAAc;ADzDhB;;AC4DA;EACE,gBAAgB;ADzDlB;;AC4DA;;EAEE,mBCwIkC;AFjMpC;;AC4DA;EExFI,cAAW;AHgCf;;ACiEA;;EAEE,kBAAkB;EEnGhB,cAAW;EFqGb,cAAc;EACd,wBAAwB;AD9D1B;;ACiEA;EAAM,cAAc;AD7DpB;;AC8DA;EAAM,UAAU;AD1DhB;;ACiEA;EACE,cCtJe;EDuJf,qBCR4C;EDS5C,6BAA6B;AD9D/B;;AIlHE;EHmLE,cCX8D;EDY9D,0BCX+C;AFlDnD;;ACsEA;EACE,cAAc;EACd,qBAAqB;ADnEvB;;AI5HE;EHkME,cAAc;EACd,qBAAqB;ADlEzB;;AC2EA;;;;EAIE,iGC6DgH;ECjN9G,cAAW;AH6Ef;;AC2EA;EAEE,aAAa;EAEb,mBAAmB;EAEnB,cAAc;AD3EhB;;ACmFA;EAEE,gBAAgB;ADjFlB;;ACyFA;EACE,sBAAsB;EACtB,kBAAkB;ADtFpB;;ACyFA;EAGE,gBAAgB;EAChB,sBAAsB;ADxFxB;;ACgGA;EACE,yBAAyB;AD7F3B;;ACgGA;EACE,oBCoFkC;EDnFlC,uBCmFkC;EDlFlC,cCnQgB;EDoQhB,gBAAgB;EAChB,oBAAoB;AD7FtB;;ACgGA;EAGE,mBAAmB;AD/FrB;;ACuGA;EAEE,qBAAqB;EACrB,qBCqK2C;AF1Q7C;;AC2GA;EAEE,gBAAgB;ADzGlB;;ACgHA;EACE,mBAAmB;EACnB,0CAA0C;AD7G5C;;ACgHA;;;;;EAKE,SAAS;EACT,oBAAoB;EErPlB,kBAAW;EFuPb,oBAAoB;AD7GtB;;ACgHA;;EAEE,iBAAiB;AD7GnB;;ACgHA;;EAEE,oBAAoB;AD7GtB;;ACmHA;EACE,iBAAiB;ADhHnB;;ACuHA;;;;EAIE,0BAA0B;ADpH5B;;ACyHE;;;;EAKI,eAAe;ADvHrB;;AC6HA;;;;EAIE,UAAU;EACV,kBAAkB;AD1HpB;;AC6HA;;EAEE,sBAAsB;EACtB,UAAU;AD1HZ;;AC8HA;;;;EASE,2BAA2B;ADhI7B;;ACmIA;EACE,cAAc;EAEd,gBAAgB;ADjIlB;;ACoIA;EAME,YAAY;EAEZ,UAAU;EACV,SAAS;EACT,SAAS;ADvIX;;AC4IA;EACE,cAAc;EACd,WAAW;EACX,eAAe;EACf,UAAU;EACV,oBAAoB;EEjShB,iBAtCY;EFyUhB,oBAAoB;EACpB,cAAc;EACd,mBAAmB;ADzIrB;;AC4IA;EACE,wBAAwB;ADzI1B;;AAEA;;EC6IE,YAAY;AD1Id;;AAEA;ECgJE,oBAAoB;EACpB,wBAAwB;AD9I1B;;AAEA;ECoJE,wBAAwB;ADlJ1B;;AC0JA;EACE,aAAa;EACb,0BAA0B;ADvJ5B;;AC8JA;EACE,qBAAqB;AD3JvB;;AC8JA;EACE,kBAAkB;EAClB,eAAe;AD3JjB;;AC8JA;EACE,aAAa;AD3Jf;;AAEA;EC+JE,wBAAwB;AD7J1B","file":"bootstrap-reboot.css","sourcesContent":["/*!\n * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"reboot\";\n","/*!\n * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg {\n overflow: hidden;\n vertical-align: middle;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nselect {\n word-wrap: normal;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton:not(:disabled),\n[type=\"button\"]:not(:disabled),\n[type=\"reset\"]:not(:disabled),\n[type=\"submit\"]:not(:disabled) {\n cursor: pointer;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\n/*# sourceMappingURL=bootstrap-reboot.css.map */","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -webkit-tap-highlight-color: rgba($black, 0); // 5\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\n// TODO: remove in v5\n// stylelint-disable-next-line selector-list-comma-newline-after\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use\n// the `inherit` value on things like `` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline\n// on elements that programmatically receive focus but wouldn't normally show a visible\n// focus outline. In general, this would mean that the outline is only applied if the\n// interaction that led to the element receiving programmatic focus was a keyboard interaction,\n// or the browser has somehow determined that the user is primarily a keyboard user and/or\n// wants focus outlines to always be presented.\n//\n// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible\n// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `

    `-`

    ` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

    `s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n\n @include hover() {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `` alignment by inheriting from the ``, or the\n // closest parent with a set `text-align`.\n text-align: inherit;\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n // stylelint-disable-next-line property-blacklist\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// Remove the inheritance of word-wrap in Safari.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24990\nselect {\n word-wrap: normal;\n}\n\n\n// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n// controls in Android 4.\n// 2. Correct the inability to style clickable types in iOS and Safari.\nbutton,\n[type=\"button\"], // 1\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button; // 2\n}\n\n// Opinionated: add \"hand\" cursor to non-disabled button elements.\n@if $enable-pointer-cursor-for-buttons {\n button,\n [type=\"button\"],\n [type=\"reset\"],\n [type=\"submit\"] {\n &:not(:disabled) {\n cursor: pointer;\n }\n }\n}\n\n// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box; // 1. Add the correct box sizing in IE 10-\n padding: 0; // 2. Remove the padding in IE 10-\n}\n\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n // Remove the default appearance of temporal inputs to avoid a Mobile Safari\n // bug where setting a custom line-height prevents text from being vertically\n // centered within the input.\n // See https://bugs.webkit.org/show_bug.cgi?id=139848\n // and https://github.com/twbs/bootstrap/issues/11266\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto; // Remove the default vertical scrollbar in IE.\n // Textareas should really only resize vertically so they don't break their (horizontal) containers.\n resize: vertical;\n}\n\nfieldset {\n // Browsers set a default `min-width: min-content;` on fieldsets,\n // unlike e.g. `

    `s, which have `min-width: 0;` by default.\n // So we reset that to ensure fieldsets behave more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359\n // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements\n min-width: 0;\n // Reset the default outline behavior of fieldsets so they don't affect page layout.\n padding: 0;\n margin: 0;\n border: 0;\n}\n\n// 1. Correct the text wrapping in Edge and IE.\n// 2. Correct the color inheritance from `fieldset` elements in IE.\nlegend {\n display: block;\n width: 100%;\n max-width: 100%; // 1\n padding: 0;\n margin-bottom: .5rem;\n @include font-size(1.5rem);\n line-height: inherit;\n color: inherit; // 2\n white-space: normal; // 1\n}\n\nprogress {\n vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.\n}\n\n// Correct the cursor style of increment and decrement buttons in Chrome.\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n outline-offset: -2px; // 2. Correct the outline style in Safari.\n -webkit-appearance: none;\n}\n\n//\n// Remove the inner padding in Chrome and Safari on macOS.\n//\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// 1. Correct the inability to style clickable types in iOS and Safari.\n// 2. Change font properties to `inherit` in Safari.\n//\n\n::-webkit-file-upload-button {\n font: inherit; // 2\n -webkit-appearance: button; // 1\n}\n\n//\n// Correct element displays\n//\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item; // Add the correct display in all browsers\n cursor: pointer;\n}\n\ntemplate {\n display: none; // Add the correct display in IE\n}\n\n// Always hide an element with the `hidden` HTML attribute (from PureCSS).\n// Needed for proper display in IE 10-.\n[hidden] {\n display: none !important;\n}\n","// Variables\n//\n// Variables should follow the `$component-state-property-size` formula for\n// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.\n\n// Color system\n\n$white: #fff !default;\n$gray-100: #f8f9fa !default;\n$gray-200: #e9ecef !default;\n$gray-300: #dee2e6 !default;\n$gray-400: #ced4da !default;\n$gray-500: #adb5bd !default;\n$gray-600: #6c757d !default;\n$gray-700: #495057 !default;\n$gray-800: #343a40 !default;\n$gray-900: #212529 !default;\n$black: #000 !default;\n\n$grays: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$grays: map-merge(\n (\n \"100\": $gray-100,\n \"200\": $gray-200,\n \"300\": $gray-300,\n \"400\": $gray-400,\n \"500\": $gray-500,\n \"600\": $gray-600,\n \"700\": $gray-700,\n \"800\": $gray-800,\n \"900\": $gray-900\n ),\n $grays\n);\n\n$blue: #007bff !default;\n$indigo: #6610f2 !default;\n$purple: #6f42c1 !default;\n$pink: #e83e8c !default;\n$red: #dc3545 !default;\n$orange: #fd7e14 !default;\n$yellow: #ffc107 !default;\n$green: #28a745 !default;\n$teal: #20c997 !default;\n$cyan: #17a2b8 !default;\n\n$colors: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$colors: map-merge(\n (\n \"blue\": $blue,\n \"indigo\": $indigo,\n \"purple\": $purple,\n \"pink\": $pink,\n \"red\": $red,\n \"orange\": $orange,\n \"yellow\": $yellow,\n \"green\": $green,\n \"teal\": $teal,\n \"cyan\": $cyan,\n \"white\": $white,\n \"gray\": $gray-600,\n \"gray-dark\": $gray-800\n ),\n $colors\n);\n\n$primary: $blue !default;\n$secondary: $gray-600 !default;\n$success: $green !default;\n$info: $cyan !default;\n$warning: $yellow !default;\n$danger: $red !default;\n$light: $gray-100 !default;\n$dark: $gray-800 !default;\n\n$theme-colors: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$theme-colors: map-merge(\n (\n \"primary\": $primary,\n \"secondary\": $secondary,\n \"success\": $success,\n \"info\": $info,\n \"warning\": $warning,\n \"danger\": $danger,\n \"light\": $light,\n \"dark\": $dark\n ),\n $theme-colors\n);\n\n// Set a specific jump point for requesting color jumps\n$theme-color-interval: 8% !default;\n\n// The yiq lightness value that determines when the lightness of color changes from \"dark\" to \"light\". Acceptable values are between 0 and 255.\n$yiq-contrasted-threshold: 150 !default;\n\n// Customize the light and dark text colors for use in our YIQ color contrast function.\n$yiq-text-dark: $gray-900 !default;\n$yiq-text-light: $white !default;\n\n// Characters which are escaped by the escape-svg function\n$escaped-characters: (\n (\"<\",\"%3c\"),\n (\">\",\"%3e\"),\n (\"#\",\"%23\"),\n) !default;\n\n\n// Options\n//\n// Quickly modify global styling by enabling or disabling optional features.\n\n$enable-caret: true !default;\n$enable-rounded: true !default;\n$enable-shadows: false !default;\n$enable-gradients: false !default;\n$enable-transitions: true !default;\n$enable-prefers-reduced-motion-media-query: true !default;\n$enable-hover-media-query: false !default; // Deprecated, no longer affects any compiled CSS\n$enable-grid-classes: true !default;\n$enable-pointer-cursor-for-buttons: true !default;\n$enable-print-styles: true !default;\n$enable-responsive-font-sizes: false !default;\n$enable-validation-icons: true !default;\n$enable-deprecation-messages: true !default;\n\n\n// Spacing\n//\n// Control the default styling of most Bootstrap elements by modifying these\n// variables. Mostly focused on spacing.\n// You can add more entries to the $spacers map, should you need more variation.\n\n$spacer: 1rem !default;\n$spacers: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$spacers: map-merge(\n (\n 0: 0,\n 1: ($spacer * .25),\n 2: ($spacer * .5),\n 3: $spacer,\n 4: ($spacer * 1.5),\n 5: ($spacer * 3)\n ),\n $spacers\n);\n\n// This variable affects the `.h-*` and `.w-*` classes.\n$sizes: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$sizes: map-merge(\n (\n 25: 25%,\n 50: 50%,\n 75: 75%,\n 100: 100%,\n auto: auto\n ),\n $sizes\n);\n\n\n// Body\n//\n// Settings for the `` element.\n\n$body-bg: $white !default;\n$body-color: $gray-900 !default;\n\n\n// Links\n//\n// Style anchor elements.\n\n$link-color: theme-color(\"primary\") !default;\n$link-decoration: none !default;\n$link-hover-color: darken($link-color, 15%) !default;\n$link-hover-decoration: underline !default;\n// Darken percentage for links with `.text-*` class (e.g. `.text-success`)\n$emphasized-link-hover-darken-percentage: 15% !default;\n\n// Paragraphs\n//\n// Style p element.\n\n$paragraph-margin-bottom: 1rem !default;\n\n\n// Grid breakpoints\n//\n// Define the minimum dimensions at which your layout will change,\n// adapting to different screen sizes, for use in media queries.\n\n$grid-breakpoints: (\n xs: 0,\n sm: 576px,\n md: 768px,\n lg: 992px,\n xl: 1200px\n) !default;\n\n@include _assert-ascending($grid-breakpoints, \"$grid-breakpoints\");\n@include _assert-starts-at-zero($grid-breakpoints, \"$grid-breakpoints\");\n\n\n// Grid containers\n//\n// Define the maximum width of `.container` for different screen sizes.\n\n$container-max-widths: (\n sm: 540px,\n md: 720px,\n lg: 960px,\n xl: 1140px\n) !default;\n\n@include _assert-ascending($container-max-widths, \"$container-max-widths\");\n\n\n// Grid columns\n//\n// Set the number of columns and specify the width of the gutters.\n\n$grid-columns: 12 !default;\n$grid-gutter-width: 30px !default;\n$grid-row-columns: 6 !default;\n\n\n// Components\n//\n// Define common padding and border radius sizes and more.\n\n$line-height-lg: 1.5 !default;\n$line-height-sm: 1.5 !default;\n\n$border-width: 1px !default;\n$border-color: $gray-300 !default;\n\n$border-radius: .25rem !default;\n$border-radius-lg: .3rem !default;\n$border-radius-sm: .2rem !default;\n\n$rounded-pill: 50rem !default;\n\n$box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default;\n$box-shadow: 0 .5rem 1rem rgba($black, .15) !default;\n$box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default;\n\n$component-active-color: $white !default;\n$component-active-bg: theme-color(\"primary\") !default;\n\n$caret-width: .3em !default;\n$caret-vertical-align: $caret-width * .85 !default;\n$caret-spacing: $caret-width * .85 !default;\n\n$transition-base: all .2s ease-in-out !default;\n$transition-fade: opacity .15s linear !default;\n$transition-collapse: height .35s ease !default;\n\n$embed-responsive-aspect-ratios: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$embed-responsive-aspect-ratios: join(\n (\n (21 9),\n (16 9),\n (4 3),\n (1 1),\n ),\n $embed-responsive-aspect-ratios\n);\n\n// Typography\n//\n// Font, line-height, and color for body text, headings, and more.\n\n// stylelint-disable value-keyword-case\n$font-family-sans-serif: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\" !default;\n$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !default;\n$font-family-base: $font-family-sans-serif !default;\n// stylelint-enable value-keyword-case\n\n$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`\n$font-size-lg: $font-size-base * 1.25 !default;\n$font-size-sm: $font-size-base * .875 !default;\n\n$font-weight-lighter: lighter !default;\n$font-weight-light: 300 !default;\n$font-weight-normal: 400 !default;\n$font-weight-bold: 700 !default;\n$font-weight-bolder: bolder !default;\n\n$font-weight-base: $font-weight-normal !default;\n$line-height-base: 1.5 !default;\n\n$h1-font-size: $font-size-base * 2.5 !default;\n$h2-font-size: $font-size-base * 2 !default;\n$h3-font-size: $font-size-base * 1.75 !default;\n$h4-font-size: $font-size-base * 1.5 !default;\n$h5-font-size: $font-size-base * 1.25 !default;\n$h6-font-size: $font-size-base !default;\n\n$headings-margin-bottom: $spacer / 2 !default;\n$headings-font-family: null !default;\n$headings-font-weight: 500 !default;\n$headings-line-height: 1.2 !default;\n$headings-color: null !default;\n\n$display1-size: 6rem !default;\n$display2-size: 5.5rem !default;\n$display3-size: 4.5rem !default;\n$display4-size: 3.5rem !default;\n\n$display1-weight: 300 !default;\n$display2-weight: 300 !default;\n$display3-weight: 300 !default;\n$display4-weight: 300 !default;\n$display-line-height: $headings-line-height !default;\n\n$lead-font-size: $font-size-base * 1.25 !default;\n$lead-font-weight: 300 !default;\n\n$small-font-size: 80% !default;\n\n$text-muted: $gray-600 !default;\n\n$blockquote-small-color: $gray-600 !default;\n$blockquote-small-font-size: $small-font-size !default;\n$blockquote-font-size: $font-size-base * 1.25 !default;\n\n$hr-border-color: rgba($black, .1) !default;\n$hr-border-width: $border-width !default;\n\n$mark-padding: .2em !default;\n\n$dt-font-weight: $font-weight-bold !default;\n\n$kbd-box-shadow: inset 0 -.1rem 0 rgba($black, .25) !default;\n$nested-kbd-font-weight: $font-weight-bold !default;\n\n$list-inline-padding: .5rem !default;\n\n$mark-bg: #fcf8e3 !default;\n\n$hr-margin-y: $spacer !default;\n\n\n// Tables\n//\n// Customizes the `.table` component with basic values, each used across all table variations.\n\n$table-cell-padding: .75rem !default;\n$table-cell-padding-sm: .3rem !default;\n\n$table-color: $body-color !default;\n$table-bg: null !default;\n$table-accent-bg: rgba($black, .05) !default;\n$table-hover-color: $table-color !default;\n$table-hover-bg: rgba($black, .075) !default;\n$table-active-bg: $table-hover-bg !default;\n\n$table-border-width: $border-width !default;\n$table-border-color: $border-color !default;\n\n$table-head-bg: $gray-200 !default;\n$table-head-color: $gray-700 !default;\n\n$table-dark-color: $white !default;\n$table-dark-bg: $gray-800 !default;\n$table-dark-accent-bg: rgba($white, .05) !default;\n$table-dark-hover-color: $table-dark-color !default;\n$table-dark-hover-bg: rgba($white, .075) !default;\n$table-dark-border-color: lighten($table-dark-bg, 7.5%) !default;\n\n$table-striped-order: odd !default;\n\n$table-caption-color: $text-muted !default;\n\n$table-bg-level: -9 !default;\n$table-border-level: -6 !default;\n\n\n// Buttons + Forms\n//\n// Shared variables that are reassigned to `$input-` and `$btn-` specific variables.\n\n$input-btn-padding-y: .375rem !default;\n$input-btn-padding-x: .75rem !default;\n$input-btn-font-family: null !default;\n$input-btn-font-size: $font-size-base !default;\n$input-btn-line-height: $line-height-base !default;\n\n$input-btn-focus-width: .2rem !default;\n$input-btn-focus-color: rgba($component-active-bg, .25) !default;\n$input-btn-focus-box-shadow: 0 0 0 $input-btn-focus-width $input-btn-focus-color !default;\n\n$input-btn-padding-y-sm: .25rem !default;\n$input-btn-padding-x-sm: .5rem !default;\n$input-btn-font-size-sm: $font-size-sm !default;\n$input-btn-line-height-sm: $line-height-sm !default;\n\n$input-btn-padding-y-lg: .5rem !default;\n$input-btn-padding-x-lg: 1rem !default;\n$input-btn-font-size-lg: $font-size-lg !default;\n$input-btn-line-height-lg: $line-height-lg !default;\n\n$input-btn-border-width: $border-width !default;\n\n\n// Buttons\n//\n// For each of Bootstrap's buttons, define text, background, and border color.\n\n$btn-padding-y: $input-btn-padding-y !default;\n$btn-padding-x: $input-btn-padding-x !default;\n$btn-font-family: $input-btn-font-family !default;\n$btn-font-size: $input-btn-font-size !default;\n$btn-line-height: $input-btn-line-height !default;\n$btn-white-space: null !default; // Set to `nowrap` to prevent text wrapping\n\n$btn-padding-y-sm: $input-btn-padding-y-sm !default;\n$btn-padding-x-sm: $input-btn-padding-x-sm !default;\n$btn-font-size-sm: $input-btn-font-size-sm !default;\n$btn-line-height-sm: $input-btn-line-height-sm !default;\n\n$btn-padding-y-lg: $input-btn-padding-y-lg !default;\n$btn-padding-x-lg: $input-btn-padding-x-lg !default;\n$btn-font-size-lg: $input-btn-font-size-lg !default;\n$btn-line-height-lg: $input-btn-line-height-lg !default;\n\n$btn-border-width: $input-btn-border-width !default;\n\n$btn-font-weight: $font-weight-normal !default;\n$btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default;\n$btn-focus-width: $input-btn-focus-width !default;\n$btn-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$btn-disabled-opacity: .65 !default;\n$btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default;\n\n$btn-link-disabled-color: $gray-600 !default;\n\n$btn-block-spacing-y: .5rem !default;\n\n// Allows for customizing button radius independently from global border radius\n$btn-border-radius: $border-radius !default;\n$btn-border-radius-lg: $border-radius-lg !default;\n$btn-border-radius-sm: $border-radius-sm !default;\n\n$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n\n// Forms\n\n$label-margin-bottom: .5rem !default;\n\n$input-padding-y: $input-btn-padding-y !default;\n$input-padding-x: $input-btn-padding-x !default;\n$input-font-family: $input-btn-font-family !default;\n$input-font-size: $input-btn-font-size !default;\n$input-font-weight: $font-weight-base !default;\n$input-line-height: $input-btn-line-height !default;\n\n$input-padding-y-sm: $input-btn-padding-y-sm !default;\n$input-padding-x-sm: $input-btn-padding-x-sm !default;\n$input-font-size-sm: $input-btn-font-size-sm !default;\n$input-line-height-sm: $input-btn-line-height-sm !default;\n\n$input-padding-y-lg: $input-btn-padding-y-lg !default;\n$input-padding-x-lg: $input-btn-padding-x-lg !default;\n$input-font-size-lg: $input-btn-font-size-lg !default;\n$input-line-height-lg: $input-btn-line-height-lg !default;\n\n$input-bg: $white !default;\n$input-disabled-bg: $gray-200 !default;\n\n$input-color: $gray-700 !default;\n$input-border-color: $gray-400 !default;\n$input-border-width: $input-btn-border-width !default;\n$input-box-shadow: inset 0 1px 1px rgba($black, .075) !default;\n\n$input-border-radius: $border-radius !default;\n$input-border-radius-lg: $border-radius-lg !default;\n$input-border-radius-sm: $border-radius-sm !default;\n\n$input-focus-bg: $input-bg !default;\n$input-focus-border-color: lighten($component-active-bg, 25%) !default;\n$input-focus-color: $input-color !default;\n$input-focus-width: $input-btn-focus-width !default;\n$input-focus-box-shadow: $input-btn-focus-box-shadow !default;\n\n$input-placeholder-color: $gray-600 !default;\n$input-plaintext-color: $body-color !default;\n\n$input-height-border: $input-border-width * 2 !default;\n\n$input-height-inner: add($input-line-height * 1em, $input-padding-y * 2) !default;\n$input-height-inner-half: add($input-line-height * .5em, $input-padding-y) !default;\n$input-height-inner-quarter: add($input-line-height * .25em, $input-padding-y / 2) !default;\n\n$input-height: add($input-line-height * 1em, add($input-padding-y * 2, $input-height-border, false)) !default;\n$input-height-sm: add($input-line-height-sm * 1em, add($input-padding-y-sm * 2, $input-height-border, false)) !default;\n$input-height-lg: add($input-line-height-lg * 1em, add($input-padding-y-lg * 2, $input-height-border, false)) !default;\n\n$input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$form-text-margin-top: .25rem !default;\n\n$form-check-input-gutter: 1.25rem !default;\n$form-check-input-margin-y: .3rem !default;\n$form-check-input-margin-x: .25rem !default;\n\n$form-check-inline-margin-x: .75rem !default;\n$form-check-inline-input-margin-x: .3125rem !default;\n\n$form-grid-gutter-width: 10px !default;\n$form-group-margin-bottom: 1rem !default;\n\n$input-group-addon-color: $input-color !default;\n$input-group-addon-bg: $gray-200 !default;\n$input-group-addon-border-color: $input-border-color !default;\n\n$custom-forms-transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$custom-control-gutter: .5rem !default;\n$custom-control-spacer-x: 1rem !default;\n$custom-control-cursor: null !default;\n\n$custom-control-indicator-size: 1rem !default;\n$custom-control-indicator-bg: $input-bg !default;\n\n$custom-control-indicator-bg-size: 50% 50% !default;\n$custom-control-indicator-box-shadow: $input-box-shadow !default;\n$custom-control-indicator-border-color: $gray-500 !default;\n$custom-control-indicator-border-width: $input-border-width !default;\n\n$custom-control-label-color: null !default;\n\n$custom-control-indicator-disabled-bg: $input-disabled-bg !default;\n$custom-control-label-disabled-color: $gray-600 !default;\n\n$custom-control-indicator-checked-color: $component-active-color !default;\n$custom-control-indicator-checked-bg: $component-active-bg !default;\n$custom-control-indicator-checked-disabled-bg: rgba(theme-color(\"primary\"), .5) !default;\n$custom-control-indicator-checked-box-shadow: none !default;\n$custom-control-indicator-checked-border-color: $custom-control-indicator-checked-bg !default;\n\n$custom-control-indicator-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-control-indicator-focus-border-color: $input-focus-border-color !default;\n\n$custom-control-indicator-active-color: $component-active-color !default;\n$custom-control-indicator-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-control-indicator-active-box-shadow: none !default;\n$custom-control-indicator-active-border-color: $custom-control-indicator-active-bg !default;\n\n$custom-checkbox-indicator-border-radius: $border-radius !default;\n$custom-checkbox-indicator-icon-checked: url(\"data:image/svg+xml,\") !default;\n\n$custom-checkbox-indicator-indeterminate-bg: $component-active-bg !default;\n$custom-checkbox-indicator-indeterminate-color: $custom-control-indicator-checked-color !default;\n$custom-checkbox-indicator-icon-indeterminate: url(\"data:image/svg+xml,\") !default;\n$custom-checkbox-indicator-indeterminate-box-shadow: none !default;\n$custom-checkbox-indicator-indeterminate-border-color: $custom-checkbox-indicator-indeterminate-bg !default;\n\n$custom-radio-indicator-border-radius: 50% !default;\n$custom-radio-indicator-icon-checked: url(\"data:image/svg+xml,\") !default;\n\n$custom-switch-width: $custom-control-indicator-size * 1.75 !default;\n$custom-switch-indicator-border-radius: $custom-control-indicator-size / 2 !default;\n$custom-switch-indicator-size: subtract($custom-control-indicator-size, $custom-control-indicator-border-width * 4) !default;\n\n$custom-select-padding-y: $input-padding-y !default;\n$custom-select-padding-x: $input-padding-x !default;\n$custom-select-font-family: $input-font-family !default;\n$custom-select-font-size: $input-font-size !default;\n$custom-select-height: $input-height !default;\n$custom-select-indicator-padding: 1rem !default; // Extra padding to account for the presence of the background-image based indicator\n$custom-select-font-weight: $input-font-weight !default;\n$custom-select-line-height: $input-line-height !default;\n$custom-select-color: $input-color !default;\n$custom-select-disabled-color: $gray-600 !default;\n$custom-select-bg: $input-bg !default;\n$custom-select-disabled-bg: $gray-200 !default;\n$custom-select-bg-size: 8px 10px !default; // In pixels because image dimensions\n$custom-select-indicator-color: $gray-800 !default;\n$custom-select-indicator: url(\"data:image/svg+xml,\") !default;\n$custom-select-background: escape-svg($custom-select-indicator) no-repeat right $custom-select-padding-x center / $custom-select-bg-size !default; // Used so we can have multiple background elements (e.g., arrow and feedback icon)\n\n$custom-select-feedback-icon-padding-right: add(1em * .75, (2 * $custom-select-padding-y * .75) + $custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-position: center right ($custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-size: $input-height-inner-half $input-height-inner-half !default;\n\n$custom-select-border-width: $input-border-width !default;\n$custom-select-border-color: $input-border-color !default;\n$custom-select-border-radius: $border-radius !default;\n$custom-select-box-shadow: inset 0 1px 2px rgba($black, .075) !default;\n\n$custom-select-focus-border-color: $input-focus-border-color !default;\n$custom-select-focus-width: $input-focus-width !default;\n$custom-select-focus-box-shadow: 0 0 0 $custom-select-focus-width $input-btn-focus-color !default;\n\n$custom-select-padding-y-sm: $input-padding-y-sm !default;\n$custom-select-padding-x-sm: $input-padding-x-sm !default;\n$custom-select-font-size-sm: $input-font-size-sm !default;\n$custom-select-height-sm: $input-height-sm !default;\n\n$custom-select-padding-y-lg: $input-padding-y-lg !default;\n$custom-select-padding-x-lg: $input-padding-x-lg !default;\n$custom-select-font-size-lg: $input-font-size-lg !default;\n$custom-select-height-lg: $input-height-lg !default;\n\n$custom-range-track-width: 100% !default;\n$custom-range-track-height: .5rem !default;\n$custom-range-track-cursor: pointer !default;\n$custom-range-track-bg: $gray-300 !default;\n$custom-range-track-border-radius: 1rem !default;\n$custom-range-track-box-shadow: inset 0 .25rem .25rem rgba($black, .1) !default;\n\n$custom-range-thumb-width: 1rem !default;\n$custom-range-thumb-height: $custom-range-thumb-width !default;\n$custom-range-thumb-bg: $component-active-bg !default;\n$custom-range-thumb-border: 0 !default;\n$custom-range-thumb-border-radius: 1rem !default;\n$custom-range-thumb-box-shadow: 0 .1rem .25rem rgba($black, .1) !default;\n$custom-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow !default;\n$custom-range-thumb-focus-box-shadow-width: $input-focus-width !default; // For focus box shadow issue in IE/Edge\n$custom-range-thumb-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-range-thumb-disabled-bg: $gray-500 !default;\n\n$custom-file-height: $input-height !default;\n$custom-file-height-inner: $input-height-inner !default;\n$custom-file-focus-border-color: $input-focus-border-color !default;\n$custom-file-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-file-disabled-bg: $input-disabled-bg !default;\n\n$custom-file-padding-y: $input-padding-y !default;\n$custom-file-padding-x: $input-padding-x !default;\n$custom-file-line-height: $input-line-height !default;\n$custom-file-font-family: $input-font-family !default;\n$custom-file-font-weight: $input-font-weight !default;\n$custom-file-color: $input-color !default;\n$custom-file-bg: $input-bg !default;\n$custom-file-border-width: $input-border-width !default;\n$custom-file-border-color: $input-border-color !default;\n$custom-file-border-radius: $input-border-radius !default;\n$custom-file-box-shadow: $input-box-shadow !default;\n$custom-file-button-color: $custom-file-color !default;\n$custom-file-button-bg: $input-group-addon-bg !default;\n$custom-file-text: (\n en: \"Browse\"\n) !default;\n\n\n// Form validation\n\n$form-feedback-margin-top: $form-text-margin-top !default;\n$form-feedback-font-size: $small-font-size !default;\n$form-feedback-valid-color: theme-color(\"success\") !default;\n$form-feedback-invalid-color: theme-color(\"danger\") !default;\n\n$form-feedback-icon-valid-color: $form-feedback-valid-color !default;\n$form-feedback-icon-valid: url(\"data:image/svg+xml,\") !default;\n$form-feedback-icon-invalid-color: $form-feedback-invalid-color !default;\n$form-feedback-icon-invalid: url(\"data:image/svg+xml,\") !default;\n\n$form-validation-states: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$form-validation-states: map-merge(\n (\n \"valid\": (\n \"color\": $form-feedback-valid-color,\n \"icon\": $form-feedback-icon-valid\n ),\n \"invalid\": (\n \"color\": $form-feedback-invalid-color,\n \"icon\": $form-feedback-icon-invalid\n ),\n ),\n $form-validation-states\n);\n\n// Z-index master list\n//\n// Warning: Avoid customizing these values. They're used for a bird's eye view\n// of components dependent on the z-axis and are designed to all work together.\n\n$zindex-dropdown: 1000 !default;\n$zindex-sticky: 1020 !default;\n$zindex-fixed: 1030 !default;\n$zindex-modal-backdrop: 1040 !default;\n$zindex-modal: 1050 !default;\n$zindex-popover: 1060 !default;\n$zindex-tooltip: 1070 !default;\n\n\n// Navs\n\n$nav-link-padding-y: .5rem !default;\n$nav-link-padding-x: 1rem !default;\n$nav-link-disabled-color: $gray-600 !default;\n\n$nav-tabs-border-color: $gray-300 !default;\n$nav-tabs-border-width: $border-width !default;\n$nav-tabs-border-radius: $border-radius !default;\n$nav-tabs-link-hover-border-color: $gray-200 $gray-200 $nav-tabs-border-color !default;\n$nav-tabs-link-active-color: $gray-700 !default;\n$nav-tabs-link-active-bg: $body-bg !default;\n$nav-tabs-link-active-border-color: $gray-300 $gray-300 $nav-tabs-link-active-bg !default;\n\n$nav-pills-border-radius: $border-radius !default;\n$nav-pills-link-active-color: $component-active-color !default;\n$nav-pills-link-active-bg: $component-active-bg !default;\n\n$nav-divider-color: $gray-200 !default;\n$nav-divider-margin-y: $spacer / 2 !default;\n\n\n// Navbar\n\n$navbar-padding-y: $spacer / 2 !default;\n$navbar-padding-x: $spacer !default;\n\n$navbar-nav-link-padding-x: .5rem !default;\n\n$navbar-brand-font-size: $font-size-lg !default;\n// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link\n$nav-link-height: $font-size-base * $line-height-base + $nav-link-padding-y * 2 !default;\n$navbar-brand-height: $navbar-brand-font-size * $line-height-base !default;\n$navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) / 2 !default;\n\n$navbar-toggler-padding-y: .25rem !default;\n$navbar-toggler-padding-x: .75rem !default;\n$navbar-toggler-font-size: $font-size-lg !default;\n$navbar-toggler-border-radius: $btn-border-radius !default;\n\n$navbar-dark-color: rgba($white, .5) !default;\n$navbar-dark-hover-color: rgba($white, .75) !default;\n$navbar-dark-active-color: $white !default;\n$navbar-dark-disabled-color: rgba($white, .25) !default;\n$navbar-dark-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-dark-toggler-border-color: rgba($white, .1) !default;\n\n$navbar-light-color: rgba($black, .5) !default;\n$navbar-light-hover-color: rgba($black, .7) !default;\n$navbar-light-active-color: rgba($black, .9) !default;\n$navbar-light-disabled-color: rgba($black, .3) !default;\n$navbar-light-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-light-toggler-border-color: rgba($black, .1) !default;\n\n$navbar-light-brand-color: $navbar-light-active-color !default;\n$navbar-light-brand-hover-color: $navbar-light-active-color !default;\n$navbar-dark-brand-color: $navbar-dark-active-color !default;\n$navbar-dark-brand-hover-color: $navbar-dark-active-color !default;\n\n\n// Dropdowns\n//\n// Dropdown menu container and contents.\n\n$dropdown-min-width: 10rem !default;\n$dropdown-padding-y: .5rem !default;\n$dropdown-spacer: .125rem !default;\n$dropdown-font-size: $font-size-base !default;\n$dropdown-color: $body-color !default;\n$dropdown-bg: $white !default;\n$dropdown-border-color: rgba($black, .15) !default;\n$dropdown-border-radius: $border-radius !default;\n$dropdown-border-width: $border-width !default;\n$dropdown-inner-border-radius: subtract($dropdown-border-radius, $dropdown-border-width) !default;\n$dropdown-divider-bg: $gray-200 !default;\n$dropdown-divider-margin-y: $nav-divider-margin-y !default;\n$dropdown-box-shadow: 0 .5rem 1rem rgba($black, .175) !default;\n\n$dropdown-link-color: $gray-900 !default;\n$dropdown-link-hover-color: darken($gray-900, 5%) !default;\n$dropdown-link-hover-bg: $gray-100 !default;\n\n$dropdown-link-active-color: $component-active-color !default;\n$dropdown-link-active-bg: $component-active-bg !default;\n\n$dropdown-link-disabled-color: $gray-600 !default;\n\n$dropdown-item-padding-y: .25rem !default;\n$dropdown-item-padding-x: 1.5rem !default;\n\n$dropdown-header-color: $gray-600 !default;\n\n\n// Pagination\n\n$pagination-padding-y: .5rem !default;\n$pagination-padding-x: .75rem !default;\n$pagination-padding-y-sm: .25rem !default;\n$pagination-padding-x-sm: .5rem !default;\n$pagination-padding-y-lg: .75rem !default;\n$pagination-padding-x-lg: 1.5rem !default;\n$pagination-line-height: 1.25 !default;\n\n$pagination-color: $link-color !default;\n$pagination-bg: $white !default;\n$pagination-border-width: $border-width !default;\n$pagination-border-color: $gray-300 !default;\n\n$pagination-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$pagination-focus-outline: 0 !default;\n\n$pagination-hover-color: $link-hover-color !default;\n$pagination-hover-bg: $gray-200 !default;\n$pagination-hover-border-color: $gray-300 !default;\n\n$pagination-active-color: $component-active-color !default;\n$pagination-active-bg: $component-active-bg !default;\n$pagination-active-border-color: $pagination-active-bg !default;\n\n$pagination-disabled-color: $gray-600 !default;\n$pagination-disabled-bg: $white !default;\n$pagination-disabled-border-color: $gray-300 !default;\n\n\n// Jumbotron\n\n$jumbotron-padding: 2rem !default;\n$jumbotron-color: null !default;\n$jumbotron-bg: $gray-200 !default;\n\n\n// Cards\n\n$card-spacer-y: .75rem !default;\n$card-spacer-x: 1.25rem !default;\n$card-border-width: $border-width !default;\n$card-border-radius: $border-radius !default;\n$card-border-color: rgba($black, .125) !default;\n$card-inner-border-radius: subtract($card-border-radius, $card-border-width) !default;\n$card-cap-bg: rgba($black, .03) !default;\n$card-cap-color: null !default;\n$card-height: null !default;\n$card-color: null !default;\n$card-bg: $white !default;\n\n$card-img-overlay-padding: 1.25rem !default;\n\n$card-group-margin: $grid-gutter-width / 2 !default;\n$card-deck-margin: $card-group-margin !default;\n\n$card-columns-count: 3 !default;\n$card-columns-gap: 1.25rem !default;\n$card-columns-margin: $card-spacer-y !default;\n\n\n// Tooltips\n\n$tooltip-font-size: $font-size-sm !default;\n$tooltip-max-width: 200px !default;\n$tooltip-color: $white !default;\n$tooltip-bg: $black !default;\n$tooltip-border-radius: $border-radius !default;\n$tooltip-opacity: .9 !default;\n$tooltip-padding-y: .25rem !default;\n$tooltip-padding-x: .5rem !default;\n$tooltip-margin: 0 !default;\n\n$tooltip-arrow-width: .8rem !default;\n$tooltip-arrow-height: .4rem !default;\n$tooltip-arrow-color: $tooltip-bg !default;\n\n// Form tooltips must come after regular tooltips\n$form-feedback-tooltip-padding-y: $tooltip-padding-y !default;\n$form-feedback-tooltip-padding-x: $tooltip-padding-x !default;\n$form-feedback-tooltip-font-size: $tooltip-font-size !default;\n$form-feedback-tooltip-line-height: $line-height-base !default;\n$form-feedback-tooltip-opacity: $tooltip-opacity !default;\n$form-feedback-tooltip-border-radius: $tooltip-border-radius !default;\n\n\n// Popovers\n\n$popover-font-size: $font-size-sm !default;\n$popover-bg: $white !default;\n$popover-max-width: 276px !default;\n$popover-border-width: $border-width !default;\n$popover-border-color: rgba($black, .2) !default;\n$popover-border-radius: $border-radius-lg !default;\n$popover-inner-border-radius: subtract($popover-border-radius, $popover-border-width) !default;\n$popover-box-shadow: 0 .25rem .5rem rgba($black, .2) !default;\n\n$popover-header-bg: darken($popover-bg, 3%) !default;\n$popover-header-color: $headings-color !default;\n$popover-header-padding-y: .5rem !default;\n$popover-header-padding-x: .75rem !default;\n\n$popover-body-color: $body-color !default;\n$popover-body-padding-y: $popover-header-padding-y !default;\n$popover-body-padding-x: $popover-header-padding-x !default;\n\n$popover-arrow-width: 1rem !default;\n$popover-arrow-height: .5rem !default;\n$popover-arrow-color: $popover-bg !default;\n\n$popover-arrow-outer-color: fade-in($popover-border-color, .05) !default;\n\n\n// Toasts\n\n$toast-max-width: 350px !default;\n$toast-padding-x: .75rem !default;\n$toast-padding-y: .25rem !default;\n$toast-font-size: .875rem !default;\n$toast-color: null !default;\n$toast-background-color: rgba($white, .85) !default;\n$toast-border-width: 1px !default;\n$toast-border-color: rgba(0, 0, 0, .1) !default;\n$toast-border-radius: .25rem !default;\n$toast-box-shadow: 0 .25rem .75rem rgba($black, .1) !default;\n\n$toast-header-color: $gray-600 !default;\n$toast-header-background-color: rgba($white, .85) !default;\n$toast-header-border-color: rgba(0, 0, 0, .05) !default;\n\n\n// Badges\n\n$badge-font-size: 75% !default;\n$badge-font-weight: $font-weight-bold !default;\n$badge-padding-y: .25em !default;\n$badge-padding-x: .4em !default;\n$badge-border-radius: $border-radius !default;\n\n$badge-transition: $btn-transition !default;\n$badge-focus-width: $input-btn-focus-width !default;\n\n$badge-pill-padding-x: .6em !default;\n// Use a higher than normal value to ensure completely rounded edges when\n// customizing padding or font-size on labels.\n$badge-pill-border-radius: 10rem !default;\n\n\n// Modals\n\n// Padding applied to the modal body\n$modal-inner-padding: 1rem !default;\n\n// Margin between elements in footer, must be lower than or equal to 2 * $modal-inner-padding\n$modal-footer-margin-between: .5rem !default;\n\n$modal-dialog-margin: .5rem !default;\n$modal-dialog-margin-y-sm-up: 1.75rem !default;\n\n$modal-title-line-height: $line-height-base !default;\n\n$modal-content-color: null !default;\n$modal-content-bg: $white !default;\n$modal-content-border-color: rgba($black, .2) !default;\n$modal-content-border-width: $border-width !default;\n$modal-content-border-radius: $border-radius-lg !default;\n$modal-content-inner-border-radius: subtract($modal-content-border-radius, $modal-content-border-width) !default;\n$modal-content-box-shadow-xs: 0 .25rem .5rem rgba($black, .5) !default;\n$modal-content-box-shadow-sm-up: 0 .5rem 1rem rgba($black, .5) !default;\n\n$modal-backdrop-bg: $black !default;\n$modal-backdrop-opacity: .5 !default;\n$modal-header-border-color: $border-color !default;\n$modal-footer-border-color: $modal-header-border-color !default;\n$modal-header-border-width: $modal-content-border-width !default;\n$modal-footer-border-width: $modal-header-border-width !default;\n$modal-header-padding-y: 1rem !default;\n$modal-header-padding-x: 1rem !default;\n$modal-header-padding: $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility\n\n$modal-xl: 1140px !default;\n$modal-lg: 800px !default;\n$modal-md: 500px !default;\n$modal-sm: 300px !default;\n\n$modal-fade-transform: translate(0, -50px) !default;\n$modal-show-transform: none !default;\n$modal-transition: transform .3s ease-out !default;\n$modal-scale-transform: scale(1.02) !default;\n\n\n// Alerts\n//\n// Define alert colors, border radius, and padding.\n\n$alert-padding-y: .75rem !default;\n$alert-padding-x: 1.25rem !default;\n$alert-margin-bottom: 1rem !default;\n$alert-border-radius: $border-radius !default;\n$alert-link-font-weight: $font-weight-bold !default;\n$alert-border-width: $border-width !default;\n\n$alert-bg-level: -10 !default;\n$alert-border-level: -9 !default;\n$alert-color-level: 6 !default;\n\n\n// Progress bars\n\n$progress-height: 1rem !default;\n$progress-font-size: $font-size-base * .75 !default;\n$progress-bg: $gray-200 !default;\n$progress-border-radius: $border-radius !default;\n$progress-box-shadow: inset 0 .1rem .1rem rgba($black, .1) !default;\n$progress-bar-color: $white !default;\n$progress-bar-bg: theme-color(\"primary\") !default;\n$progress-bar-animation-timing: 1s linear infinite !default;\n$progress-bar-transition: width .6s ease !default;\n\n\n// List group\n\n$list-group-color: null !default;\n$list-group-bg: $white !default;\n$list-group-border-color: rgba($black, .125) !default;\n$list-group-border-width: $border-width !default;\n$list-group-border-radius: $border-radius !default;\n\n$list-group-item-padding-y: .75rem !default;\n$list-group-item-padding-x: 1.25rem !default;\n\n$list-group-hover-bg: $gray-100 !default;\n$list-group-active-color: $component-active-color !default;\n$list-group-active-bg: $component-active-bg !default;\n$list-group-active-border-color: $list-group-active-bg !default;\n\n$list-group-disabled-color: $gray-600 !default;\n$list-group-disabled-bg: $list-group-bg !default;\n\n$list-group-action-color: $gray-700 !default;\n$list-group-action-hover-color: $list-group-action-color !default;\n\n$list-group-action-active-color: $body-color !default;\n$list-group-action-active-bg: $gray-200 !default;\n\n\n// Image thumbnails\n\n$thumbnail-padding: .25rem !default;\n$thumbnail-bg: $body-bg !default;\n$thumbnail-border-width: $border-width !default;\n$thumbnail-border-color: $gray-300 !default;\n$thumbnail-border-radius: $border-radius !default;\n$thumbnail-box-shadow: 0 1px 2px rgba($black, .075) !default;\n\n\n// Figures\n\n$figure-caption-font-size: 90% !default;\n$figure-caption-color: $gray-600 !default;\n\n\n// Breadcrumbs\n\n$breadcrumb-font-size: null !default;\n\n$breadcrumb-padding-y: .75rem !default;\n$breadcrumb-padding-x: 1rem !default;\n$breadcrumb-item-padding: .5rem !default;\n\n$breadcrumb-margin-bottom: 1rem !default;\n\n$breadcrumb-bg: $gray-200 !default;\n$breadcrumb-divider-color: $gray-600 !default;\n$breadcrumb-active-color: $gray-600 !default;\n$breadcrumb-divider: quote(\"/\") !default;\n\n$breadcrumb-border-radius: $border-radius !default;\n\n\n// Carousel\n\n$carousel-control-color: $white !default;\n$carousel-control-width: 15% !default;\n$carousel-control-opacity: .5 !default;\n$carousel-control-hover-opacity: .9 !default;\n$carousel-control-transition: opacity .15s ease !default;\n\n$carousel-indicator-width: 30px !default;\n$carousel-indicator-height: 3px !default;\n$carousel-indicator-hit-area-height: 10px !default;\n$carousel-indicator-spacer: 3px !default;\n$carousel-indicator-active-bg: $white !default;\n$carousel-indicator-transition: opacity .6s ease !default;\n\n$carousel-caption-width: 70% !default;\n$carousel-caption-color: $white !default;\n\n$carousel-control-icon-width: 20px !default;\n\n$carousel-control-prev-icon-bg: url(\"data:image/svg+xml,\") !default;\n$carousel-control-next-icon-bg: url(\"data:image/svg+xml,\") !default;\n\n$carousel-transition-duration: .6s !default;\n$carousel-transition: transform $carousel-transition-duration ease-in-out !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`)\n\n\n// Spinners\n\n$spinner-width: 2rem !default;\n$spinner-height: $spinner-width !default;\n$spinner-border-width: .25em !default;\n\n$spinner-width-sm: 1rem !default;\n$spinner-height-sm: $spinner-width-sm !default;\n$spinner-border-width-sm: .2em !default;\n\n\n// Close\n\n$close-font-size: $font-size-base * 1.5 !default;\n$close-font-weight: $font-weight-bold !default;\n$close-color: $black !default;\n$close-text-shadow: 0 1px 0 $white !default;\n\n\n// Code\n\n$code-font-size: 87.5% !default;\n$code-color: $pink !default;\n\n$kbd-padding-y: .2rem !default;\n$kbd-padding-x: .4rem !default;\n$kbd-font-size: $code-font-size !default;\n$kbd-color: $white !default;\n$kbd-bg: $gray-900 !default;\n\n$pre-color: $gray-900 !default;\n$pre-scrollable-max-height: 340px !default;\n\n\n// Utilities\n\n$displays: none, inline, inline-block, block, table, table-row, table-cell, flex, inline-flex !default;\n$overflows: auto, hidden !default;\n$positions: static, relative, absolute, fixed, sticky !default;\n\n\n// Printing\n\n$print-page-size: a3 !default;\n$print-body-min-width: map-get($grid-breakpoints, \"lg\") !default;\n","// stylelint-disable property-blacklist, scss/dollar-variable-default\n\n// SCSS RFS mixin\n//\n// Automated font-resizing\n//\n// See https://github.com/twbs/rfs\n\n// Configuration\n\n// Base font size\n$rfs-base-font-size: 1.25rem !default;\n$rfs-font-size-unit: rem !default;\n\n// Breakpoint at where font-size starts decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n// Resize font-size based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != \"number\" or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-responsive-font-sizes to false\n$enable-responsive-font-sizes: true !default;\n\n// Cache $rfs-base-font-size unit\n$rfs-base-font-size-unit: unit($rfs-base-font-size);\n\n// Remove px-unit from $rfs-base-font-size for calculations\n@if $rfs-base-font-size-unit == \"px\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1);\n}\n@else if $rfs-base-font-size-unit == \"rem\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1 / $rfs-rem-value);\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == \"px\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == \"rem\" or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1 / $rfs-rem-value);\n}\n\n// Responsive font-size mixin\n@mixin rfs($fs, $important: false) {\n // Cache $fs unit\n $fs-unit: if(type-of($fs) == \"number\", unit($fs), false);\n\n // Add !important suffix if needed\n $rfs-suffix: if($important, \" !important\", \"\");\n\n // If $fs isn't a number (like inherit) or $fs has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $fs-unit or $fs-unit != \"\" and $fs-unit != \"px\" and $fs-unit != \"rem\" or $fs == 0 {\n font-size: #{$fs}#{$rfs-suffix};\n }\n @else {\n // Variables for storing static and fluid rescaling\n $rfs-static: null;\n $rfs-fluid: null;\n\n // Remove px-unit from $fs for calculations\n @if $fs-unit == \"px\" {\n $fs: $fs / ($fs * 0 + 1);\n }\n @else if $fs-unit == \"rem\" {\n $fs: $fs / ($fs * 0 + 1 / $rfs-rem-value);\n }\n\n // Set default font-size\n @if $rfs-font-size-unit == rem {\n $rfs-static: #{$fs / $rfs-rem-value}rem#{$rfs-suffix};\n }\n @else if $rfs-font-size-unit == px {\n $rfs-static: #{$fs}px#{$rfs-suffix};\n }\n @else {\n @error \"`#{$rfs-font-size-unit}` is not a valid unit for $rfs-font-size-unit. Use `px` or `rem`.\";\n }\n\n // Only add media query if font-size is bigger as the minimum font-size\n // If $rfs-factor == 1, no rescaling will take place\n @if $fs > $rfs-base-font-size and $enable-responsive-font-sizes {\n $min-width: null;\n $variable-unit: null;\n\n // Calculate minimum font-size for given font-size\n $fs-min: $rfs-base-font-size + ($fs - $rfs-base-font-size) / $rfs-factor;\n\n // Calculate difference between given font-size and minimum font-size for given font-size\n $fs-diff: $fs - $fs-min;\n\n // Base font-size formatting\n // No need to check if the unit is valid, because we did that before\n $min-width: if($rfs-font-size-unit == rem, #{$fs-min / $rfs-rem-value}rem, #{$fs-min}px);\n\n // If two-dimensional, use smallest of screen width and height\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{$fs-diff * 100 / $rfs-breakpoint}#{$variable-unit};\n\n // Set the calculated font-size.\n $rfs-fluid: calc(#{$min-width} + #{$variable-width}) #{$rfs-suffix};\n }\n\n // Rendering\n @if $rfs-fluid == null {\n // Only render static font-size if no fluid font-size is available\n font-size: $rfs-static;\n }\n @else {\n $mq-value: null;\n\n // RFS breakpoint formatting\n @if $rfs-breakpoint-unit == em or $rfs-breakpoint-unit == rem {\n $mq-value: #{$rfs-breakpoint / $rfs-rem-value}#{$rfs-breakpoint-unit};\n }\n @else if $rfs-breakpoint-unit == px {\n $mq-value: #{$rfs-breakpoint}px;\n }\n @else {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n }\n\n @if $rfs-class == \"disable\" {\n // Adding an extra class increases specificity,\n // which prevents the media query to override the font size\n &,\n .disable-responsive-font-size &,\n &.disable-responsive-font-size {\n font-size: $rfs-static;\n }\n }\n @else {\n font-size: $rfs-static;\n }\n\n @if $rfs-two-dimensional {\n @media (max-width: #{$mq-value}), (max-height: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n @else {\n @media (max-width: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n }\n }\n}\n\n// The font-size & responsive-font-size mixin uses RFS to rescale font sizes\n@mixin font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n\n@mixin responsive-font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n","// Hover mixin and `$enable-hover-media-query` are deprecated.\n//\n// Originally added during our alphas and maintained during betas, this mixin was\n// designed to prevent `:hover` stickiness on iOS-an issue where hover styles\n// would persist after initial touch.\n//\n// For backward compatibility, we've kept these mixins and updated them to\n// always return their regular pseudo-classes instead of a shimmed media query.\n//\n// Issue: https://github.com/twbs/bootstrap/issues/25195\n\n@mixin hover() {\n &:hover { @content; }\n}\n\n@mixin hover-focus() {\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin plain-hover-focus() {\n &,\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin hover-focus-active() {\n &:hover,\n &:focus,\n &:active {\n @content;\n }\n}\n"]} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css new file mode 100644 index 00000000..5308df61 --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css @@ -0,0 +1,8 @@ +/*! + * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) + */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} +/*# sourceMappingURL=bootstrap-reboot.min.css.map */ \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map new file mode 100644 index 00000000..b8551f7c --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap-reboot.scss","../../scss/_reboot.scss","dist/css/bootstrap-reboot.css","../../scss/vendor/_rfs.scss","bootstrap-reboot.css","../../scss/mixins/_hover.scss"],"names":[],"mappings":"AAAA;;;;;;ACkBA,ECTA,QADA,SDaE,WAAA,WAGF,KACE,YAAA,WACA,YAAA,KACA,yBAAA,KACA,4BAAA,YAMF,QAAA,MAAA,WAAA,OAAA,OAAA,OAAA,OAAA,KAAA,IAAA,QACE,QAAA,MAUF,KACE,OAAA,EACA,YAAA,aAAA,CAAA,kBAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,WAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBEgFI,UAAA,KF9EJ,YAAA,IACA,YAAA,IACA,MAAA,QACA,WAAA,KACA,iBAAA,KGlBF,0CH+BE,QAAA,YASF,GACE,WAAA,YACA,OAAA,EACA,SAAA,QAaF,GAAA,GAAA,GAAA,GAAA,GAAA,GACE,WAAA,EACA,cAAA,MAOF,EACE,WAAA,EACA,cAAA,KC9CF,0BDyDA,YAEE,gBAAA,UACA,wBAAA,UAAA,OAAA,gBAAA,UAAA,OACA,OAAA,KACA,cAAA,EACA,iCAAA,KAAA,yBAAA,KAGF,QACE,cAAA,KACA,WAAA,OACA,YAAA,QCnDF,GDsDA,GCvDA,GD0DE,WAAA,EACA,cAAA,KAGF,MCtDA,MACA,MAFA,MD2DE,cAAA,EAGF,GACE,YAAA,IAGF,GACE,cAAA,MACA,YAAA,EAGF,WACE,OAAA,EAAA,EAAA,KAGF,ECvDA,ODyDE,YAAA,OAGF,MExFI,UAAA,IFiGJ,IC5DA,ID8DE,SAAA,SEnGE,UAAA,IFqGF,YAAA,EACA,eAAA,SAGF,IAAM,OAAA,OACN,IAAM,IAAA,MAON,EACE,MAAA,QACA,gBAAA,KACA,iBAAA,YIhLA,QJmLE,MAAA,QACA,gBAAA,UASJ,cACE,MAAA,QACA,gBAAA,KI/LA,oBJkME,MAAA,QACA,gBAAA,KC7DJ,KACA,IDqEA,ICpEA,KDwEE,YAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,UEpJE,UAAA,IFwJJ,IAEE,WAAA,EAEA,cAAA,KAEA,SAAA,KAQF,OAEE,OAAA,EAAA,EAAA,KAQF,IACE,eAAA,OACA,aAAA,KAGF,IAGE,SAAA,OACA,eAAA,OAQF,MACE,gBAAA,SAGF,QACE,YAAA,OACA,eAAA,OACA,MAAA,QACA,WAAA,KACA,aAAA,OAGF,GAGE,WAAA,QAQF,MAEE,QAAA,aACA,cAAA,MAMF,OAEE,cAAA,EAOF,aACE,QAAA,IAAA,OACA,QAAA,IAAA,KAAA,yBCxGF,OD2GA,MCzGA,SADA,OAEA,SD6GE,OAAA,EACA,YAAA,QErPE,UAAA,QFuPF,YAAA,QAGF,OC3GA,MD6GE,SAAA,QAGF,OC3GA,OD6GE,eAAA,KAMF,OACE,UAAA,OC3GF,cACA,aACA,cDgHA,OAIE,mBAAA,OC/GF,6BACA,4BACA,6BDkHE,sBAKI,OAAA,QClHN,gCACA,+BACA,gCDsHA,yBAIE,QAAA,EACA,aAAA,KCrHF,qBDwHA,kBAEE,WAAA,WACA,QAAA,EAIF,iBCxHA,2BACA,kBAFA,iBDkIE,mBAAA,QAGF,SACE,SAAA,KAEA,OAAA,SAGF,SAME,UAAA,EAEA,QAAA,EACA,OAAA,EACA,OAAA,EAKF,OACE,QAAA,MACA,MAAA,KACA,UAAA,KACA,QAAA,EACA,cAAA,MEjSI,UAAA,OFmSJ,YAAA,QACA,MAAA,QACA,YAAA,OAGF,SACE,eAAA,SGvIF,yCFGA,yCD0IE,OAAA,KGxIF,cHgJE,eAAA,KACA,mBAAA,KG5IF,yCHoJE,mBAAA,KAQF,6BACE,KAAA,QACA,mBAAA,OAOF,OACE,QAAA,aAGF,QACE,QAAA,UACA,OAAA,QAGF,SACE,QAAA,KGzJF,SH+JE,QAAA","sourcesContent":["/*!\n * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"reboot\";\n","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -webkit-tap-highlight-color: rgba($black, 0); // 5\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\n// TODO: remove in v5\n// stylelint-disable-next-line selector-list-comma-newline-after\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use\n// the `inherit` value on things like `` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline\n// on elements that programmatically receive focus but wouldn't normally show a visible\n// focus outline. In general, this would mean that the outline is only applied if the\n// interaction that led to the element receiving programmatic focus was a keyboard interaction,\n// or the browser has somehow determined that the user is primarily a keyboard user and/or\n// wants focus outlines to always be presented.\n//\n// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible\n// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `

    `-`

    ` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

    `s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n\n @include hover() {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `` alignment by inheriting from the ``, or the\n // closest parent with a set `text-align`.\n text-align: inherit;\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n // stylelint-disable-next-line property-blacklist\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// Remove the inheritance of word-wrap in Safari.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24990\nselect {\n word-wrap: normal;\n}\n\n\n// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n// controls in Android 4.\n// 2. Correct the inability to style clickable types in iOS and Safari.\nbutton,\n[type=\"button\"], // 1\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button; // 2\n}\n\n// Opinionated: add \"hand\" cursor to non-disabled button elements.\n@if $enable-pointer-cursor-for-buttons {\n button,\n [type=\"button\"],\n [type=\"reset\"],\n [type=\"submit\"] {\n &:not(:disabled) {\n cursor: pointer;\n }\n }\n}\n\n// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box; // 1. Add the correct box sizing in IE 10-\n padding: 0; // 2. Remove the padding in IE 10-\n}\n\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n // Remove the default appearance of temporal inputs to avoid a Mobile Safari\n // bug where setting a custom line-height prevents text from being vertically\n // centered within the input.\n // See https://bugs.webkit.org/show_bug.cgi?id=139848\n // and https://github.com/twbs/bootstrap/issues/11266\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto; // Remove the default vertical scrollbar in IE.\n // Textareas should really only resize vertically so they don't break their (horizontal) containers.\n resize: vertical;\n}\n\nfieldset {\n // Browsers set a default `min-width: min-content;` on fieldsets,\n // unlike e.g. `

    `s, which have `min-width: 0;` by default.\n // So we reset that to ensure fieldsets behave more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359\n // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements\n min-width: 0;\n // Reset the default outline behavior of fieldsets so they don't affect page layout.\n padding: 0;\n margin: 0;\n border: 0;\n}\n\n// 1. Correct the text wrapping in Edge and IE.\n// 2. Correct the color inheritance from `fieldset` elements in IE.\nlegend {\n display: block;\n width: 100%;\n max-width: 100%; // 1\n padding: 0;\n margin-bottom: .5rem;\n @include font-size(1.5rem);\n line-height: inherit;\n color: inherit; // 2\n white-space: normal; // 1\n}\n\nprogress {\n vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.\n}\n\n// Correct the cursor style of increment and decrement buttons in Chrome.\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n outline-offset: -2px; // 2. Correct the outline style in Safari.\n -webkit-appearance: none;\n}\n\n//\n// Remove the inner padding in Chrome and Safari on macOS.\n//\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// 1. Correct the inability to style clickable types in iOS and Safari.\n// 2. Change font properties to `inherit` in Safari.\n//\n\n::-webkit-file-upload-button {\n font: inherit; // 2\n -webkit-appearance: button; // 1\n}\n\n//\n// Correct element displays\n//\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item; // Add the correct display in all browsers\n cursor: pointer;\n}\n\ntemplate {\n display: none; // Add the correct display in IE\n}\n\n// Always hide an element with the `hidden` HTML attribute (from PureCSS).\n// Needed for proper display in IE 10-.\n[hidden] {\n display: none !important;\n}\n","/*!\n * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n -webkit-text-decoration-skip-ink: none;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg {\n overflow: hidden;\n vertical-align: middle;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nselect {\n word-wrap: normal;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton:not(:disabled),\n[type=\"button\"]:not(:disabled),\n[type=\"reset\"]:not(:disabled),\n[type=\"submit\"]:not(:disabled) {\n cursor: pointer;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n/*# sourceMappingURL=bootstrap-reboot.css.map */","// stylelint-disable property-blacklist, scss/dollar-variable-default\n\n// SCSS RFS mixin\n//\n// Automated font-resizing\n//\n// See https://github.com/twbs/rfs\n\n// Configuration\n\n// Base font size\n$rfs-base-font-size: 1.25rem !default;\n$rfs-font-size-unit: rem !default;\n\n// Breakpoint at where font-size starts decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n// Resize font-size based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != \"number\" or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-responsive-font-sizes to false\n$enable-responsive-font-sizes: true !default;\n\n// Cache $rfs-base-font-size unit\n$rfs-base-font-size-unit: unit($rfs-base-font-size);\n\n// Remove px-unit from $rfs-base-font-size for calculations\n@if $rfs-base-font-size-unit == \"px\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1);\n}\n@else if $rfs-base-font-size-unit == \"rem\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1 / $rfs-rem-value);\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == \"px\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == \"rem\" or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1 / $rfs-rem-value);\n}\n\n// Responsive font-size mixin\n@mixin rfs($fs, $important: false) {\n // Cache $fs unit\n $fs-unit: if(type-of($fs) == \"number\", unit($fs), false);\n\n // Add !important suffix if needed\n $rfs-suffix: if($important, \" !important\", \"\");\n\n // If $fs isn't a number (like inherit) or $fs has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $fs-unit or $fs-unit != \"\" and $fs-unit != \"px\" and $fs-unit != \"rem\" or $fs == 0 {\n font-size: #{$fs}#{$rfs-suffix};\n }\n @else {\n // Variables for storing static and fluid rescaling\n $rfs-static: null;\n $rfs-fluid: null;\n\n // Remove px-unit from $fs for calculations\n @if $fs-unit == \"px\" {\n $fs: $fs / ($fs * 0 + 1);\n }\n @else if $fs-unit == \"rem\" {\n $fs: $fs / ($fs * 0 + 1 / $rfs-rem-value);\n }\n\n // Set default font-size\n @if $rfs-font-size-unit == rem {\n $rfs-static: #{$fs / $rfs-rem-value}rem#{$rfs-suffix};\n }\n @else if $rfs-font-size-unit == px {\n $rfs-static: #{$fs}px#{$rfs-suffix};\n }\n @else {\n @error \"`#{$rfs-font-size-unit}` is not a valid unit for $rfs-font-size-unit. Use `px` or `rem`.\";\n }\n\n // Only add media query if font-size is bigger as the minimum font-size\n // If $rfs-factor == 1, no rescaling will take place\n @if $fs > $rfs-base-font-size and $enable-responsive-font-sizes {\n $min-width: null;\n $variable-unit: null;\n\n // Calculate minimum font-size for given font-size\n $fs-min: $rfs-base-font-size + ($fs - $rfs-base-font-size) / $rfs-factor;\n\n // Calculate difference between given font-size and minimum font-size for given font-size\n $fs-diff: $fs - $fs-min;\n\n // Base font-size formatting\n // No need to check if the unit is valid, because we did that before\n $min-width: if($rfs-font-size-unit == rem, #{$fs-min / $rfs-rem-value}rem, #{$fs-min}px);\n\n // If two-dimensional, use smallest of screen width and height\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{$fs-diff * 100 / $rfs-breakpoint}#{$variable-unit};\n\n // Set the calculated font-size.\n $rfs-fluid: calc(#{$min-width} + #{$variable-width}) #{$rfs-suffix};\n }\n\n // Rendering\n @if $rfs-fluid == null {\n // Only render static font-size if no fluid font-size is available\n font-size: $rfs-static;\n }\n @else {\n $mq-value: null;\n\n // RFS breakpoint formatting\n @if $rfs-breakpoint-unit == em or $rfs-breakpoint-unit == rem {\n $mq-value: #{$rfs-breakpoint / $rfs-rem-value}#{$rfs-breakpoint-unit};\n }\n @else if $rfs-breakpoint-unit == px {\n $mq-value: #{$rfs-breakpoint}px;\n }\n @else {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n }\n\n @if $rfs-class == \"disable\" {\n // Adding an extra class increases specificity,\n // which prevents the media query to override the font size\n &,\n .disable-responsive-font-size &,\n &.disable-responsive-font-size {\n font-size: $rfs-static;\n }\n }\n @else {\n font-size: $rfs-static;\n }\n\n @if $rfs-two-dimensional {\n @media (max-width: #{$mq-value}), (max-height: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n @else {\n @media (max-width: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n }\n }\n}\n\n// The font-size & responsive-font-size mixin uses RFS to rescale font sizes\n@mixin font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n\n@mixin responsive-font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n","/*!\n * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg {\n overflow: hidden;\n vertical-align: middle;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nselect {\n word-wrap: normal;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton:not(:disabled),\n[type=\"button\"]:not(:disabled),\n[type=\"reset\"]:not(:disabled),\n[type=\"submit\"]:not(:disabled) {\n cursor: pointer;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\n/*# sourceMappingURL=bootstrap-reboot.css.map */","// Hover mixin and `$enable-hover-media-query` are deprecated.\n//\n// Originally added during our alphas and maintained during betas, this mixin was\n// designed to prevent `:hover` stickiness on iOS-an issue where hover styles\n// would persist after initial touch.\n//\n// For backward compatibility, we've kept these mixins and updated them to\n// always return their regular pseudo-classes instead of a shimmed media query.\n//\n// Issue: https://github.com/twbs/bootstrap/issues/25195\n\n@mixin hover() {\n &:hover { @content; }\n}\n\n@mixin hover-focus() {\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin plain-hover-focus() {\n &,\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin hover-focus-active() {\n &:hover,\n &:focus,\n &:active {\n @content;\n }\n}\n"]} \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap.css b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap.css new file mode 100644 index 00000000..8eac957a --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap.css @@ -0,0 +1,10224 @@ +/*! + * Bootstrap v4.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +:root { + --blue: #007bff; + --indigo: #6610f2; + --purple: #6f42c1; + --pink: #e83e8c; + --red: #dc3545; + --orange: #fd7e14; + --yellow: #ffc107; + --green: #28a745; + --teal: #20c997; + --cyan: #17a2b8; + --white: #fff; + --gray: #6c757d; + --gray-dark: #343a40; + --primary: #007bff; + --secondary: #6c757d; + --success: #28a745; + --info: #17a2b8; + --warning: #ffc107; + --danger: #dc3545; + --light: #f8f9fa; + --dark: #343a40; + --breakpoint-xs: 0; + --breakpoint-sm: 576px; + --breakpoint-md: 768px; + --breakpoint-lg: 992px; + --breakpoint-xl: 1200px; + --font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + line-height: 1.15; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { + display: block; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #212529; + text-align: left; + background-color: #fff; +} + +[tabindex="-1"]:focus:not(:focus-visible) { + outline: 0 !important; +} + +hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} + +h1, h2, h3, h4, h5, h6 { + margin-top: 0; + margin-bottom: 0.5rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title], +abbr[data-original-title] { + text-decoration: underline; + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + cursor: help; + border-bottom: 0; + -webkit-text-decoration-skip-ink: none; + text-decoration-skip-ink: none; +} + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; +} + +dt { + font-weight: 700; +} + +dd { + margin-bottom: .5rem; + margin-left: 0; +} + +blockquote { + margin: 0 0 1rem; +} + +b, +strong { + font-weight: bolder; +} + +small { + font-size: 80%; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sub { + bottom: -.25em; +} + +sup { + top: -.5em; +} + +a { + color: #007bff; + text-decoration: none; + background-color: transparent; +} + +a:hover { + color: #0056b3; + text-decoration: underline; +} + +a:not([href]) { + color: inherit; + text-decoration: none; +} + +a:not([href]):hover { + color: inherit; + text-decoration: none; +} + +pre, +code, +kbd, +samp { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 1em; +} + +pre { + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; +} + +figure { + margin: 0 0 1rem; +} + +img { + vertical-align: middle; + border-style: none; +} + +svg { + overflow: hidden; + vertical-align: middle; +} + +table { + border-collapse: collapse; +} + +caption { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + color: #6c757d; + text-align: left; + caption-side: bottom; +} + +th { + text-align: inherit; +} + +label { + display: inline-block; + margin-bottom: 0.5rem; +} + +button { + border-radius: 0; +} + +button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} + +input, +button, +select, +optgroup, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +input { + overflow: visible; +} + +button, +select { + text-transform: none; +} + +select { + word-wrap: normal; +} + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +button:not(:disabled), +[type="button"]:not(:disabled), +[type="reset"]:not(:disabled), +[type="submit"]:not(:disabled) { + cursor: pointer; +} + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + padding: 0; + border-style: none; +} + +input[type="radio"], +input[type="checkbox"] { + box-sizing: border-box; + padding: 0; +} + +input[type="date"], +input[type="time"], +input[type="datetime-local"], +input[type="month"] { + -webkit-appearance: listbox; +} + +textarea { + overflow: auto; + resize: vertical; +} + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + max-width: 100%; + padding: 0; + margin-bottom: .5rem; + font-size: 1.5rem; + line-height: inherit; + color: inherit; + white-space: normal; +} + +progress { + vertical-align: baseline; +} + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +[type="search"] { + outline-offset: -2px; + -webkit-appearance: none; +} + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-file-upload-button { + font: inherit; + -webkit-appearance: button; +} + +output { + display: inline-block; +} + +summary { + display: list-item; + cursor: pointer; +} + +template { + display: none; +} + +[hidden] { + display: none !important; +} + +h1, h2, h3, h4, h5, h6, +.h1, .h2, .h3, .h4, .h5, .h6 { + margin-bottom: 0.5rem; + font-weight: 500; + line-height: 1.2; +} + +h1, .h1 { + font-size: 2.5rem; +} + +h2, .h2 { + font-size: 2rem; +} + +h3, .h3 { + font-size: 1.75rem; +} + +h4, .h4 { + font-size: 1.5rem; +} + +h5, .h5 { + font-size: 1.25rem; +} + +h6, .h6 { + font-size: 1rem; +} + +.lead { + font-size: 1.25rem; + font-weight: 300; +} + +.display-1 { + font-size: 6rem; + font-weight: 300; + line-height: 1.2; +} + +.display-2 { + font-size: 5.5rem; + font-weight: 300; + line-height: 1.2; +} + +.display-3 { + font-size: 4.5rem; + font-weight: 300; + line-height: 1.2; +} + +.display-4 { + font-size: 3.5rem; + font-weight: 300; + line-height: 1.2; +} + +hr { + margin-top: 1rem; + margin-bottom: 1rem; + border: 0; + border-top: 1px solid rgba(0, 0, 0, 0.1); +} + +small, +.small { + font-size: 80%; + font-weight: 400; +} + +mark, +.mark { + padding: 0.2em; + background-color: #fcf8e3; +} + +.list-unstyled { + padding-left: 0; + list-style: none; +} + +.list-inline { + padding-left: 0; + list-style: none; +} + +.list-inline-item { + display: inline-block; +} + +.list-inline-item:not(:last-child) { + margin-right: 0.5rem; +} + +.initialism { + font-size: 90%; + text-transform: uppercase; +} + +.blockquote { + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.blockquote-footer { + display: block; + font-size: 80%; + color: #6c757d; +} + +.blockquote-footer::before { + content: "\2014\00A0"; +} + +.img-fluid { + max-width: 100%; + height: auto; +} + +.img-thumbnail { + padding: 0.25rem; + background-color: #fff; + border: 1px solid #dee2e6; + border-radius: 0.25rem; + max-width: 100%; + height: auto; +} + +.figure { + display: inline-block; +} + +.figure-img { + margin-bottom: 0.5rem; + line-height: 1; +} + +.figure-caption { + font-size: 90%; + color: #6c757d; +} + +code { + font-size: 87.5%; + color: #e83e8c; + word-wrap: break-word; +} + +a > code { + color: inherit; +} + +kbd { + padding: 0.2rem 0.4rem; + font-size: 87.5%; + color: #fff; + background-color: #212529; + border-radius: 0.2rem; +} + +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: 700; +} + +pre { + display: block; + font-size: 87.5%; + color: #212529; +} + +pre code { + font-size: inherit; + color: inherit; + word-break: normal; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +.container { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1140px; + } +} + +.container-fluid, .container-sm, .container-md, .container-lg, .container-xl { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container, .container-sm { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container, .container-sm, .container-md { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container, .container-sm, .container-md, .container-lg { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container, .container-sm, .container-md, .container-lg, .container-xl { + max-width: 1140px; + } +} + +.row { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; +} + +.no-gutters { + margin-right: 0; + margin-left: 0; +} + +.no-gutters > .col, +.no-gutters > [class*="col-"] { + padding-right: 0; + padding-left: 0; +} + +.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, +.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm, +.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md, +.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg, +.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl, +.col-xl-auto { + position: relative; + width: 100%; + padding-right: 15px; + padding-left: 15px; +} + +.col { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; +} + +.row-cols-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; +} + +.row-cols-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; +} + +.row-cols-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; +} + +.row-cols-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; +} + +.row-cols-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; +} + +.row-cols-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; +} + +.col-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; +} + +.col-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; +} + +.col-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; +} + +.col-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; +} + +.col-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; +} + +.col-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; +} + +.col-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; +} + +.col-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; +} + +.col-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; +} + +.col-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; +} + +.col-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; +} + +.col-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; +} + +.col-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; +} + +.order-first { + -ms-flex-order: -1; + order: -1; +} + +.order-last { + -ms-flex-order: 13; + order: 13; +} + +.order-0 { + -ms-flex-order: 0; + order: 0; +} + +.order-1 { + -ms-flex-order: 1; + order: 1; +} + +.order-2 { + -ms-flex-order: 2; + order: 2; +} + +.order-3 { + -ms-flex-order: 3; + order: 3; +} + +.order-4 { + -ms-flex-order: 4; + order: 4; +} + +.order-5 { + -ms-flex-order: 5; + order: 5; +} + +.order-6 { + -ms-flex-order: 6; + order: 6; +} + +.order-7 { + -ms-flex-order: 7; + order: 7; +} + +.order-8 { + -ms-flex-order: 8; + order: 8; +} + +.order-9 { + -ms-flex-order: 9; + order: 9; +} + +.order-10 { + -ms-flex-order: 10; + order: 10; +} + +.order-11 { + -ms-flex-order: 11; + order: 11; +} + +.order-12 { + -ms-flex-order: 12; + order: 12; +} + +.offset-1 { + margin-left: 8.333333%; +} + +.offset-2 { + margin-left: 16.666667%; +} + +.offset-3 { + margin-left: 25%; +} + +.offset-4 { + margin-left: 33.333333%; +} + +.offset-5 { + margin-left: 41.666667%; +} + +.offset-6 { + margin-left: 50%; +} + +.offset-7 { + margin-left: 58.333333%; +} + +.offset-8 { + margin-left: 66.666667%; +} + +.offset-9 { + margin-left: 75%; +} + +.offset-10 { + margin-left: 83.333333%; +} + +.offset-11 { + margin-left: 91.666667%; +} + +@media (min-width: 576px) { + .col-sm { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-sm-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-sm-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-sm-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-sm-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-sm-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-sm-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-sm-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-sm-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-sm-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-sm-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-sm-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-sm-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-sm-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-sm-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-sm-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-sm-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-sm-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-sm-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-sm-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-sm-first { + -ms-flex-order: -1; + order: -1; + } + .order-sm-last { + -ms-flex-order: 13; + order: 13; + } + .order-sm-0 { + -ms-flex-order: 0; + order: 0; + } + .order-sm-1 { + -ms-flex-order: 1; + order: 1; + } + .order-sm-2 { + -ms-flex-order: 2; + order: 2; + } + .order-sm-3 { + -ms-flex-order: 3; + order: 3; + } + .order-sm-4 { + -ms-flex-order: 4; + order: 4; + } + .order-sm-5 { + -ms-flex-order: 5; + order: 5; + } + .order-sm-6 { + -ms-flex-order: 6; + order: 6; + } + .order-sm-7 { + -ms-flex-order: 7; + order: 7; + } + .order-sm-8 { + -ms-flex-order: 8; + order: 8; + } + .order-sm-9 { + -ms-flex-order: 9; + order: 9; + } + .order-sm-10 { + -ms-flex-order: 10; + order: 10; + } + .order-sm-11 { + -ms-flex-order: 11; + order: 11; + } + .order-sm-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-sm-0 { + margin-left: 0; + } + .offset-sm-1 { + margin-left: 8.333333%; + } + .offset-sm-2 { + margin-left: 16.666667%; + } + .offset-sm-3 { + margin-left: 25%; + } + .offset-sm-4 { + margin-left: 33.333333%; + } + .offset-sm-5 { + margin-left: 41.666667%; + } + .offset-sm-6 { + margin-left: 50%; + } + .offset-sm-7 { + margin-left: 58.333333%; + } + .offset-sm-8 { + margin-left: 66.666667%; + } + .offset-sm-9 { + margin-left: 75%; + } + .offset-sm-10 { + margin-left: 83.333333%; + } + .offset-sm-11 { + margin-left: 91.666667%; + } +} + +@media (min-width: 768px) { + .col-md { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-md-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-md-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-md-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-md-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-md-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-md-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-md-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-md-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-md-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-md-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-md-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-md-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-md-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-md-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-md-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-md-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-md-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-md-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-md-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-md-first { + -ms-flex-order: -1; + order: -1; + } + .order-md-last { + -ms-flex-order: 13; + order: 13; + } + .order-md-0 { + -ms-flex-order: 0; + order: 0; + } + .order-md-1 { + -ms-flex-order: 1; + order: 1; + } + .order-md-2 { + -ms-flex-order: 2; + order: 2; + } + .order-md-3 { + -ms-flex-order: 3; + order: 3; + } + .order-md-4 { + -ms-flex-order: 4; + order: 4; + } + .order-md-5 { + -ms-flex-order: 5; + order: 5; + } + .order-md-6 { + -ms-flex-order: 6; + order: 6; + } + .order-md-7 { + -ms-flex-order: 7; + order: 7; + } + .order-md-8 { + -ms-flex-order: 8; + order: 8; + } + .order-md-9 { + -ms-flex-order: 9; + order: 9; + } + .order-md-10 { + -ms-flex-order: 10; + order: 10; + } + .order-md-11 { + -ms-flex-order: 11; + order: 11; + } + .order-md-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-md-0 { + margin-left: 0; + } + .offset-md-1 { + margin-left: 8.333333%; + } + .offset-md-2 { + margin-left: 16.666667%; + } + .offset-md-3 { + margin-left: 25%; + } + .offset-md-4 { + margin-left: 33.333333%; + } + .offset-md-5 { + margin-left: 41.666667%; + } + .offset-md-6 { + margin-left: 50%; + } + .offset-md-7 { + margin-left: 58.333333%; + } + .offset-md-8 { + margin-left: 66.666667%; + } + .offset-md-9 { + margin-left: 75%; + } + .offset-md-10 { + margin-left: 83.333333%; + } + .offset-md-11 { + margin-left: 91.666667%; + } +} + +@media (min-width: 992px) { + .col-lg { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-lg-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-lg-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-lg-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-lg-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-lg-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-lg-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-lg-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-lg-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-lg-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-lg-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-lg-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-lg-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-lg-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-lg-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-lg-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-lg-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-lg-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-lg-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-lg-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-lg-first { + -ms-flex-order: -1; + order: -1; + } + .order-lg-last { + -ms-flex-order: 13; + order: 13; + } + .order-lg-0 { + -ms-flex-order: 0; + order: 0; + } + .order-lg-1 { + -ms-flex-order: 1; + order: 1; + } + .order-lg-2 { + -ms-flex-order: 2; + order: 2; + } + .order-lg-3 { + -ms-flex-order: 3; + order: 3; + } + .order-lg-4 { + -ms-flex-order: 4; + order: 4; + } + .order-lg-5 { + -ms-flex-order: 5; + order: 5; + } + .order-lg-6 { + -ms-flex-order: 6; + order: 6; + } + .order-lg-7 { + -ms-flex-order: 7; + order: 7; + } + .order-lg-8 { + -ms-flex-order: 8; + order: 8; + } + .order-lg-9 { + -ms-flex-order: 9; + order: 9; + } + .order-lg-10 { + -ms-flex-order: 10; + order: 10; + } + .order-lg-11 { + -ms-flex-order: 11; + order: 11; + } + .order-lg-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-lg-0 { + margin-left: 0; + } + .offset-lg-1 { + margin-left: 8.333333%; + } + .offset-lg-2 { + margin-left: 16.666667%; + } + .offset-lg-3 { + margin-left: 25%; + } + .offset-lg-4 { + margin-left: 33.333333%; + } + .offset-lg-5 { + margin-left: 41.666667%; + } + .offset-lg-6 { + margin-left: 50%; + } + .offset-lg-7 { + margin-left: 58.333333%; + } + .offset-lg-8 { + margin-left: 66.666667%; + } + .offset-lg-9 { + margin-left: 75%; + } + .offset-lg-10 { + margin-left: 83.333333%; + } + .offset-lg-11 { + margin-left: 91.666667%; + } +} + +@media (min-width: 1200px) { + .col-xl { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-xl-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-xl-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-xl-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-xl-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-xl-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-xl-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-xl-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-xl-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-xl-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-xl-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-xl-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-xl-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-xl-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-xl-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-xl-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-xl-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-xl-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-xl-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-xl-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-xl-first { + -ms-flex-order: -1; + order: -1; + } + .order-xl-last { + -ms-flex-order: 13; + order: 13; + } + .order-xl-0 { + -ms-flex-order: 0; + order: 0; + } + .order-xl-1 { + -ms-flex-order: 1; + order: 1; + } + .order-xl-2 { + -ms-flex-order: 2; + order: 2; + } + .order-xl-3 { + -ms-flex-order: 3; + order: 3; + } + .order-xl-4 { + -ms-flex-order: 4; + order: 4; + } + .order-xl-5 { + -ms-flex-order: 5; + order: 5; + } + .order-xl-6 { + -ms-flex-order: 6; + order: 6; + } + .order-xl-7 { + -ms-flex-order: 7; + order: 7; + } + .order-xl-8 { + -ms-flex-order: 8; + order: 8; + } + .order-xl-9 { + -ms-flex-order: 9; + order: 9; + } + .order-xl-10 { + -ms-flex-order: 10; + order: 10; + } + .order-xl-11 { + -ms-flex-order: 11; + order: 11; + } + .order-xl-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-xl-0 { + margin-left: 0; + } + .offset-xl-1 { + margin-left: 8.333333%; + } + .offset-xl-2 { + margin-left: 16.666667%; + } + .offset-xl-3 { + margin-left: 25%; + } + .offset-xl-4 { + margin-left: 33.333333%; + } + .offset-xl-5 { + margin-left: 41.666667%; + } + .offset-xl-6 { + margin-left: 50%; + } + .offset-xl-7 { + margin-left: 58.333333%; + } + .offset-xl-8 { + margin-left: 66.666667%; + } + .offset-xl-9 { + margin-left: 75%; + } + .offset-xl-10 { + margin-left: 83.333333%; + } + .offset-xl-11 { + margin-left: 91.666667%; + } +} + +.table { + width: 100%; + margin-bottom: 1rem; + color: #212529; +} + +.table th, +.table td { + padding: 0.75rem; + vertical-align: top; + border-top: 1px solid #dee2e6; +} + +.table thead th { + vertical-align: bottom; + border-bottom: 2px solid #dee2e6; +} + +.table tbody + tbody { + border-top: 2px solid #dee2e6; +} + +.table-sm th, +.table-sm td { + padding: 0.3rem; +} + +.table-bordered { + border: 1px solid #dee2e6; +} + +.table-bordered th, +.table-bordered td { + border: 1px solid #dee2e6; +} + +.table-bordered thead th, +.table-bordered thead td { + border-bottom-width: 2px; +} + +.table-borderless th, +.table-borderless td, +.table-borderless thead th, +.table-borderless tbody + tbody { + border: 0; +} + +.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(0, 0, 0, 0.05); +} + +.table-hover tbody tr:hover { + color: #212529; + background-color: rgba(0, 0, 0, 0.075); +} + +.table-primary, +.table-primary > th, +.table-primary > td { + background-color: #b8daff; +} + +.table-primary th, +.table-primary td, +.table-primary thead th, +.table-primary tbody + tbody { + border-color: #7abaff; +} + +.table-hover .table-primary:hover { + background-color: #9fcdff; +} + +.table-hover .table-primary:hover > td, +.table-hover .table-primary:hover > th { + background-color: #9fcdff; +} + +.table-secondary, +.table-secondary > th, +.table-secondary > td { + background-color: #d6d8db; +} + +.table-secondary th, +.table-secondary td, +.table-secondary thead th, +.table-secondary tbody + tbody { + border-color: #b3b7bb; +} + +.table-hover .table-secondary:hover { + background-color: #c8cbcf; +} + +.table-hover .table-secondary:hover > td, +.table-hover .table-secondary:hover > th { + background-color: #c8cbcf; +} + +.table-success, +.table-success > th, +.table-success > td { + background-color: #c3e6cb; +} + +.table-success th, +.table-success td, +.table-success thead th, +.table-success tbody + tbody { + border-color: #8fd19e; +} + +.table-hover .table-success:hover { + background-color: #b1dfbb; +} + +.table-hover .table-success:hover > td, +.table-hover .table-success:hover > th { + background-color: #b1dfbb; +} + +.table-info, +.table-info > th, +.table-info > td { + background-color: #bee5eb; +} + +.table-info th, +.table-info td, +.table-info thead th, +.table-info tbody + tbody { + border-color: #86cfda; +} + +.table-hover .table-info:hover { + background-color: #abdde5; +} + +.table-hover .table-info:hover > td, +.table-hover .table-info:hover > th { + background-color: #abdde5; +} + +.table-warning, +.table-warning > th, +.table-warning > td { + background-color: #ffeeba; +} + +.table-warning th, +.table-warning td, +.table-warning thead th, +.table-warning tbody + tbody { + border-color: #ffdf7e; +} + +.table-hover .table-warning:hover { + background-color: #ffe8a1; +} + +.table-hover .table-warning:hover > td, +.table-hover .table-warning:hover > th { + background-color: #ffe8a1; +} + +.table-danger, +.table-danger > th, +.table-danger > td { + background-color: #f5c6cb; +} + +.table-danger th, +.table-danger td, +.table-danger thead th, +.table-danger tbody + tbody { + border-color: #ed969e; +} + +.table-hover .table-danger:hover { + background-color: #f1b0b7; +} + +.table-hover .table-danger:hover > td, +.table-hover .table-danger:hover > th { + background-color: #f1b0b7; +} + +.table-light, +.table-light > th, +.table-light > td { + background-color: #fdfdfe; +} + +.table-light th, +.table-light td, +.table-light thead th, +.table-light tbody + tbody { + border-color: #fbfcfc; +} + +.table-hover .table-light:hover { + background-color: #ececf6; +} + +.table-hover .table-light:hover > td, +.table-hover .table-light:hover > th { + background-color: #ececf6; +} + +.table-dark, +.table-dark > th, +.table-dark > td { + background-color: #c6c8ca; +} + +.table-dark th, +.table-dark td, +.table-dark thead th, +.table-dark tbody + tbody { + border-color: #95999c; +} + +.table-hover .table-dark:hover { + background-color: #b9bbbe; +} + +.table-hover .table-dark:hover > td, +.table-hover .table-dark:hover > th { + background-color: #b9bbbe; +} + +.table-active, +.table-active > th, +.table-active > td { + background-color: rgba(0, 0, 0, 0.075); +} + +.table-hover .table-active:hover { + background-color: rgba(0, 0, 0, 0.075); +} + +.table-hover .table-active:hover > td, +.table-hover .table-active:hover > th { + background-color: rgba(0, 0, 0, 0.075); +} + +.table .thead-dark th { + color: #fff; + background-color: #343a40; + border-color: #454d55; +} + +.table .thead-light th { + color: #495057; + background-color: #e9ecef; + border-color: #dee2e6; +} + +.table-dark { + color: #fff; + background-color: #343a40; +} + +.table-dark th, +.table-dark td, +.table-dark thead th { + border-color: #454d55; +} + +.table-dark.table-bordered { + border: 0; +} + +.table-dark.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(255, 255, 255, 0.05); +} + +.table-dark.table-hover tbody tr:hover { + color: #fff; + background-color: rgba(255, 255, 255, 0.075); +} + +@media (max-width: 575.98px) { + .table-responsive-sm { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + .table-responsive-sm > .table-bordered { + border: 0; + } +} + +@media (max-width: 767.98px) { + .table-responsive-md { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + .table-responsive-md > .table-bordered { + border: 0; + } +} + +@media (max-width: 991.98px) { + .table-responsive-lg { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + .table-responsive-lg > .table-bordered { + border: 0; + } +} + +@media (max-width: 1199.98px) { + .table-responsive-xl { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + .table-responsive-xl > .table-bordered { + border: 0; + } +} + +.table-responsive { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.table-responsive > .table-bordered { + border: 0; +} + +.form-control { + display: block; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + padding: 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #ced4da; + border-radius: 0.25rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .form-control { + transition: none; + } +} + +.form-control::-ms-expand { + background-color: transparent; + border: 0; +} + +.form-control:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #495057; +} + +.form-control:focus { + color: #495057; + background-color: #fff; + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.form-control::-webkit-input-placeholder { + color: #6c757d; + opacity: 1; +} + +.form-control::-moz-placeholder { + color: #6c757d; + opacity: 1; +} + +.form-control:-ms-input-placeholder { + color: #6c757d; + opacity: 1; +} + +.form-control::-ms-input-placeholder { + color: #6c757d; + opacity: 1; +} + +.form-control::placeholder { + color: #6c757d; + opacity: 1; +} + +.form-control:disabled, .form-control[readonly] { + background-color: #e9ecef; + opacity: 1; +} + +select.form-control:focus::-ms-value { + color: #495057; + background-color: #fff; +} + +.form-control-file, +.form-control-range { + display: block; + width: 100%; +} + +.col-form-label { + padding-top: calc(0.375rem + 1px); + padding-bottom: calc(0.375rem + 1px); + margin-bottom: 0; + font-size: inherit; + line-height: 1.5; +} + +.col-form-label-lg { + padding-top: calc(0.5rem + 1px); + padding-bottom: calc(0.5rem + 1px); + font-size: 1.25rem; + line-height: 1.5; +} + +.col-form-label-sm { + padding-top: calc(0.25rem + 1px); + padding-bottom: calc(0.25rem + 1px); + font-size: 0.875rem; + line-height: 1.5; +} + +.form-control-plaintext { + display: block; + width: 100%; + padding: 0.375rem 0; + margin-bottom: 0; + font-size: 1rem; + line-height: 1.5; + color: #212529; + background-color: transparent; + border: solid transparent; + border-width: 1px 0; +} + +.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg { + padding-right: 0; + padding-left: 0; +} + +.form-control-sm { + height: calc(1.5em + 0.5rem + 2px); + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.form-control-lg { + height: calc(1.5em + 1rem + 2px); + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +select.form-control[size], select.form-control[multiple] { + height: auto; +} + +textarea.form-control { + height: auto; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-text { + display: block; + margin-top: 0.25rem; +} + +.form-row { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-right: -5px; + margin-left: -5px; +} + +.form-row > .col, +.form-row > [class*="col-"] { + padding-right: 5px; + padding-left: 5px; +} + +.form-check { + position: relative; + display: block; + padding-left: 1.25rem; +} + +.form-check-input { + position: absolute; + margin-top: 0.3rem; + margin-left: -1.25rem; +} + +.form-check-input[disabled] ~ .form-check-label, +.form-check-input:disabled ~ .form-check-label { + color: #6c757d; +} + +.form-check-label { + margin-bottom: 0; +} + +.form-check-inline { + display: -ms-inline-flexbox; + display: inline-flex; + -ms-flex-align: center; + align-items: center; + padding-left: 0; + margin-right: 0.75rem; +} + +.form-check-inline .form-check-input { + position: static; + margin-top: 0; + margin-right: 0.3125rem; + margin-left: 0; +} + +.valid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 80%; + color: #28a745; +} + +.valid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: .1rem; + font-size: 0.875rem; + line-height: 1.5; + color: #fff; + background-color: rgba(40, 167, 69, 0.9); + border-radius: 0.25rem; +} + +.was-validated :valid ~ .valid-feedback, +.was-validated :valid ~ .valid-tooltip, +.is-valid ~ .valid-feedback, +.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .form-control:valid, .form-control.is-valid { + border-color: #28a745; + padding-right: calc(1.5em + 0.75rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .form-control:valid:focus, .form-control.is-valid:focus { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.was-validated textarea.form-control:valid, textarea.form-control.is-valid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .custom-select:valid, .custom-select.is-valid { + border-color: #28a745; + padding-right: calc(0.75em + 2.3125rem); + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px, url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .custom-select:valid:focus, .custom-select.is-valid:focus { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { + color: #28a745; +} + +.was-validated .form-check-input:valid ~ .valid-feedback, +.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback, +.form-check-input.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label { + color: #28a745; +} + +.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before { + border-color: #28a745; +} + +.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before { + border-color: #34ce57; + background-color: #34ce57; +} + +.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before { + border-color: #28a745; +} + +.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label { + border-color: #28a745; +} + +.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.invalid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 80%; + color: #dc3545; +} + +.invalid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: .1rem; + font-size: 0.875rem; + line-height: 1.5; + color: #fff; + background-color: rgba(220, 53, 69, 0.9); + border-radius: 0.25rem; +} + +.was-validated :invalid ~ .invalid-feedback, +.was-validated :invalid ~ .invalid-tooltip, +.is-invalid ~ .invalid-feedback, +.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .form-control:invalid, .form-control.is-invalid { + border-color: #dc3545; + padding-right: calc(1.5em + 0.75rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .custom-select:invalid, .custom-select.is-invalid { + border-color: #dc3545; + padding-right: calc(0.75em + 2.3125rem); + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px, url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { + color: #dc3545; +} + +.was-validated .form-check-input:invalid ~ .invalid-feedback, +.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback, +.form-check-input.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label { + color: #dc3545; +} + +.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before { + border-color: #dc3545; +} + +.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before { + border-color: #e4606d; + background-color: #e4606d; +} + +.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before { + border-color: #dc3545; +} + +.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label { + border-color: #dc3545; +} + +.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.form-inline { + display: -ms-flexbox; + display: flex; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + -ms-flex-align: center; + align-items: center; +} + +.form-inline .form-check { + width: 100%; +} + +@media (min-width: 576px) { + .form-inline label { + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: center; + justify-content: center; + margin-bottom: 0; + } + .form-inline .form-group { + display: -ms-flexbox; + display: flex; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + -ms-flex-align: center; + align-items: center; + margin-bottom: 0; + } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .form-inline .form-control-plaintext { + display: inline-block; + } + .form-inline .input-group, + .form-inline .custom-select { + width: auto; + } + .form-inline .form-check { + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: center; + justify-content: center; + width: auto; + padding-left: 0; + } + .form-inline .form-check-input { + position: relative; + -ms-flex-negative: 0; + flex-shrink: 0; + margin-top: 0; + margin-right: 0.25rem; + margin-left: 0; + } + .form-inline .custom-control { + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: center; + justify-content: center; + } + .form-inline .custom-control-label { + margin-bottom: 0; + } +} + +.btn { + display: inline-block; + font-weight: 400; + color: #212529; + text-align: center; + vertical-align: middle; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-color: transparent; + border: 1px solid transparent; + padding: 0.375rem 0.75rem; + font-size: 1rem; + line-height: 1.5; + border-radius: 0.25rem; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .btn { + transition: none; + } +} + +.btn:hover { + color: #212529; + text-decoration: none; +} + +.btn:focus, .btn.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.btn.disabled, .btn:disabled { + opacity: 0.65; +} + +a.btn.disabled, +fieldset:disabled a.btn { + pointer-events: none; +} + +.btn-primary { + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.btn-primary:hover { + color: #fff; + background-color: #0069d9; + border-color: #0062cc; +} + +.btn-primary:focus, .btn-primary.focus { + color: #fff; + background-color: #0069d9; + border-color: #0062cc; + box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5); +} + +.btn-primary.disabled, .btn-primary:disabled { + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active, +.show > .btn-primary.dropdown-toggle { + color: #fff; + background-color: #0062cc; + border-color: #005cbf; +} + +.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus, +.show > .btn-primary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5); +} + +.btn-secondary { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-secondary:hover { + color: #fff; + background-color: #5a6268; + border-color: #545b62; +} + +.btn-secondary:focus, .btn-secondary.focus { + color: #fff; + background-color: #5a6268; + border-color: #545b62; + box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5); +} + +.btn-secondary.disabled, .btn-secondary:disabled { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active, +.show > .btn-secondary.dropdown-toggle { + color: #fff; + background-color: #545b62; + border-color: #4e555b; +} + +.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus, +.show > .btn-secondary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5); +} + +.btn-success { + color: #fff; + background-color: #28a745; + border-color: #28a745; +} + +.btn-success:hover { + color: #fff; + background-color: #218838; + border-color: #1e7e34; +} + +.btn-success:focus, .btn-success.focus { + color: #fff; + background-color: #218838; + border-color: #1e7e34; + box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5); +} + +.btn-success.disabled, .btn-success:disabled { + color: #fff; + background-color: #28a745; + border-color: #28a745; +} + +.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active, +.show > .btn-success.dropdown-toggle { + color: #fff; + background-color: #1e7e34; + border-color: #1c7430; +} + +.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus, +.show > .btn-success.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5); +} + +.btn-info { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} + +.btn-info:hover { + color: #fff; + background-color: #138496; + border-color: #117a8b; +} + +.btn-info:focus, .btn-info.focus { + color: #fff; + background-color: #138496; + border-color: #117a8b; + box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5); +} + +.btn-info.disabled, .btn-info:disabled { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} + +.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active, +.show > .btn-info.dropdown-toggle { + color: #fff; + background-color: #117a8b; + border-color: #10707f; +} + +.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus, +.show > .btn-info.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5); +} + +.btn-warning { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} + +.btn-warning:hover { + color: #212529; + background-color: #e0a800; + border-color: #d39e00; +} + +.btn-warning:focus, .btn-warning.focus { + color: #212529; + background-color: #e0a800; + border-color: #d39e00; + box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5); +} + +.btn-warning.disabled, .btn-warning:disabled { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} + +.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active, +.show > .btn-warning.dropdown-toggle { + color: #212529; + background-color: #d39e00; + border-color: #c69500; +} + +.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus, +.show > .btn-warning.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5); +} + +.btn-danger { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-danger:hover { + color: #fff; + background-color: #c82333; + border-color: #bd2130; +} + +.btn-danger:focus, .btn-danger.focus { + color: #fff; + background-color: #c82333; + border-color: #bd2130; + box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5); +} + +.btn-danger.disabled, .btn-danger:disabled { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active, +.show > .btn-danger.dropdown-toggle { + color: #fff; + background-color: #bd2130; + border-color: #b21f2d; +} + +.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus, +.show > .btn-danger.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5); +} + +.btn-light { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; +} + +.btn-light:hover { + color: #212529; + background-color: #e2e6ea; + border-color: #dae0e5; +} + +.btn-light:focus, .btn-light.focus { + color: #212529; + background-color: #e2e6ea; + border-color: #dae0e5; + box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5); +} + +.btn-light.disabled, .btn-light:disabled { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; +} + +.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active, +.show > .btn-light.dropdown-toggle { + color: #212529; + background-color: #dae0e5; + border-color: #d3d9df; +} + +.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus, +.show > .btn-light.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5); +} + +.btn-dark { + color: #fff; + background-color: #343a40; + border-color: #343a40; +} + +.btn-dark:hover { + color: #fff; + background-color: #23272b; + border-color: #1d2124; +} + +.btn-dark:focus, .btn-dark.focus { + color: #fff; + background-color: #23272b; + border-color: #1d2124; + box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5); +} + +.btn-dark.disabled, .btn-dark:disabled { + color: #fff; + background-color: #343a40; + border-color: #343a40; +} + +.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active, +.show > .btn-dark.dropdown-toggle { + color: #fff; + background-color: #1d2124; + border-color: #171a1d; +} + +.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus, +.show > .btn-dark.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5); +} + +.btn-outline-primary { + color: #007bff; + border-color: #007bff; +} + +.btn-outline-primary:hover { + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.btn-outline-primary:focus, .btn-outline-primary.focus { + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); +} + +.btn-outline-primary.disabled, .btn-outline-primary:disabled { + color: #007bff; + background-color: transparent; +} + +.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active, +.show > .btn-outline-primary.dropdown-toggle { + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-primary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); +} + +.btn-outline-secondary { + color: #6c757d; + border-color: #6c757d; +} + +.btn-outline-secondary:hover { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-outline-secondary:focus, .btn-outline-secondary.focus { + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); +} + +.btn-outline-secondary.disabled, .btn-outline-secondary:disabled { + color: #6c757d; + background-color: transparent; +} + +.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active, +.show > .btn-outline-secondary.dropdown-toggle { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-secondary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); +} + +.btn-outline-success { + color: #28a745; + border-color: #28a745; +} + +.btn-outline-success:hover { + color: #fff; + background-color: #28a745; + border-color: #28a745; +} + +.btn-outline-success:focus, .btn-outline-success.focus { + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); +} + +.btn-outline-success.disabled, .btn-outline-success:disabled { + color: #28a745; + background-color: transparent; +} + +.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active, +.show > .btn-outline-success.dropdown-toggle { + color: #fff; + background-color: #28a745; + border-color: #28a745; +} + +.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-success.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); +} + +.btn-outline-info { + color: #17a2b8; + border-color: #17a2b8; +} + +.btn-outline-info:hover { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} + +.btn-outline-info:focus, .btn-outline-info.focus { + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} + +.btn-outline-info.disabled, .btn-outline-info:disabled { + color: #17a2b8; + background-color: transparent; +} + +.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active, +.show > .btn-outline-info.dropdown-toggle { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} + +.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-info.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} + +.btn-outline-warning { + color: #ffc107; + border-color: #ffc107; +} + +.btn-outline-warning:hover { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} + +.btn-outline-warning:focus, .btn-outline-warning.focus { + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); +} + +.btn-outline-warning.disabled, .btn-outline-warning:disabled { + color: #ffc107; + background-color: transparent; +} + +.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active, +.show > .btn-outline-warning.dropdown-toggle { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} + +.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-warning.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); +} + +.btn-outline-danger { + color: #dc3545; + border-color: #dc3545; +} + +.btn-outline-danger:hover { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-outline-danger:focus, .btn-outline-danger.focus { + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); +} + +.btn-outline-danger.disabled, .btn-outline-danger:disabled { + color: #dc3545; + background-color: transparent; +} + +.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active, +.show > .btn-outline-danger.dropdown-toggle { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-danger.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); +} + +.btn-outline-light { + color: #f8f9fa; + border-color: #f8f9fa; +} + +.btn-outline-light:hover { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; +} + +.btn-outline-light:focus, .btn-outline-light.focus { + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); +} + +.btn-outline-light.disabled, .btn-outline-light:disabled { + color: #f8f9fa; + background-color: transparent; +} + +.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active, +.show > .btn-outline-light.dropdown-toggle { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; +} + +.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-light.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); +} + +.btn-outline-dark { + color: #343a40; + border-color: #343a40; +} + +.btn-outline-dark:hover { + color: #fff; + background-color: #343a40; + border-color: #343a40; +} + +.btn-outline-dark:focus, .btn-outline-dark.focus { + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); +} + +.btn-outline-dark.disabled, .btn-outline-dark:disabled { + color: #343a40; + background-color: transparent; +} + +.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active, +.show > .btn-outline-dark.dropdown-toggle { + color: #fff; + background-color: #343a40; + border-color: #343a40; +} + +.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-dark.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); +} + +.btn-link { + font-weight: 400; + color: #007bff; + text-decoration: none; +} + +.btn-link:hover { + color: #0056b3; + text-decoration: underline; +} + +.btn-link:focus, .btn-link.focus { + text-decoration: underline; + box-shadow: none; +} + +.btn-link:disabled, .btn-link.disabled { + color: #6c757d; + pointer-events: none; +} + +.btn-lg, .btn-group-lg > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +.btn-sm, .btn-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.btn-block { + display: block; + width: 100%; +} + +.btn-block + .btn-block { + margin-top: 0.5rem; +} + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} + +.fade { + transition: opacity 0.15s linear; +} + +@media (prefers-reduced-motion: reduce) { + .fade { + transition: none; + } +} + +.fade:not(.show) { + opacity: 0; +} + +.collapse:not(.show) { + display: none; +} + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + transition: height 0.35s ease; +} + +@media (prefers-reduced-motion: reduce) { + .collapsing { + transition: none; + } +} + +.dropup, +.dropright, +.dropdown, +.dropleft { + position: relative; +} + +.dropdown-toggle { + white-space: nowrap; +} + +.dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-bottom: 0; + border-left: 0.3em solid transparent; +} + +.dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 10rem; + padding: 0.5rem 0; + margin: 0.125rem 0 0; + font-size: 1rem; + color: #212529; + text-align: left; + list-style: none; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; +} + +.dropdown-menu-left { + right: auto; + left: 0; +} + +.dropdown-menu-right { + right: 0; + left: auto; +} + +@media (min-width: 576px) { + .dropdown-menu-sm-left { + right: auto; + left: 0; + } + .dropdown-menu-sm-right { + right: 0; + left: auto; + } +} + +@media (min-width: 768px) { + .dropdown-menu-md-left { + right: auto; + left: 0; + } + .dropdown-menu-md-right { + right: 0; + left: auto; + } +} + +@media (min-width: 992px) { + .dropdown-menu-lg-left { + right: auto; + left: 0; + } + .dropdown-menu-lg-right { + right: 0; + left: auto; + } +} + +@media (min-width: 1200px) { + .dropdown-menu-xl-left { + right: auto; + left: 0; + } + .dropdown-menu-xl-right { + right: 0; + left: auto; + } +} + +.dropup .dropdown-menu { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: 0.125rem; +} + +.dropup .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0; + border-right: 0.3em solid transparent; + border-bottom: 0.3em solid; + border-left: 0.3em solid transparent; +} + +.dropup .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropright .dropdown-menu { + top: 0; + right: auto; + left: 100%; + margin-top: 0; + margin-left: 0.125rem; +} + +.dropright .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0; + border-bottom: 0.3em solid transparent; + border-left: 0.3em solid; +} + +.dropright .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropright .dropdown-toggle::after { + vertical-align: 0; +} + +.dropleft .dropdown-menu { + top: 0; + right: 100%; + left: auto; + margin-top: 0; + margin-right: 0.125rem; +} + +.dropleft .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; +} + +.dropleft .dropdown-toggle::after { + display: none; +} + +.dropleft .dropdown-toggle::before { + display: inline-block; + margin-right: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0.3em solid; + border-bottom: 0.3em solid transparent; +} + +.dropleft .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropleft .dropdown-toggle::before { + vertical-align: 0; +} + +.dropdown-menu[x-placement^="top"], .dropdown-menu[x-placement^="right"], .dropdown-menu[x-placement^="bottom"], .dropdown-menu[x-placement^="left"] { + right: auto; + bottom: auto; +} + +.dropdown-divider { + height: 0; + margin: 0.5rem 0; + overflow: hidden; + border-top: 1px solid #e9ecef; +} + +.dropdown-item { + display: block; + width: 100%; + padding: 0.25rem 1.5rem; + clear: both; + font-weight: 400; + color: #212529; + text-align: inherit; + white-space: nowrap; + background-color: transparent; + border: 0; +} + +.dropdown-item:hover, .dropdown-item:focus { + color: #16181b; + text-decoration: none; + background-color: #f8f9fa; +} + +.dropdown-item.active, .dropdown-item:active { + color: #fff; + text-decoration: none; + background-color: #007bff; +} + +.dropdown-item.disabled, .dropdown-item:disabled { + color: #6c757d; + pointer-events: none; + background-color: transparent; +} + +.dropdown-menu.show { + display: block; +} + +.dropdown-header { + display: block; + padding: 0.5rem 1.5rem; + margin-bottom: 0; + font-size: 0.875rem; + color: #6c757d; + white-space: nowrap; +} + +.dropdown-item-text { + display: block; + padding: 0.25rem 1.5rem; + color: #212529; +} + +.btn-group, +.btn-group-vertical { + position: relative; + display: -ms-inline-flexbox; + display: inline-flex; + vertical-align: middle; +} + +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + -ms-flex: 1 1 auto; + flex: 1 1 auto; +} + +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover { + z-index: 1; +} + +.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active, +.btn-group-vertical > .btn:focus, +.btn-group-vertical > .btn:active, +.btn-group-vertical > .btn.active { + z-index: 1; +} + +.btn-toolbar { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.btn-toolbar .input-group { + width: auto; +} + +.btn-group > .btn:not(:first-child), +.btn-group > .btn-group:not(:first-child) { + margin-left: -1px; +} + +.btn-group > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.btn-group > .btn:not(:first-child), +.btn-group > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.dropdown-toggle-split { + padding-right: 0.5625rem; + padding-left: 0.5625rem; +} + +.dropdown-toggle-split::after, +.dropup .dropdown-toggle-split::after, +.dropright .dropdown-toggle-split::after { + margin-left: 0; +} + +.dropleft .dropdown-toggle-split::before { + margin-right: 0; +} + +.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split { + padding-right: 0.375rem; + padding-left: 0.375rem; +} + +.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split { + padding-right: 0.75rem; + padding-left: 0.75rem; +} + +.btn-group-vertical { + -ms-flex-direction: column; + flex-direction: column; + -ms-flex-align: start; + align-items: flex-start; + -ms-flex-pack: center; + justify-content: center; +} + +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group { + width: 100%; +} + +.btn-group-vertical > .btn:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) { + margin-top: -1px; +} + +.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group-vertical > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical > .btn:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.btn-group-toggle > .btn, +.btn-group-toggle > .btn-group > .btn { + margin-bottom: 0; +} + +.btn-group-toggle > .btn input[type="radio"], +.btn-group-toggle > .btn input[type="checkbox"], +.btn-group-toggle > .btn-group > .btn input[type="radio"], +.btn-group-toggle > .btn-group > .btn input[type="checkbox"] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} + +.input-group { + position: relative; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-align: stretch; + align-items: stretch; + width: 100%; +} + +.input-group > .form-control, +.input-group > .form-control-plaintext, +.input-group > .custom-select, +.input-group > .custom-file { + position: relative; + -ms-flex: 1 1 0%; + flex: 1 1 0%; + min-width: 0; + margin-bottom: 0; +} + +.input-group > .form-control + .form-control, +.input-group > .form-control + .custom-select, +.input-group > .form-control + .custom-file, +.input-group > .form-control-plaintext + .form-control, +.input-group > .form-control-plaintext + .custom-select, +.input-group > .form-control-plaintext + .custom-file, +.input-group > .custom-select + .form-control, +.input-group > .custom-select + .custom-select, +.input-group > .custom-select + .custom-file, +.input-group > .custom-file + .form-control, +.input-group > .custom-file + .custom-select, +.input-group > .custom-file + .custom-file { + margin-left: -1px; +} + +.input-group > .form-control:focus, +.input-group > .custom-select:focus, +.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label { + z-index: 3; +} + +.input-group > .custom-file .custom-file-input:focus { + z-index: 4; +} + +.input-group > .form-control:not(:last-child), +.input-group > .custom-select:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group > .form-control:not(:first-child), +.input-group > .custom-select:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.input-group > .custom-file { + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; +} + +.input-group > .custom-file:not(:last-child) .custom-file-label, +.input-group > .custom-file:not(:last-child) .custom-file-label::after { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group > .custom-file:not(:first-child) .custom-file-label { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.input-group-prepend, +.input-group-append { + display: -ms-flexbox; + display: flex; +} + +.input-group-prepend .btn, +.input-group-append .btn { + position: relative; + z-index: 2; +} + +.input-group-prepend .btn:focus, +.input-group-append .btn:focus { + z-index: 3; +} + +.input-group-prepend .btn + .btn, +.input-group-prepend .btn + .input-group-text, +.input-group-prepend .input-group-text + .input-group-text, +.input-group-prepend .input-group-text + .btn, +.input-group-append .btn + .btn, +.input-group-append .btn + .input-group-text, +.input-group-append .input-group-text + .input-group-text, +.input-group-append .input-group-text + .btn { + margin-left: -1px; +} + +.input-group-prepend { + margin-right: -1px; +} + +.input-group-append { + margin-left: -1px; +} + +.input-group-text { + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + padding: 0.375rem 0.75rem; + margin-bottom: 0; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + text-align: center; + white-space: nowrap; + background-color: #e9ecef; + border: 1px solid #ced4da; + border-radius: 0.25rem; +} + +.input-group-text input[type="radio"], +.input-group-text input[type="checkbox"] { + margin-top: 0; +} + +.input-group-lg > .form-control:not(textarea), +.input-group-lg > .custom-select { + height: calc(1.5em + 1rem + 2px); +} + +.input-group-lg > .form-control, +.input-group-lg > .custom-select, +.input-group-lg > .input-group-prepend > .input-group-text, +.input-group-lg > .input-group-append > .input-group-text, +.input-group-lg > .input-group-prepend > .btn, +.input-group-lg > .input-group-append > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +.input-group-sm > .form-control:not(textarea), +.input-group-sm > .custom-select { + height: calc(1.5em + 0.5rem + 2px); +} + +.input-group-sm > .form-control, +.input-group-sm > .custom-select, +.input-group-sm > .input-group-prepend > .input-group-text, +.input-group-sm > .input-group-append > .input-group-text, +.input-group-sm > .input-group-prepend > .btn, +.input-group-sm > .input-group-append > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.input-group-lg > .custom-select, +.input-group-sm > .custom-select { + padding-right: 1.75rem; +} + +.input-group > .input-group-prepend > .btn, +.input-group > .input-group-prepend > .input-group-text, +.input-group > .input-group-append:not(:last-child) > .btn, +.input-group > .input-group-append:not(:last-child) > .input-group-text, +.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group > .input-group-append > .btn, +.input-group > .input-group-append > .input-group-text, +.input-group > .input-group-prepend:not(:first-child) > .btn, +.input-group > .input-group-prepend:not(:first-child) > .input-group-text, +.input-group > .input-group-prepend:first-child > .btn:not(:first-child), +.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.custom-control { + position: relative; + display: block; + min-height: 1.5rem; + padding-left: 1.5rem; +} + +.custom-control-inline { + display: -ms-inline-flexbox; + display: inline-flex; + margin-right: 1rem; +} + +.custom-control-input { + position: absolute; + left: 0; + z-index: -1; + width: 1rem; + height: 1.25rem; + opacity: 0; +} + +.custom-control-input:checked ~ .custom-control-label::before { + color: #fff; + border-color: #007bff; + background-color: #007bff; +} + +.custom-control-input:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-control-input:focus:not(:checked) ~ .custom-control-label::before { + border-color: #80bdff; +} + +.custom-control-input:not(:disabled):active ~ .custom-control-label::before { + color: #fff; + background-color: #b3d7ff; + border-color: #b3d7ff; +} + +.custom-control-input[disabled] ~ .custom-control-label, .custom-control-input:disabled ~ .custom-control-label { + color: #6c757d; +} + +.custom-control-input[disabled] ~ .custom-control-label::before, .custom-control-input:disabled ~ .custom-control-label::before { + background-color: #e9ecef; +} + +.custom-control-label { + position: relative; + margin-bottom: 0; + vertical-align: top; +} + +.custom-control-label::before { + position: absolute; + top: 0.25rem; + left: -1.5rem; + display: block; + width: 1rem; + height: 1rem; + pointer-events: none; + content: ""; + background-color: #fff; + border: #adb5bd solid 1px; +} + +.custom-control-label::after { + position: absolute; + top: 0.25rem; + left: -1.5rem; + display: block; + width: 1rem; + height: 1rem; + content: ""; + background: no-repeat 50% / 50% 50%; +} + +.custom-checkbox .custom-control-label::before { + border-radius: 0.25rem; +} + +.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e"); +} + +.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before { + border-color: #007bff; + background-color: #007bff; +} + +.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e"); +} + +.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.5); +} + +.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.5); +} + +.custom-radio .custom-control-label::before { + border-radius: 50%; +} + +.custom-radio .custom-control-input:checked ~ .custom-control-label::after { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e"); +} + +.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.5); +} + +.custom-switch { + padding-left: 2.25rem; +} + +.custom-switch .custom-control-label::before { + left: -2.25rem; + width: 1.75rem; + pointer-events: all; + border-radius: 0.5rem; +} + +.custom-switch .custom-control-label::after { + top: calc(0.25rem + 2px); + left: calc(-2.25rem + 2px); + width: calc(1rem - 4px); + height: calc(1rem - 4px); + background-color: #adb5bd; + border-radius: 0.5rem; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out; + transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .custom-switch .custom-control-label::after { + transition: none; + } +} + +.custom-switch .custom-control-input:checked ~ .custom-control-label::after { + background-color: #fff; + -webkit-transform: translateX(0.75rem); + transform: translateX(0.75rem); +} + +.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.5); +} + +.custom-select { + display: inline-block; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + padding: 0.375rem 1.75rem 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + vertical-align: middle; + background: #fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px; + border: 1px solid #ced4da; + border-radius: 0.25rem; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.custom-select:focus { + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-select:focus::-ms-value { + color: #495057; + background-color: #fff; +} + +.custom-select[multiple], .custom-select[size]:not([size="1"]) { + height: auto; + padding-right: 0.75rem; + background-image: none; +} + +.custom-select:disabled { + color: #6c757d; + background-color: #e9ecef; +} + +.custom-select::-ms-expand { + display: none; +} + +.custom-select:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #495057; +} + +.custom-select-sm { + height: calc(1.5em + 0.5rem + 2px); + padding-top: 0.25rem; + padding-bottom: 0.25rem; + padding-left: 0.5rem; + font-size: 0.875rem; +} + +.custom-select-lg { + height: calc(1.5em + 1rem + 2px); + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + font-size: 1.25rem; +} + +.custom-file { + position: relative; + display: inline-block; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + margin-bottom: 0; +} + +.custom-file-input { + position: relative; + z-index: 2; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + margin: 0; + opacity: 0; +} + +.custom-file-input:focus ~ .custom-file-label { + border-color: #80bdff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-file-input[disabled] ~ .custom-file-label, +.custom-file-input:disabled ~ .custom-file-label { + background-color: #e9ecef; +} + +.custom-file-input:lang(en) ~ .custom-file-label::after { + content: "Browse"; +} + +.custom-file-input ~ .custom-file-label[data-browse]::after { + content: attr(data-browse); +} + +.custom-file-label { + position: absolute; + top: 0; + right: 0; + left: 0; + z-index: 1; + height: calc(1.5em + 0.75rem + 2px); + padding: 0.375rem 0.75rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + background-color: #fff; + border: 1px solid #ced4da; + border-radius: 0.25rem; +} + +.custom-file-label::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + z-index: 3; + display: block; + height: calc(1.5em + 0.75rem); + padding: 0.375rem 0.75rem; + line-height: 1.5; + color: #495057; + content: "Browse"; + background-color: #e9ecef; + border-left: inherit; + border-radius: 0 0.25rem 0.25rem 0; +} + +.custom-range { + width: 100%; + height: 1.4rem; + padding: 0; + background-color: transparent; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.custom-range:focus { + outline: none; +} + +.custom-range:focus::-webkit-slider-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-range:focus::-moz-range-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-range:focus::-ms-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-range::-moz-focus-outer { + border: 0; +} + +.custom-range::-webkit-slider-thumb { + width: 1rem; + height: 1rem; + margin-top: -0.25rem; + background-color: #007bff; + border: 0; + border-radius: 1rem; + -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + -webkit-appearance: none; + appearance: none; +} + +@media (prefers-reduced-motion: reduce) { + .custom-range::-webkit-slider-thumb { + -webkit-transition: none; + transition: none; + } +} + +.custom-range::-webkit-slider-thumb:active { + background-color: #b3d7ff; +} + +.custom-range::-webkit-slider-runnable-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: #dee2e6; + border-color: transparent; + border-radius: 1rem; +} + +.custom-range::-moz-range-thumb { + width: 1rem; + height: 1rem; + background-color: #007bff; + border: 0; + border-radius: 1rem; + -moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + -moz-appearance: none; + appearance: none; +} + +@media (prefers-reduced-motion: reduce) { + .custom-range::-moz-range-thumb { + -moz-transition: none; + transition: none; + } +} + +.custom-range::-moz-range-thumb:active { + background-color: #b3d7ff; +} + +.custom-range::-moz-range-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: #dee2e6; + border-color: transparent; + border-radius: 1rem; +} + +.custom-range::-ms-thumb { + width: 1rem; + height: 1rem; + margin-top: 0; + margin-right: 0.2rem; + margin-left: 0.2rem; + background-color: #007bff; + border: 0; + border-radius: 1rem; + -ms-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + appearance: none; +} + +@media (prefers-reduced-motion: reduce) { + .custom-range::-ms-thumb { + -ms-transition: none; + transition: none; + } +} + +.custom-range::-ms-thumb:active { + background-color: #b3d7ff; +} + +.custom-range::-ms-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: transparent; + border-color: transparent; + border-width: 0.5rem; +} + +.custom-range::-ms-fill-lower { + background-color: #dee2e6; + border-radius: 1rem; +} + +.custom-range::-ms-fill-upper { + margin-right: 15px; + background-color: #dee2e6; + border-radius: 1rem; +} + +.custom-range:disabled::-webkit-slider-thumb { + background-color: #adb5bd; +} + +.custom-range:disabled::-webkit-slider-runnable-track { + cursor: default; +} + +.custom-range:disabled::-moz-range-thumb { + background-color: #adb5bd; +} + +.custom-range:disabled::-moz-range-track { + cursor: default; +} + +.custom-range:disabled::-ms-thumb { + background-color: #adb5bd; +} + +.custom-control-label::before, +.custom-file-label, +.custom-select { + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .custom-control-label::before, + .custom-file-label, + .custom-select { + transition: none; + } +} + +.nav { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.nav-link { + display: block; + padding: 0.5rem 1rem; +} + +.nav-link:hover, .nav-link:focus { + text-decoration: none; +} + +.nav-link.disabled { + color: #6c757d; + pointer-events: none; + cursor: default; +} + +.nav-tabs { + border-bottom: 1px solid #dee2e6; +} + +.nav-tabs .nav-item { + margin-bottom: -1px; +} + +.nav-tabs .nav-link { + border: 1px solid transparent; + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus { + border-color: #e9ecef #e9ecef #dee2e6; +} + +.nav-tabs .nav-link.disabled { + color: #6c757d; + background-color: transparent; + border-color: transparent; +} + +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + color: #495057; + background-color: #fff; + border-color: #dee2e6 #dee2e6 #fff; +} + +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.nav-pills .nav-link { + border-radius: 0.25rem; +} + +.nav-pills .nav-link.active, +.nav-pills .show > .nav-link { + color: #fff; + background-color: #007bff; +} + +.nav-fill .nav-item { + -ms-flex: 1 1 auto; + flex: 1 1 auto; + text-align: center; +} + +.nav-justified .nav-item { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + text-align: center; +} + +.tab-content > .tab-pane { + display: none; +} + +.tab-content > .active { + display: block; +} + +.navbar { + position: relative; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: justify; + justify-content: space-between; + padding: 0.5rem 1rem; +} + +.navbar .container, +.navbar .container-fluid, .navbar .container-sm, .navbar .container-md, .navbar .container-lg, .navbar .container-xl { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.navbar-brand { + display: inline-block; + padding-top: 0.3125rem; + padding-bottom: 0.3125rem; + margin-right: 1rem; + font-size: 1.25rem; + line-height: inherit; + white-space: nowrap; +} + +.navbar-brand:hover, .navbar-brand:focus { + text-decoration: none; +} + +.navbar-nav { + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.navbar-nav .nav-link { + padding-right: 0; + padding-left: 0; +} + +.navbar-nav .dropdown-menu { + position: static; + float: none; +} + +.navbar-text { + display: inline-block; + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.navbar-collapse { + -ms-flex-preferred-size: 100%; + flex-basis: 100%; + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-align: center; + align-items: center; +} + +.navbar-toggler { + padding: 0.25rem 0.75rem; + font-size: 1.25rem; + line-height: 1; + background-color: transparent; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +.navbar-toggler:hover, .navbar-toggler:focus { + text-decoration: none; +} + +.navbar-toggler-icon { + display: inline-block; + width: 1.5em; + height: 1.5em; + vertical-align: middle; + content: ""; + background: no-repeat center center; + background-size: 100% 100%; +} + +@media (max-width: 575.98px) { + .navbar-expand-sm > .container, + .navbar-expand-sm > .container-fluid, .navbar-expand-sm > .container-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .navbar-expand-sm > .container-xl { + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 576px) { + .navbar-expand-sm { + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -ms-flex-pack: start; + justify-content: flex-start; + } + .navbar-expand-sm .navbar-nav { + -ms-flex-direction: row; + flex-direction: row; + } + .navbar-expand-sm .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-sm .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-sm > .container, + .navbar-expand-sm > .container-fluid, .navbar-expand-sm > .container-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .navbar-expand-sm > .container-xl { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + } + .navbar-expand-sm .navbar-collapse { + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; + } + .navbar-expand-sm .navbar-toggler { + display: none; + } +} + +@media (max-width: 767.98px) { + .navbar-expand-md > .container, + .navbar-expand-md > .container-fluid, .navbar-expand-md > .container-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .navbar-expand-md > .container-xl { + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 768px) { + .navbar-expand-md { + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -ms-flex-pack: start; + justify-content: flex-start; + } + .navbar-expand-md .navbar-nav { + -ms-flex-direction: row; + flex-direction: row; + } + .navbar-expand-md .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-md .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-md > .container, + .navbar-expand-md > .container-fluid, .navbar-expand-md > .container-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .navbar-expand-md > .container-xl { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + } + .navbar-expand-md .navbar-collapse { + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; + } + .navbar-expand-md .navbar-toggler { + display: none; + } +} + +@media (max-width: 991.98px) { + .navbar-expand-lg > .container, + .navbar-expand-lg > .container-fluid, .navbar-expand-lg > .container-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .navbar-expand-lg > .container-xl { + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 992px) { + .navbar-expand-lg { + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -ms-flex-pack: start; + justify-content: flex-start; + } + .navbar-expand-lg .navbar-nav { + -ms-flex-direction: row; + flex-direction: row; + } + .navbar-expand-lg .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-lg .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-lg > .container, + .navbar-expand-lg > .container-fluid, .navbar-expand-lg > .container-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .navbar-expand-lg > .container-xl { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + } + .navbar-expand-lg .navbar-collapse { + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; + } + .navbar-expand-lg .navbar-toggler { + display: none; + } +} + +@media (max-width: 1199.98px) { + .navbar-expand-xl > .container, + .navbar-expand-xl > .container-fluid, .navbar-expand-xl > .container-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .navbar-expand-xl > .container-xl { + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 1200px) { + .navbar-expand-xl { + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -ms-flex-pack: start; + justify-content: flex-start; + } + .navbar-expand-xl .navbar-nav { + -ms-flex-direction: row; + flex-direction: row; + } + .navbar-expand-xl .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-xl .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-xl > .container, + .navbar-expand-xl > .container-fluid, .navbar-expand-xl > .container-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .navbar-expand-xl > .container-xl { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + } + .navbar-expand-xl .navbar-collapse { + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; + } + .navbar-expand-xl .navbar-toggler { + display: none; + } +} + +.navbar-expand { + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.navbar-expand > .container, +.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl { + padding-right: 0; + padding-left: 0; +} + +.navbar-expand .navbar-nav { + -ms-flex-direction: row; + flex-direction: row; +} + +.navbar-expand .navbar-nav .dropdown-menu { + position: absolute; +} + +.navbar-expand .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; +} + +.navbar-expand > .container, +.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; +} + +.navbar-expand .navbar-collapse { + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; +} + +.navbar-expand .navbar-toggler { + display: none; +} + +.navbar-light .navbar-brand { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-nav .nav-link { + color: rgba(0, 0, 0, 0.5); +} + +.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus { + color: rgba(0, 0, 0, 0.7); +} + +.navbar-light .navbar-nav .nav-link.disabled { + color: rgba(0, 0, 0, 0.3); +} + +.navbar-light .navbar-nav .show > .nav-link, +.navbar-light .navbar-nav .active > .nav-link, +.navbar-light .navbar-nav .nav-link.show, +.navbar-light .navbar-nav .nav-link.active { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-toggler { + color: rgba(0, 0, 0, 0.5); + border-color: rgba(0, 0, 0, 0.1); +} + +.navbar-light .navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} + +.navbar-light .navbar-text { + color: rgba(0, 0, 0, 0.5); +} + +.navbar-light .navbar-text a { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-dark .navbar-brand { + color: #fff; +} + +.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus { + color: #fff; +} + +.navbar-dark .navbar-nav .nav-link { + color: rgba(255, 255, 255, 0.5); +} + +.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus { + color: rgba(255, 255, 255, 0.75); +} + +.navbar-dark .navbar-nav .nav-link.disabled { + color: rgba(255, 255, 255, 0.25); +} + +.navbar-dark .navbar-nav .show > .nav-link, +.navbar-dark .navbar-nav .active > .nav-link, +.navbar-dark .navbar-nav .nav-link.show, +.navbar-dark .navbar-nav .nav-link.active { + color: #fff; +} + +.navbar-dark .navbar-toggler { + color: rgba(255, 255, 255, 0.5); + border-color: rgba(255, 255, 255, 0.1); +} + +.navbar-dark .navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} + +.navbar-dark .navbar-text { + color: rgba(255, 255, 255, 0.5); +} + +.navbar-dark .navbar-text a { + color: #fff; +} + +.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus { + color: #fff; +} + +.card { + position: relative; + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + border: 1px solid rgba(0, 0, 0, 0.125); + border-radius: 0.25rem; +} + +.card > hr { + margin-right: 0; + margin-left: 0; +} + +.card > .list-group:first-child .list-group-item:first-child { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.card > .list-group:last-child .list-group-item:last-child { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.card-body { + -ms-flex: 1 1 auto; + flex: 1 1 auto; + min-height: 1px; + padding: 1.25rem; +} + +.card-title { + margin-bottom: 0.75rem; +} + +.card-subtitle { + margin-top: -0.375rem; + margin-bottom: 0; +} + +.card-text:last-child { + margin-bottom: 0; +} + +.card-link:hover { + text-decoration: none; +} + +.card-link + .card-link { + margin-left: 1.25rem; +} + +.card-header { + padding: 0.75rem 1.25rem; + margin-bottom: 0; + background-color: rgba(0, 0, 0, 0.03); + border-bottom: 1px solid rgba(0, 0, 0, 0.125); +} + +.card-header:first-child { + border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0; +} + +.card-header + .list-group .list-group-item:first-child { + border-top: 0; +} + +.card-footer { + padding: 0.75rem 1.25rem; + background-color: rgba(0, 0, 0, 0.03); + border-top: 1px solid rgba(0, 0, 0, 0.125); +} + +.card-footer:last-child { + border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px); +} + +.card-header-tabs { + margin-right: -0.625rem; + margin-bottom: -0.75rem; + margin-left: -0.625rem; + border-bottom: 0; +} + +.card-header-pills { + margin-right: -0.625rem; + margin-left: -0.625rem; +} + +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: 1.25rem; +} + +.card-img, +.card-img-top, +.card-img-bottom { + -ms-flex-negative: 0; + flex-shrink: 0; + width: 100%; +} + +.card-img, +.card-img-top { + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); +} + +.card-img, +.card-img-bottom { + border-bottom-right-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.25rem - 1px); +} + +.card-deck .card { + margin-bottom: 15px; +} + +@media (min-width: 576px) { + .card-deck { + display: -ms-flexbox; + display: flex; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + margin-right: -15px; + margin-left: -15px; + } + .card-deck .card { + -ms-flex: 1 0 0%; + flex: 1 0 0%; + margin-right: 15px; + margin-bottom: 0; + margin-left: 15px; + } +} + +.card-group > .card { + margin-bottom: 15px; +} + +@media (min-width: 576px) { + .card-group { + display: -ms-flexbox; + display: flex; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + } + .card-group > .card { + -ms-flex: 1 0 0%; + flex: 1 0 0%; + margin-bottom: 0; + } + .card-group > .card + .card { + margin-left: 0; + border-left: 0; + } + .card-group > .card:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + .card-group > .card:not(:last-child) .card-img-top, + .card-group > .card:not(:last-child) .card-header { + border-top-right-radius: 0; + } + .card-group > .card:not(:last-child) .card-img-bottom, + .card-group > .card:not(:last-child) .card-footer { + border-bottom-right-radius: 0; + } + .card-group > .card:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + .card-group > .card:not(:first-child) .card-img-top, + .card-group > .card:not(:first-child) .card-header { + border-top-left-radius: 0; + } + .card-group > .card:not(:first-child) .card-img-bottom, + .card-group > .card:not(:first-child) .card-footer { + border-bottom-left-radius: 0; + } +} + +.card-columns .card { + margin-bottom: 0.75rem; +} + +@media (min-width: 576px) { + .card-columns { + -webkit-column-count: 3; + -moz-column-count: 3; + column-count: 3; + -webkit-column-gap: 1.25rem; + -moz-column-gap: 1.25rem; + column-gap: 1.25rem; + orphans: 1; + widows: 1; + } + .card-columns .card { + display: inline-block; + width: 100%; + } +} + +.accordion > .card { + overflow: hidden; +} + +.accordion > .card:not(:last-of-type) { + border-bottom: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.accordion > .card:not(:first-of-type) { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.accordion > .card > .card-header { + border-radius: 0; + margin-bottom: -1px; +} + +.breadcrumb { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + padding: 0.75rem 1rem; + margin-bottom: 1rem; + list-style: none; + background-color: #e9ecef; + border-radius: 0.25rem; +} + +.breadcrumb-item + .breadcrumb-item { + padding-left: 0.5rem; +} + +.breadcrumb-item + .breadcrumb-item::before { + display: inline-block; + padding-right: 0.5rem; + color: #6c757d; + content: "/"; +} + +.breadcrumb-item + .breadcrumb-item:hover::before { + text-decoration: underline; +} + +.breadcrumb-item + .breadcrumb-item:hover::before { + text-decoration: none; +} + +.breadcrumb-item.active { + color: #6c757d; +} + +.pagination { + display: -ms-flexbox; + display: flex; + padding-left: 0; + list-style: none; + border-radius: 0.25rem; +} + +.page-link { + position: relative; + display: block; + padding: 0.5rem 0.75rem; + margin-left: -1px; + line-height: 1.25; + color: #007bff; + background-color: #fff; + border: 1px solid #dee2e6; +} + +.page-link:hover { + z-index: 2; + color: #0056b3; + text-decoration: none; + background-color: #e9ecef; + border-color: #dee2e6; +} + +.page-link:focus { + z-index: 3; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.page-item:first-child .page-link { + margin-left: 0; + border-top-left-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.page-item:last-child .page-link { + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.25rem; +} + +.page-item.active .page-link { + z-index: 3; + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.page-item.disabled .page-link { + color: #6c757d; + pointer-events: none; + cursor: auto; + background-color: #fff; + border-color: #dee2e6; +} + +.pagination-lg .page-link { + padding: 0.75rem 1.5rem; + font-size: 1.25rem; + line-height: 1.5; +} + +.pagination-lg .page-item:first-child .page-link { + border-top-left-radius: 0.3rem; + border-bottom-left-radius: 0.3rem; +} + +.pagination-lg .page-item:last-child .page-link { + border-top-right-radius: 0.3rem; + border-bottom-right-radius: 0.3rem; +} + +.pagination-sm .page-link { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; +} + +.pagination-sm .page-item:first-child .page-link { + border-top-left-radius: 0.2rem; + border-bottom-left-radius: 0.2rem; +} + +.pagination-sm .page-item:last-child .page-link { + border-top-right-radius: 0.2rem; + border-bottom-right-radius: 0.2rem; +} + +.badge { + display: inline-block; + padding: 0.25em 0.4em; + font-size: 75%; + font-weight: 700; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .badge { + transition: none; + } +} + +a.badge:hover, a.badge:focus { + text-decoration: none; +} + +.badge:empty { + display: none; +} + +.btn .badge { + position: relative; + top: -1px; +} + +.badge-pill { + padding-right: 0.6em; + padding-left: 0.6em; + border-radius: 10rem; +} + +.badge-primary { + color: #fff; + background-color: #007bff; +} + +a.badge-primary:hover, a.badge-primary:focus { + color: #fff; + background-color: #0062cc; +} + +a.badge-primary:focus, a.badge-primary.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); +} + +.badge-secondary { + color: #fff; + background-color: #6c757d; +} + +a.badge-secondary:hover, a.badge-secondary:focus { + color: #fff; + background-color: #545b62; +} + +a.badge-secondary:focus, a.badge-secondary.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); +} + +.badge-success { + color: #fff; + background-color: #28a745; +} + +a.badge-success:hover, a.badge-success:focus { + color: #fff; + background-color: #1e7e34; +} + +a.badge-success:focus, a.badge-success.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); +} + +.badge-info { + color: #fff; + background-color: #17a2b8; +} + +a.badge-info:hover, a.badge-info:focus { + color: #fff; + background-color: #117a8b; +} + +a.badge-info:focus, a.badge-info.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} + +.badge-warning { + color: #212529; + background-color: #ffc107; +} + +a.badge-warning:hover, a.badge-warning:focus { + color: #212529; + background-color: #d39e00; +} + +a.badge-warning:focus, a.badge-warning.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); +} + +.badge-danger { + color: #fff; + background-color: #dc3545; +} + +a.badge-danger:hover, a.badge-danger:focus { + color: #fff; + background-color: #bd2130; +} + +a.badge-danger:focus, a.badge-danger.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); +} + +.badge-light { + color: #212529; + background-color: #f8f9fa; +} + +a.badge-light:hover, a.badge-light:focus { + color: #212529; + background-color: #dae0e5; +} + +a.badge-light:focus, a.badge-light.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); +} + +.badge-dark { + color: #fff; + background-color: #343a40; +} + +a.badge-dark:hover, a.badge-dark:focus { + color: #fff; + background-color: #1d2124; +} + +a.badge-dark:focus, a.badge-dark.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); +} + +.jumbotron { + padding: 2rem 1rem; + margin-bottom: 2rem; + background-color: #e9ecef; + border-radius: 0.3rem; +} + +@media (min-width: 576px) { + .jumbotron { + padding: 4rem 2rem; + } +} + +.jumbotron-fluid { + padding-right: 0; + padding-left: 0; + border-radius: 0; +} + +.alert { + position: relative; + padding: 0.75rem 1.25rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +.alert-heading { + color: inherit; +} + +.alert-link { + font-weight: 700; +} + +.alert-dismissible { + padding-right: 4rem; +} + +.alert-dismissible .close { + position: absolute; + top: 0; + right: 0; + padding: 0.75rem 1.25rem; + color: inherit; +} + +.alert-primary { + color: #004085; + background-color: #cce5ff; + border-color: #b8daff; +} + +.alert-primary hr { + border-top-color: #9fcdff; +} + +.alert-primary .alert-link { + color: #002752; +} + +.alert-secondary { + color: #383d41; + background-color: #e2e3e5; + border-color: #d6d8db; +} + +.alert-secondary hr { + border-top-color: #c8cbcf; +} + +.alert-secondary .alert-link { + color: #202326; +} + +.alert-success { + color: #155724; + background-color: #d4edda; + border-color: #c3e6cb; +} + +.alert-success hr { + border-top-color: #b1dfbb; +} + +.alert-success .alert-link { + color: #0b2e13; +} + +.alert-info { + color: #0c5460; + background-color: #d1ecf1; + border-color: #bee5eb; +} + +.alert-info hr { + border-top-color: #abdde5; +} + +.alert-info .alert-link { + color: #062c33; +} + +.alert-warning { + color: #856404; + background-color: #fff3cd; + border-color: #ffeeba; +} + +.alert-warning hr { + border-top-color: #ffe8a1; +} + +.alert-warning .alert-link { + color: #533f03; +} + +.alert-danger { + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; +} + +.alert-danger hr { + border-top-color: #f1b0b7; +} + +.alert-danger .alert-link { + color: #491217; +} + +.alert-light { + color: #818182; + background-color: #fefefe; + border-color: #fdfdfe; +} + +.alert-light hr { + border-top-color: #ececf6; +} + +.alert-light .alert-link { + color: #686868; +} + +.alert-dark { + color: #1b1e21; + background-color: #d6d8d9; + border-color: #c6c8ca; +} + +.alert-dark hr { + border-top-color: #b9bbbe; +} + +.alert-dark .alert-link { + color: #040505; +} + +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 1rem 0; + } + to { + background-position: 0 0; + } +} + +@keyframes progress-bar-stripes { + from { + background-position: 1rem 0; + } + to { + background-position: 0 0; + } +} + +.progress { + display: -ms-flexbox; + display: flex; + height: 1rem; + overflow: hidden; + font-size: 0.75rem; + background-color: #e9ecef; + border-radius: 0.25rem; +} + +.progress-bar { + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + -ms-flex-pack: center; + justify-content: center; + overflow: hidden; + color: #fff; + text-align: center; + white-space: nowrap; + background-color: #007bff; + transition: width 0.6s ease; +} + +@media (prefers-reduced-motion: reduce) { + .progress-bar { + transition: none; + } +} + +.progress-bar-striped { + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-size: 1rem 1rem; +} + +.progress-bar-animated { + -webkit-animation: progress-bar-stripes 1s linear infinite; + animation: progress-bar-stripes 1s linear infinite; +} + +@media (prefers-reduced-motion: reduce) { + .progress-bar-animated { + -webkit-animation: none; + animation: none; + } +} + +.media { + display: -ms-flexbox; + display: flex; + -ms-flex-align: start; + align-items: flex-start; +} + +.media-body { + -ms-flex: 1; + flex: 1; +} + +.list-group { + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; +} + +.list-group-item-action { + width: 100%; + color: #495057; + text-align: inherit; +} + +.list-group-item-action:hover, .list-group-item-action:focus { + z-index: 1; + color: #495057; + text-decoration: none; + background-color: #f8f9fa; +} + +.list-group-item-action:active { + color: #212529; + background-color: #e9ecef; +} + +.list-group-item { + position: relative; + display: block; + padding: 0.75rem 1.25rem; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.125); +} + +.list-group-item:first-child { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.list-group-item:last-child { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.list-group-item.disabled, .list-group-item:disabled { + color: #6c757d; + pointer-events: none; + background-color: #fff; +} + +.list-group-item.active { + z-index: 2; + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.list-group-item + .list-group-item { + border-top-width: 0; +} + +.list-group-item + .list-group-item.active { + margin-top: -1px; + border-top-width: 1px; +} + +.list-group-horizontal { + -ms-flex-direction: row; + flex-direction: row; +} + +.list-group-horizontal .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; +} + +.list-group-horizontal .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; +} + +.list-group-horizontal .list-group-item.active { + margin-top: 0; +} + +.list-group-horizontal .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; +} + +.list-group-horizontal .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; +} + +@media (min-width: 576px) { + .list-group-horizontal-sm { + -ms-flex-direction: row; + flex-direction: row; + } + .list-group-horizontal-sm .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-sm .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; + } + .list-group-horizontal-sm .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-sm .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; + } + .list-group-horizontal-sm .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; + } +} + +@media (min-width: 768px) { + .list-group-horizontal-md { + -ms-flex-direction: row; + flex-direction: row; + } + .list-group-horizontal-md .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-md .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; + } + .list-group-horizontal-md .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-md .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; + } + .list-group-horizontal-md .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; + } +} + +@media (min-width: 992px) { + .list-group-horizontal-lg { + -ms-flex-direction: row; + flex-direction: row; + } + .list-group-horizontal-lg .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-lg .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; + } + .list-group-horizontal-lg .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-lg .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; + } + .list-group-horizontal-lg .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; + } +} + +@media (min-width: 1200px) { + .list-group-horizontal-xl { + -ms-flex-direction: row; + flex-direction: row; + } + .list-group-horizontal-xl .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-xl .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; + } + .list-group-horizontal-xl .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-xl .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; + } + .list-group-horizontal-xl .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; + } +} + +.list-group-flush .list-group-item { + border-right-width: 0; + border-left-width: 0; + border-radius: 0; +} + +.list-group-flush .list-group-item:first-child { + border-top-width: 0; +} + +.list-group-flush:last-child .list-group-item:last-child { + border-bottom-width: 0; +} + +.list-group-item-primary { + color: #004085; + background-color: #b8daff; +} + +.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus { + color: #004085; + background-color: #9fcdff; +} + +.list-group-item-primary.list-group-item-action.active { + color: #fff; + background-color: #004085; + border-color: #004085; +} + +.list-group-item-secondary { + color: #383d41; + background-color: #d6d8db; +} + +.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus { + color: #383d41; + background-color: #c8cbcf; +} + +.list-group-item-secondary.list-group-item-action.active { + color: #fff; + background-color: #383d41; + border-color: #383d41; +} + +.list-group-item-success { + color: #155724; + background-color: #c3e6cb; +} + +.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus { + color: #155724; + background-color: #b1dfbb; +} + +.list-group-item-success.list-group-item-action.active { + color: #fff; + background-color: #155724; + border-color: #155724; +} + +.list-group-item-info { + color: #0c5460; + background-color: #bee5eb; +} + +.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus { + color: #0c5460; + background-color: #abdde5; +} + +.list-group-item-info.list-group-item-action.active { + color: #fff; + background-color: #0c5460; + border-color: #0c5460; +} + +.list-group-item-warning { + color: #856404; + background-color: #ffeeba; +} + +.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus { + color: #856404; + background-color: #ffe8a1; +} + +.list-group-item-warning.list-group-item-action.active { + color: #fff; + background-color: #856404; + border-color: #856404; +} + +.list-group-item-danger { + color: #721c24; + background-color: #f5c6cb; +} + +.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus { + color: #721c24; + background-color: #f1b0b7; +} + +.list-group-item-danger.list-group-item-action.active { + color: #fff; + background-color: #721c24; + border-color: #721c24; +} + +.list-group-item-light { + color: #818182; + background-color: #fdfdfe; +} + +.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus { + color: #818182; + background-color: #ececf6; +} + +.list-group-item-light.list-group-item-action.active { + color: #fff; + background-color: #818182; + border-color: #818182; +} + +.list-group-item-dark { + color: #1b1e21; + background-color: #c6c8ca; +} + +.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus { + color: #1b1e21; + background-color: #b9bbbe; +} + +.list-group-item-dark.list-group-item-action.active { + color: #fff; + background-color: #1b1e21; + border-color: #1b1e21; +} + +.close { + float: right; + font-size: 1.5rem; + font-weight: 700; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + opacity: .5; +} + +.close:hover { + color: #000; + text-decoration: none; +} + +.close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus { + opacity: .75; +} + +button.close { + padding: 0; + background-color: transparent; + border: 0; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +a.close.disabled { + pointer-events: none; +} + +.toast { + max-width: 350px; + overflow: hidden; + font-size: 0.875rem; + background-color: rgba(255, 255, 255, 0.85); + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.1); + box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + opacity: 0; + border-radius: 0.25rem; +} + +.toast:not(:last-child) { + margin-bottom: 0.75rem; +} + +.toast.showing { + opacity: 1; +} + +.toast.show { + display: block; + opacity: 1; +} + +.toast.hide { + display: none; +} + +.toast-header { + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + padding: 0.25rem 0.75rem; + color: #6c757d; + background-color: rgba(255, 255, 255, 0.85); + background-clip: padding-box; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.toast-body { + padding: 0.75rem; +} + +.modal-open { + overflow: hidden; +} + +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} + +.modal { + position: fixed; + top: 0; + left: 0; + z-index: 1050; + display: none; + width: 100%; + height: 100%; + overflow: hidden; + outline: 0; +} + +.modal-dialog { + position: relative; + width: auto; + margin: 0.5rem; + pointer-events: none; +} + +.modal.fade .modal-dialog { + transition: -webkit-transform 0.3s ease-out; + transition: transform 0.3s ease-out; + transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out; + -webkit-transform: translate(0, -50px); + transform: translate(0, -50px); +} + +@media (prefers-reduced-motion: reduce) { + .modal.fade .modal-dialog { + transition: none; + } +} + +.modal.show .modal-dialog { + -webkit-transform: none; + transform: none; +} + +.modal.modal-static .modal-dialog { + -webkit-transform: scale(1.02); + transform: scale(1.02); +} + +.modal-dialog-scrollable { + display: -ms-flexbox; + display: flex; + max-height: calc(100% - 1rem); +} + +.modal-dialog-scrollable .modal-content { + max-height: calc(100vh - 1rem); + overflow: hidden; +} + +.modal-dialog-scrollable .modal-header, +.modal-dialog-scrollable .modal-footer { + -ms-flex-negative: 0; + flex-shrink: 0; +} + +.modal-dialog-scrollable .modal-body { + overflow-y: auto; +} + +.modal-dialog-centered { + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + min-height: calc(100% - 1rem); +} + +.modal-dialog-centered::before { + display: block; + height: calc(100vh - 1rem); + content: ""; +} + +.modal-dialog-centered.modal-dialog-scrollable { + -ms-flex-direction: column; + flex-direction: column; + -ms-flex-pack: center; + justify-content: center; + height: 100%; +} + +.modal-dialog-centered.modal-dialog-scrollable .modal-content { + max-height: none; +} + +.modal-dialog-centered.modal-dialog-scrollable::before { + content: none; +} + +.modal-content { + position: relative; + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + width: 100%; + pointer-events: auto; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; + outline: 0; +} + +.modal-backdrop { + position: fixed; + top: 0; + left: 0; + z-index: 1040; + width: 100vw; + height: 100vh; + background-color: #000; +} + +.modal-backdrop.fade { + opacity: 0; +} + +.modal-backdrop.show { + opacity: 0.5; +} + +.modal-header { + display: -ms-flexbox; + display: flex; + -ms-flex-align: start; + align-items: flex-start; + -ms-flex-pack: justify; + justify-content: space-between; + padding: 1rem 1rem; + border-bottom: 1px solid #dee2e6; + border-top-left-radius: calc(0.3rem - 1px); + border-top-right-radius: calc(0.3rem - 1px); +} + +.modal-header .close { + padding: 1rem 1rem; + margin: -1rem -1rem -1rem auto; +} + +.modal-title { + margin-bottom: 0; + line-height: 1.5; +} + +.modal-body { + position: relative; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + padding: 1rem; +} + +.modal-footer { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: end; + justify-content: flex-end; + padding: 0.75rem; + border-top: 1px solid #dee2e6; + border-bottom-right-radius: calc(0.3rem - 1px); + border-bottom-left-radius: calc(0.3rem - 1px); +} + +.modal-footer > * { + margin: 0.25rem; +} + +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} + +@media (min-width: 576px) { + .modal-dialog { + max-width: 500px; + margin: 1.75rem auto; + } + .modal-dialog-scrollable { + max-height: calc(100% - 3.5rem); + } + .modal-dialog-scrollable .modal-content { + max-height: calc(100vh - 3.5rem); + } + .modal-dialog-centered { + min-height: calc(100% - 3.5rem); + } + .modal-dialog-centered::before { + height: calc(100vh - 3.5rem); + } + .modal-sm { + max-width: 300px; + } +} + +@media (min-width: 992px) { + .modal-lg, + .modal-xl { + max-width: 800px; + } +} + +@media (min-width: 1200px) { + .modal-xl { + max-width: 1140px; + } +} + +.tooltip { + position: absolute; + z-index: 1070; + display: block; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + white-space: normal; + line-break: auto; + font-size: 0.875rem; + word-wrap: break-word; + opacity: 0; +} + +.tooltip.show { + opacity: 0.9; +} + +.tooltip .arrow { + position: absolute; + display: block; + width: 0.8rem; + height: 0.4rem; +} + +.tooltip .arrow::before { + position: absolute; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-tooltip-top, .bs-tooltip-auto[x-placement^="top"] { + padding: 0.4rem 0; +} + +.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^="top"] .arrow { + bottom: 0; +} + +.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^="top"] .arrow::before { + top: 0; + border-width: 0.4rem 0.4rem 0; + border-top-color: #000; +} + +.bs-tooltip-right, .bs-tooltip-auto[x-placement^="right"] { + padding: 0 0.4rem; +} + +.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^="right"] .arrow { + left: 0; + width: 0.4rem; + height: 0.8rem; +} + +.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^="right"] .arrow::before { + right: 0; + border-width: 0.4rem 0.4rem 0.4rem 0; + border-right-color: #000; +} + +.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^="bottom"] { + padding: 0.4rem 0; +} + +.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^="bottom"] .arrow { + top: 0; +} + +.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^="bottom"] .arrow::before { + bottom: 0; + border-width: 0 0.4rem 0.4rem; + border-bottom-color: #000; +} + +.bs-tooltip-left, .bs-tooltip-auto[x-placement^="left"] { + padding: 0 0.4rem; +} + +.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^="left"] .arrow { + right: 0; + width: 0.4rem; + height: 0.8rem; +} + +.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^="left"] .arrow::before { + left: 0; + border-width: 0.4rem 0 0.4rem 0.4rem; + border-left-color: #000; +} + +.tooltip-inner { + max-width: 200px; + padding: 0.25rem 0.5rem; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 0.25rem; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: block; + max-width: 276px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + white-space: normal; + line-break: auto; + font-size: 0.875rem; + word-wrap: break-word; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; +} + +.popover .arrow { + position: absolute; + display: block; + width: 1rem; + height: 0.5rem; + margin: 0 0.3rem; +} + +.popover .arrow::before, .popover .arrow::after { + position: absolute; + display: block; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-popover-top, .bs-popover-auto[x-placement^="top"] { + margin-bottom: 0.5rem; +} + +.bs-popover-top > .arrow, .bs-popover-auto[x-placement^="top"] > .arrow { + bottom: calc(-0.5rem - 1px); +} + +.bs-popover-top > .arrow::before, .bs-popover-auto[x-placement^="top"] > .arrow::before { + bottom: 0; + border-width: 0.5rem 0.5rem 0; + border-top-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-top > .arrow::after, .bs-popover-auto[x-placement^="top"] > .arrow::after { + bottom: 1px; + border-width: 0.5rem 0.5rem 0; + border-top-color: #fff; +} + +.bs-popover-right, .bs-popover-auto[x-placement^="right"] { + margin-left: 0.5rem; +} + +.bs-popover-right > .arrow, .bs-popover-auto[x-placement^="right"] > .arrow { + left: calc(-0.5rem - 1px); + width: 0.5rem; + height: 1rem; + margin: 0.3rem 0; +} + +.bs-popover-right > .arrow::before, .bs-popover-auto[x-placement^="right"] > .arrow::before { + left: 0; + border-width: 0.5rem 0.5rem 0.5rem 0; + border-right-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-right > .arrow::after, .bs-popover-auto[x-placement^="right"] > .arrow::after { + left: 1px; + border-width: 0.5rem 0.5rem 0.5rem 0; + border-right-color: #fff; +} + +.bs-popover-bottom, .bs-popover-auto[x-placement^="bottom"] { + margin-top: 0.5rem; +} + +.bs-popover-bottom > .arrow, .bs-popover-auto[x-placement^="bottom"] > .arrow { + top: calc(-0.5rem - 1px); +} + +.bs-popover-bottom > .arrow::before, .bs-popover-auto[x-placement^="bottom"] > .arrow::before { + top: 0; + border-width: 0 0.5rem 0.5rem 0.5rem; + border-bottom-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-bottom > .arrow::after, .bs-popover-auto[x-placement^="bottom"] > .arrow::after { + top: 1px; + border-width: 0 0.5rem 0.5rem 0.5rem; + border-bottom-color: #fff; +} + +.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^="bottom"] .popover-header::before { + position: absolute; + top: 0; + left: 50%; + display: block; + width: 1rem; + margin-left: -0.5rem; + content: ""; + border-bottom: 1px solid #f7f7f7; +} + +.bs-popover-left, .bs-popover-auto[x-placement^="left"] { + margin-right: 0.5rem; +} + +.bs-popover-left > .arrow, .bs-popover-auto[x-placement^="left"] > .arrow { + right: calc(-0.5rem - 1px); + width: 0.5rem; + height: 1rem; + margin: 0.3rem 0; +} + +.bs-popover-left > .arrow::before, .bs-popover-auto[x-placement^="left"] > .arrow::before { + right: 0; + border-width: 0.5rem 0 0.5rem 0.5rem; + border-left-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-left > .arrow::after, .bs-popover-auto[x-placement^="left"] > .arrow::after { + right: 1px; + border-width: 0.5rem 0 0.5rem 0.5rem; + border-left-color: #fff; +} + +.popover-header { + padding: 0.5rem 0.75rem; + margin-bottom: 0; + font-size: 1rem; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-top-left-radius: calc(0.3rem - 1px); + border-top-right-radius: calc(0.3rem - 1px); +} + +.popover-header:empty { + display: none; +} + +.popover-body { + padding: 0.5rem 0.75rem; + color: #212529; +} + +.carousel { + position: relative; +} + +.carousel.pointer-event { + -ms-touch-action: pan-y; + touch-action: pan-y; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel-inner::after { + display: block; + clear: both; + content: ""; +} + +.carousel-item { + position: relative; + display: none; + float: left; + width: 100%; + margin-right: -100%; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + transition: -webkit-transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-item { + transition: none; + } +} + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: block; +} + +.carousel-item-next:not(.carousel-item-left), +.active.carousel-item-right { + -webkit-transform: translateX(100%); + transform: translateX(100%); +} + +.carousel-item-prev:not(.carousel-item-right), +.active.carousel-item-left { + -webkit-transform: translateX(-100%); + transform: translateX(-100%); +} + +.carousel-fade .carousel-item { + opacity: 0; + transition-property: opacity; + -webkit-transform: none; + transform: none; +} + +.carousel-fade .carousel-item.active, +.carousel-fade .carousel-item-next.carousel-item-left, +.carousel-fade .carousel-item-prev.carousel-item-right { + z-index: 1; + opacity: 1; +} + +.carousel-fade .active.carousel-item-left, +.carousel-fade .active.carousel-item-right { + z-index: 0; + opacity: 0; + transition: opacity 0s 0.6s; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-fade .active.carousel-item-left, + .carousel-fade .active.carousel-item-right { + transition: none; + } +} + +.carousel-control-prev, +.carousel-control-next { + position: absolute; + top: 0; + bottom: 0; + z-index: 1; + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: center; + justify-content: center; + width: 15%; + color: #fff; + text-align: center; + opacity: 0.5; + transition: opacity 0.15s ease; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-control-prev, + .carousel-control-next { + transition: none; + } +} + +.carousel-control-prev:hover, .carousel-control-prev:focus, +.carousel-control-next:hover, +.carousel-control-next:focus { + color: #fff; + text-decoration: none; + outline: 0; + opacity: 0.9; +} + +.carousel-control-prev { + left: 0; +} + +.carousel-control-next { + right: 0; +} + +.carousel-control-prev-icon, +.carousel-control-next-icon { + display: inline-block; + width: 20px; + height: 20px; + background: no-repeat 50% / 100% 100%; +} + +.carousel-control-prev-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e"); +} + +.carousel-control-next-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e"); +} + +.carousel-indicators { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 15; + display: -ms-flexbox; + display: flex; + -ms-flex-pack: center; + justify-content: center; + padding-left: 0; + margin-right: 15%; + margin-left: 15%; + list-style: none; +} + +.carousel-indicators li { + box-sizing: content-box; + -ms-flex: 0 1 auto; + flex: 0 1 auto; + width: 30px; + height: 3px; + margin-right: 3px; + margin-left: 3px; + text-indent: -999px; + cursor: pointer; + background-color: #fff; + background-clip: padding-box; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + opacity: .5; + transition: opacity 0.6s ease; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-indicators li { + transition: none; + } +} + +.carousel-indicators .active { + opacity: 1; +} + +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; +} + +@-webkit-keyframes spinner-border { + to { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes spinner-border { + to { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +.spinner-border { + display: inline-block; + width: 2rem; + height: 2rem; + vertical-align: text-bottom; + border: 0.25em solid currentColor; + border-right-color: transparent; + border-radius: 50%; + -webkit-animation: spinner-border .75s linear infinite; + animation: spinner-border .75s linear infinite; +} + +.spinner-border-sm { + width: 1rem; + height: 1rem; + border-width: 0.2em; +} + +@-webkit-keyframes spinner-grow { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + } + 50% { + opacity: 1; + } +} + +@keyframes spinner-grow { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + } + 50% { + opacity: 1; + } +} + +.spinner-grow { + display: inline-block; + width: 2rem; + height: 2rem; + vertical-align: text-bottom; + background-color: currentColor; + border-radius: 50%; + opacity: 0; + -webkit-animation: spinner-grow .75s linear infinite; + animation: spinner-grow .75s linear infinite; +} + +.spinner-grow-sm { + width: 1rem; + height: 1rem; +} + +.align-baseline { + vertical-align: baseline !important; +} + +.align-top { + vertical-align: top !important; +} + +.align-middle { + vertical-align: middle !important; +} + +.align-bottom { + vertical-align: bottom !important; +} + +.align-text-bottom { + vertical-align: text-bottom !important; +} + +.align-text-top { + vertical-align: text-top !important; +} + +.bg-primary { + background-color: #007bff !important; +} + +a.bg-primary:hover, a.bg-primary:focus, +button.bg-primary:hover, +button.bg-primary:focus { + background-color: #0062cc !important; +} + +.bg-secondary { + background-color: #6c757d !important; +} + +a.bg-secondary:hover, a.bg-secondary:focus, +button.bg-secondary:hover, +button.bg-secondary:focus { + background-color: #545b62 !important; +} + +.bg-success { + background-color: #28a745 !important; +} + +a.bg-success:hover, a.bg-success:focus, +button.bg-success:hover, +button.bg-success:focus { + background-color: #1e7e34 !important; +} + +.bg-info { + background-color: #17a2b8 !important; +} + +a.bg-info:hover, a.bg-info:focus, +button.bg-info:hover, +button.bg-info:focus { + background-color: #117a8b !important; +} + +.bg-warning { + background-color: #ffc107 !important; +} + +a.bg-warning:hover, a.bg-warning:focus, +button.bg-warning:hover, +button.bg-warning:focus { + background-color: #d39e00 !important; +} + +.bg-danger { + background-color: #dc3545 !important; +} + +a.bg-danger:hover, a.bg-danger:focus, +button.bg-danger:hover, +button.bg-danger:focus { + background-color: #bd2130 !important; +} + +.bg-light { + background-color: #f8f9fa !important; +} + +a.bg-light:hover, a.bg-light:focus, +button.bg-light:hover, +button.bg-light:focus { + background-color: #dae0e5 !important; +} + +.bg-dark { + background-color: #343a40 !important; +} + +a.bg-dark:hover, a.bg-dark:focus, +button.bg-dark:hover, +button.bg-dark:focus { + background-color: #1d2124 !important; +} + +.bg-white { + background-color: #fff !important; +} + +.bg-transparent { + background-color: transparent !important; +} + +.border { + border: 1px solid #dee2e6 !important; +} + +.border-top { + border-top: 1px solid #dee2e6 !important; +} + +.border-right { + border-right: 1px solid #dee2e6 !important; +} + +.border-bottom { + border-bottom: 1px solid #dee2e6 !important; +} + +.border-left { + border-left: 1px solid #dee2e6 !important; +} + +.border-0 { + border: 0 !important; +} + +.border-top-0 { + border-top: 0 !important; +} + +.border-right-0 { + border-right: 0 !important; +} + +.border-bottom-0 { + border-bottom: 0 !important; +} + +.border-left-0 { + border-left: 0 !important; +} + +.border-primary { + border-color: #007bff !important; +} + +.border-secondary { + border-color: #6c757d !important; +} + +.border-success { + border-color: #28a745 !important; +} + +.border-info { + border-color: #17a2b8 !important; +} + +.border-warning { + border-color: #ffc107 !important; +} + +.border-danger { + border-color: #dc3545 !important; +} + +.border-light { + border-color: #f8f9fa !important; +} + +.border-dark { + border-color: #343a40 !important; +} + +.border-white { + border-color: #fff !important; +} + +.rounded-sm { + border-radius: 0.2rem !important; +} + +.rounded { + border-radius: 0.25rem !important; +} + +.rounded-top { + border-top-left-radius: 0.25rem !important; + border-top-right-radius: 0.25rem !important; +} + +.rounded-right { + border-top-right-radius: 0.25rem !important; + border-bottom-right-radius: 0.25rem !important; +} + +.rounded-bottom { + border-bottom-right-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.rounded-left { + border-top-left-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.rounded-lg { + border-radius: 0.3rem !important; +} + +.rounded-circle { + border-radius: 50% !important; +} + +.rounded-pill { + border-radius: 50rem !important; +} + +.rounded-0 { + border-radius: 0 !important; +} + +.clearfix::after { + display: block; + clear: both; + content: ""; +} + +.d-none { + display: none !important; +} + +.d-inline { + display: inline !important; +} + +.d-inline-block { + display: inline-block !important; +} + +.d-block { + display: block !important; +} + +.d-table { + display: table !important; +} + +.d-table-row { + display: table-row !important; +} + +.d-table-cell { + display: table-cell !important; +} + +.d-flex { + display: -ms-flexbox !important; + display: flex !important; +} + +.d-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; +} + +@media (min-width: 576px) { + .d-sm-none { + display: none !important; + } + .d-sm-inline { + display: inline !important; + } + .d-sm-inline-block { + display: inline-block !important; + } + .d-sm-block { + display: block !important; + } + .d-sm-table { + display: table !important; + } + .d-sm-table-row { + display: table-row !important; + } + .d-sm-table-cell { + display: table-cell !important; + } + .d-sm-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-sm-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 768px) { + .d-md-none { + display: none !important; + } + .d-md-inline { + display: inline !important; + } + .d-md-inline-block { + display: inline-block !important; + } + .d-md-block { + display: block !important; + } + .d-md-table { + display: table !important; + } + .d-md-table-row { + display: table-row !important; + } + .d-md-table-cell { + display: table-cell !important; + } + .d-md-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-md-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 992px) { + .d-lg-none { + display: none !important; + } + .d-lg-inline { + display: inline !important; + } + .d-lg-inline-block { + display: inline-block !important; + } + .d-lg-block { + display: block !important; + } + .d-lg-table { + display: table !important; + } + .d-lg-table-row { + display: table-row !important; + } + .d-lg-table-cell { + display: table-cell !important; + } + .d-lg-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-lg-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 1200px) { + .d-xl-none { + display: none !important; + } + .d-xl-inline { + display: inline !important; + } + .d-xl-inline-block { + display: inline-block !important; + } + .d-xl-block { + display: block !important; + } + .d-xl-table { + display: table !important; + } + .d-xl-table-row { + display: table-row !important; + } + .d-xl-table-cell { + display: table-cell !important; + } + .d-xl-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-xl-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media print { + .d-print-none { + display: none !important; + } + .d-print-inline { + display: inline !important; + } + .d-print-inline-block { + display: inline-block !important; + } + .d-print-block { + display: block !important; + } + .d-print-table { + display: table !important; + } + .d-print-table-row { + display: table-row !important; + } + .d-print-table-cell { + display: table-cell !important; + } + .d-print-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-print-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +.embed-responsive { + position: relative; + display: block; + width: 100%; + padding: 0; + overflow: hidden; +} + +.embed-responsive::before { + display: block; + content: ""; +} + +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} + +.embed-responsive-21by9::before { + padding-top: 42.857143%; +} + +.embed-responsive-16by9::before { + padding-top: 56.25%; +} + +.embed-responsive-4by3::before { + padding-top: 75%; +} + +.embed-responsive-1by1::before { + padding-top: 100%; +} + +.flex-row { + -ms-flex-direction: row !important; + flex-direction: row !important; +} + +.flex-column { + -ms-flex-direction: column !important; + flex-direction: column !important; +} + +.flex-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; +} + +.flex-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; +} + +.flex-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; +} + +.flex-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; +} + +.flex-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; +} + +.flex-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; +} + +.flex-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; +} + +.flex-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; +} + +.flex-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; +} + +.flex-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; +} + +.justify-content-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; +} + +.justify-content-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; +} + +.justify-content-center { + -ms-flex-pack: center !important; + justify-content: center !important; +} + +.justify-content-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; +} + +.justify-content-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; +} + +.align-items-start { + -ms-flex-align: start !important; + align-items: flex-start !important; +} + +.align-items-end { + -ms-flex-align: end !important; + align-items: flex-end !important; +} + +.align-items-center { + -ms-flex-align: center !important; + align-items: center !important; +} + +.align-items-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; +} + +.align-items-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; +} + +.align-content-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; +} + +.align-content-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; +} + +.align-content-center { + -ms-flex-line-pack: center !important; + align-content: center !important; +} + +.align-content-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; +} + +.align-content-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; +} + +.align-content-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; +} + +.align-self-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; +} + +.align-self-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; +} + +.align-self-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; +} + +.align-self-center { + -ms-flex-item-align: center !important; + align-self: center !important; +} + +.align-self-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; +} + +.align-self-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; +} + +@media (min-width: 576px) { + .flex-sm-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-sm-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-sm-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-sm-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-sm-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-sm-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-sm-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-sm-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-sm-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-sm-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-sm-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-sm-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-sm-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-sm-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-sm-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-sm-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-sm-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-sm-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-sm-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-sm-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-sm-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-sm-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-sm-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-sm-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-sm-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-sm-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-sm-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-sm-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-sm-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-sm-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-sm-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-sm-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-sm-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-sm-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 768px) { + .flex-md-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-md-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-md-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-md-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-md-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-md-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-md-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-md-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-md-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-md-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-md-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-md-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-md-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-md-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-md-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-md-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-md-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-md-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-md-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-md-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-md-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-md-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-md-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-md-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-md-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-md-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-md-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-md-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-md-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-md-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-md-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-md-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-md-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-md-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 992px) { + .flex-lg-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-lg-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-lg-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-lg-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-lg-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-lg-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-lg-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-lg-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-lg-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-lg-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-lg-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-lg-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-lg-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-lg-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-lg-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-lg-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-lg-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-lg-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-lg-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-lg-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-lg-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-lg-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-lg-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-lg-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-lg-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-lg-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-lg-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-lg-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-lg-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-lg-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-lg-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-lg-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-lg-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-lg-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 1200px) { + .flex-xl-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-xl-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-xl-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-xl-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-xl-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-xl-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-xl-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-xl-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-xl-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-xl-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-xl-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-xl-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-xl-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-xl-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-xl-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-xl-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-xl-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-xl-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-xl-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-xl-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-xl-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-xl-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-xl-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-xl-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-xl-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-xl-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-xl-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-xl-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-xl-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-xl-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-xl-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-xl-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-xl-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-xl-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +.float-left { + float: left !important; +} + +.float-right { + float: right !important; +} + +.float-none { + float: none !important; +} + +@media (min-width: 576px) { + .float-sm-left { + float: left !important; + } + .float-sm-right { + float: right !important; + } + .float-sm-none { + float: none !important; + } +} + +@media (min-width: 768px) { + .float-md-left { + float: left !important; + } + .float-md-right { + float: right !important; + } + .float-md-none { + float: none !important; + } +} + +@media (min-width: 992px) { + .float-lg-left { + float: left !important; + } + .float-lg-right { + float: right !important; + } + .float-lg-none { + float: none !important; + } +} + +@media (min-width: 1200px) { + .float-xl-left { + float: left !important; + } + .float-xl-right { + float: right !important; + } + .float-xl-none { + float: none !important; + } +} + +.overflow-auto { + overflow: auto !important; +} + +.overflow-hidden { + overflow: hidden !important; +} + +.position-static { + position: static !important; +} + +.position-relative { + position: relative !important; +} + +.position-absolute { + position: absolute !important; +} + +.position-fixed { + position: fixed !important; +} + +.position-sticky { + position: -webkit-sticky !important; + position: sticky !important; +} + +.fixed-top { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; +} + +.fixed-bottom { + position: fixed; + right: 0; + bottom: 0; + left: 0; + z-index: 1030; +} + +@supports ((position: -webkit-sticky) or (position: sticky)) { + .sticky-top { + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1020; + } +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.sr-only-focusable:active, .sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + overflow: visible; + clip: auto; + white-space: normal; +} + +.shadow-sm { + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; +} + +.shadow { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; +} + +.shadow-lg { + box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important; +} + +.shadow-none { + box-shadow: none !important; +} + +.w-25 { + width: 25% !important; +} + +.w-50 { + width: 50% !important; +} + +.w-75 { + width: 75% !important; +} + +.w-100 { + width: 100% !important; +} + +.w-auto { + width: auto !important; +} + +.h-25 { + height: 25% !important; +} + +.h-50 { + height: 50% !important; +} + +.h-75 { + height: 75% !important; +} + +.h-100 { + height: 100% !important; +} + +.h-auto { + height: auto !important; +} + +.mw-100 { + max-width: 100% !important; +} + +.mh-100 { + max-height: 100% !important; +} + +.min-vw-100 { + min-width: 100vw !important; +} + +.min-vh-100 { + min-height: 100vh !important; +} + +.vw-100 { + width: 100vw !important; +} + +.vh-100 { + height: 100vh !important; +} + +.stretched-link::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1; + pointer-events: auto; + content: ""; + background-color: rgba(0, 0, 0, 0); +} + +.m-0 { + margin: 0 !important; +} + +.mt-0, +.my-0 { + margin-top: 0 !important; +} + +.mr-0, +.mx-0 { + margin-right: 0 !important; +} + +.mb-0, +.my-0 { + margin-bottom: 0 !important; +} + +.ml-0, +.mx-0 { + margin-left: 0 !important; +} + +.m-1 { + margin: 0.25rem !important; +} + +.mt-1, +.my-1 { + margin-top: 0.25rem !important; +} + +.mr-1, +.mx-1 { + margin-right: 0.25rem !important; +} + +.mb-1, +.my-1 { + margin-bottom: 0.25rem !important; +} + +.ml-1, +.mx-1 { + margin-left: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.mt-2, +.my-2 { + margin-top: 0.5rem !important; +} + +.mr-2, +.mx-2 { + margin-right: 0.5rem !important; +} + +.mb-2, +.my-2 { + margin-bottom: 0.5rem !important; +} + +.ml-2, +.mx-2 { + margin-left: 0.5rem !important; +} + +.m-3 { + margin: 1rem !important; +} + +.mt-3, +.my-3 { + margin-top: 1rem !important; +} + +.mr-3, +.mx-3 { + margin-right: 1rem !important; +} + +.mb-3, +.my-3 { + margin-bottom: 1rem !important; +} + +.ml-3, +.mx-3 { + margin-left: 1rem !important; +} + +.m-4 { + margin: 1.5rem !important; +} + +.mt-4, +.my-4 { + margin-top: 1.5rem !important; +} + +.mr-4, +.mx-4 { + margin-right: 1.5rem !important; +} + +.mb-4, +.my-4 { + margin-bottom: 1.5rem !important; +} + +.ml-4, +.mx-4 { + margin-left: 1.5rem !important; +} + +.m-5 { + margin: 3rem !important; +} + +.mt-5, +.my-5 { + margin-top: 3rem !important; +} + +.mr-5, +.mx-5 { + margin-right: 3rem !important; +} + +.mb-5, +.my-5 { + margin-bottom: 3rem !important; +} + +.ml-5, +.mx-5 { + margin-left: 3rem !important; +} + +.p-0 { + padding: 0 !important; +} + +.pt-0, +.py-0 { + padding-top: 0 !important; +} + +.pr-0, +.px-0 { + padding-right: 0 !important; +} + +.pb-0, +.py-0 { + padding-bottom: 0 !important; +} + +.pl-0, +.px-0 { + padding-left: 0 !important; +} + +.p-1 { + padding: 0.25rem !important; +} + +.pt-1, +.py-1 { + padding-top: 0.25rem !important; +} + +.pr-1, +.px-1 { + padding-right: 0.25rem !important; +} + +.pb-1, +.py-1 { + padding-bottom: 0.25rem !important; +} + +.pl-1, +.px-1 { + padding-left: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.pt-2, +.py-2 { + padding-top: 0.5rem !important; +} + +.pr-2, +.px-2 { + padding-right: 0.5rem !important; +} + +.pb-2, +.py-2 { + padding-bottom: 0.5rem !important; +} + +.pl-2, +.px-2 { + padding-left: 0.5rem !important; +} + +.p-3 { + padding: 1rem !important; +} + +.pt-3, +.py-3 { + padding-top: 1rem !important; +} + +.pr-3, +.px-3 { + padding-right: 1rem !important; +} + +.pb-3, +.py-3 { + padding-bottom: 1rem !important; +} + +.pl-3, +.px-3 { + padding-left: 1rem !important; +} + +.p-4 { + padding: 1.5rem !important; +} + +.pt-4, +.py-4 { + padding-top: 1.5rem !important; +} + +.pr-4, +.px-4 { + padding-right: 1.5rem !important; +} + +.pb-4, +.py-4 { + padding-bottom: 1.5rem !important; +} + +.pl-4, +.px-4 { + padding-left: 1.5rem !important; +} + +.p-5 { + padding: 3rem !important; +} + +.pt-5, +.py-5 { + padding-top: 3rem !important; +} + +.pr-5, +.px-5 { + padding-right: 3rem !important; +} + +.pb-5, +.py-5 { + padding-bottom: 3rem !important; +} + +.pl-5, +.px-5 { + padding-left: 3rem !important; +} + +.m-n1 { + margin: -0.25rem !important; +} + +.mt-n1, +.my-n1 { + margin-top: -0.25rem !important; +} + +.mr-n1, +.mx-n1 { + margin-right: -0.25rem !important; +} + +.mb-n1, +.my-n1 { + margin-bottom: -0.25rem !important; +} + +.ml-n1, +.mx-n1 { + margin-left: -0.25rem !important; +} + +.m-n2 { + margin: -0.5rem !important; +} + +.mt-n2, +.my-n2 { + margin-top: -0.5rem !important; +} + +.mr-n2, +.mx-n2 { + margin-right: -0.5rem !important; +} + +.mb-n2, +.my-n2 { + margin-bottom: -0.5rem !important; +} + +.ml-n2, +.mx-n2 { + margin-left: -0.5rem !important; +} + +.m-n3 { + margin: -1rem !important; +} + +.mt-n3, +.my-n3 { + margin-top: -1rem !important; +} + +.mr-n3, +.mx-n3 { + margin-right: -1rem !important; +} + +.mb-n3, +.my-n3 { + margin-bottom: -1rem !important; +} + +.ml-n3, +.mx-n3 { + margin-left: -1rem !important; +} + +.m-n4 { + margin: -1.5rem !important; +} + +.mt-n4, +.my-n4 { + margin-top: -1.5rem !important; +} + +.mr-n4, +.mx-n4 { + margin-right: -1.5rem !important; +} + +.mb-n4, +.my-n4 { + margin-bottom: -1.5rem !important; +} + +.ml-n4, +.mx-n4 { + margin-left: -1.5rem !important; +} + +.m-n5 { + margin: -3rem !important; +} + +.mt-n5, +.my-n5 { + margin-top: -3rem !important; +} + +.mr-n5, +.mx-n5 { + margin-right: -3rem !important; +} + +.mb-n5, +.my-n5 { + margin-bottom: -3rem !important; +} + +.ml-n5, +.mx-n5 { + margin-left: -3rem !important; +} + +.m-auto { + margin: auto !important; +} + +.mt-auto, +.my-auto { + margin-top: auto !important; +} + +.mr-auto, +.mx-auto { + margin-right: auto !important; +} + +.mb-auto, +.my-auto { + margin-bottom: auto !important; +} + +.ml-auto, +.mx-auto { + margin-left: auto !important; +} + +@media (min-width: 576px) { + .m-sm-0 { + margin: 0 !important; + } + .mt-sm-0, + .my-sm-0 { + margin-top: 0 !important; + } + .mr-sm-0, + .mx-sm-0 { + margin-right: 0 !important; + } + .mb-sm-0, + .my-sm-0 { + margin-bottom: 0 !important; + } + .ml-sm-0, + .mx-sm-0 { + margin-left: 0 !important; + } + .m-sm-1 { + margin: 0.25rem !important; + } + .mt-sm-1, + .my-sm-1 { + margin-top: 0.25rem !important; + } + .mr-sm-1, + .mx-sm-1 { + margin-right: 0.25rem !important; + } + .mb-sm-1, + .my-sm-1 { + margin-bottom: 0.25rem !important; + } + .ml-sm-1, + .mx-sm-1 { + margin-left: 0.25rem !important; + } + .m-sm-2 { + margin: 0.5rem !important; + } + .mt-sm-2, + .my-sm-2 { + margin-top: 0.5rem !important; + } + .mr-sm-2, + .mx-sm-2 { + margin-right: 0.5rem !important; + } + .mb-sm-2, + .my-sm-2 { + margin-bottom: 0.5rem !important; + } + .ml-sm-2, + .mx-sm-2 { + margin-left: 0.5rem !important; + } + .m-sm-3 { + margin: 1rem !important; + } + .mt-sm-3, + .my-sm-3 { + margin-top: 1rem !important; + } + .mr-sm-3, + .mx-sm-3 { + margin-right: 1rem !important; + } + .mb-sm-3, + .my-sm-3 { + margin-bottom: 1rem !important; + } + .ml-sm-3, + .mx-sm-3 { + margin-left: 1rem !important; + } + .m-sm-4 { + margin: 1.5rem !important; + } + .mt-sm-4, + .my-sm-4 { + margin-top: 1.5rem !important; + } + .mr-sm-4, + .mx-sm-4 { + margin-right: 1.5rem !important; + } + .mb-sm-4, + .my-sm-4 { + margin-bottom: 1.5rem !important; + } + .ml-sm-4, + .mx-sm-4 { + margin-left: 1.5rem !important; + } + .m-sm-5 { + margin: 3rem !important; + } + .mt-sm-5, + .my-sm-5 { + margin-top: 3rem !important; + } + .mr-sm-5, + .mx-sm-5 { + margin-right: 3rem !important; + } + .mb-sm-5, + .my-sm-5 { + margin-bottom: 3rem !important; + } + .ml-sm-5, + .mx-sm-5 { + margin-left: 3rem !important; + } + .p-sm-0 { + padding: 0 !important; + } + .pt-sm-0, + .py-sm-0 { + padding-top: 0 !important; + } + .pr-sm-0, + .px-sm-0 { + padding-right: 0 !important; + } + .pb-sm-0, + .py-sm-0 { + padding-bottom: 0 !important; + } + .pl-sm-0, + .px-sm-0 { + padding-left: 0 !important; + } + .p-sm-1 { + padding: 0.25rem !important; + } + .pt-sm-1, + .py-sm-1 { + padding-top: 0.25rem !important; + } + .pr-sm-1, + .px-sm-1 { + padding-right: 0.25rem !important; + } + .pb-sm-1, + .py-sm-1 { + padding-bottom: 0.25rem !important; + } + .pl-sm-1, + .px-sm-1 { + padding-left: 0.25rem !important; + } + .p-sm-2 { + padding: 0.5rem !important; + } + .pt-sm-2, + .py-sm-2 { + padding-top: 0.5rem !important; + } + .pr-sm-2, + .px-sm-2 { + padding-right: 0.5rem !important; + } + .pb-sm-2, + .py-sm-2 { + padding-bottom: 0.5rem !important; + } + .pl-sm-2, + .px-sm-2 { + padding-left: 0.5rem !important; + } + .p-sm-3 { + padding: 1rem !important; + } + .pt-sm-3, + .py-sm-3 { + padding-top: 1rem !important; + } + .pr-sm-3, + .px-sm-3 { + padding-right: 1rem !important; + } + .pb-sm-3, + .py-sm-3 { + padding-bottom: 1rem !important; + } + .pl-sm-3, + .px-sm-3 { + padding-left: 1rem !important; + } + .p-sm-4 { + padding: 1.5rem !important; + } + .pt-sm-4, + .py-sm-4 { + padding-top: 1.5rem !important; + } + .pr-sm-4, + .px-sm-4 { + padding-right: 1.5rem !important; + } + .pb-sm-4, + .py-sm-4 { + padding-bottom: 1.5rem !important; + } + .pl-sm-4, + .px-sm-4 { + padding-left: 1.5rem !important; + } + .p-sm-5 { + padding: 3rem !important; + } + .pt-sm-5, + .py-sm-5 { + padding-top: 3rem !important; + } + .pr-sm-5, + .px-sm-5 { + padding-right: 3rem !important; + } + .pb-sm-5, + .py-sm-5 { + padding-bottom: 3rem !important; + } + .pl-sm-5, + .px-sm-5 { + padding-left: 3rem !important; + } + .m-sm-n1 { + margin: -0.25rem !important; + } + .mt-sm-n1, + .my-sm-n1 { + margin-top: -0.25rem !important; + } + .mr-sm-n1, + .mx-sm-n1 { + margin-right: -0.25rem !important; + } + .mb-sm-n1, + .my-sm-n1 { + margin-bottom: -0.25rem !important; + } + .ml-sm-n1, + .mx-sm-n1 { + margin-left: -0.25rem !important; + } + .m-sm-n2 { + margin: -0.5rem !important; + } + .mt-sm-n2, + .my-sm-n2 { + margin-top: -0.5rem !important; + } + .mr-sm-n2, + .mx-sm-n2 { + margin-right: -0.5rem !important; + } + .mb-sm-n2, + .my-sm-n2 { + margin-bottom: -0.5rem !important; + } + .ml-sm-n2, + .mx-sm-n2 { + margin-left: -0.5rem !important; + } + .m-sm-n3 { + margin: -1rem !important; + } + .mt-sm-n3, + .my-sm-n3 { + margin-top: -1rem !important; + } + .mr-sm-n3, + .mx-sm-n3 { + margin-right: -1rem !important; + } + .mb-sm-n3, + .my-sm-n3 { + margin-bottom: -1rem !important; + } + .ml-sm-n3, + .mx-sm-n3 { + margin-left: -1rem !important; + } + .m-sm-n4 { + margin: -1.5rem !important; + } + .mt-sm-n4, + .my-sm-n4 { + margin-top: -1.5rem !important; + } + .mr-sm-n4, + .mx-sm-n4 { + margin-right: -1.5rem !important; + } + .mb-sm-n4, + .my-sm-n4 { + margin-bottom: -1.5rem !important; + } + .ml-sm-n4, + .mx-sm-n4 { + margin-left: -1.5rem !important; + } + .m-sm-n5 { + margin: -3rem !important; + } + .mt-sm-n5, + .my-sm-n5 { + margin-top: -3rem !important; + } + .mr-sm-n5, + .mx-sm-n5 { + margin-right: -3rem !important; + } + .mb-sm-n5, + .my-sm-n5 { + margin-bottom: -3rem !important; + } + .ml-sm-n5, + .mx-sm-n5 { + margin-left: -3rem !important; + } + .m-sm-auto { + margin: auto !important; + } + .mt-sm-auto, + .my-sm-auto { + margin-top: auto !important; + } + .mr-sm-auto, + .mx-sm-auto { + margin-right: auto !important; + } + .mb-sm-auto, + .my-sm-auto { + margin-bottom: auto !important; + } + .ml-sm-auto, + .mx-sm-auto { + margin-left: auto !important; + } +} + +@media (min-width: 768px) { + .m-md-0 { + margin: 0 !important; + } + .mt-md-0, + .my-md-0 { + margin-top: 0 !important; + } + .mr-md-0, + .mx-md-0 { + margin-right: 0 !important; + } + .mb-md-0, + .my-md-0 { + margin-bottom: 0 !important; + } + .ml-md-0, + .mx-md-0 { + margin-left: 0 !important; + } + .m-md-1 { + margin: 0.25rem !important; + } + .mt-md-1, + .my-md-1 { + margin-top: 0.25rem !important; + } + .mr-md-1, + .mx-md-1 { + margin-right: 0.25rem !important; + } + .mb-md-1, + .my-md-1 { + margin-bottom: 0.25rem !important; + } + .ml-md-1, + .mx-md-1 { + margin-left: 0.25rem !important; + } + .m-md-2 { + margin: 0.5rem !important; + } + .mt-md-2, + .my-md-2 { + margin-top: 0.5rem !important; + } + .mr-md-2, + .mx-md-2 { + margin-right: 0.5rem !important; + } + .mb-md-2, + .my-md-2 { + margin-bottom: 0.5rem !important; + } + .ml-md-2, + .mx-md-2 { + margin-left: 0.5rem !important; + } + .m-md-3 { + margin: 1rem !important; + } + .mt-md-3, + .my-md-3 { + margin-top: 1rem !important; + } + .mr-md-3, + .mx-md-3 { + margin-right: 1rem !important; + } + .mb-md-3, + .my-md-3 { + margin-bottom: 1rem !important; + } + .ml-md-3, + .mx-md-3 { + margin-left: 1rem !important; + } + .m-md-4 { + margin: 1.5rem !important; + } + .mt-md-4, + .my-md-4 { + margin-top: 1.5rem !important; + } + .mr-md-4, + .mx-md-4 { + margin-right: 1.5rem !important; + } + .mb-md-4, + .my-md-4 { + margin-bottom: 1.5rem !important; + } + .ml-md-4, + .mx-md-4 { + margin-left: 1.5rem !important; + } + .m-md-5 { + margin: 3rem !important; + } + .mt-md-5, + .my-md-5 { + margin-top: 3rem !important; + } + .mr-md-5, + .mx-md-5 { + margin-right: 3rem !important; + } + .mb-md-5, + .my-md-5 { + margin-bottom: 3rem !important; + } + .ml-md-5, + .mx-md-5 { + margin-left: 3rem !important; + } + .p-md-0 { + padding: 0 !important; + } + .pt-md-0, + .py-md-0 { + padding-top: 0 !important; + } + .pr-md-0, + .px-md-0 { + padding-right: 0 !important; + } + .pb-md-0, + .py-md-0 { + padding-bottom: 0 !important; + } + .pl-md-0, + .px-md-0 { + padding-left: 0 !important; + } + .p-md-1 { + padding: 0.25rem !important; + } + .pt-md-1, + .py-md-1 { + padding-top: 0.25rem !important; + } + .pr-md-1, + .px-md-1 { + padding-right: 0.25rem !important; + } + .pb-md-1, + .py-md-1 { + padding-bottom: 0.25rem !important; + } + .pl-md-1, + .px-md-1 { + padding-left: 0.25rem !important; + } + .p-md-2 { + padding: 0.5rem !important; + } + .pt-md-2, + .py-md-2 { + padding-top: 0.5rem !important; + } + .pr-md-2, + .px-md-2 { + padding-right: 0.5rem !important; + } + .pb-md-2, + .py-md-2 { + padding-bottom: 0.5rem !important; + } + .pl-md-2, + .px-md-2 { + padding-left: 0.5rem !important; + } + .p-md-3 { + padding: 1rem !important; + } + .pt-md-3, + .py-md-3 { + padding-top: 1rem !important; + } + .pr-md-3, + .px-md-3 { + padding-right: 1rem !important; + } + .pb-md-3, + .py-md-3 { + padding-bottom: 1rem !important; + } + .pl-md-3, + .px-md-3 { + padding-left: 1rem !important; + } + .p-md-4 { + padding: 1.5rem !important; + } + .pt-md-4, + .py-md-4 { + padding-top: 1.5rem !important; + } + .pr-md-4, + .px-md-4 { + padding-right: 1.5rem !important; + } + .pb-md-4, + .py-md-4 { + padding-bottom: 1.5rem !important; + } + .pl-md-4, + .px-md-4 { + padding-left: 1.5rem !important; + } + .p-md-5 { + padding: 3rem !important; + } + .pt-md-5, + .py-md-5 { + padding-top: 3rem !important; + } + .pr-md-5, + .px-md-5 { + padding-right: 3rem !important; + } + .pb-md-5, + .py-md-5 { + padding-bottom: 3rem !important; + } + .pl-md-5, + .px-md-5 { + padding-left: 3rem !important; + } + .m-md-n1 { + margin: -0.25rem !important; + } + .mt-md-n1, + .my-md-n1 { + margin-top: -0.25rem !important; + } + .mr-md-n1, + .mx-md-n1 { + margin-right: -0.25rem !important; + } + .mb-md-n1, + .my-md-n1 { + margin-bottom: -0.25rem !important; + } + .ml-md-n1, + .mx-md-n1 { + margin-left: -0.25rem !important; + } + .m-md-n2 { + margin: -0.5rem !important; + } + .mt-md-n2, + .my-md-n2 { + margin-top: -0.5rem !important; + } + .mr-md-n2, + .mx-md-n2 { + margin-right: -0.5rem !important; + } + .mb-md-n2, + .my-md-n2 { + margin-bottom: -0.5rem !important; + } + .ml-md-n2, + .mx-md-n2 { + margin-left: -0.5rem !important; + } + .m-md-n3 { + margin: -1rem !important; + } + .mt-md-n3, + .my-md-n3 { + margin-top: -1rem !important; + } + .mr-md-n3, + .mx-md-n3 { + margin-right: -1rem !important; + } + .mb-md-n3, + .my-md-n3 { + margin-bottom: -1rem !important; + } + .ml-md-n3, + .mx-md-n3 { + margin-left: -1rem !important; + } + .m-md-n4 { + margin: -1.5rem !important; + } + .mt-md-n4, + .my-md-n4 { + margin-top: -1.5rem !important; + } + .mr-md-n4, + .mx-md-n4 { + margin-right: -1.5rem !important; + } + .mb-md-n4, + .my-md-n4 { + margin-bottom: -1.5rem !important; + } + .ml-md-n4, + .mx-md-n4 { + margin-left: -1.5rem !important; + } + .m-md-n5 { + margin: -3rem !important; + } + .mt-md-n5, + .my-md-n5 { + margin-top: -3rem !important; + } + .mr-md-n5, + .mx-md-n5 { + margin-right: -3rem !important; + } + .mb-md-n5, + .my-md-n5 { + margin-bottom: -3rem !important; + } + .ml-md-n5, + .mx-md-n5 { + margin-left: -3rem !important; + } + .m-md-auto { + margin: auto !important; + } + .mt-md-auto, + .my-md-auto { + margin-top: auto !important; + } + .mr-md-auto, + .mx-md-auto { + margin-right: auto !important; + } + .mb-md-auto, + .my-md-auto { + margin-bottom: auto !important; + } + .ml-md-auto, + .mx-md-auto { + margin-left: auto !important; + } +} + +@media (min-width: 992px) { + .m-lg-0 { + margin: 0 !important; + } + .mt-lg-0, + .my-lg-0 { + margin-top: 0 !important; + } + .mr-lg-0, + .mx-lg-0 { + margin-right: 0 !important; + } + .mb-lg-0, + .my-lg-0 { + margin-bottom: 0 !important; + } + .ml-lg-0, + .mx-lg-0 { + margin-left: 0 !important; + } + .m-lg-1 { + margin: 0.25rem !important; + } + .mt-lg-1, + .my-lg-1 { + margin-top: 0.25rem !important; + } + .mr-lg-1, + .mx-lg-1 { + margin-right: 0.25rem !important; + } + .mb-lg-1, + .my-lg-1 { + margin-bottom: 0.25rem !important; + } + .ml-lg-1, + .mx-lg-1 { + margin-left: 0.25rem !important; + } + .m-lg-2 { + margin: 0.5rem !important; + } + .mt-lg-2, + .my-lg-2 { + margin-top: 0.5rem !important; + } + .mr-lg-2, + .mx-lg-2 { + margin-right: 0.5rem !important; + } + .mb-lg-2, + .my-lg-2 { + margin-bottom: 0.5rem !important; + } + .ml-lg-2, + .mx-lg-2 { + margin-left: 0.5rem !important; + } + .m-lg-3 { + margin: 1rem !important; + } + .mt-lg-3, + .my-lg-3 { + margin-top: 1rem !important; + } + .mr-lg-3, + .mx-lg-3 { + margin-right: 1rem !important; + } + .mb-lg-3, + .my-lg-3 { + margin-bottom: 1rem !important; + } + .ml-lg-3, + .mx-lg-3 { + margin-left: 1rem !important; + } + .m-lg-4 { + margin: 1.5rem !important; + } + .mt-lg-4, + .my-lg-4 { + margin-top: 1.5rem !important; + } + .mr-lg-4, + .mx-lg-4 { + margin-right: 1.5rem !important; + } + .mb-lg-4, + .my-lg-4 { + margin-bottom: 1.5rem !important; + } + .ml-lg-4, + .mx-lg-4 { + margin-left: 1.5rem !important; + } + .m-lg-5 { + margin: 3rem !important; + } + .mt-lg-5, + .my-lg-5 { + margin-top: 3rem !important; + } + .mr-lg-5, + .mx-lg-5 { + margin-right: 3rem !important; + } + .mb-lg-5, + .my-lg-5 { + margin-bottom: 3rem !important; + } + .ml-lg-5, + .mx-lg-5 { + margin-left: 3rem !important; + } + .p-lg-0 { + padding: 0 !important; + } + .pt-lg-0, + .py-lg-0 { + padding-top: 0 !important; + } + .pr-lg-0, + .px-lg-0 { + padding-right: 0 !important; + } + .pb-lg-0, + .py-lg-0 { + padding-bottom: 0 !important; + } + .pl-lg-0, + .px-lg-0 { + padding-left: 0 !important; + } + .p-lg-1 { + padding: 0.25rem !important; + } + .pt-lg-1, + .py-lg-1 { + padding-top: 0.25rem !important; + } + .pr-lg-1, + .px-lg-1 { + padding-right: 0.25rem !important; + } + .pb-lg-1, + .py-lg-1 { + padding-bottom: 0.25rem !important; + } + .pl-lg-1, + .px-lg-1 { + padding-left: 0.25rem !important; + } + .p-lg-2 { + padding: 0.5rem !important; + } + .pt-lg-2, + .py-lg-2 { + padding-top: 0.5rem !important; + } + .pr-lg-2, + .px-lg-2 { + padding-right: 0.5rem !important; + } + .pb-lg-2, + .py-lg-2 { + padding-bottom: 0.5rem !important; + } + .pl-lg-2, + .px-lg-2 { + padding-left: 0.5rem !important; + } + .p-lg-3 { + padding: 1rem !important; + } + .pt-lg-3, + .py-lg-3 { + padding-top: 1rem !important; + } + .pr-lg-3, + .px-lg-3 { + padding-right: 1rem !important; + } + .pb-lg-3, + .py-lg-3 { + padding-bottom: 1rem !important; + } + .pl-lg-3, + .px-lg-3 { + padding-left: 1rem !important; + } + .p-lg-4 { + padding: 1.5rem !important; + } + .pt-lg-4, + .py-lg-4 { + padding-top: 1.5rem !important; + } + .pr-lg-4, + .px-lg-4 { + padding-right: 1.5rem !important; + } + .pb-lg-4, + .py-lg-4 { + padding-bottom: 1.5rem !important; + } + .pl-lg-4, + .px-lg-4 { + padding-left: 1.5rem !important; + } + .p-lg-5 { + padding: 3rem !important; + } + .pt-lg-5, + .py-lg-5 { + padding-top: 3rem !important; + } + .pr-lg-5, + .px-lg-5 { + padding-right: 3rem !important; + } + .pb-lg-5, + .py-lg-5 { + padding-bottom: 3rem !important; + } + .pl-lg-5, + .px-lg-5 { + padding-left: 3rem !important; + } + .m-lg-n1 { + margin: -0.25rem !important; + } + .mt-lg-n1, + .my-lg-n1 { + margin-top: -0.25rem !important; + } + .mr-lg-n1, + .mx-lg-n1 { + margin-right: -0.25rem !important; + } + .mb-lg-n1, + .my-lg-n1 { + margin-bottom: -0.25rem !important; + } + .ml-lg-n1, + .mx-lg-n1 { + margin-left: -0.25rem !important; + } + .m-lg-n2 { + margin: -0.5rem !important; + } + .mt-lg-n2, + .my-lg-n2 { + margin-top: -0.5rem !important; + } + .mr-lg-n2, + .mx-lg-n2 { + margin-right: -0.5rem !important; + } + .mb-lg-n2, + .my-lg-n2 { + margin-bottom: -0.5rem !important; + } + .ml-lg-n2, + .mx-lg-n2 { + margin-left: -0.5rem !important; + } + .m-lg-n3 { + margin: -1rem !important; + } + .mt-lg-n3, + .my-lg-n3 { + margin-top: -1rem !important; + } + .mr-lg-n3, + .mx-lg-n3 { + margin-right: -1rem !important; + } + .mb-lg-n3, + .my-lg-n3 { + margin-bottom: -1rem !important; + } + .ml-lg-n3, + .mx-lg-n3 { + margin-left: -1rem !important; + } + .m-lg-n4 { + margin: -1.5rem !important; + } + .mt-lg-n4, + .my-lg-n4 { + margin-top: -1.5rem !important; + } + .mr-lg-n4, + .mx-lg-n4 { + margin-right: -1.5rem !important; + } + .mb-lg-n4, + .my-lg-n4 { + margin-bottom: -1.5rem !important; + } + .ml-lg-n4, + .mx-lg-n4 { + margin-left: -1.5rem !important; + } + .m-lg-n5 { + margin: -3rem !important; + } + .mt-lg-n5, + .my-lg-n5 { + margin-top: -3rem !important; + } + .mr-lg-n5, + .mx-lg-n5 { + margin-right: -3rem !important; + } + .mb-lg-n5, + .my-lg-n5 { + margin-bottom: -3rem !important; + } + .ml-lg-n5, + .mx-lg-n5 { + margin-left: -3rem !important; + } + .m-lg-auto { + margin: auto !important; + } + .mt-lg-auto, + .my-lg-auto { + margin-top: auto !important; + } + .mr-lg-auto, + .mx-lg-auto { + margin-right: auto !important; + } + .mb-lg-auto, + .my-lg-auto { + margin-bottom: auto !important; + } + .ml-lg-auto, + .mx-lg-auto { + margin-left: auto !important; + } +} + +@media (min-width: 1200px) { + .m-xl-0 { + margin: 0 !important; + } + .mt-xl-0, + .my-xl-0 { + margin-top: 0 !important; + } + .mr-xl-0, + .mx-xl-0 { + margin-right: 0 !important; + } + .mb-xl-0, + .my-xl-0 { + margin-bottom: 0 !important; + } + .ml-xl-0, + .mx-xl-0 { + margin-left: 0 !important; + } + .m-xl-1 { + margin: 0.25rem !important; + } + .mt-xl-1, + .my-xl-1 { + margin-top: 0.25rem !important; + } + .mr-xl-1, + .mx-xl-1 { + margin-right: 0.25rem !important; + } + .mb-xl-1, + .my-xl-1 { + margin-bottom: 0.25rem !important; + } + .ml-xl-1, + .mx-xl-1 { + margin-left: 0.25rem !important; + } + .m-xl-2 { + margin: 0.5rem !important; + } + .mt-xl-2, + .my-xl-2 { + margin-top: 0.5rem !important; + } + .mr-xl-2, + .mx-xl-2 { + margin-right: 0.5rem !important; + } + .mb-xl-2, + .my-xl-2 { + margin-bottom: 0.5rem !important; + } + .ml-xl-2, + .mx-xl-2 { + margin-left: 0.5rem !important; + } + .m-xl-3 { + margin: 1rem !important; + } + .mt-xl-3, + .my-xl-3 { + margin-top: 1rem !important; + } + .mr-xl-3, + .mx-xl-3 { + margin-right: 1rem !important; + } + .mb-xl-3, + .my-xl-3 { + margin-bottom: 1rem !important; + } + .ml-xl-3, + .mx-xl-3 { + margin-left: 1rem !important; + } + .m-xl-4 { + margin: 1.5rem !important; + } + .mt-xl-4, + .my-xl-4 { + margin-top: 1.5rem !important; + } + .mr-xl-4, + .mx-xl-4 { + margin-right: 1.5rem !important; + } + .mb-xl-4, + .my-xl-4 { + margin-bottom: 1.5rem !important; + } + .ml-xl-4, + .mx-xl-4 { + margin-left: 1.5rem !important; + } + .m-xl-5 { + margin: 3rem !important; + } + .mt-xl-5, + .my-xl-5 { + margin-top: 3rem !important; + } + .mr-xl-5, + .mx-xl-5 { + margin-right: 3rem !important; + } + .mb-xl-5, + .my-xl-5 { + margin-bottom: 3rem !important; + } + .ml-xl-5, + .mx-xl-5 { + margin-left: 3rem !important; + } + .p-xl-0 { + padding: 0 !important; + } + .pt-xl-0, + .py-xl-0 { + padding-top: 0 !important; + } + .pr-xl-0, + .px-xl-0 { + padding-right: 0 !important; + } + .pb-xl-0, + .py-xl-0 { + padding-bottom: 0 !important; + } + .pl-xl-0, + .px-xl-0 { + padding-left: 0 !important; + } + .p-xl-1 { + padding: 0.25rem !important; + } + .pt-xl-1, + .py-xl-1 { + padding-top: 0.25rem !important; + } + .pr-xl-1, + .px-xl-1 { + padding-right: 0.25rem !important; + } + .pb-xl-1, + .py-xl-1 { + padding-bottom: 0.25rem !important; + } + .pl-xl-1, + .px-xl-1 { + padding-left: 0.25rem !important; + } + .p-xl-2 { + padding: 0.5rem !important; + } + .pt-xl-2, + .py-xl-2 { + padding-top: 0.5rem !important; + } + .pr-xl-2, + .px-xl-2 { + padding-right: 0.5rem !important; + } + .pb-xl-2, + .py-xl-2 { + padding-bottom: 0.5rem !important; + } + .pl-xl-2, + .px-xl-2 { + padding-left: 0.5rem !important; + } + .p-xl-3 { + padding: 1rem !important; + } + .pt-xl-3, + .py-xl-3 { + padding-top: 1rem !important; + } + .pr-xl-3, + .px-xl-3 { + padding-right: 1rem !important; + } + .pb-xl-3, + .py-xl-3 { + padding-bottom: 1rem !important; + } + .pl-xl-3, + .px-xl-3 { + padding-left: 1rem !important; + } + .p-xl-4 { + padding: 1.5rem !important; + } + .pt-xl-4, + .py-xl-4 { + padding-top: 1.5rem !important; + } + .pr-xl-4, + .px-xl-4 { + padding-right: 1.5rem !important; + } + .pb-xl-4, + .py-xl-4 { + padding-bottom: 1.5rem !important; + } + .pl-xl-4, + .px-xl-4 { + padding-left: 1.5rem !important; + } + .p-xl-5 { + padding: 3rem !important; + } + .pt-xl-5, + .py-xl-5 { + padding-top: 3rem !important; + } + .pr-xl-5, + .px-xl-5 { + padding-right: 3rem !important; + } + .pb-xl-5, + .py-xl-5 { + padding-bottom: 3rem !important; + } + .pl-xl-5, + .px-xl-5 { + padding-left: 3rem !important; + } + .m-xl-n1 { + margin: -0.25rem !important; + } + .mt-xl-n1, + .my-xl-n1 { + margin-top: -0.25rem !important; + } + .mr-xl-n1, + .mx-xl-n1 { + margin-right: -0.25rem !important; + } + .mb-xl-n1, + .my-xl-n1 { + margin-bottom: -0.25rem !important; + } + .ml-xl-n1, + .mx-xl-n1 { + margin-left: -0.25rem !important; + } + .m-xl-n2 { + margin: -0.5rem !important; + } + .mt-xl-n2, + .my-xl-n2 { + margin-top: -0.5rem !important; + } + .mr-xl-n2, + .mx-xl-n2 { + margin-right: -0.5rem !important; + } + .mb-xl-n2, + .my-xl-n2 { + margin-bottom: -0.5rem !important; + } + .ml-xl-n2, + .mx-xl-n2 { + margin-left: -0.5rem !important; + } + .m-xl-n3 { + margin: -1rem !important; + } + .mt-xl-n3, + .my-xl-n3 { + margin-top: -1rem !important; + } + .mr-xl-n3, + .mx-xl-n3 { + margin-right: -1rem !important; + } + .mb-xl-n3, + .my-xl-n3 { + margin-bottom: -1rem !important; + } + .ml-xl-n3, + .mx-xl-n3 { + margin-left: -1rem !important; + } + .m-xl-n4 { + margin: -1.5rem !important; + } + .mt-xl-n4, + .my-xl-n4 { + margin-top: -1.5rem !important; + } + .mr-xl-n4, + .mx-xl-n4 { + margin-right: -1.5rem !important; + } + .mb-xl-n4, + .my-xl-n4 { + margin-bottom: -1.5rem !important; + } + .ml-xl-n4, + .mx-xl-n4 { + margin-left: -1.5rem !important; + } + .m-xl-n5 { + margin: -3rem !important; + } + .mt-xl-n5, + .my-xl-n5 { + margin-top: -3rem !important; + } + .mr-xl-n5, + .mx-xl-n5 { + margin-right: -3rem !important; + } + .mb-xl-n5, + .my-xl-n5 { + margin-bottom: -3rem !important; + } + .ml-xl-n5, + .mx-xl-n5 { + margin-left: -3rem !important; + } + .m-xl-auto { + margin: auto !important; + } + .mt-xl-auto, + .my-xl-auto { + margin-top: auto !important; + } + .mr-xl-auto, + .mx-xl-auto { + margin-right: auto !important; + } + .mb-xl-auto, + .my-xl-auto { + margin-bottom: auto !important; + } + .ml-xl-auto, + .mx-xl-auto { + margin-left: auto !important; + } +} + +.text-monospace { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important; +} + +.text-justify { + text-align: justify !important; +} + +.text-wrap { + white-space: normal !important; +} + +.text-nowrap { + white-space: nowrap !important; +} + +.text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.text-left { + text-align: left !important; +} + +.text-right { + text-align: right !important; +} + +.text-center { + text-align: center !important; +} + +@media (min-width: 576px) { + .text-sm-left { + text-align: left !important; + } + .text-sm-right { + text-align: right !important; + } + .text-sm-center { + text-align: center !important; + } +} + +@media (min-width: 768px) { + .text-md-left { + text-align: left !important; + } + .text-md-right { + text-align: right !important; + } + .text-md-center { + text-align: center !important; + } +} + +@media (min-width: 992px) { + .text-lg-left { + text-align: left !important; + } + .text-lg-right { + text-align: right !important; + } + .text-lg-center { + text-align: center !important; + } +} + +@media (min-width: 1200px) { + .text-xl-left { + text-align: left !important; + } + .text-xl-right { + text-align: right !important; + } + .text-xl-center { + text-align: center !important; + } +} + +.text-lowercase { + text-transform: lowercase !important; +} + +.text-uppercase { + text-transform: uppercase !important; +} + +.text-capitalize { + text-transform: capitalize !important; +} + +.font-weight-light { + font-weight: 300 !important; +} + +.font-weight-lighter { + font-weight: lighter !important; +} + +.font-weight-normal { + font-weight: 400 !important; +} + +.font-weight-bold { + font-weight: 700 !important; +} + +.font-weight-bolder { + font-weight: bolder !important; +} + +.font-italic { + font-style: italic !important; +} + +.text-white { + color: #fff !important; +} + +.text-primary { + color: #007bff !important; +} + +a.text-primary:hover, a.text-primary:focus { + color: #0056b3 !important; +} + +.text-secondary { + color: #6c757d !important; +} + +a.text-secondary:hover, a.text-secondary:focus { + color: #494f54 !important; +} + +.text-success { + color: #28a745 !important; +} + +a.text-success:hover, a.text-success:focus { + color: #19692c !important; +} + +.text-info { + color: #17a2b8 !important; +} + +a.text-info:hover, a.text-info:focus { + color: #0f6674 !important; +} + +.text-warning { + color: #ffc107 !important; +} + +a.text-warning:hover, a.text-warning:focus { + color: #ba8b00 !important; +} + +.text-danger { + color: #dc3545 !important; +} + +a.text-danger:hover, a.text-danger:focus { + color: #a71d2a !important; +} + +.text-light { + color: #f8f9fa !important; +} + +a.text-light:hover, a.text-light:focus { + color: #cbd3da !important; +} + +.text-dark { + color: #343a40 !important; +} + +a.text-dark:hover, a.text-dark:focus { + color: #121416 !important; +} + +.text-body { + color: #212529 !important; +} + +.text-muted { + color: #6c757d !important; +} + +.text-black-50 { + color: rgba(0, 0, 0, 0.5) !important; +} + +.text-white-50 { + color: rgba(255, 255, 255, 0.5) !important; +} + +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.text-decoration-none { + text-decoration: none !important; +} + +.text-break { + word-break: break-word !important; + overflow-wrap: break-word !important; +} + +.text-reset { + color: inherit !important; +} + +.visible { + visibility: visible !important; +} + +.invisible { + visibility: hidden !important; +} + +@media print { + *, + *::before, + *::after { + text-shadow: none !important; + box-shadow: none !important; + } + a:not(.btn) { + text-decoration: underline; + } + abbr[title]::after { + content: " (" attr(title) ")"; + } + pre { + white-space: pre-wrap !important; + } + pre, + blockquote { + border: 1px solid #adb5bd; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + @page { + size: a3; + } + body { + min-width: 992px !important; + } + .container { + min-width: 992px !important; + } + .navbar { + display: none; + } + .badge { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table td, + .table th { + background-color: #fff !important; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #dee2e6 !important; + } + .table-dark { + color: inherit; + } + .table-dark th, + .table-dark td, + .table-dark thead th, + .table-dark tbody + tbody { + border-color: #dee2e6; + } + .table .thead-dark th { + color: inherit; + border-color: #dee2e6; + } +} +/*# sourceMappingURL=bootstrap.css.map */ \ No newline at end of file diff --git a/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map new file mode 100644 index 00000000..521afc5b --- /dev/null +++ b/IRaCIS.Core.IdentityServer4.MVC/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap.scss","bootstrap.css","../../scss/_root.scss","../../scss/_reboot.scss","../../scss/_variables.scss","../../scss/vendor/_rfs.scss","../../scss/mixins/_hover.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/mixins/_border-radius.scss","../../scss/_code.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_breakpoints.scss","../../scss/mixins/_grid-framework.scss","../../scss/_tables.scss","../../scss/mixins/_table-row.scss","../../scss/_functions.scss","../../scss/_forms.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_forms.scss","../../scss/mixins/_gradients.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/mixins/_nav-divider.scss","../../scss/_button-group.scss","../../scss/_input-group.scss","../../scss/_custom-forms.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/mixins/_badge.scss","../../scss/_jumbotron.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_media.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/utilities/_align.scss","../../scss/mixins/_background-variant.scss","../../scss/utilities/_background.scss","../../scss/utilities/_borders.scss","../../scss/utilities/_display.scss","../../scss/utilities/_embed.scss","../../scss/utilities/_flex.scss","../../scss/utilities/_float.scss","../../scss/utilities/_overflow.scss","../../scss/utilities/_position.scss","../../scss/utilities/_screenreaders.scss","../../scss/mixins/_screen-reader.scss","../../scss/utilities/_shadows.scss","../../scss/utilities/_sizing.scss","../../scss/utilities/_stretched-link.scss","../../scss/utilities/_spacing.scss","../../scss/utilities/_text.scss","../../scss/mixins/_text-truncate.scss","../../scss/mixins/_text-emphasis.scss","../../scss/mixins/_text-hide.scss","../../scss/utilities/_visibility.scss","../../scss/_print.scss"],"names":[],"mappings":"AAAA;;;;;ECKE;ACJF;EAGI,eAAc;EAAd,iBAAc;EAAd,iBAAc;EAAd,eAAc;EAAd,cAAc;EAAd,iBAAc;EAAd,iBAAc;EAAd,gBAAc;EAAd,eAAc;EAAd,eAAc;EAAd,aAAc;EAAd,eAAc;EAAd,oBAAc;EAId,kBAAc;EAAd,oBAAc;EAAd,kBAAc;EAAd,eAAc;EAAd,kBAAc;EAAd,iBAAc;EAAd,gBAAc;EAAd,eAAc;EAId,kBAAiC;EAAjC,sBAAiC;EAAjC,sBAAiC;EAAjC,sBAAiC;EAAjC,uBAAiC;EAKnC,+MAAyB;EACzB,6GAAwB;ADiB1B;;AEjBA;;;EAGE,sBAAsB;AFoBxB;;AEjBA;EACE,uBAAuB;EACvB,iBAAiB;EACjB,8BAA8B;EAC9B,6CCXa;AH+Bf;;AEdA;EACE,cAAc;AFiBhB;;AEPA;EACE,SAAS;EACT,kMCyOiN;ECzJ7M,eAtCY;EFxChB,gBCkP+B;EDjP/B,gBCsP+B;EDrP/B,cCnCgB;EDoChB,gBAAgB;EAChB,sBC9Ca;AHwDf;;AAEA;EECE,qBAAqB;AFCvB;;AEQA;EACE,uBAAuB;EACvB,SAAS;EACT,iBAAiB;AFLnB;;AEkBA;EACE,aAAa;EACb,qBCoNuC;AHnOzC;;AEsBA;EACE,aAAa;EACb,mBCuF8B;AH1GhC;;AE8BA;;EAEE,0BAA0B;EAC1B,yCAAiC;EAAjC,iCAAiC;EACjC,YAAY;EACZ,gBAAgB;EAChB,sCAA8B;EAA9B,8BAA8B;AF3BhC;;AE8BA;EACE,mBAAmB;EACnB,kBAAkB;EAClB,oBAAoB;AF3BtB;;AE8BA;;;EAGE,aAAa;EACb,mBAAmB;AF3BrB;;AE8BA;;;;EAIE,gBAAgB;AF3BlB;;AE8BA;EACE,gBCqJ+B;AHhLjC;;AE8BA;EACE,oBAAoB;EACpB,cAAc;AF3BhB;;AE8BA;EACE,gBAAgB;AF3BlB;;AE8BA;;EAEE,mBCwIkC;AHnKpC;;AE8BA;EExFI,cAAW;AJ8Df;;AEmCA;;EAEE,kBAAkB;EEnGhB,cAAW;EFqGb,cAAc;EACd,wBAAwB;AFhC1B;;AEmCA;EAAM,cAAc;AF/BpB;;AEgCA;EAAM,UAAU;AF5BhB;;AEmCA;EACE,cCtJe;EDuJf,qBCR4C;EDS5C,6BAA6B;AFhC/B;;AKhJE;EHmLE,cCX8D;EDY9D,0BCX+C;AHpBnD;;AEwCA;EACE,cAAc;EACd,qBAAqB;AFrCvB;;AK1JE;EHkME,cAAc;EACd,qBAAqB;AFpCzB;;AE6CA;;;;EAIE,iGC6DgH;ECjN9G,cAAW;AJ2Gf;;AE6CA;EAEE,aAAa;EAEb,mBAAmB;EAEnB,cAAc;AF7ChB;;AEqDA;EAEE,gBAAgB;AFnDlB;;AE2DA;EACE,sBAAsB;EACtB,kBAAkB;AFxDpB;;AE2DA;EAGE,gBAAgB;EAChB,sBAAsB;AF1DxB;;AEkEA;EACE,yBAAyB;AF/D3B;;AEkEA;EACE,oBCoFkC;EDnFlC,uBCmFkC;EDlFlC,cCnQgB;EDoQhB,gBAAgB;EAChB,oBAAoB;AF/DtB;;AEkEA;EAGE,mBAAmB;AFjErB;;AEyEA;EAEE,qBAAqB;EACrB,qBCqK2C;AH5O7C;;AE6EA;EAEE,gBAAgB;AF3ElB;;AEkFA;EACE,mBAAmB;EACnB,0CAA0C;AF/E5C;;AEkFA;;;;;EAKE,SAAS;EACT,oBAAoB;EErPlB,kBAAW;EFuPb,oBAAoB;AF/EtB;;AEkFA;;EAEE,iBAAiB;AF/EnB;;AEkFA;;EAEE,oBAAoB;AF/EtB;;AEqFA;EACE,iBAAiB;AFlFnB;;AEyFA;;;;EAIE,0BAA0B;AFtF5B;;AE2FE;;;;EAKI,eAAe;AFzFrB;;AE+FA;;;;EAIE,UAAU;EACV,kBAAkB;AF5FpB;;AE+FA;;EAEE,sBAAsB;EACtB,UAAU;AF5FZ;;AEgGA;;;;EASE,2BAA2B;AFlG7B;;AEqGA;EACE,cAAc;EAEd,gBAAgB;AFnGlB;;AEsGA;EAME,YAAY;EAEZ,UAAU;EACV,SAAS;EACT,SAAS;AFzGX;;AE8GA;EACE,cAAc;EACd,WAAW;EACX,eAAe;EACf,UAAU;EACV,oBAAoB;EEjShB,iBAtCY;EFyUhB,oBAAoB;EACpB,cAAc;EACd,mBAAmB;AF3GrB;;AE8GA;EACE,wBAAwB;AF3G1B;;AAEA;;EE+GE,YAAY;AF5Gd;;AAEA;EEkHE,oBAAoB;EACpB,wBAAwB;AFhH1B;;AAEA;EEsHE,wBAAwB;AFpH1B;;AE4HA;EACE,aAAa;EACb,0BAA0B;AFzH5B;;AEgIA;EACE,qBAAqB;AF7HvB;;AEgIA;EACE,kBAAkB;EAClB,eAAe;AF7HjB;;AEgIA;EACE,aAAa;AF7Hf;;AAEA;EEiIE,wBAAwB;AF/H1B;;AM3VA;;EAEE,qBHySuC;EGvSvC,gBHyS+B;EGxS/B,gBHyS+B;AHoDjC;;AMzVA;EFgHM,iBAtCY;AJmRlB;;AM5VA;EF+GM,eAtCY;AJuRlB;;AM/VA;EF8GM,kBAtCY;AJ2RlB;;AMlWA;EF6GM,iBAtCY;AJ+RlB;;AMrWA;EF4GM,kBAtCY;AJmSlB;;AMxWA;EF2GM,eAtCY;AJuSlB;;AM1WA;EFyGM,kBAtCY;EEjEhB,gBH2S+B;AHkEjC;;AMzWA;EFmGM,eAtCY;EE3DhB,gBH8R+B;EG7R/B,gBHqR+B;AHuFjC;;AM1WA;EF8FM,iBAtCY;EEtDhB,gBH0R+B;EGzR/B,gBHgR+B;AH6FjC;;AM3WA;EFyFM,iBAtCY;EEjDhB,gBHsR+B;EGrR/B,gBH2Q+B;AHmGjC;;AM5WA;EFoFM,iBAtCY;EE5ChB,gBHkR+B;EGjR/B,gBHsQ+B;AHyGjC;;AElVA;EIpBE,gBHiFW;EGhFX,mBHgFW;EG/EX,SAAS;EACT,wCHzCa;AHmZf;;AMlWA;;EFMI,cAAW;EEHb,gBH8N+B;AHuIjC;;AMlWA;;EAEE,cHsQgC;EGrQhC,yBH8QmC;AHuFrC;;AM7VA;EC/EE,eAAe;EACf,gBAAgB;APgblB;;AM7VA;ECpFE,eAAe;EACf,gBAAgB;APqblB;;AM/VA;EACE,qBAAqB;ANkWvB;;AMnWA;EAII,oBHwP+B;AH2GnC;;AMzVA;EFjCI,cAAW;EEmCb,yBAAyB;AN4V3B;;AMxVA;EACE,mBHwBW;ECTP,kBAtCY;AJmXlB;;AMxVA;EACE,cAAc;EF7CZ,cAAW;EE+Cb,cH1GgB;AHqclB;;AM9VA;EAMI,qBAAqB;AN4VzB;;AQ/cA;ECIE,eAAe;EAGf,YAAY;AT6cd;;AQ9cA;EACE,gBLigCwC;EKhgCxC,sBLRa;EKSb,yBLNgB;EOLd,sBP6OgC;EMvOlC,eAAe;EAGf,YAAY;ATsdd;;AQxcA;EAEE,qBAAqB;AR0cvB;;AQvcA;EACE,qBAA0B;EAC1B,cAAc;AR0chB;;AQvcA;EJkCI,cAAW;EIhCb,cL3BgB;AHqelB;;AWjfA;EPuEI,gBAAW;EOrEb,cRoCe;EQnCf,qBAAqB;AXofvB;;AWjfE;EACE,cAAc;AXoflB;;AW/eA;EACE,sBRqlCuC;EC3hCrC,gBAAW;EOxDb,WRTa;EQUb,yBRDgB;EOXd,qBP+O+B;AHgRnC;;AWvfA;EASI,UAAU;EPkDV,eAAW;EOhDX,gBR4Q6B;AHsOjC;;AE1SA;ESjME,cAAc;EPyCZ,gBAAW;EOvCb,cRjBgB;AHggBlB;;AWlfA;EP0CI,kBAAW;EOlCX,cAAc;EACd,kBAAkB;AX+etB;;AW1eA;EACE,iBR4jCuC;EQ3jCvC,kBAAkB;AX6epB;;AYrhBE;ECDA,WAAW;EACX,mBAA0B;EAC1B,kBAAyB;EACzB,kBAAkB;EAClB,iBAAiB;Ab0hBnB;;AcveI;EFtDF;ICWI,gBVqMK;EHkVT;AACF;;Ac7eI;EFtDF;ICWI,gBVsMK;EHuVT;AACF;;AcnfI;EFtDF;ICWI,gBVuMK;EH4VT;AACF;;AczfI;EFtDF;ICWI,iBVwMM;EHiWV;AACF;;AY/iBE;ECPA,WAAW;EACX,mBAA0B;EAC1B,kBAAyB;EACzB,kBAAkB;EAClB,iBAAiB;Ab0jBnB;;AcvgBI;EFrCE;IACE,gBT8LG;EHkXT;AACF;;Ac7gBI;EFrCE;IACE,gBT+LG;EHuXT;AACF;;AcnhBI;EFrCE;IACE,gBTgMG;EH4XT;AACF;;AczhBI;EFrCE;IACE,iBTiMI;EHiYV;AACF;;AY/iBE;ECrBA,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,mBAA0B;EAC1B,kBAAyB;AbwkB3B;;AYhjBE;EACE,eAAe;EACf,cAAc;AZmjBlB;;AYrjBE;;EAMI,gBAAgB;EAChB,eAAe;AZojBrB;;AetmBE;;;;;;EACE,kBAAkB;EAClB,WAAW;EACX,mBAA0B;EAC1B,kBAAyB;Af8mB7B;;Ae3lBM;EACE,0BAAa;EAAb,aAAa;EACb,oBAAY;EAAZ,YAAY;EACZ,eAAe;Af8lBvB;;Ae1lBQ;EF4BJ,kBAAuB;EAAvB,cAAuB;EACvB,eAAwB;AbkkB5B;;Ae/lBQ;EF4BJ,iBAAuB;EAAvB,aAAuB;EACvB,cAAwB;AbukB5B;;AepmBQ;EF4BJ,wBAAuB;EAAvB,oBAAuB;EACvB,qBAAwB;Ab4kB5B;;AezmBQ;EF4BJ,iBAAuB;EAAvB,aAAuB;EACvB,cAAwB;AbilB5B;;Ae9mBQ;EF4BJ,iBAAuB;EAAvB,aAAuB;EACvB,cAAwB;AbslB5B;;AennBQ;EF4BJ,wBAAuB;EAAvB,oBAAuB;EACvB,qBAAwB;Ab2lB5B;;AennBM;EFMJ,kBAAc;EAAd,cAAc;EACd,WAAW;EACX,eAAe;AbinBjB;;AepnBQ;EFPN,uBAAsC;EAAtC,mBAAsC;EAItC,oBAAuC;Ab4nBzC;;AeznBQ;EFPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AbioBzC;;Ae9nBQ;EFPN,iBAAsC;EAAtC,aAAsC;EAItC,cAAuC;AbsoBzC;;AenoBQ;EFPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;Ab2oBzC;;AexoBQ;EFPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AbgpBzC;;Ae7oBQ;EFPN,iBAAsC;EAAtC,aAAsC;EAItC,cAAuC;AbqpBzC;;AelpBQ;EFPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;Ab0pBzC;;AevpBQ;EFPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;Ab+pBzC;;Ae5pBQ;EFPN,iBAAsC;EAAtC,aAAsC;EAItC,cAAuC;AboqBzC;;AejqBQ;EFPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AbyqBzC;;AetqBQ;EFPN,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;Ab8qBzC;;Ae3qBQ;EFPN,kBAAsC;EAAtC,cAAsC;EAItC,eAAuC;AbmrBzC;;Ae3qBM;EAAwB,kBAAS;EAAT,SAAS;Af+qBvC;;Ae7qBM;EAAuB,kBZ6KG;EY7KH,SZ6KG;AHogBhC;;Ae9qBQ;EAAwB,iBADZ;EACY,QADZ;AfmrBpB;;AelrBQ;EAAwB,iBADZ;EACY,QADZ;AfurBpB;;AetrBQ;EAAwB,iBADZ;EACY,QADZ;Af2rBpB;;Ae1rBQ;EAAwB,iBADZ;EACY,QADZ;Af+rBpB;;Ae9rBQ;EAAwB,iBADZ;EACY,QADZ;AfmsBpB;;AelsBQ;EAAwB,iBADZ;EACY,QADZ;AfusBpB;;AetsBQ;EAAwB,iBADZ;EACY,QADZ;Af2sBpB;;Ae1sBQ;EAAwB,iBADZ;EACY,QADZ;Af+sBpB;;Ae9sBQ;EAAwB,iBADZ;EACY,QADZ;AfmtBpB;;AeltBQ;EAAwB,iBADZ;EACY,QADZ;AfutBpB;;AettBQ;EAAwB,kBADZ;EACY,SADZ;Af2tBpB;;Ae1tBQ;EAAwB,kBADZ;EACY,SADZ;Af+tBpB;;Ae9tBQ;EAAwB,kBADZ;EACY,SADZ;AfmuBpB;;Ae5tBU;EFRR,sBAA8C;AbwuBhD;;AehuBU;EFRR,uBAA8C;Ab4uBhD;;AepuBU;EFRR,gBAA8C;AbgvBhD;;AexuBU;EFRR,uBAA8C;AbovBhD;;Ae5uBU;EFRR,uBAA8C;AbwvBhD;;AehvBU;EFRR,gBAA8C;Ab4vBhD;;AepvBU;EFRR,uBAA8C;AbgwBhD;;AexvBU;EFRR,uBAA8C;AbowBhD;;Ae5vBU;EFRR,gBAA8C;AbwwBhD;;AehwBU;EFRR,uBAA8C;Ab4wBhD;;AepwBU;EFRR,uBAA8C;AbgxBhD;;Ac3wBI;EC9BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;Ef6yBrB;EezyBM;IF4BJ,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;EbgxB1B;Ee7yBM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EboxB1B;EejzBM;IF4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EbwxB1B;EerzBM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;Eb4xB1B;EezzBM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EbgyB1B;Ee7zBM;IF4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EboyB1B;Ee5zBI;IFMJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;EbyzBf;Ee5zBM;IFPN,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;Ebm0BvC;Eeh0BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebu0BvC;Eep0BM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Eb20BvC;Eex0BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb+0BvC;Ee50BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebm1BvC;Eeh1BM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Ebu1BvC;Eep1BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb21BvC;Eex1BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb+1BvC;Ee51BM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Ebm2BvC;Eeh2BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebu2BvC;Eep2BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb22BvC;Eex2BM;IFPN,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;Eb+2BvC;Eev2BI;IAAwB,kBAAS;IAAT,SAAS;Ef02BrC;Eex2BI;IAAuB,kBZ6KG;IY7KH,SZ6KG;EH8rB9B;Eex2BM;IAAwB,iBADZ;IACY,QADZ;Ef42BlB;Ee32BM;IAAwB,iBADZ;IACY,QADZ;Ef+2BlB;Ee92BM;IAAwB,iBADZ;IACY,QADZ;Efk3BlB;Eej3BM;IAAwB,iBADZ;IACY,QADZ;Efq3BlB;Eep3BM;IAAwB,iBADZ;IACY,QADZ;Efw3BlB;Eev3BM;IAAwB,iBADZ;IACY,QADZ;Ef23BlB;Ee13BM;IAAwB,iBADZ;IACY,QADZ;Ef83BlB;Ee73BM;IAAwB,iBADZ;IACY,QADZ;Efi4BlB;Eeh4BM;IAAwB,iBADZ;IACY,QADZ;Efo4BlB;Een4BM;IAAwB,iBADZ;IACY,QADZ;Efu4BlB;Eet4BM;IAAwB,kBADZ;IACY,SADZ;Ef04BlB;Eez4BM;IAAwB,kBADZ;IACY,SADZ;Ef64BlB;Ee54BM;IAAwB,kBADZ;IACY,SADZ;Efg5BlB;Eez4BQ;IFRR,cAA4B;Ebo5B5B;Ee54BQ;IFRR,sBAA8C;Ebu5B9C;Ee/4BQ;IFRR,uBAA8C;Eb05B9C;Eel5BQ;IFRR,gBAA8C;Eb65B9C;Eer5BQ;IFRR,uBAA8C;Ebg6B9C;Eex5BQ;IFRR,uBAA8C;Ebm6B9C;Ee35BQ;IFRR,gBAA8C;Ebs6B9C;Ee95BQ;IFRR,uBAA8C;Eby6B9C;Eej6BQ;IFRR,uBAA8C;Eb46B9C;Eep6BQ;IFRR,gBAA8C;Eb+6B9C;Eev6BQ;IFRR,uBAA8C;Ebk7B9C;Ee16BQ;IFRR,uBAA8C;Ebq7B9C;AACF;;Acj7BI;EC9BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;Efm9BrB;Ee/8BM;IF4BJ,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;Ebs7B1B;Een9BM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;Eb07B1B;Eev9BM;IF4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;Eb87B1B;Ee39BM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;Ebk8B1B;Ee/9BM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;Ebs8B1B;Een+BM;IF4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;Eb08B1B;Eel+BI;IFMJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;Eb+9Bf;Eel+BM;IFPN,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;Eby+BvC;Eet+BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb6+BvC;Ee1+BM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Ebi/BvC;Ee9+BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebq/BvC;Eel/BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eby/BvC;Eet/BM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Eb6/BvC;Ee1/BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbigCvC;Ee9/BM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbqgCvC;EelgCM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EbygCvC;EetgCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb6gCvC;Ee1gCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbihCvC;Ee9gCM;IFPN,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;EbqhCvC;Ee7gCI;IAAwB,kBAAS;IAAT,SAAS;EfghCrC;Ee9gCI;IAAuB,kBZ6KG;IY7KH,SZ6KG;EHo2B9B;Ee9gCM;IAAwB,iBADZ;IACY,QADZ;EfkhClB;EejhCM;IAAwB,iBADZ;IACY,QADZ;EfqhClB;EephCM;IAAwB,iBADZ;IACY,QADZ;EfwhClB;EevhCM;IAAwB,iBADZ;IACY,QADZ;Ef2hClB;Ee1hCM;IAAwB,iBADZ;IACY,QADZ;Ef8hClB;Ee7hCM;IAAwB,iBADZ;IACY,QADZ;EfiiClB;EehiCM;IAAwB,iBADZ;IACY,QADZ;EfoiClB;EeniCM;IAAwB,iBADZ;IACY,QADZ;EfuiClB;EetiCM;IAAwB,iBADZ;IACY,QADZ;Ef0iClB;EeziCM;IAAwB,iBADZ;IACY,QADZ;Ef6iClB;Ee5iCM;IAAwB,kBADZ;IACY,SADZ;EfgjClB;Ee/iCM;IAAwB,kBADZ;IACY,SADZ;EfmjClB;EeljCM;IAAwB,kBADZ;IACY,SADZ;EfsjClB;Ee/iCQ;IFRR,cAA4B;Eb0jC5B;EeljCQ;IFRR,sBAA8C;Eb6jC9C;EerjCQ;IFRR,uBAA8C;EbgkC9C;EexjCQ;IFRR,gBAA8C;EbmkC9C;Ee3jCQ;IFRR,uBAA8C;EbskC9C;Ee9jCQ;IFRR,uBAA8C;EbykC9C;EejkCQ;IFRR,gBAA8C;Eb4kC9C;EepkCQ;IFRR,uBAA8C;Eb+kC9C;EevkCQ;IFRR,uBAA8C;EbklC9C;Ee1kCQ;IFRR,gBAA8C;EbqlC9C;Ee7kCQ;IFRR,uBAA8C;EbwlC9C;EehlCQ;IFRR,uBAA8C;Eb2lC9C;AACF;;AcvlCI;EC9BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;EfynCrB;EernCM;IF4BJ,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;Eb4lC1B;EeznCM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EbgmC1B;Ee7nCM;IF4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EbomC1B;EejoCM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EbwmC1B;EeroCM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;Eb4mC1B;EezoCM;IF4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EbgnC1B;EexoCI;IFMJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;EbqoCf;EexoCM;IFPN,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;Eb+oCvC;Ee5oCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbmpCvC;EehpCM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EbupCvC;EeppCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb2pCvC;EexpCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb+pCvC;Ee5pCM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EbmqCvC;EehqCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbuqCvC;EepqCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb2qCvC;EexqCM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Eb+qCvC;Ee5qCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbmrCvC;EehrCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EburCvC;EeprCM;IFPN,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;Eb2rCvC;EenrCI;IAAwB,kBAAS;IAAT,SAAS;EfsrCrC;EeprCI;IAAuB,kBZ6KG;IY7KH,SZ6KG;EH0gC9B;EeprCM;IAAwB,iBADZ;IACY,QADZ;EfwrClB;EevrCM;IAAwB,iBADZ;IACY,QADZ;Ef2rClB;Ee1rCM;IAAwB,iBADZ;IACY,QADZ;Ef8rClB;Ee7rCM;IAAwB,iBADZ;IACY,QADZ;EfisClB;EehsCM;IAAwB,iBADZ;IACY,QADZ;EfosClB;EensCM;IAAwB,iBADZ;IACY,QADZ;EfusClB;EetsCM;IAAwB,iBADZ;IACY,QADZ;Ef0sClB;EezsCM;IAAwB,iBADZ;IACY,QADZ;Ef6sClB;Ee5sCM;IAAwB,iBADZ;IACY,QADZ;EfgtClB;Ee/sCM;IAAwB,iBADZ;IACY,QADZ;EfmtClB;EeltCM;IAAwB,kBADZ;IACY,SADZ;EfstClB;EertCM;IAAwB,kBADZ;IACY,SADZ;EfytClB;EextCM;IAAwB,kBADZ;IACY,SADZ;Ef4tClB;EertCQ;IFRR,cAA4B;EbguC5B;EextCQ;IFRR,sBAA8C;EbmuC9C;Ee3tCQ;IFRR,uBAA8C;EbsuC9C;Ee9tCQ;IFRR,gBAA8C;EbyuC9C;EejuCQ;IFRR,uBAA8C;Eb4uC9C;EepuCQ;IFRR,uBAA8C;Eb+uC9C;EevuCQ;IFRR,gBAA8C;EbkvC9C;Ee1uCQ;IFRR,uBAA8C;EbqvC9C;Ee7uCQ;IFRR,uBAA8C;EbwvC9C;EehvCQ;IFRR,gBAA8C;Eb2vC9C;EenvCQ;IFRR,uBAA8C;Eb8vC9C;EetvCQ;IFRR,uBAA8C;EbiwC9C;AACF;;Ac7vCI;EC9BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;Ef+xCrB;Ee3xCM;IF4BJ,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;EbkwC1B;Ee/xCM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EbswC1B;EenyCM;IF4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;Eb0wC1B;EevyCM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;Eb8wC1B;Ee3yCM;IF4BJ,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EbkxC1B;Ee/yCM;IF4BJ,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EbsxC1B;Ee9yCI;IFMJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;Eb2yCf;Ee9yCM;IFPN,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;EbqzCvC;EelzCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbyzCvC;EetzCM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Eb6zCvC;Ee1zCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebi0CvC;Ee9zCM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebq0CvC;Eel0CM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Eby0CvC;Eet0CM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb60CvC;Ee10CM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebi1CvC;Ee90CM;IFPN,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Ebq1CvC;Eel1CM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eby1CvC;Eet1CM;IFPN,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb61CvC;Ee11CM;IFPN,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;Ebi2CvC;Eez1CI;IAAwB,kBAAS;IAAT,SAAS;Ef41CrC;Ee11CI;IAAuB,kBZ6KG;IY7KH,SZ6KG;EHgrC9B;Ee11CM;IAAwB,iBADZ;IACY,QADZ;Ef81ClB;Ee71CM;IAAwB,iBADZ;IACY,QADZ;Efi2ClB;Eeh2CM;IAAwB,iBADZ;IACY,QADZ;Efo2ClB;Een2CM;IAAwB,iBADZ;IACY,QADZ;Efu2ClB;Eet2CM;IAAwB,iBADZ;IACY,QADZ;Ef02ClB;Eez2CM;IAAwB,iBADZ;IACY,QADZ;Ef62ClB;Ee52CM;IAAwB,iBADZ;IACY,QADZ;Efg3ClB;Ee/2CM;IAAwB,iBADZ;IACY,QADZ;Efm3ClB;Eel3CM;IAAwB,iBADZ;IACY,QADZ;Efs3ClB;Eer3CM;IAAwB,iBADZ;IACY,QADZ;Efy3ClB;Eex3CM;IAAwB,kBADZ;IACY,SADZ;Ef43ClB;Ee33CM;IAAwB,kBADZ;IACY,SADZ;Ef+3ClB;Ee93CM;IAAwB,kBADZ;IACY,SADZ;Efk4ClB;Ee33CQ;IFRR,cAA4B;Ebs4C5B;Ee93CQ;IFRR,sBAA8C;Eby4C9C;Eej4CQ;IFRR,uBAA8C;Eb44C9C;Eep4CQ;IFRR,gBAA8C;Eb+4C9C;Eev4CQ;IFRR,uBAA8C;Ebk5C9C;Ee14CQ;IFRR,uBAA8C;Ebq5C9C;Ee74CQ;IFRR,gBAA8C;Ebw5C9C;Eeh5CQ;IFRR,uBAA8C;Eb25C9C;Een5CQ;IFRR,uBAA8C;Eb85C9C;Eet5CQ;IFRR,gBAA8C;Ebi6C9C;Eez5CQ;IFRR,uBAA8C;Ebo6C9C;Ee55CQ;IFRR,uBAA8C;Ebu6C9C;AACF;;AgB39CA;EACE,WAAW;EACX,mBbkIW;EajIX,cbSgB;AHq9ClB;;AgBj+CA;;EAQI,gBbsVgC;EarVhC,mBAAmB;EACnB,6BbJc;AHk+ClB;;AgBx+CA;EAcI,sBAAsB;EACtB,gCbTc;AHu+ClB;;AgB7+CA;EAmBI,6Bbbc;AH2+ClB;;AgBr9CA;;EAGI,ebgU+B;AHupCnC;;AgB98CA;EACE,yBbnCgB;AHo/ClB;;AgBl9CA;;EAKI,yBbvCc;AHy/ClB;;AgBv9CA;;EAWM,wBAA4C;AhBi9ClD;;AgB58CA;;;;EAKI,SAAS;AhB88Cb;;AgBt8CA;EAEI,qCb1DW;AHkgDf;;AKvgDE;EW2EI,cbvEY;EawEZ,sCbvES;AHugDf;;AiBnhDE;;;EAII,yBCsF4D;AlB+7ClE;;AiBzhDE;;;;EAYM,qBC8E0D;AlBs8ClE;;AKzhDE;EYiBM,yBAJsC;AjBghD9C;;AiBjhDE;;EASQ,yBARoC;AjBqhD9C;;AiBziDE;;;EAII,yBCsF4D;AlBq9ClE;;AiB/iDE;;;;EAYM,qBC8E0D;AlB49ClE;;AK/iDE;EYiBM,yBAJsC;AjBsiD9C;;AiBviDE;;EASQ,yBARoC;AjB2iD9C;;AiB/jDE;;;EAII,yBCsF4D;AlB2+ClE;;AiBrkDE;;;;EAYM,qBC8E0D;AlBk/ClE;;AKrkDE;EYiBM,yBAJsC;AjB4jD9C;;AiB7jDE;;EASQ,yBARoC;AjBikD9C;;AiBrlDE;;;EAII,yBCsF4D;AlBigDlE;;AiB3lDE;;;;EAYM,qBC8E0D;AlBwgDlE;;AK3lDE;EYiBM,yBAJsC;AjBklD9C;;AiBnlDE;;EASQ,yBARoC;AjBulD9C;;AiB3mDE;;;EAII,yBCsF4D;AlBuhDlE;;AiBjnDE;;;;EAYM,qBC8E0D;AlB8hDlE;;AKjnDE;EYiBM,yBAJsC;AjBwmD9C;;AiBzmDE;;EASQ,yBARoC;AjB6mD9C;;AiBjoDE;;;EAII,yBCsF4D;AlB6iDlE;;AiBvoDE;;;;EAYM,qBC8E0D;AlBojDlE;;AKvoDE;EYiBM,yBAJsC;AjB8nD9C;;AiB/nDE;;EASQ,yBARoC;AjBmoD9C;;AiBvpDE;;;EAII,yBCsF4D;AlBmkDlE;;AiB7pDE;;;;EAYM,qBC8E0D;AlB0kDlE;;AK7pDE;EYiBM,yBAJsC;AjBopD9C;;AiBrpDE;;EASQ,yBARoC;AjBypD9C;;AiB7qDE;;;EAII,yBCsF4D;AlBylDlE;;AiBnrDE;;;;EAYM,qBC8E0D;AlBgmDlE;;AKnrDE;EYiBM,yBAJsC;AjB0qD9C;;AiB3qDE;;EASQ,yBARoC;AjB+qD9C;;AiBnsDE;;;EAII,sCdQS;AH6rDf;;AKlsDE;EYiBM,sCAJsC;AjByrD9C;;AiB1rDE;;EASQ,sCARoC;AjB8rD9C;;AgBxmDA;EAGM,Wb3GS;Ea4GT,yBbpGY;EaqGZ,qBbmQqD;AHs2C3D;;AgB9mDA;EAWM,cb5GY;Ea6GZ,yBblHY;EamHZ,qBblHY;AHytDlB;;AgBlmDA;EACE,Wb3Ha;Ea4Hb,yBbpHgB;AHytDlB;;AgBvmDA;;;EAOI,qBb+OuD;AHu3C3D;;AgB7mDA;EAWI,SAAS;AhBsmDb;;AgBjnDA;EAgBM,2Cb1IS;AH+uDf;;AK1uDE;EW4IM,WbjJO;EakJP,4CblJO;AHovDf;;AclrDI;EEiGA;IAEI,cAAc;IACd,WAAW;IACX,gBAAgB;IAChB,iCAAiC;EhBolDvC;EgBzlDG;IASK,SAAS;EhBmlDjB;AACF;;Ac9rDI;EEiGA;IAEI,cAAc;IACd,WAAW;IACX,gBAAgB;IAChB,iCAAiC;EhBgmDvC;EgBrmDG;IASK,SAAS;EhB+lDjB;AACF;;Ac1sDI;EEiGA;IAEI,cAAc;IACd,WAAW;IACX,gBAAgB;IAChB,iCAAiC;EhB4mDvC;EgBjnDG;IASK,SAAS;EhB2mDjB;AACF;;ActtDI;EEiGA;IAEI,cAAc;IACd,WAAW;IACX,gBAAgB;IAChB,iCAAiC;EhBwnDvC;EgB7nDG;IASK,SAAS;EhBunDjB;AACF;;AgBtoDA;EAOQ,cAAc;EACd,WAAW;EACX,gBAAgB;EAChB,iCAAiC;AhBmoDzC;;AgB7oDA;EAcU,SAAS;AhBmoDnB;;AmBhzDA;EACE,cAAc;EACd,WAAW;EACX,mCDuG8D;ECtG9D,yBhB4XkC;ECvQ9B,eAtCY;Ee5EhB,gBhBsR+B;EgBrR/B,gBhB0R+B;EgBzR/B,chBDgB;EgBEhB,sBhBTa;EgBUb,4BAA4B;EAC5B,yBhBPgB;EONd,sBP6OgC;EiB5O9B,wEjBof4F;AH40ClG;;AoB3zDI;EDLJ;ICMM,gBAAgB;EpB+zDpB;AACF;;AmBt0DA;EAsBI,6BAA6B;EAC7B,SAAS;AnBozDb;;AmB30DA;EA4BI,kBAAkB;EAClB,0BhBrBc;AHw0DlB;;AqBz0DE;EACE,clBAc;EkBCd,sBlBRW;EkBSX,qBlBwdsE;EkBvdtE,UAAU;EAKR,gDlBcW;AH0zDjB;;AmBx1DA;EAqCI,chB9Bc;EgBgCd,UAAU;AnBszDd;;AmB71DA;EAqCI,chB9Bc;EgBgCd,UAAU;AnBszDd;;AmB71DA;EAqCI,chB9Bc;EgBgCd,UAAU;AnBszDd;;AmB71DA;EAqCI,chB9Bc;EgBgCd,UAAU;AnBszDd;;AmB71DA;EAqCI,chB9Bc;EgBgCd,UAAU;AnBszDd;;AmB71DA;EAiDI,yBhB9Cc;EgBgDd,UAAU;AnB+yDd;;AmB3yDA;EAOI,chBtDc;EgBuDd,sBhB9DW;AHs2Df;;AmBnyDA;;EAEE,cAAc;EACd,WAAW;AnBsyDb;;AmB5xDA;EACE,iCDwB8D;ECvB9D,oCDuB8D;ECtB9D,gBAAgB;EflBd,kBAAW;EeoBb,gBhB4M+B;AHmlDjC;;AmB5xDA;EACE,+BDgB8D;ECf9D,kCDe8D;Ede1D,kBAtCY;EeUhB,gBhByI+B;AHspDjC;;AmB5xDA;EACE,gCDS8D;ECR9D,mCDQ8D;Ede1D,mBAtCY;EeiBhB,gBhBmI+B;AH4pDjC;;AmBtxDA;EACE,cAAc;EACd,WAAW;EACX,mBAA2B;EAC3B,gBAAgB;EfQZ,eAtCY;EegChB,gBhB+K+B;EgB9K/B,chB1GgB;EgB2GhB,6BAA6B;EAC7B,yBAAyB;EACzB,mBAAmC;AnByxDrC;;AmBnyDA;EAcI,gBAAgB;EAChB,eAAe;AnByxDnB;;AmB7wDA;EACE,kCD/B8D;ECgC9D,uBhBgQiC;ECjR7B,mBAtCY;EeyDhB,gBhB2F+B;EOxO7B,qBP+O+B;AH+qDnC;;AmB7wDA;EACE,gCDvC8D;ECwC9D,oBhB6PgC;ECtR5B,kBAtCY;EeiEhB,gBhBkF+B;EOvO7B,qBP8O+B;AHwrDnC;;AmB5wDA;EAGI,YAAY;AnB6wDhB;;AmBzwDA;EACE,YAAY;AnB4wDd;;AmBpwDA;EACE,mBhBsV0C;AHi7C5C;;AmBpwDA;EACE,cAAc;EACd,mBhBuU4C;AHg8C9C;;AmB/vDA;EACE,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,kBAA0C;EAC1C,iBAAyC;AnBkwD3C;;AmBtwDA;;EAQI,kBAA0C;EAC1C,iBAAyC;AnBmwD7C;;AmB1vDA;EACE,kBAAkB;EAClB,cAAc;EACd,qBhB4S6C;AHi9C/C;;AmB1vDA;EACE,kBAAkB;EAClB,kBhBwS2C;EgBvS3C,qBhBsS6C;AHu9C/C;;AmBhwDA;;EAQI,chBhNc;AH68DlB;;AmBzvDA;EACE,gBAAgB;AnB4vDlB;;AmBzvDA;EACE,2BAAoB;EAApB,oBAAoB;EACpB,sBAAmB;EAAnB,mBAAmB;EACnB,eAAe;EACf,qBhByR4C;AHm+C9C;;AmBhwDA;EAQI,gBAAgB;EAChB,aAAa;EACb,uBhBoR4C;EgBnR5C,cAAc;AnB4vDlB;;AqBh8DE;EACE,aAAa;EACb,WAAW;EACX,mBlB6c0C;ECpb1C,cAAW;EiBvBX,clBNa;AHy8DjB;;AqBh8DE;EACE,kBAAkB;EAClB,SAAS;EACT,UAAU;EACV,aAAa;EACb,eAAe;EACf,uBlBmyBqC;EkBlyBrC,iBAAiB;EjBoEf,mBAtCY;EiB5Bd,gBlB2O6B;EkB1O7B,WlBvDW;EkBwDX,wClBpBa;EOtCb,sBP6OgC;AHixDpC;;AqBn+DI;;;;EAsCE,cAAc;ArBo8DpB;;AqB1+DI;EA4CE,qBlBjCW;EkBoCT,oCHiCwD;EGhCxD,iRHpCmI;EGqCnI,4BAA4B;EAC5B,2DAA6D;EAC7D,gEH6BwD;AlBm6DhE;;AqBn/DI;EAuDI,qBlB5CS;EkB6CT,gDlB7CS;AH6+DjB;;AqBx/DI;EAiEI,oCHewD;EGdxD,kFHcwD;AlB66DhE;;AqB7/DI;EAyEE,qBlB9DW;EkBiET,uCHIwD;EGHxD,ujBAA8J;ArBs7DtK;;AqBngEI;EAiFI,qBlBtES;EkBuET,gDlBvES;AH6/DjB;;AqBxgEI;EA0FI,clB/ES;AHigEjB;;AqB5gEI;;;EA+FI,cAAc;ArBm7DtB;;AqBlhEI;EAuGI,clB5FS;AH2gEjB;;AqBthEI;EA0GM,qBlB/FO;AH+gEjB;;AqB1hEI;EAgHM,qBAAkC;EC1IxC,yBD2I+C;ArB86DnD;;AqB/hEI;EAuHM,gDlB5GO;AHwhEjB;;AqBniEI;EA2HM,qBlBhHO;AH4hEjB;;AqBviEI;EAqII,qBlB1HS;AHgiEjB;;AqB3iEI;EA0IM,qBlB/HO;EkBgIP,gDlBhIO;AHqiEjB;;AqBpiEE;EACE,aAAa;EACb,WAAW;EACX,mBlB6c0C;ECpb1C,cAAW;EiBvBX,clBTa;AHgjEjB;;AqBpiEE;EACE,kBAAkB;EAClB,SAAS;EACT,UAAU;EACV,aAAa;EACb,eAAe;EACf,uBlBmyBqC;EkBlyBrC,iBAAiB;EjBoEf,mBAtCY;EiB5Bd,gBlB2O6B;EkB1O7B,WlBvDW;EkBwDX,wClBvBa;EOnCb,sBP6OgC;AHq3DpC;;AqBvkEI;;;;EAsCE,cAAc;ArBwiEpB;;AqB9kEI;EA4CE,qBlBpCW;EkBuCT,oCHiCwD;EGhCxD,4UHpCmI;EGqCnI,4BAA4B;EAC5B,2DAA6D;EAC7D,gEH6BwD;AlBugEhE;;AqBvlEI;EAuDI,qBlB/CS;EkBgDT,gDlBhDS;AHolEjB;;AqB5lEI;EAiEI,oCHewD;EGdxD,kFHcwD;AlBihEhE;;AqBjmEI;EAyEE,qBlBjEW;EkBoET,uCHIwD;EGHxD,knBAA8J;ArB0hEtK;;AqBvmEI;EAiFI,qBlBzES;EkB0ET,gDlB1ES;AHomEjB;;AqB5mEI;EA0FI,clBlFS;AHwmEjB;;AqBhnEI;;;EA+FI,cAAc;ArBuhEtB;;AqBtnEI;EAuGI,clB/FS;AHknEjB;;AqB1nEI;EA0GM,qBlBlGO;AHsnEjB;;AqB9nEI;EAgHM,qBAAkC;EC1IxC,yBD2I+C;ArBkhEnD;;AqBnoEI;EAuHM,gDlB/GO;AH+nEjB;;AqBvoEI;EA2HM,qBlBnHO;AHmoEjB;;AqB3oEI;EAqII,qBlB7HS;AHuoEjB;;AqB/oEI;EA0IM,qBlBlIO;EkBmIP,gDlBnIO;AH4oEjB;;AmB36DA;EACE,oBAAa;EAAb,aAAa;EACb,uBAAmB;EAAnB,mBAAmB;EACnB,sBAAmB;EAAnB,mBAAmB;AnB86DrB;;AmBj7DA;EASI,WAAW;AnB46Df;;AcloEI;EK6MJ;IAeM,oBAAa;IAAb,aAAa;IACb,sBAAmB;IAAnB,mBAAmB;IACnB,qBAAuB;IAAvB,uBAAuB;IACvB,gBAAgB;EnB26DpB;EmB77DF;IAuBM,oBAAa;IAAb,aAAa;IACb,kBAAc;IAAd,cAAc;IACd,uBAAmB;IAAnB,mBAAmB;IACnB,sBAAmB;IAAnB,mBAAmB;IACnB,gBAAgB;EnBy6DpB;EmBp8DF;IAgCM,qBAAqB;IACrB,WAAW;IACX,sBAAsB;EnBu6D1B;EmBz8DF;IAuCM,qBAAqB;EnBq6DzB;EmB58DF;;IA4CM,WAAW;EnBo6Df;EmBh9DF;IAkDM,oBAAa;IAAb,aAAa;IACb,sBAAmB;IAAnB,mBAAmB;IACnB,qBAAuB;IAAvB,uBAAuB;IACvB,WAAW;IACX,eAAe;EnBi6DnB;EmBv9DF;IAyDM,kBAAkB;IAClB,oBAAc;IAAd,cAAc;IACd,aAAa;IACb,qBhB2LwC;IgB1LxC,cAAc;EnBi6DlB;EmB99DF;IAiEM,sBAAmB;IAAnB,mBAAmB;IACnB,qBAAuB;IAAvB,uBAAuB;EnBg6D3B;EmBl+DF;IAqEM,gBAAgB;EnBg6DpB;AACF;;AuBzuEA;EACE,qBAAqB;EAErB,gBpB0R+B;EoBzR/B,cpBMgB;EoBLhB,kBAAkB;EAElB,sBAAsB;EACtB,eAAsD;EACtD,yBAAiB;EAAjB,sBAAiB;EAAjB,qBAAiB;EAAjB,iBAAiB;EACjB,6BAA6B;EAC7B,6BAA2C;ECuF3C,yBrB8RkC;ECvQ9B,eAtCY;EoBiBhB,gBrB8L+B;EOnS7B,sBP6OgC;EiB5O9B,qIjB6b6I;AH6zDnJ;;AoBrvEI;EGLJ;IHMM,gBAAgB;EpByvEpB;AACF;;AK1vEE;EkBUE,cpBNc;EoBOd,qBAAqB;AvBovEzB;;AuBrwEA;EAsBI,UAAU;EACV,gDpBOa;AH4uEjB;;AuB1wEA;EA6BI,apBoZ6B;AH61DjC;;AuBluEA;;EAEE,oBAAoB;AvBquEtB;;AuB5tEE;ECvDA,WrBCa;EmBDX,yBnB8Ba;EqB5Bf,qBrB4Be;AH2vEjB;;AKnxEE;EmBAE,WrBLW;EmBDX,yBEDoF;EASpF,qBATyH;AxBgyE7H;;AwBpxEE;EAEE,WrBZW;EmBDX,yBEDoF;EAgBpF,qBAhByH;EAqBvH,gDAAiF;AxBkxEvF;;AwB7wEE;EAEE,WrB1BW;EqB2BX,yBrBEa;EqBDb,qBrBCa;AH8wEjB;;AwBxwEE;;EAGE,WrBtCW;EqBuCX,yBAzCuK;EA6CvK,qBA7C+M;AxBozEnN;;AwBrwEI;;EAKI,gDAAiF;AxBqwEzF;;AuBjwEE;ECvDA,WrBCa;EmBDX,yBnBOc;EqBLhB,qBrBKgB;AHuzElB;;AKxzEE;EmBAE,WrBLW;EmBDX,yBEDoF;EASpF,qBATyH;AxBq0E7H;;AwBzzEE;EAEE,WrBZW;EmBDX,yBEDoF;EAgBpF,qBAhByH;EAqBvH,iDAAiF;AxBuzEvF;;AwBlzEE;EAEE,WrB1BW;EqB2BX,yBrBrBc;EqBsBd,qBrBtBc;AH00ElB;;AwB7yEE;;EAGE,WrBtCW;EqBuCX,yBAzCuK;EA6CvK,qBA7C+M;AxBy1EnN;;AwB1yEI;;EAKI,iDAAiF;AxB0yEzF;;AuBtyEE;ECvDA,WrBCa;EmBDX,yBnBqCa;EqBnCf,qBrBmCe;AH8zEjB;;AK71EE;EmBAE,WrBLW;EmBDX,yBEDoF;EASpF,qBATyH;AxB02E7H;;AwB91EE;EAEE,WrBZW;EmBDX,yBEDoF;EAgBpF,qBAhByH;EAqBvH,+CAAiF;AxB41EvF;;AwBv1EE;EAEE,WrB1BW;EqB2BX,yBrBSa;EqBRb,qBrBQa;AHi1EjB;;AwBl1EE;;EAGE,WrBtCW;EqBuCX,yBAzCuK;EA6CvK,qBA7C+M;AxB83EnN;;AwB/0EI;;EAKI,+CAAiF;AxB+0EzF;;AuB30EE;ECvDA,WrBCa;EmBDX,yBnBuCa;EqBrCf,qBrBqCe;AHi2EjB;;AKl4EE;EmBAE,WrBLW;EmBDX,yBEDoF;EASpF,qBATyH;AxB+4E7H;;AwBn4EE;EAEE,WrBZW;EmBDX,yBEDoF;EAgBpF,qBAhByH;EAqBvH,gDAAiF;AxBi4EvF;;AwB53EE;EAEE,WrB1BW;EqB2BX,yBrBWa;EqBVb,qBrBUa;AHo3EjB;;AwBv3EE;;EAGE,WrBtCW;EqBuCX,yBAzCuK;EA6CvK,qBA7C+M;AxBm6EnN;;AwBp3EI;;EAKI,gDAAiF;AxBo3EzF;;AuBh3EE;ECvDA,crBUgB;EmBVd,yBnBoCa;EqBlCf,qBrBkCe;AHy4EjB;;AKv6EE;EmBAE,crBIc;EmBVd,yBEDoF;EASpF,qBATyH;AxBo7E7H;;AwBx6EE;EAEE,crBHc;EmBVd,yBEDoF;EAgBpF,qBAhByH;EAqBvH,gDAAiF;AxBs6EvF;;AwBj6EE;EAEE,crBjBc;EqBkBd,yBrBQa;EqBPb,qBrBOa;AH45EjB;;AwB55EE;;EAGE,crB7Bc;EqB8Bd,yBAzCuK;EA6CvK,qBA7C+M;AxBw8EnN;;AwBz5EI;;EAKI,gDAAiF;AxBy5EzF;;AuBr5EE;ECvDA,WrBCa;EmBDX,yBnBkCa;EqBhCf,qBrBgCe;AHg7EjB;;AK58EE;EmBAE,WrBLW;EmBDX,yBEDoF;EASpF,qBATyH;AxBy9E7H;;AwB78EE;EAEE,WrBZW;EmBDX,yBEDoF;EAgBpF,qBAhByH;EAqBvH,+CAAiF;AxB28EvF;;AwBt8EE;EAEE,WrB1BW;EqB2BX,yBrBMa;EqBLb,qBrBKa;AHm8EjB;;AwBj8EE;;EAGE,WrBtCW;EqBuCX,yBAzCuK;EA6CvK,qBA7C+M;AxB6+EnN;;AwB97EI;;EAKI,+CAAiF;AxB87EzF;;AuB17EE;ECvDA,crBUgB;EmBVd,yBnBEc;EqBAhB,qBrBAgB;AHq/ElB;;AKj/EE;EmBAE,crBIc;EmBVd,yBEDoF;EASpF,qBATyH;AxB8/E7H;;AwBl/EE;EAEE,crBHc;EmBVd,yBEDoF;EAgBpF,qBAhByH;EAqBvH,iDAAiF;AxBg/EvF;;AwB3+EE;EAEE,crBjBc;EqBkBd,yBrB1Bc;EqB2Bd,qBrB3Bc;AHwgFlB;;AwBt+EE;;EAGE,crB7Bc;EqB8Bd,yBAzCuK;EA6CvK,qBA7C+M;AxBkhFnN;;AwBn+EI;;EAKI,iDAAiF;AxBm+EzF;;AuB/9EE;ECvDA,WrBCa;EmBDX,yBnBSc;EqBPhB,qBrBOgB;AHmhFlB;;AKthFE;EmBAE,WrBLW;EmBDX,yBEDoF;EASpF,qBATyH;AxBmiF7H;;AwBvhFE;EAEE,WrBZW;EmBDX,yBEDoF;EAgBpF,qBAhByH;EAqBvH,8CAAiF;AxBqhFvF;;AwBhhFE;EAEE,WrB1BW;EqB2BX,yBrBnBc;EqBoBd,qBrBpBc;AHsiFlB;;AwB3gFE;;EAGE,WrBtCW;EqBuCX,yBAzCuK;EA6CvK,qBA7C+M;AxBujFnN;;AwBxgFI;;EAKI,8CAAiF;AxBwgFzF;;AuB9/EE;ECHA,crB5Be;EqB6Bf,qBrB7Be;AHkiFjB;;AK1jFE;EmBwDE,WrB7DW;EqB8DX,yBrBjCa;EqBkCb,qBrBlCa;AHwiFjB;;AwBngFE;EAEE,+CrBvCa;AH4iFjB;;AwBlgFE;EAEE,crB5Ca;EqB6Cb,6BAA6B;AxBogFjC;;AwBjgFE;;EAGE,WrBhFW;EqBiFX,yBrBpDa;EqBqDb,qBrBrDa;AHwjFjB;;AwBjgFI;;EAKI,+CrB5DS;AH6jFjB;;AuB9hFE;ECHA,crBnDgB;EqBoDhB,qBrBpDgB;AHylFlB;;AK1lFE;EmBwDE,WrB7DW;EqB8DX,yBrBxDc;EqByDd,qBrBzDc;AH+lFlB;;AwBniFE;EAEE,iDrB9Dc;AHmmFlB;;AwBliFE;EAEE,crBnEc;EqBoEd,6BAA6B;AxBoiFjC;;AwBjiFE;;EAGE,WrBhFW;EqBiFX,yBrB3Ec;EqB4Ed,qBrB5Ec;AH+mFlB;;AwBjiFI;;EAKI,iDrBnFU;AHonFlB;;AuB9jFE;ECHA,crBrBe;EqBsBf,qBrBtBe;AH2lFjB;;AK1nFE;EmBwDE,WrB7DW;EqB8DX,yBrB1Ba;EqB2Bb,qBrB3Ba;AHimFjB;;AwBnkFE;EAEE,+CrBhCa;AHqmFjB;;AwBlkFE;EAEE,crBrCa;EqBsCb,6BAA6B;AxBokFjC;;AwBjkFE;;EAGE,WrBhFW;EqBiFX,yBrB7Ca;EqB8Cb,qBrB9Ca;AHinFjB;;AwBjkFI;;EAKI,+CrBrDS;AHsnFjB;;AuB9lFE;ECHA,crBnBe;EqBoBf,qBrBpBe;AHynFjB;;AK1pFE;EmBwDE,WrB7DW;EqB8DX,yBrBxBa;EqByBb,qBrBzBa;AH+nFjB;;AwBnmFE;EAEE,gDrB9Ba;AHmoFjB;;AwBlmFE;EAEE,crBnCa;EqBoCb,6BAA6B;AxBomFjC;;AwBjmFE;;EAGE,WrBhFW;EqBiFX,yBrB3Ca;EqB4Cb,qBrB5Ca;AH+oFjB;;AwBjmFI;;EAKI,gDrBnDS;AHopFjB;;AuB9nFE;ECHA,crBtBe;EqBuBf,qBrBvBe;AH4pFjB;;AK1rFE;EmBwDE,crBpDc;EqBqDd,yBrB3Ba;EqB4Bb,qBrB5Ba;AHkqFjB;;AwBnoFE;EAEE,+CrBjCa;AHsqFjB;;AwBloFE;EAEE,crBtCa;EqBuCb,6BAA6B;AxBooFjC;;AwBjoFE;;EAGE,crBvEc;EqBwEd,yBrB9Ca;EqB+Cb,qBrB/Ca;AHkrFjB;;AwBjoFI;;EAKI,+CrBtDS;AHurFjB;;AuB9pFE;ECHA,crBxBe;EqByBf,qBrBzBe;AH8rFjB;;AK1tFE;EmBwDE,WrB7DW;EqB8DX,yBrB7Ba;EqB8Bb,qBrB9Ba;AHosFjB;;AwBnqFE;EAEE,+CrBnCa;AHwsFjB;;AwBlqFE;EAEE,crBxCa;EqByCb,6BAA6B;AxBoqFjC;;AwBjqFE;;EAGE,WrBhFW;EqBiFX,yBrBhDa;EqBiDb,qBrBjDa;AHotFjB;;AwBjqFI;;EAKI,+CrBxDS;AHytFjB;;AuB9rFE;ECHA,crBxDgB;EqByDhB,qBrBzDgB;AH8vFlB;;AK1vFE;EmBwDE,crBpDc;EqBqDd,yBrB7Dc;EqB8Dd,qBrB9Dc;AHowFlB;;AwBnsFE;EAEE,iDrBnEc;AHwwFlB;;AwBlsFE;EAEE,crBxEc;EqByEd,6BAA6B;AxBosFjC;;AwBjsFE;;EAGE,crBvEc;EqBwEd,yBrBhFc;EqBiFd,qBrBjFc;AHoxFlB;;AwBjsFI;;EAKI,iDrBxFU;AHyxFlB;;AuB9tFE;ECHA,crBjDgB;EqBkDhB,qBrBlDgB;AHuxFlB;;AK1xFE;EmBwDE,WrB7DW;EqB8DX,yBrBtDc;EqBuDd,qBrBvDc;AH6xFlB;;AwBnuFE;EAEE,8CrB5Dc;AHiyFlB;;AwBluFE;EAEE,crBjEc;EqBkEd,6BAA6B;AxBouFjC;;AwBjuFE;;EAGE,WrBhFW;EqBiFX,yBrBzEc;EqB0Ed,qBrB1Ec;AH6yFlB;;AwBjuFI;;EAKI,8CrBjFU;AHkzFlB;;AuBnvFA;EACE,gBpBoN+B;EoBnN/B,cpB5Ce;EoB6Cf,qBpBkG4C;AHopF9C;;AK3zFE;EkBwEE,cpBgG8D;EoB/F9D,0BpBgG+C;AHupFnD;;AuB9vFA;EAYI,0BpB2F+C;EoB1F/C,gBAAgB;AvBsvFpB;;AuBnwFA;EAkBI,cpBnFc;EoBoFd,oBAAoB;AvBqvFxB;;AuB1uFA;ECJE,oBrB6SgC;ECtR5B,kBAtCY;EoBiBhB,gBrBkI+B;EOvO7B,qBP8O+B;AH0mFnC;;AuB7uFA;ECRE,uBrBwSiC;ECjR7B,mBAtCY;EoBiBhB,gBrBmI+B;EOxO7B,qBP+O+B;AHgnFnC;;AuB3uFA;EACE,cAAc;EACd,WAAW;AvB8uFb;;AuBhvFA;EAMI,kBpB6T+B;AHi7EnC;;AuBzuFA;;;EAII,WAAW;AvB2uFf;;AyBn3FA;ELMM,gCjB8P2C;AHmnFjD;;AoB52FI;EKXJ;ILYM,gBAAgB;EpBg3FpB;AACF;;AyB73FA;EAII,UAAU;AzB63Fd;;AyBz3FA;EAEI,aAAa;AzB23FjB;;AyBv3FA;EACE,kBAAkB;EAClB,SAAS;EACT,gBAAgB;ELXZ,6BjB+PwC;AHuoF9C;;AoBj4FI;EKGJ;ILFM,gBAAgB;EpBq4FpB;AACF;;A0Bj5FA;;;;EAIE,kBAAkB;A1Bo5FpB;;A0Bj5FA;EACE,mBAAmB;A1Bo5FrB;;A2Bh4FI;EACE,qBAAqB;EACrB,oBxBkO0C;EwBjO1C,uBxBgO0C;EwB/N1C,WAAW;EAhCf,uBAA8B;EAC9B,qCAA4C;EAC5C,gBAAgB;EAChB,oCAA2C;A3Bo6F7C;;A2B/2FI;EACE,cAAc;A3Bk3FpB;;A0B55FA;EACE,kBAAkB;EAClB,SAAS;EACT,OAAO;EACP,avB4pBsC;EuB3pBtC,aAAa;EACb,WAAW;EACX,gBvBkuBuC;EuBjuBvC,iBAA8B;EAC9B,oBAA4B;EtBsGxB,eAtCY;EsB9DhB,cvBXgB;EuBYhB,gBAAgB;EAChB,gBAAgB;EAChB,sBvBvBa;EuBwBb,4BAA4B;EAC5B,qCvBfa;EOZX,sBP6OgC;AH8sFpC;;A0Bv5FI;EACE,WAAW;EACX,OAAO;A1B05Fb;;A0Bv5FI;EACE,QAAQ;EACR,UAAU;A1B05FhB;;Ac94FI;EYnBA;IACE,WAAW;IACX,OAAO;E1Bq6FX;E0Bl6FE;IACE,QAAQ;IACR,UAAU;E1Bo6Fd;AACF;;Acz5FI;EYnBA;IACE,WAAW;IACX,OAAO;E1Bg7FX;E0B76FE;IACE,QAAQ;IACR,UAAU;E1B+6Fd;AACF;;Acp6FI;EYnBA;IACE,WAAW;IACX,OAAO;E1B27FX;E0Bx7FE;IACE,QAAQ;IACR,UAAU;E1B07Fd;AACF;;Ac/6FI;EYnBA;IACE,WAAW;IACX,OAAO;E1Bs8FX;E0Bn8FE;IACE,QAAQ;IACR,UAAU;E1Bq8Fd;AACF;;A0B/7FA;EAEI,SAAS;EACT,YAAY;EACZ,aAAa;EACb,uBvB+rBuC;AHkwE3C;;A2Bh+FI;EACE,qBAAqB;EACrB,oBxBkO0C;EwBjO1C,uBxBgO0C;EwB/N1C,WAAW;EAzBf,aAAa;EACb,qCAA4C;EAC5C,0BAAiC;EACjC,oCAA2C;A3B6/F7C;;A2B/8FI;EACE,cAAc;A3Bk9FpB;;A0Bx8FA;EAEI,MAAM;EACN,WAAW;EACX,UAAU;EACV,aAAa;EACb,qBvBirBuC;AHyxE3C;;A2Bv/FI;EACE,qBAAqB;EACrB,oBxBkO0C;EwBjO1C,uBxBgO0C;EwB/N1C,WAAW;EAlBf,mCAA0C;EAC1C,eAAe;EACf,sCAA6C;EAC7C,wBAA+B;A3B6gGjC;;A2Bt+FI;EACE,cAAc;A3By+FpB;;A2BtgGI;EDmDE,iBAAiB;A1Bu9FvB;;A0Bl9FA;EAEI,MAAM;EACN,WAAW;EACX,UAAU;EACV,aAAa;EACb,sBvBgqBuC;AHozE3C;;A2BlhGI;EACE,qBAAqB;EACrB,oBxBkO0C;EwBjO1C,uBxBgO0C;EwB/N1C,WAAW;A3BqhGjB;;A2BzhGI;EAgBI,aAAa;A3B6gGrB;;A2B1gGM;EACE,qBAAqB;EACrB,qBxB+MwC;EwB9MxC,uBxB6MwC;EwB5MxC,WAAW;EA9BjB,mCAA0C;EAC1C,yBAAgC;EAChC,sCAA6C;A3B4iG/C;;A2B3gGI;EACE,cAAc;A3B8gGpB;;A2BxhGM;EDiDA,iBAAiB;A1B2+FvB;;A0Bp+FA;EAKI,WAAW;EACX,YAAY;A1Bm+FhB;;A0B99FA;EE9GE,SAAS;EACT,gBAAmB;EACnB,gBAAgB;EAChB,6BzBCgB;AH+kGlB;;A0B99FA;EACE,cAAc;EACd,WAAW;EACX,uBvBopBwC;EuBnpBxC,WAAW;EACX,gBvBoK+B;EuBnK/B,cvBhHgB;EuBiHhB,mBAAmB;EACnB,mBAAmB;EACnB,6BAA6B;EAC7B,SAAS;A1Bi+FX;;AKrlGE;EqBmIE,cvBqnBqD;EuBpnBrD,qBAAqB;EJ9IrB,yBnBEc;AHmmGlB;;A0Bj/FA;EAgCI,WvBnJW;EuBoJX,qBAAqB;EJrJrB,yBnB8Ba;AH6kGjB;;A0Bv/FA;EAuCI,cvBpJc;EuBqJd,oBAAoB;EACpB,6BAA6B;A1Bo9FjC;;A0B58FA;EACE,cAAc;A1B+8FhB;;A0B38FA;EACE,cAAc;EACd,sBvB+lBwC;EuB9lBxC,gBAAgB;EtBpDZ,mBAtCY;EsB4FhB,cvBxKgB;EuByKhB,mBAAmB;A1B88FrB;;A0B18FA;EACE,cAAc;EACd,uBvBqlBwC;EuBplBxC,cvB7KgB;AH0nGlB;;A6BvoGA;;EAEE,kBAAkB;EAClB,2BAAoB;EAApB,oBAAoB;EACpB,sBAAsB;A7B0oGxB;;A6B9oGA;;EAOI,kBAAkB;EAClB,kBAAc;EAAd,cAAc;A7B4oGlB;;AK3oGE;;EwBII,UAAU;A7B4oGhB;;A6BzpGA;;;;EAkBM,UAAU;A7B8oGhB;;A6BxoGA;EACE,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,oBAA2B;EAA3B,2BAA2B;A7B2oG7B;;A6B9oGA;EAMI,WAAW;A7B4oGf;;A6BxoGA;;EAII,iB1BsM6B;AHm8FjC;;A6B7oGA;;EnBhBI,0BmB0B8B;EnBzB9B,6BmByB8B;A7ByoGlC;;A6BnpGA;;EnBFI,yBmBiB6B;EnBhB7B,4BmBgB6B;A7B0oGjC;;A6B1nGA;EACE,wBAAmC;EACnC,uBAAkC;A7B6nGpC;;A6B/nGA;;;EAOI,cAAc;A7B8nGlB;;A6B3nGE;EACE,eAAe;A7B8nGnB;;A6B1nGA;EACE,uBAAsC;EACtC,sBAAqC;A7B6nGvC;;A6B1nGA;EACE,sBAAsC;EACtC,qBAAqC;A7B6nGvC;;A6BzmGA;EACE,0BAAsB;EAAtB,sBAAsB;EACtB,qBAAuB;EAAvB,uBAAuB;EACvB,qBAAuB;EAAvB,uBAAuB;A7B4mGzB;;A6B/mGA;;EAOI,WAAW;A7B6mGf;;A6BpnGA;;EAYI,gB1BqH6B;AHw/FjC;;A6BznGA;;EnBlFI,6BmBoG+B;EnBnG/B,4BmBmG+B;A7B6mGnC;;A6B/nGA;;EnBhGI,yBmBuH4B;EnBtH5B,0BmBsH4B;A7B8mGhC;;A6B7lGA;;EAGI,gBAAgB;A7B+lGpB;;A6BlmGA;;;;EAOM,kBAAkB;EAClB,sBAAsB;EACtB,oBAAoB;A7BkmG1B;;A8B3vGA;EACE,kBAAkB;EAClB,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,uBAAoB;EAApB,oBAAoB;EACpB,WAAW;A9B8vGb;;A8BnwGA;;;;EAWI,kBAAkB;EAClB,gBAAY;EAAZ,YAAY;EACZ,YAAY;EACZ,gBAAgB;A9B+vGpB;;A8B7wGA;;;;;;;;;;;;EAmBM,iB3BsN2B;AHmjGjC;;A8B5xGA;;;EA2BI,UAAU;A9BuwGd;;A8BlyGA;EAgCI,UAAU;A9BswGd;;A8BtyGA;;EpBeI,0BoBsBmD;EpBrBnD,6BoBqBmD;A9BuwGvD;;A8B5yGA;;EpB6BI,yBoBSmD;EpBRnD,4BoBQmD;A9B4wGvD;;A8BlzGA;EA4CI,oBAAa;EAAb,aAAa;EACb,sBAAmB;EAAnB,mBAAmB;A9B0wGvB;;A8BvzGA;;EpBeI,0BoBiC6E;EpBhC7E,6BoBgC6E;A9B6wGjF;;A8B7zGA;EpB6BI,yBoBoBsE;EpBnBtE,4BoBmBsE;A9BixG1E;;A8BtwGA;;EAEE,oBAAa;EAAb,aAAa;A9BywGf;;A8B3wGA;;EAQI,kBAAkB;EAClB,UAAU;A9BwwGd;;A8BjxGA;;EAYM,UAAU;A9B0wGhB;;A8BtxGA;;;;;;;;EAoBI,iB3ByJ6B;AHonGjC;;A8BzwGA;EAAuB,kB3BqJU;AHwnGjC;;A8B5wGA;EAAsB,iB3BoJW;AH4nGjC;;A8BxwGA;EACE,oBAAa;EAAb,aAAa;EACb,sBAAmB;EAAnB,mBAAmB;EACnB,yB3BgSkC;E2B/RlC,gBAAgB;E1BwBZ,eAtCY;E0BgBhB,gB3B0L+B;E2BzL/B,gB3B8L+B;E2B7L/B,c3B7FgB;E2B8FhB,kBAAkB;EAClB,mBAAmB;EACnB,yB3BrGgB;E2BsGhB,yB3BpGgB;EONd,sBP6OgC;AHyoGpC;;A8BxxGA;;EAkBI,aAAa;A9B2wGjB;;A8BjwGA;;EAEE,gCZjB8D;AlBqxGhE;;A8BjwGA;;;;;;EAME,oB3B2QgC;ECtR5B,kBAtCY;E0BmDhB,gB3BgG+B;EOvO7B,qBP8O+B;AH8pGnC;;A8BjwGA;;EAEE,kCZlC8D;AlBsyGhE;;A8BjwGA;;;;;;EAME,uB3BqPiC;ECjR7B,mBAtCY;E0BoEhB,gB3BgF+B;EOxO7B,qBP+O+B;AH8qGnC;;A8BjwGA;;EAEE,sBAA0E;A9BowG5E;;A8BzvGA;;;;;;EpBzJI,0BoB+J4B;EpB9J5B,6BoB8J4B;A9B6vGhC;;A8B1vGA;;;;;;EpBpJI,yBoB0J2B;EpBzJ3B,4BoByJ2B;A9B8vG/B;;A+Bl7GA;EACE,kBAAkB;EAClB,cAAc;EACd,kBAA+C;EAC/C,oBAAqE;A/Bq7GvE;;A+Bl7GA;EACE,2BAAoB;EAApB,oBAAoB;EACpB,kB5B6f0C;AHw7F5C;;A+Bl7GA;EACE,kBAAkB;EAClB,OAAO;EACP,WAAW;EACX,W5Byf0C;E4Bxf1C,eAAkF;EAClF,UAAU;A/Bq7GZ;;A+B37GA;EASI,W5BvBW;E4BwBX,qB5BKa;EmB9Bb,yBnB8Ba;AHk7GjB;;A+Bj8GA;EAoBM,gD5BLW;AHs7GjB;;A+Br8GA;EAyBI,qB5B0bsE;AHs/F1E;;A+Bz8GA;EA6BI,W5B3CW;E4B4CX,yB5Bsf8E;E4Brf9E,qB5Bqf8E;AH27FlF;;A+B/8GA;EAuCM,c5B/CY;AH29GlB;;A+Bn9GA;EA0CQ,yB5BtDU;AHm+GlB;;A+Bn6GA;EACE,kBAAkB;EAClB,gBAAgB;EAEhB,mBAAmB;A/Bq6GrB;;A+Bz6GA;EASI,kBAAkB;EAClB,YAA+E;EAC/E,aAA+D;EAC/D,cAAc;EACd,W5B4bwC;E4B3bxC,Y5B2bwC;E4B1bxC,oBAAoB;EACpB,WAAW;EACX,sB5BnFW;E4BoFX,yB5BoJ6B;AHgxGjC;;A+Bt7GA;EAwBI,kBAAkB;EAClB,YAA+E;EAC/E,aAA+D;EAC/D,cAAc;EACd,W5B6awC;E4B5axC,Y5B4awC;E4B3axC,WAAW;EACX,mCAAgE;A/Bk6GpE;;A+Bz5GA;ErB5GI,sBP6OgC;AH4xGpC;;A+B75GA;EAOM,kOb5EqI;AlBs+G3I;;A+Bj6GA;EAaM,qB5B1FW;EmB9Bb,yBnB8Ba;AHm/GjB;;A+Bt6GA;EAkBM,+KbvFqI;AlB++G3I;;A+B16GA;EAwBM,wC5BrGW;AH2/GjB;;A+B96GA;EA2BM,wC5BxGW;AH+/GjB;;A+B94GA;EAGI,kB5B8Z+C;AHi/FnD;;A+Bl5GA;EAQM,8KbjHqI;AlB+/G3I;;A+Bt5GA;EAcM,wC5B/HW;AH2gHjB;;A+Bl4GA;EACE,qBAA2D;A/Bq4G7D;;A+Bt4GA;EAKM,cAAqD;EACrD,c5BsY+E;E4BrY/E,mBAAmB;EAEnB,qB5BoY4E;AHggGlF;;A+B74GA;EAaM,wBb1E0D;Ea2E1D,0Bb3E0D;Ea4E1D,uBbxD0D;EayD1D,wBbzD0D;Ea0D1D,yB5BlLY;E4BoLZ,qB5B0X4E;EiBpjB5E,iJjBsgB+H;EiBtgB/H,yIjBsgB+H;EiBtgB/H,8KjBsgB+H;AHwjGrI;;AoBzjHI;EWkKJ;IXjKM,gBAAgB;EpB6jHpB;AACF;;A+B75GA;EA0BM,sB5BhMS;E4BiMT,sCAA4E;EAA5E,8BAA4E;A/Bu4GlF;;A+Bl6GA;EAiCM,wC5B1KW;AH+iHjB;;A+Bz3GA;EACE,qBAAqB;EACrB,WAAW;EACX,mCb7G8D;Ea8G9D,0C5BwKkC;ECvQ9B,eAtCY;E2BwIhB,gB5BkE+B;E4BjE/B,gB5BsE+B;E4BrE/B,c5BrNgB;E4BsNhB,sBAAsB;EACtB,uO5BuW+I;E4BtW/I,yB5B3NgB;EONd,sBP6OgC;E4BTlC,wBAAgB;EAAhB,qBAAgB;EAAhB,gBAAgB;A/B03GlB;;A+Bz4GA;EAkBI,qB5B4PsE;E4B3PtE,UAAU;EAIR,gD5B7MW;AHqkHjB;;A+B/4GA;EAgCM,c5B5OY;E4B6OZ,sB5BpPS;AHumHf;;A+Bp5GA;EAuCI,YAAY;EACZ,sB5BoIgC;E4BnIhC,sBAAsB;A/Bi3G1B;;A+B15GA;EA6CI,c5B1Pc;E4B2Pd,yB5B/Pc;AHgnHlB;;A+B/5GA;EAmDI,aAAa;A/Bg3GjB;;A+Bn6GA;EAwDI,kBAAkB;EAClB,0B5BrQc;AHonHlB;;A+B32GA;EACE,kCbxK8D;EayK9D,oB5BsHkC;E4BrHlC,uB5BqHkC;E4BpHlC,oB5BqHiC;ECjR7B,mBAtCY;AJijHlB;;A+B32GA;EACE,gCbhL8D;EaiL9D,mB5BmHiC;E4BlHjC,sB5BkHiC;E4BjHjC,kB5BkHgC;ECtR5B,kBAtCY;AJyjHlB;;A+Bt2GA;EACE,kBAAkB;EAClB,qBAAqB;EACrB,WAAW;EACX,mCbhM8D;EaiM9D,gBAAgB;A/By2GlB;;A+Bt2GA;EACE,kBAAkB;EAClB,UAAU;EACV,WAAW;EACX,mCbxM8D;EayM9D,SAAS;EACT,UAAU;A/By2GZ;;A+B/2GA;EASI,qB5B2KsE;E4B1KtE,gD5B1Ra;AHooHjB;;A+Bp3GA;;EAgBI,yB5B3Tc;AHoqHlB;;A+Bz3GA;EAqBM,iB5BkUQ;AHsiGd;;A+B73GA;EA0BI,0BAA0B;A/Bu2G9B;;A+Bn2GA;EACE,kBAAkB;EAClB,MAAM;EACN,QAAQ;EACR,OAAO;EACP,UAAU;EACV,mCbxO8D;EayO9D,yB5B6CkC;E4B3ClC,gB5BxD+B;E4ByD/B,gB5BpD+B;E4BqD/B,c5B/UgB;E4BgVhB,sB5BvVa;E4BwVb,yB5BpVgB;EONd,sBP6OgC;AHm9GpC;;A+Bn3GA;EAkBI,kBAAkB;EAClB,MAAM;EACN,QAAQ;EACR,SAAS;EACT,UAAU;EACV,cAAc;EACd,6Bb1P4D;Ea2P5D,yB5B2BgC;E4B1BhC,gB5BpE6B;E4BqE7B,c5B/Vc;E4BgWd,iBAAiB;ETxWjB,yBnBGc;E4BuWd,oBAAoB;ErB3WpB,kCqB4WgF;A/Bq2GpF;;A+B31GA;EACE,WAAW;EACX,cbhR2B;EaiR3B,UAAU;EACV,6BAA6B;EAC7B,wBAAgB;EAAhB,qBAAgB;EAAhB,gBAAgB;A/B81GlB;;A+Bn2GA;EAQI,aAAa;A/B+1GjB;;A+Bv2GA;EAY8B,gE5BnWb;AHksHjB;;A+B32GA;EAa8B,gE5BpWb;AHssHjB;;A+B/2GA;EAc8B,gE5BrWb;AH0sHjB;;A+Bn3GA;EAkBI,SAAS;A/Bq2Gb;;A+Bv3GA;EAsBI,W5B2N6C;E4B1N7C,Y5B0N6C;E4BzN7C,oBAAyE;ET7YzE,yBnB8Ba;E4BiXb,S5B0N0C;EO1mB1C,mBP2mB6C;EiB1mB3C,oHjBsgB+H;EiBtgB/H,4GjBsgB+H;E4BnHjI,wBAAgB;EAAhB,gBAAgB;A/Bo2GpB;;AoBlvHI;EWgXJ;IX/WM,wBAAgB;IAAhB,gBAAgB;EpBsvHpB;AACF;;A+Bx4GA;ETrXI,yBnB8mB2E;AHmpG/E;;A+B54GA;EAsCI,W5BoMoC;E4BnMpC,c5BoMqC;E4BnMrC,kBAAkB;EAClB,e5BmMuC;E4BlMvC,yB5B3Zc;E4B4Zd,yBAAyB;ErBjazB,mBPomBoC;AHwqGxC;;A+Bt5GA;EAiDI,W5BgM6C;E4B/L7C,Y5B+L6C;EmBtmB7C,yBnB8Ba;E4B2Yb,S5BgM0C;EO1mB1C,mBP2mB6C;EiB1mB3C,iHjBsgB+H;EiBtgB/H,4GjBsgB+H;E4BzFjI,qBAAgB;EAAhB,gBAAgB;A/Bw2GpB;;AoBhxHI;EWgXJ;IX/WM,qBAAgB;IAAhB,gBAAgB;EpBoxHpB;AACF;;A+Bt6GA;ETrXI,yBnB8mB2E;AHirG/E;;A+B16GA;EAgEI,W5B0KoC;E4BzKpC,c5B0KqC;E4BzKrC,kBAAkB;EAClB,e5ByKuC;E4BxKvC,yB5Brbc;E4Bsbd,yBAAyB;ErB3bzB,mBPomBoC;AHssGxC;;A+Bp7GA;EA2EI,W5BsK6C;E4BrK7C,Y5BqK6C;E4BpK7C,aAAa;EACb,oB5B9D+B;E4B+D/B,mB5B/D+B;EmBrY/B,yBnB8Ba;E4Bwab,S5BmK0C;EO1mB1C,mBP2mB6C;EiB1mB3C,gHjBsgB+H;EiBtgB/H,4GjBsgB+H;E4B5DjI,gBAAgB;A/B42GpB;;AoBjzHI;EWgXJ;IX/WM,oBAAgB;IAAhB,gBAAgB;EpBqzHpB;AACF;;A+Bv8GA;ETrXI,yBnB8mB2E;AHktG/E;;A+B38GA;EA6FI,W5B6IoC;E4B5IpC,c5B6IqC;E4B5IrC,kBAAkB;EAClB,e5B4IuC;E4B3IvC,6BAA6B;EAC7B,yBAAyB;EACzB,oBAA4C;A/Bk3GhD;;A+Br9GA;EAwGI,yB5Bzdc;EOLd,mBPomBoC;AH4uGxC;;A+B19GA;EA6GI,kBAAkB;EAClB,yB5B/dc;EOLd,mBPomBoC;AHkvGxC;;A+Bh+GA;EAoHM,yB5BneY;AHm1HlB;;A+Bp+GA;EAwHM,eAAe;A/Bg3GrB;;A+Bx+GA;EA4HM,yB5B3eY;AH21HlB;;A+B5+GA;EAgIM,eAAe;A/Bg3GrB;;A+Bh/GA;EAoIM,yB5BnfY;AHm2HlB;;A+B32GA;;;EX9fM,4GjBsgB+H;AHy2GrI;;AoB12HI;EWyfJ;;;IXxfM,gBAAgB;EpBg3HpB;AACF;;AgCx3HA;EACE,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,eAAe;EACf,gBAAgB;EAChB,gBAAgB;AhC23HlB;;AgCx3HA;EACE,cAAc;EACd,oB7B6qBsC;AH8sGxC;;AK13HE;E2BEE,qBAAqB;AhC43HzB;;AgCj4HA;EAUI,c7BVc;E6BWd,oBAAoB;EACpB,eAAe;AhC23HnB;;AgCn3HA;EACE,gC7BxBgB;AH84HlB;;AgCv3HA;EAII,mB7B0M6B;AH6qHjC;;AgC33HA;EAQI,6BAAgD;EtB3BhD,+BPoOgC;EOnOhC,gCPmOgC;AHgrHpC;;AKl5HE;E2B6BI,qC7BnCY;AH45HlB;;AgCr4HA;EAgBM,c7BpCY;E6BqCZ,6BAA6B;EAC7B,yBAAyB;AhCy3H/B;;AgC34HA;;EAwBI,c7B3Cc;E6B4Cd,sB7BnDW;E6BoDX,kC7BpDW;AH46Hf;;AgCl5HA;EA+BI,gB7B+K6B;EOjO7B,yBsBoD4B;EtBnD5B,0BsBmD4B;AhCu3HhC;;AgC92HA;EtBtEI,sBP6OgC;AH2sHpC;;AgCl3HA;;EAOI,W7B3EW;E6B4EX,yB7B/Ca;AH+5HjB;;AgCv2HA;EAEI,kBAAc;EAAd,cAAc;EACd,kBAAkB;AhCy2HtB;;AgCr2HA;EAEI,0BAAa;EAAb,aAAa;EACb,oBAAY;EAAZ,YAAY;EACZ,kBAAkB;AhCu2HtB;;AgC91HA;EAEI,aAAa;AhCg2HjB;;AgCl2HA;EAKI,cAAc;AhCi2HlB;;AiCr8HA;EACE,kBAAkB;EAClB,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,sBAAmB;EAAnB,mBAAmB;EACnB,sBAA8B;EAA9B,8BAA8B;EAC9B,oB9BiHW;AHu1Hb;;AiC98HA;;EAWI,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,sBAAmB;EAAnB,mBAAmB;EACnB,sBAA8B;EAA9B,8BAA8B;AjCw8HlC;;AiCp7HA;EACE,qBAAqB;EACrB,sB9BqqB+E;E8BpqB/E,yB9BoqB+E;E8BnqB/E,kB9BiFW;ECTP,kBAtCY;E6BhChB,oBAAoB;EACpB,mBAAmB;AjCu7HrB;;AKj+HE;E4B6CE,qBAAqB;AjCw7HzB;;AiC/6HA;EACE,oBAAa;EAAb,aAAa;EACb,0BAAsB;EAAtB,sBAAsB;EACtB,eAAe;EACf,gBAAgB;EAChB,gBAAgB;AjCk7HlB;;AiCv7HA;EAQI,gBAAgB;EAChB,eAAe;AjCm7HnB;;AiC57HA;EAaI,gBAAgB;EAChB,WAAW;AjCm7Hf;;AiC16HA;EACE,qBAAqB;EACrB,mB9B4lBuC;E8B3lBvC,sB9B2lBuC;AHk1GzC;;AiCj6HA;EACE,6BAAgB;EAAhB,gBAAgB;EAChB,oBAAY;EAAZ,YAAY;EAGZ,sBAAmB;EAAnB,mBAAmB;AjCk6HrB;;AiC95HA;EACE,wB9BumBwC;EC9lBpC,kBAtCY;E6B+BhB,cAAc;EACd,6BAA6B;EAC7B,6BAAuC;EvBrHrC,sBP6OgC;AH0yHpC;;AK5gIE;E4B8GE,qBAAqB;AjCk6HzB;;AiC55HA;EACE,qBAAqB;EACrB,YAAY;EACZ,aAAa;EACb,sBAAsB;EACtB,WAAW;EACX,mCAAmC;EACnC,0BAA0B;AjC+5H5B;;Acj+HI;EmB4EC;;IAGK,gBAAgB;IAChB,eAAe;EjCw5HvB;AACF;;Act/HI;EmByFA;IAoBI,yBAAqB;IAArB,qBAAqB;IACrB,oBAA2B;IAA3B,2BAA2B;EjC84HjC;EiCn6HG;IAwBK,uBAAmB;IAAnB,mBAAmB;EjC84H3B;EiCt6HG;IA2BO,kBAAkB;EjC84H5B;EiCz6HG;IA+BO,qB9BgiB6B;I8B/hB7B,oB9B+hB6B;EH82GvC;EiC76HG;;IAsCK,qBAAiB;IAAjB,iBAAiB;EjC24HzB;EiCj7HG;IAqDK,+BAAwB;IAAxB,wBAAwB;IAGxB,6BAAgB;IAAhB,gBAAgB;EjC63HxB;EiCr7HG;IA4DK,aAAa;EjC43HrB;AACF;;AcrgII;EmB4EC;;IAGK,gBAAgB;IAChB,eAAe;EjC47HvB;AACF;;Ac1hII;EmByFA;IAoBI,yBAAqB;IAArB,qBAAqB;IACrB,oBAA2B;IAA3B,2BAA2B;EjCk7HjC;EiCv8HG;IAwBK,uBAAmB;IAAnB,mBAAmB;EjCk7H3B;EiC18HG;IA2BO,kBAAkB;EjCk7H5B;EiC78HG;IA+BO,qB9BgiB6B;I8B/hB7B,oB9B+hB6B;EHk5GvC;EiCj9HG;;IAsCK,qBAAiB;IAAjB,iBAAiB;EjC+6HzB;EiCr9HG;IAqDK,+BAAwB;IAAxB,wBAAwB;IAGxB,6BAAgB;IAAhB,gBAAgB;EjCi6HxB;EiCz9HG;IA4DK,aAAa;EjCg6HrB;AACF;;AcziII;EmB4EC;;IAGK,gBAAgB;IAChB,eAAe;EjCg+HvB;AACF;;Ac9jII;EmByFA;IAoBI,yBAAqB;IAArB,qBAAqB;IACrB,oBAA2B;IAA3B,2BAA2B;EjCs9HjC;EiC3+HG;IAwBK,uBAAmB;IAAnB,mBAAmB;EjCs9H3B;EiC9+HG;IA2BO,kBAAkB;EjCs9H5B;EiCj/HG;IA+BO,qB9BgiB6B;I8B/hB7B,oB9B+hB6B;EHs7GvC;EiCr/HG;;IAsCK,qBAAiB;IAAjB,iBAAiB;EjCm9HzB;EiCz/HG;IAqDK,+BAAwB;IAAxB,wBAAwB;IAGxB,6BAAgB;IAAhB,gBAAgB;EjCq8HxB;EiC7/HG;IA4DK,aAAa;EjCo8HrB;AACF;;Ac7kII;EmB4EC;;IAGK,gBAAgB;IAChB,eAAe;EjCogIvB;AACF;;AclmII;EmByFA;IAoBI,yBAAqB;IAArB,qBAAqB;IACrB,oBAA2B;IAA3B,2BAA2B;EjC0/HjC;EiC/gIG;IAwBK,uBAAmB;IAAnB,mBAAmB;EjC0/H3B;EiClhIG;IA2BO,kBAAkB;EjC0/H5B;EiCrhIG;IA+BO,qB9BgiB6B;I8B/hB7B,oB9B+hB6B;EH09GvC;EiCzhIG;;IAsCK,qBAAiB;IAAjB,iBAAiB;EjCu/HzB;EiC7hIG;IAqDK,+BAAwB;IAAxB,wBAAwB;IAGxB,6BAAgB;IAAhB,gBAAgB;EjCy+HxB;EiCjiIG;IA4DK,aAAa;EjCw+HrB;AACF;;AiC1iIA;EAyBQ,yBAAqB;EAArB,qBAAqB;EACrB,oBAA2B;EAA3B,2BAA2B;AjCqhInC;;AiC/iIA;;EAQU,gBAAgB;EAChB,eAAe;AjC4iIzB;;AiCrjIA;EA6BU,uBAAmB;EAAnB,mBAAmB;AjC4hI7B;;AiCzjIA;EAgCY,kBAAkB;AjC6hI9B;;AiC7jIA;EAoCY,qB9BgiB6B;E8B/hB7B,oB9B+hB6B;AH8/GzC;;AiClkIA;;EA2CU,qBAAiB;EAAjB,iBAAiB;AjC4hI3B;;AiCvkIA;EA0DU,+BAAwB;EAAxB,wBAAwB;EAGxB,6BAAgB;EAAhB,gBAAgB;AjC+gI1B;;AiC5kIA;EAiEU,aAAa;AjC+gIvB;;AiClgIA;EAEI,yB9B/MW;AHmtIf;;AKptIE;E4BmNI,yB9BlNS;AHutIf;;AiC1gIA;EAWM,yB9BxNS;AH2tIf;;AK5tIE;E4B4NM,yB9B3NO;AH+tIf;;AiClhIA;EAkBQ,yB9B/NO;AHmuIf;;AiCthIA;;;;EA0BM,yB9BvOS;AH0uIf;;AiC7hIA;EA+BI,yB9B5OW;E8B6OX,gC9B7OW;AH+uIf;;AiCliIA;EAoCI,+QftNuI;AlBwtI3I;;AiCtiIA;EAwCI,yB9BrPW;AHuvIf;;AiC1iIA;EA0CM,yB9BvPS;AH2vIf;;AK5vIE;E4B2PM,yB9B1PO;AH+vIf;;AiC9/HA;EAEI,W9B7QW;AH6wIf;;AKpwIE;E4BuQI,W9BhRS;AHixIf;;AiCtgIA;EAWM,+B9BtRS;AHqxIf;;AK5wIE;E4BgRM,gC9BzRO;AHyxIf;;AiC9gIA;EAkBQ,gC9B7RO;AH6xIf;;AiClhIA;;;;EA0BM,W9BrSS;AHoyIf;;AiCzhIA;EA+BI,+B9B1SW;E8B2SX,sC9B3SW;AHyyIf;;AiC9hIA;EAoCI,qRf1QuI;AlBwwI3I;;AiCliIA;EAwCI,+B9BnTW;AHizIf;;AiCtiIA;EA0CM,W9BrTS;AHqzIf;;AK5yIE;E4B+SM,W9BxTO;AHyzIf;;AkC5zIA;EACE,kBAAkB;EAClB,oBAAa;EAAb,aAAa;EACb,0BAAsB;EAAtB,sBAAsB;EACtB,YAAY;EAEZ,qBAAqB;EACrB,sB/BJa;E+BKb,2BAA2B;EAC3B,sC/BIa;EOZX,sBP6OgC;AH0lIpC;;AkCx0IA;EAaI,eAAe;EACf,cAAc;AlC+zIlB;;AkC70IA;ExBUI,+BPoOgC;EOnOhC,gCPmOgC;AHomIpC;;AkCl1IA;ExBwBI,mCPsNgC;EOrNhC,kCPqNgC;AHymIpC;;AkCzzIA;EAGE,kBAAc;EAAd,cAAc;EAGd,eAAe;EACf,gB/BsxByC;AHkiH3C;;AkCpzIA;EACE,sB/BgxBwC;AHuiH1C;;AkCpzIA;EACE,qBAA+B;EAC/B,gBAAgB;AlCuzIlB;;AkCpzIA;EACE,gBAAgB;AlCuzIlB;;AKl2IE;E6BgDE,qBAAqB;AlCszIzB;;AkCxzIA;EAMI,oB/B+vBuC;AHujH3C;;AkC9yIA;EACE,wB/BsvByC;E+BrvBzC,gBAAgB;EAEhB,qC/B3Da;E+B4Db,6C/B5Da;AH42If;;AkCrzIA;ExBnEI,0DwB2E8E;AlCizIlF;;AkCzzIA;EAaM,aAAa;AlCgzInB;;AkC3yIA;EACE,wB/BouByC;E+BnuBzC,qC/B3Ea;E+B4Eb,0C/B5Ea;AH03If;;AkCjzIA;ExBrFI,0DQ+H4D;AlB2wIhE;;AkCtyIA;EACE,uBAAiC;EACjC,uB/BmtBwC;E+BltBxC,sBAAgC;EAChC,gBAAgB;AlCyyIlB;;AkCtyIA;EACE,uBAAiC;EACjC,sBAAgC;AlCyyIlC;;AkCryIA;EACE,kBAAkB;EAClB,MAAM;EACN,QAAQ;EACR,SAAS;EACT,OAAO;EACP,gB/B8sByC;AH0lH3C;;AkCryIA;;;EAGE,oBAAc;EAAd,cAAc;EACd,WAAW;AlCwyIb;;AkCryIA;;ExBxHI,2CQsH4D;ERrH5D,4CQqH4D;AlB6yIhE;;AkCtyIA;;ExB/GI,+CQwG4D;ERvG5D,8CQuG4D;AlBmzIhE;;AkCpyIA;EAEI,mB/BurBsD;AH+mH1D;;Ac/3II;EoBuFJ;IAMI,oBAAa;IAAb,aAAa;IACb,uBAAmB;IAAnB,mBAAmB;IACnB,mB/BirBsD;I+BhrBtD,kB/BgrBsD;EHunHxD;EkChzIF;IAaM,gBAAY;IAAZ,YAAY;IACZ,kB/B2qBoD;I+B1qBpD,gBAAgB;IAChB,iB/ByqBoD;EH6nHxD;AACF;;AkC7xIA;EAII,mB/B2pBsD;AHkoH1D;;Acl5II;EoBiHJ;IAQI,oBAAa;IAAb,aAAa;IACb,uBAAmB;IAAnB,mBAAmB;ElC8xIrB;EkCvyIF;IAcM,gBAAY;IAAZ,YAAY;IACZ,gBAAgB;ElC4xIpB;EkC3yIF;IAkBQ,cAAc;IACd,cAAc;ElC4xIpB;EkC/yIF;IxBxJI,0BwBiLoC;IxBhLpC,6BwBgLoC;ElC0xItC;EkCnzIF;;IA8BY,0BAA0B;ElCyxIpC;EkCvzIF;;IAmCY,6BAA6B;ElCwxIvC;EkC3zIF;IxB1II,yBwBkLmC;IxBjLnC,4BwBiLmC;ElCuxIrC;EkC/zIF;;IA6CY,yBAAyB;ElCsxInC;EkCn0IF;;IAkDY,4BAA4B;ElCqxItC;AACF;;AkCzwIA;EAEI,sB/BglBsC;AH2rH1C;;Ac77II;EoBgLJ;IAMI,uB/B6lBiC;I+B7lBjC,oB/B6lBiC;I+B7lBjC,e/B6lBiC;I+B5lBjC,2B/B6lBuC;I+B7lBvC,wB/B6lBuC;I+B7lBvC,mB/B6lBuC;I+B5lBvC,UAAU;IACV,SAAS;ElC4wIX;EkCrxIF;IAYM,qBAAqB;IACrB,WAAW;ElC4wIf;AACF;;AkCnwIA;EAEI,gBAAgB;AlCqwIpB;;AkCvwIA;EAKM,gBAAgB;ExB5OlB,6BwB6OiC;ExB5OjC,4BwB4OiC;AlCuwIrC;;AkC7wIA;ExBrPI,yBwB+P8B;ExB9P9B,0BwB8P8B;AlCwwIlC;;AkClxIA;ExB9PI,gBwB4Q0B;EACxB,mB/BnC2B;AH2yIjC;;AmC1hJA;EACE,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,qBhC+hCsC;EgC9hCtC,mBhCiiCsC;EgC/hCtC,gBAAgB;EAChB,yBhCEgB;EOJd,sBP6OgC;AHkzIpC;;AmCzhJA;EAGI,oBhCqhCqC;AHqgHzC;;AmC7hJA;EAMM,qBAAqB;EACrB,qBhCihCmC;EgChhCnC,chCNY;EgCOZ,YhCshCuC;AHqgH7C;;AmCpiJA;EAoBI,0BAA0B;AnCohJ9B;;AmCxiJA;EAwBI,qBAAqB;AnCohJzB;;AmC5iJA;EA4BI,chC1Bc;AH8iJlB;;AoC3jJA;EACE,oBAAa;EAAb,aAAa;E7BGb,eAAe;EACf,gBAAgB;EGAd,sBP6OgC;AHg1IpC;;AoC5jJA;EACE,kBAAkB;EAClB,cAAc;EACd,uBjCgxBwC;EiC/wBxC,iBjCqO+B;EiCpO/B,iBjCmxBsC;EiClxBtC,cjCwBe;EiCvBf,sBjCNa;EiCOb,yBjCJgB;AHmkJlB;;AoCvkJA;EAWI,UAAU;EACV,cjCkK8D;EiCjK9D,qBAAqB;EACrB,yBjCXc;EiCYd,qBjCXc;AH2kJlB;;AoC/kJA;EAmBI,UAAU;EACV,UjC4wBiC;EiC3wBjC,gDjCSa;AHujJjB;;AoC5jJA;EAGM,cAAc;E1BChB,+BP+MgC;EO9MhC,kCP8MgC;AH+2IpC;;AoClkJA;E1BVI,gCP6NgC;EO5NhC,mCP4NgC;AHo3IpC;;AoCvkJA;EAcI,UAAU;EACV,WjCvCW;EiCwCX,yBjCXa;EiCYb,qBjCZa;AHykJjB;;AoC9kJA;EAqBI,cjCvCc;EiCwCd,oBAAoB;EAEpB,YAAY;EACZ,sBjCjDW;EiCkDX,qBjC/Cc;AH2mJlB;;AqClnJE;EACE,uBlCyxBsC;EC9pBpC,kBAtCY;EiCnFd,gBlCsO6B;AH+4IjC;;AqChnJM;E3BwBF,8BPgN+B;EO/M/B,iCP+M+B;AH64InC;;AqChnJM;E3BKF,+BP8N+B;EO7N/B,kCP6N+B;AHk5InC;;AqCloJE;EACE,uBlCuxBqC;EC5pBnC,mBAtCY;EiCnFd,gBlCuO6B;AH85IjC;;AqChoJM;E3BwBF,8BPiN+B;EOhN/B,iCPgN+B;AH45InC;;AqChoJM;E3BKF,+BP+N+B;EO9N/B,kCP8N+B;AHi6InC;;AsChpJA;EACE,qBAAqB;EACrB,qBnCw5BsC;ECv1BpC,cAAW;EkC/Db,gBnC2R+B;EmC1R/B,cAAc;EACd,kBAAkB;EAClB,mBAAmB;EACnB,wBAAwB;E5BRtB,sBP6OgC;EiB5O9B,qIjB6b6I;AH+tInJ;;AoBvpJI;EkBNJ;IlBOM,gBAAgB;EpB2pJpB;AACF;;AKxpJE;EiCGI,qBAAqB;AtCypJ3B;;AsCvqJA;EAoBI,aAAa;AtCupJjB;;AsClpJA;EACE,kBAAkB;EAClB,SAAS;AtCqpJX;;AsC9oJA;EACE,oBnC63BsC;EmC53BtC,mBnC43BsC;EOh6BpC,oBPm6BqC;AHmxHzC;;AsCzoJE;ECjDA,WpCMa;EoCLb,yBpCkCe;AH4pJjB;;AKhrJE;EkCVI,WpCCS;EoCAT,yBAAkC;AvC8rJxC;;AuCjsJU;EAQJ,UAAU;EACV,+CpCuBW;AHsqJjB;;AsCxpJE;ECjDA,WpCMa;EoCLb,yBpCWgB;AHksJlB;;AK/rJE;EkCVI,WpCCS;EoCAT,yBAAkC;AvC6sJxC;;AuChtJU;EAQJ,UAAU;EACV,iDpCAY;AH4sJlB;;AsCvqJE;ECjDA,WpCMa;EoCLb,yBpCyCe;AHmrJjB;;AK9sJE;EkCVI,WpCCS;EoCAT,yBAAkC;AvC4tJxC;;AuC/tJU;EAQJ,UAAU;EACV,+CpC8BW;AH6rJjB;;AsCtrJE;ECjDA,WpCMa;EoCLb,yBpC2Ce;AHgsJjB;;AK7tJE;EkCVI,WpCCS;EoCAT,yBAAkC;AvC2uJxC;;AuC9uJU;EAQJ,UAAU;EACV,gDpCgCW;AH0sJjB;;AsCrsJE;ECjDA,cpCegB;EoCdhB,yBpCwCe;AHktJjB;;AK5uJE;EkCVI,cpCUY;EoCTZ,yBAAkC;AvC0vJxC;;AuC7vJU;EAQJ,UAAU;EACV,+CpC6BW;AH4tJjB;;AsCptJE;ECjDA,WpCMa;EoCLb,yBpCsCe;AHmuJjB;;AK3vJE;EkCVI,WpCCS;EoCAT,yBAAkC;AvCywJxC;;AuC5wJU;EAQJ,UAAU;EACV,+CpC2BW;AH6uJjB;;AsCnuJE;ECjDA,cpCegB;EoCdhB,yBpCMgB;AHkxJlB;;AK1wJE;EkCVI,cpCUY;EoCTZ,yBAAkC;AvCwxJxC;;AuC3xJU;EAQJ,UAAU;EACV,iDpCLY;AH4xJlB;;AsClvJE;ECjDA,WpCMa;EoCLb,yBpCagB;AH0xJlB;;AKzxJE;EkCVI,WpCCS;EoCAT,yBAAkC;AvCuyJxC;;AuC1yJU;EAQJ,UAAU;EACV,8CpCEY;AHoyJlB;;AwCnzJA;EACE,kBAAoD;EACpD,mBrCqzBsC;EqCnzBtC,yBrCKgB;EOJd,qBP8O+B;AHukJnC;;Ac9vJI;E0B5DJ;IAQI,kBrC+yBoC;EHwgItC;AACF;;AwCpzJA;EACE,gBAAgB;EAChB,eAAe;E9BTb,gB8BUsB;AxCuzJ1B;;AyCl0JA;EACE,kBAAkB;EAClB,wBtCq9ByC;EsCp9BzC,mBtCq9BsC;EsCp9BtC,6BAA6C;E/BH3C,sBP6OgC;AH4lJpC;;AyCj0JA;EAEE,cAAc;AzCm0JhB;;AyC/zJA;EACE,gBtCgR+B;AHkjJjC;;AyC1zJA;EACE,mBAAsD;AzC6zJxD;;AyC9zJA;EAKI,kBAAkB;EAClB,MAAM;EACN,QAAQ;EACR,wBtCu7BuC;EsCt7BvC,cAAc;AzC6zJlB;;AyCnzJE;EC9CA,cxB8FgE;EIzF9D,yBJyF8D;EwB5FhE,qBxB4FgE;AlBywJlE;;A0Cn2JE;EACE,yBAAqC;A1Cs2JzC;;A0Cn2JE;EACE,cAA0B;A1Cs2J9B;;AyCj0JE;EC9CA,cxB8FgE;EIzF9D,yBJyF8D;EwB5FhE,qBxB4FgE;AlBuxJlE;;A0Cj3JE;EACE,yBAAqC;A1Co3JzC;;A0Cj3JE;EACE,cAA0B;A1Co3J9B;;AyC/0JE;EC9CA,cxB8FgE;EIzF9D,yBJyF8D;EwB5FhE,qBxB4FgE;AlBqyJlE;;A0C/3JE;EACE,yBAAqC;A1Ck4JzC;;A0C/3JE;EACE,cAA0B;A1Ck4J9B;;AyC71JE;EC9CA,cxB8FgE;EIzF9D,yBJyF8D;EwB5FhE,qBxB4FgE;AlBmzJlE;;A0C74JE;EACE,yBAAqC;A1Cg5JzC;;A0C74JE;EACE,cAA0B;A1Cg5J9B;;AyC32JE;EC9CA,cxB8FgE;EIzF9D,yBJyF8D;EwB5FhE,qBxB4FgE;AlBi0JlE;;A0C35JE;EACE,yBAAqC;A1C85JzC;;A0C35JE;EACE,cAA0B;A1C85J9B;;AyCz3JE;EC9CA,cxB8FgE;EIzF9D,yBJyF8D;EwB5FhE,qBxB4FgE;AlB+0JlE;;A0Cz6JE;EACE,yBAAqC;A1C46JzC;;A0Cz6JE;EACE,cAA0B;A1C46J9B;;AyCv4JE;EC9CA,cxB8FgE;EIzF9D,yBJyF8D;EwB5FhE,qBxB4FgE;AlB61JlE;;A0Cv7JE;EACE,yBAAqC;A1C07JzC;;A0Cv7JE;EACE,cAA0B;A1C07J9B;;AyCr5JE;EC9CA,cxB8FgE;EIzF9D,yBJyF8D;EwB5FhE,qBxB4FgE;AlB22JlE;;A0Cr8JE;EACE,yBAAqC;A1Cw8JzC;;A0Cr8JE;EACE,cAA0B;A1Cw8J9B;;A2Ch9JE;EACE;IAAO,2BAAuC;E3Co9JhD;E2Cn9JE;IAAK,wBAAwB;E3Cs9J/B;AACF;;A2Cz9JE;EACE;IAAO,2BAAuC;E3Co9JhD;E2Cn9JE;IAAK,wBAAwB;E3Cs9J/B;AACF;;A2Cn9JA;EACE,oBAAa;EAAb,aAAa;EACb,YxC89BsC;EwC79BtC,gBAAgB;EvCoHZ,kBAtCY;EuC5EhB,yBxCJgB;EOJd,sBP6OgC;AHkvJpC;;A2Cl9JA;EACE,oBAAa;EAAb,aAAa;EACb,0BAAsB;EAAtB,sBAAsB;EACtB,qBAAuB;EAAvB,uBAAuB;EACvB,gBAAgB;EAChB,WxChBa;EwCiBb,kBAAkB;EAClB,mBAAmB;EACnB,yBxCUe;EiB9BX,2BjB0+B4C;AHggIlD;;AoBr+JI;EuBOJ;IvBNM,gBAAgB;EpBy+JpB;AACF;;A2Cx9JA;ErBaE,qMAA6I;EqBX7I,0BxCw8BsC;AHmhIxC;;A2Cv9JE;EACE,0DxC08BkD;EwC18BlD,kDxC08BkD;AHghItD;;A2Cv9JM;EAJJ;IAKM,uBAAe;IAAf,eAAe;E3C29JrB;AACF;;A4CrgKA;EACE,oBAAa;EAAb,aAAa;EACb,qBAAuB;EAAvB,uBAAuB;A5CwgKzB;;A4CrgKA;EACE,WAAO;EAAP,OAAO;A5CwgKT;;A6C1gKA;EACE,oBAAa;EAAb,aAAa;EACb,0BAAsB;EAAtB,sBAAsB;EAGtB,eAAe;EACf,gBAAgB;A7C2gKlB;;A6ClgKA;EACE,WAAW;EACX,c1CPgB;E0CQhB,mBAAmB;A7CqgKrB;;AK3gKE;EwCUE,UAAU;EACV,c1Cbc;E0Ccd,qBAAqB;EACrB,yB1CrBc;AH0hKlB;;A6C/gKA;EAcI,c1CjBc;E0CkBd,yB1CzBc;AH8hKlB;;A6C5/JA;EACE,kBAAkB;EAClB,cAAc;EACd,wB1C88ByC;E0C58BzC,sB1CzCa;E0C0Cb,sC1ChCa;AH8hKf;;A6CpgKA;EnC7BI,+BPoOgC;EOnOhC,gCPmOgC;AHk0JpC;;A6CzgKA;EnCfI,mCPsNgC;EOrNhC,kCPqNgC;AHu0JpC;;A6C9gKA;EAkBI,c1ChDc;E0CiDd,oBAAoB;EACpB,sB1CxDW;AHwjKf;;A6CphKA;EAyBI,UAAU;EACV,W1C9DW;E0C+DX,yB1ClCa;E0CmCb,qB1CnCa;AHkiKjB;;A6C3hKA;EAgCI,mBAAmB;A7C+/JvB;;A6C/hKA;EAmCM,gB1CiK2B;E0ChK3B,qB1CgK2B;AHg2JjC;;A6Cl/JI;EACE,uBAAmB;EAAnB,mBAAmB;A7Cq/JzB;;A6Ct/JI;EnCjCA,kCPsLgC;EOlMhC,0BmCmDwC;A7Cq/J5C;;A6C3/JI;EnC7CA,gCPkMgC;EOtLhC,4BmC4C0C;A7Cq/J9C;;A6ChgKI;EAeM,aAAa;A7Cq/JvB;;A6CpgKI;EAmBM,qB1C+HuB;E0C9HvB,oBAAoB;A7Cq/J9B;;A6CzgKI;EAuBQ,iB1C2HqB;E0C1HrB,sB1C0HqB;AH43JjC;;Ac/iKI;E+BiCA;IACE,uBAAmB;IAAnB,mBAAmB;E7CkhKvB;E6CnhKE;InCjCA,kCPsLgC;IOlMhC,0BmCmDwC;E7CihK1C;E6CvhKE;InC7CA,gCPkMgC;IOtLhC,4BmC4C0C;E7CghK5C;E6C3hKE;IAeM,aAAa;E7C+gKrB;E6C9hKE;IAmBM,qB1C+HuB;I0C9HvB,oBAAoB;E7C8gK5B;E6CliKE;IAuBQ,iB1C2HqB;I0C1HrB,sB1C0HqB;EHo5J/B;AACF;;AcxkKI;E+BiCA;IACE,uBAAmB;IAAnB,mBAAmB;E7C2iKvB;E6C5iKE;InCjCA,kCPsLgC;IOlMhC,0BmCmDwC;E7C0iK1C;E6ChjKE;InC7CA,gCPkMgC;IOtLhC,4BmC4C0C;E7CyiK5C;E6CpjKE;IAeM,aAAa;E7CwiKrB;E6CvjKE;IAmBM,qB1C+HuB;I0C9HvB,oBAAoB;E7CuiK5B;E6C3jKE;IAuBQ,iB1C2HqB;I0C1HrB,sB1C0HqB;EH66J/B;AACF;;AcjmKI;E+BiCA;IACE,uBAAmB;IAAnB,mBAAmB;E7CokKvB;E6CrkKE;InCjCA,kCPsLgC;IOlMhC,0BmCmDwC;E7CmkK1C;E6CzkKE;InC7CA,gCPkMgC;IOtLhC,4BmC4C0C;E7CkkK5C;E6C7kKE;IAeM,aAAa;E7CikKrB;E6ChlKE;IAmBM,qB1C+HuB;I0C9HvB,oBAAoB;E7CgkK5B;E6CplKE;IAuBQ,iB1C2HqB;I0C1HrB,sB1C0HqB;EHs8J/B;AACF;;Ac1nKI;E+BiCA;IACE,uBAAmB;IAAnB,mBAAmB;E7C6lKvB;E6C9lKE;InCjCA,kCPsLgC;IOlMhC,0BmCmDwC;E7C4lK1C;E6ClmKE;InC7CA,gCPkMgC;IOtLhC,4BmC4C0C;E7C2lK5C;E6CtmKE;IAeM,aAAa;E7C0lKrB;E6CzmKE;IAmBM,qB1C+HuB;I0C9HvB,oBAAoB;E7CylK5B;E6C7mKE;IAuBQ,iB1C2HqB;I0C1HrB,sB1C0HqB;EH+9J/B;AACF;;A6C5kKA;EAEI,qBAAqB;EACrB,oBAAoB;EnCjIpB,gBmCkIwB;A7C8kK5B;;A6CllKA;EAOM,mBAAmB;A7C+kKzB;;A6CtlKA;EAaM,sBAAsB;A7C6kK5B;;A8C1tKE;EACE,c5B2F8D;E4B1F9D,yB5B0F8D;AlBmoKlE;;AKltKE;EyCPM,c5BsF0D;E4BrF1D,yBAAyC;A9C6tKjD;;A8CpuKE;EAWM,W3CPO;E2CQP,yB5BgF0D;E4B/E1D,qB5B+E0D;AlB8oKlE;;A8C1uKE;EACE,c5B2F8D;E4B1F9D,yB5B0F8D;AlBmpKlE;;AKluKE;EyCPM,c5BsF0D;E4BrF1D,yBAAyC;A9C6uKjD;;A8CpvKE;EAWM,W3CPO;E2CQP,yB5BgF0D;E4B/E1D,qB5B+E0D;AlB8pKlE;;A8C1vKE;EACE,c5B2F8D;E4B1F9D,yB5B0F8D;AlBmqKlE;;AKlvKE;EyCPM,c5BsF0D;E4BrF1D,yBAAyC;A9C6vKjD;;A8CpwKE;EAWM,W3CPO;E2CQP,yB5BgF0D;E4B/E1D,qB5B+E0D;AlB8qKlE;;A8C1wKE;EACE,c5B2F8D;E4B1F9D,yB5B0F8D;AlBmrKlE;;AKlwKE;EyCPM,c5BsF0D;E4BrF1D,yBAAyC;A9C6wKjD;;A8CpxKE;EAWM,W3CPO;E2CQP,yB5BgF0D;E4B/E1D,qB5B+E0D;AlB8rKlE;;A8C1xKE;EACE,c5B2F8D;E4B1F9D,yB5B0F8D;AlBmsKlE;;AKlxKE;EyCPM,c5BsF0D;E4BrF1D,yBAAyC;A9C6xKjD;;A8CpyKE;EAWM,W3CPO;E2CQP,yB5BgF0D;E4B/E1D,qB5B+E0D;AlB8sKlE;;A8C1yKE;EACE,c5B2F8D;E4B1F9D,yB5B0F8D;AlBmtKlE;;AKlyKE;EyCPM,c5BsF0D;E4BrF1D,yBAAyC;A9C6yKjD;;A8CpzKE;EAWM,W3CPO;E2CQP,yB5BgF0D;E4B/E1D,qB5B+E0D;AlB8tKlE;;A8C1zKE;EACE,c5B2F8D;E4B1F9D,yB5B0F8D;AlBmuKlE;;AKlzKE;EyCPM,c5BsF0D;E4BrF1D,yBAAyC;A9C6zKjD;;A8Cp0KE;EAWM,W3CPO;E2CQP,yB5BgF0D;E4B/E1D,qB5B+E0D;AlB8uKlE;;A8C10KE;EACE,c5B2F8D;E4B1F9D,yB5B0F8D;AlBmvKlE;;AKl0KE;EyCPM,c5BsF0D;E4BrF1D,yBAAyC;A9C60KjD;;A8Cp1KE;EAWM,W3CPO;E2CQP,yB5BgF0D;E4B/E1D,qB5B+E0D;AlB8vKlE;;A+C71KA;EACE,YAAY;E3C8HR,iBAtCY;E2CtFhB,gB5CiS+B;E4ChS/B,cAAc;EACd,W5CYa;E4CXb,yB5CCa;E4CAb,WAAW;A/Cg2Kb;;AK31KE;E0CDE,W5CMW;E4CLX,qBAAqB;A/Cg2KzB;;AK51KE;E0CCI,YAAY;A/C+1KlB;;A+Cp1KA;EACE,UAAU;EACV,6BAA6B;EAC7B,SAAS;EACT,wBAAgB;EAAhB,qBAAgB;EAAhB,gBAAgB;A/Cu1KlB;;A+Cj1KA;EACE,oBAAoB;A/Co1KtB;;AgD33KA;EACE,gB7Cy4BuC;E6Cx4BvC,gBAAgB;E5C6HZ,mBAtCY;E4CpFhB,2C7CEa;E6CDb,4BAA4B;EAC5B,oC7C04BmD;E6Cz4BnD,gD7CSa;E6CRb,mCAA2B;EAA3B,2BAA2B;EAC3B,UAAU;EtCLR,sBP64BsC;AHs/I1C;;AgDx4KA;EAcI,sB7C63BsC;AHigJ1C;;AgD54KA;EAkBI,UAAU;AhD83Kd;;AgDh5KA;EAsBI,cAAc;EACd,UAAU;AhD83Kd;;AgDr5KA;EA2BI,aAAa;AhD83KjB;;AgD13KA;EACE,oBAAa;EAAb,aAAa;EACb,sBAAmB;EAAnB,mBAAmB;EACnB,wB7Cy2BwC;E6Cx2BxC,c7CtBgB;E6CuBhB,2C7C7Ba;E6C8Bb,4BAA4B;EAC5B,4C7Ci3BoD;AH4gJtD;;AgD13KA;EACE,gB7Ci2BwC;AH4hJ1C;;AiDj6KA;EAEE,gBAAgB;AjDm6KlB;;AiDr6KA;EAKI,kBAAkB;EAClB,gBAAgB;AjDo6KpB;;AiD/5KA;EACE,eAAe;EACf,MAAM;EACN,OAAO;EACP,a9C+pBsC;E8C9pBtC,aAAa;EACb,WAAW;EACX,YAAY;EACZ,gBAAgB;EAGhB,UAAU;AjDg6KZ;;AiDz5KA;EACE,kBAAkB;EAClB,WAAW;EACX,c9C64BuC;E8C34BvC,oBAAoB;AjD25KtB;;AiDx5KE;E7BrCI,2CjB48BoD;EiB58BpD,mCjB48BoD;EiB58BpD,oEjB48BoD;E8Cr6BtD,sC9Cm6BmD;E8Cn6BnD,8B9Cm6BmD;AHw/IvD;;AoB77KI;E6BgCF;I7B/BI,gBAAgB;EpBi8KpB;AACF;;AiD/5KE;EACE,uB9Ci6BoC;E8Cj6BpC,e9Ci6BoC;AHigJxC;;AiD95KE;EACE,8B9C85B2C;E8C95B3C,sB9C85B2C;AHmgJ/C;;AiD75KA;EACE,oBAAa;EAAb,aAAa;EACb,6B/ByE8D;AlBu1KhE;;AiDl6KA;EAKI,8B/BsE4D;E+BrE5D,gBAAgB;AjDi6KpB;;AiDv6KA;;EAWI,oBAAc;EAAd,cAAc;AjDi6KlB;;AiD56KA;EAeI,gBAAgB;AjDi6KpB;;AiD75KA;EACE,oBAAa;EAAb,aAAa;EACb,sBAAmB;EAAnB,mBAAmB;EACnB,6B/BqD8D;AlB22KhE;;AiDn6KA;EAOI,cAAc;EACd,0B/BgD4D;E+B/C5D,WAAW;AjDg6Kf;;AiDz6KA;EAcI,0BAAsB;EAAtB,sBAAsB;EACtB,qBAAuB;EAAvB,uBAAuB;EACvB,YAAY;AjD+5KhB;;AiD/6KA;EAmBM,gBAAgB;AjDg6KtB;;AiDn7KA;EAuBM,aAAa;AjDg6KnB;;AiD15KA;EACE,kBAAkB;EAClB,oBAAa;EAAb,aAAa;EACb,0BAAsB;EAAtB,sBAAsB;EACtB,WAAW;EAGX,oBAAoB;EACpB,sB9C1Ga;E8C2Gb,4BAA4B;EAC5B,oC9ClGa;EOZX,qBP8O+B;E8C5HjC,UAAU;AjDy5KZ;;AiDr5KA;EACE,eAAe;EACf,MAAM;EACN,OAAO;EACP,a9CojBsC;E8CnjBtC,YAAY;EACZ,aAAa;EACb,sB9CjHa;AHygLf;;AiD/5KA;EAUW,UAAU;AjDy5KrB;;AiDn6KA;EAWW,Y9C4zB2B;AHgmJtC;;AiDv5KA;EACE,oBAAa;EAAb,aAAa;EACb,qBAAuB;EAAvB,uBAAuB;EACvB,sBAA8B;EAA9B,8BAA8B;EAC9B,kB9CyzBsC;E8CxzBtC,gC9CtIgB;EOId,0CQsH4D;ERrH5D,2CQqH4D;AlBw6KhE;;AiDj6KA;EASI,kB9CozBoC;E8ClzBpC,8BAA6F;AjD25KjG;;AiDt5KA;EACE,gBAAgB;EAChB,gB9C2I+B;AH8wKjC;;AiDp5KA;EACE,kBAAkB;EAGlB,kBAAc;EAAd,cAAc;EACd,a9CuwBsC;AH8oJxC;;AiDj5KA;EACE,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,sBAAmB;EAAnB,mBAAmB;EACnB,kBAAyB;EAAzB,yBAAyB;EACzB,gBAAgE;EAChE,6B9CvKgB;EOkBd,8CQwG4D;ERvG5D,6CQuG4D;AlBm8KhE;;AiD55KA;EAcI,eAAwC;AjDk5K5C;;AiD74KA;EACE,kBAAkB;EAClB,YAAY;EACZ,WAAW;EACX,YAAY;EACZ,gBAAgB;AjDg5KlB;;AcvhLI;EmCzBJ;IAuKI,gB9CmwBqC;I8ClwBrC,oBAAyC;EjD84K3C;EiDhiLF;IAsJI,+B/B3E4D;ElBw9K9D;EiDniLF;IAyJM,gC/B9E0D;ElB29K9D;EiDnhLF;IA2II,+B/BnF4D;ElB89K9D;EiDthLF;IA8IM,4B/BtF0D;ElBi+K9D;EiDn4KA;IAAY,gB9C4uB2B;EH0pJvC;AACF;;Ac7iLI;EmC0KF;;IAEE,gB9CouBqC;EHmqJvC;AACF;;AcpjLI;EmCiLF;IAAY,iB9C8tB4B;EH0qJxC;AACF;;AkDrnLA;EACE,kBAAkB;EAClB,a/CmrBsC;E+ClrBtC,cAAc;EACd,S/Cy1BmC;EgD71BnC,kMhDuRiN;EgDrRjN,kBAAkB;EAClB,gBhD+R+B;EgD9R/B,gBhDmS+B;EgDlS/B,gBAAgB;EAChB,iBAAiB;EACjB,qBAAqB;EACrB,iBAAiB;EACjB,oBAAoB;EACpB,sBAAsB;EACtB,kBAAkB;EAClB,oBAAoB;EACpB,mBAAmB;EACnB,gBAAgB;E/CgHZ,mBAtCY;E8C9EhB,qBAAqB;EACrB,UAAU;AlDkoLZ;;AkD7oLA;EAaW,Y/C60B2B;AHuzJtC;;AkDjpLA;EAgBI,kBAAkB;EAClB,cAAc;EACd,a/C60BqC;E+C50BrC,c/C60BqC;AHwzJzC;;AkDxpLA;EAsBM,kBAAkB;EAClB,WAAW;EACX,yBAAyB;EACzB,mBAAmB;AlDsoLzB;;AkDjoLA;EACE,iBAAgC;AlDooLlC;;AkDroLA;EAII,SAAS;AlDqoLb;;AkDzoLA;EAOM,MAAM;EACN,6BAAgE;EAChE,sB/CvBS;AH6pLf;;AkDjoLA;EACE,iB/CmzBuC;AHi1JzC;;AkDroLA;EAII,OAAO;EACP,a/C+yBqC;E+C9yBrC,c/C6yBqC;AHw1JzC;;AkD3oLA;EASM,QAAQ;EACR,oCAA2F;EAC3F,wB/CvCS;AH6qLf;;AkDjoLA;EACE,iBAAgC;AlDooLlC;;AkDroLA;EAII,MAAM;AlDqoLV;;AkDzoLA;EAOM,SAAS;EACT,6B/C4xBmC;E+C3xBnC,yB/CrDS;AH2rLf;;AkDjoLA;EACE,iB/CqxBuC;AH+2JzC;;AkDroLA;EAII,QAAQ;EACR,a/CixBqC;E+ChxBrC,c/C+wBqC;AHs3JzC;;AkD3oLA;EASM,OAAO;EACP,oC/C4wBmC;E+C3wBnC,uB/CrES;AH2sLf;;AkDjnLA;EACE,gB/C2uBuC;E+C1uBvC,uB/CgvBuC;E+C/uBvC,W/CvGa;E+CwGb,kBAAkB;EAClB,sB/C/Fa;EOZX,sBP6OgC;AHm/KpC;;AoDruLA;EACE,kBAAkB;EAClB,MAAM;EACN,OAAO;EACP,ajDirBsC;EiDhrBtC,cAAc;EACd,gBjD22BuC;EgDh3BvC,kMhDuRiN;EgDrRjN,kBAAkB;EAClB,gBhD+R+B;EgD9R/B,gBhDmS+B;EgDlS/B,gBAAgB;EAChB,iBAAiB;EACjB,qBAAqB;EACrB,iBAAiB;EACjB,oBAAoB;EACpB,sBAAsB;EACtB,kBAAkB;EAClB,oBAAoB;EACpB,mBAAmB;EACnB,gBAAgB;E/CgHZ,mBAtCY;EgD7EhB,qBAAqB;EACrB,sBjDNa;EiDOb,4BAA4B;EAC5B,oCjDEa;EOZX,qBP8O+B;AH+gLnC;;AoDlwLA;EAoBI,kBAAkB;EAClB,cAAc;EACd,WjD22BoC;EiD12BpC,cjD22BqC;EiD12BrC,gBjD2N+B;AHuhLnC;;AoD1wLA;EA4BM,kBAAkB;EAClB,cAAc;EACd,WAAW;EACX,yBAAyB;EACzB,mBAAmB;ApDkvLzB;;AoD7uLA;EACE,qBjD41BuC;AHo5JzC;;AoDjvLA;EAII,2BlC2F4D;AlBspLhE;;AoDrvLA;EAOM,SAAS;EACT,6BAAgE;EAChE,qCjDu1BiE;AH25JvE;;AoD3vLA;EAaM,WjD6L2B;EiD5L3B,6BAAgE;EAChE,sBjD7CS;AH+xLf;;AoD7uLA;EACE,mBjDw0BuC;AHw6JzC;;AoDjvLA;EAII,yBlCuE4D;EkCtE5D,ajDo0BqC;EiDn0BrC,YjDk0BoC;EiDj0BpC,gBAAgC;ApDivLpC;;AoDxvLA;EAUM,OAAO;EACP,oCAA2F;EAC3F,uCjDg0BiE;AHk7JvE;;AoD9vLA;EAgBM,SjDsK2B;EiDrK3B,oCAA2F;EAC3F,wBjDpES;AHszLf;;AoD7uLA;EACE,kBjDizBuC;AH+7JzC;;AoDjvLA;EAII,wBlCgD4D;AlBisLhE;;AoDrvLA;EAOM,MAAM;EACN,oCAA2F;EAC3F,wCjD4yBiE;AHs8JvE;;AoD3vLA;EAaM,QjDkJ2B;EiDjJ3B,oCAA2F;EAC3F,yBjDxFS;AH00Lf;;AoDjwLA;EAqBI,kBAAkB;EAClB,MAAM;EACN,SAAS;EACT,cAAc;EACd,WjDwxBoC;EiDvxBpC,oBAAsC;EACtC,WAAW;EACX,gCjD4wBuD;AHo+J3D;;AoD5uLA;EACE,oBjDixBuC;AH89JzC;;AoDhvLA;EAII,0BlCgB4D;EkCf5D,ajD6wBqC;EiD5wBrC,YjD2wBoC;EiD1wBpC,gBAAgC;ApDgvLpC;;AoDvvLA;EAUM,QAAQ;EACR,oCjDuwBmC;EiDtwBnC,sCjDywBiE;AHw+JvE;;AoD7vLA;EAgBM,UjD+G2B;EiD9G3B,oCjDiwBmC;EiDhwBnC,uBjD3HS;AH42Lf;;AoD3tLA;EACE,uBjDkuBwC;EiDjuBxC,gBAAgB;EhD3BZ,eAtCY;EgDoEhB,yBjD2tByD;EiD1tBzD,gCAAyE;E1ChJvE,0CQsH4D;ERrH5D,2CQqH4D;AlByvLhE;;AoDruLA;EAUI,aAAa;ApD+tLjB;;AoD3tLA;EACE,uBjDotBwC;EiDntBxC,cjDxJgB;AHs3LlB;;AqDz3LA;EACE,kBAAkB;ArD43LpB;;AqDz3LA;EACE,uBAAmB;EAAnB,mBAAmB;ArD43LrB;;AqDz3LA;EACE,kBAAkB;EAClB,WAAW;EACX,gBAAgB;ArD43LlB;;AsDn5LE;EACE,cAAc;EACd,WAAW;EACX,WAAW;AtDs5Lf;;AqD93LA;EACE,kBAAkB;EAClB,aAAa;EACb,WAAW;EACX,WAAW;EACX,mBAAmB;EACnB,mCAA2B;EAA3B,2BAA2B;EjC5BvB,8CjBikCkF;EiBjkClF,sCjBikCkF;EiBjkClF,0EjBikCkF;AH61JxF;;AoBz5LI;EiCiBJ;IjChBM,gBAAgB;EpB65LpB;AACF;;AqDp4LA;;;EAGE,cAAc;ArDu4LhB;;AqDp4LA;;EAEE,mCAA2B;EAA3B,2BAA2B;ArDu4L7B;;AqDp4LA;;EAEE,oCAA4B;EAA5B,4BAA4B;ArDu4L9B;;AqD/3LA;EAEI,UAAU;EACV,4BAA4B;EAC5B,uBAAe;EAAf,eAAe;ArDi4LnB;;AqDr4LA;;;EAUI,UAAU;EACV,UAAU;ArDi4Ld;;AqD54LA;;EAgBI,UAAU;EACV,UAAU;EjCtER,2BjBgkCkC;AHw4JxC;;AoBn8LI;EiCgDJ;;IjC/CM,gBAAgB;EpBw8LpB;AACF;;AqD/3LA;;EAEE,kBAAkB;EAClB,MAAM;EACN,SAAS;EACT,UAAU;EAEV,oBAAa;EAAb,aAAa;EACb,sBAAmB;EAAnB,mBAAmB;EACnB,qBAAuB;EAAvB,uBAAuB;EACvB,UlDk9BsC;EkDj9BtC,WlD1Fa;EkD2Fb,kBAAkB;EAClB,YlDg9BqC;EiB7iCjC,8BjB+iCgD;AHg7JtD;;AoB19LI;EiC2EJ;;IjC1EM,gBAAgB;EpB+9LpB;AACF;;AK59LE;;;EgDwFE,WlDjGW;EkDkGX,qBAAqB;EACrB,UAAU;EACV,YlDy8BmC;AHi8JvC;;AqDv4LA;EACE,OAAO;ArD04LT;;AqDr4LA;EACE,QAAQ;ArDw4LV;;AqDj4LA;;EAEE,qBAAqB;EACrB,WlDk8BuC;EkDj8BvC,YlDi8BuC;EkDh8BvC,qCAAqC;ArDo4LvC;;AqDl4LA;EACE,sNnCxFyI;AlB69L3I;;AqDn4LA;EACE,uNnC3FyI;AlBi+L3I;;AqD73LA;EACE,kBAAkB;EAClB,QAAQ;EACR,SAAS;EACT,OAAO;EACP,WAAW;EACX,oBAAa;EAAb,aAAa;EACb,qBAAuB;EAAvB,uBAAuB;EACvB,eAAe;EAEf,iBlDw5BsC;EkDv5BtC,gBlDu5BsC;EkDt5BtC,gBAAgB;ArD+3LlB;;AqD34LA;EAeI,uBAAuB;EACvB,kBAAc;EAAd,cAAc;EACd,WlDs5BqC;EkDr5BrC,WlDs5BoC;EkDr5BpC,iBlDu5BoC;EkDt5BpC,gBlDs5BoC;EkDr5BpC,mBAAmB;EACnB,eAAe;EACf,sBlDhKW;EkDiKX,4BAA4B;EAE5B,kCAAiE;EACjE,qCAAoE;EACpE,WAAW;EjCtKT,6BjBsjC+C;AHg/JrD;;AoBjiMI;EiCqIJ;IjCpIM,gBAAgB;EpBqiMpB;AACF;;AqDl6LA;EAiCI,UAAU;ArDq4Ld;;AqD53LA;EACE,kBAAkB;EAClB,UAA2C;EAC3C,YAAY;EACZ,SAA0C;EAC1C,WAAW;EACX,iBAAiB;EACjB,oBAAoB;EACpB,WlD3La;EkD4Lb,kBAAkB;ArD+3LpB;;AuD9jMA;EACE;IAAK,iCAAyB;IAAzB,yBAAyB;EvDkkM9B;AACF;;AuDpkMA;EACE;IAAK,iCAAyB;IAAzB,yBAAyB;EvDkkM9B;AACF;;AuDhkMA;EACE,qBAAqB;EACrB,WpDkkC0B;EoDjkC1B,YpDikC0B;EoDhkC1B,2BAA2B;EAC3B,iCAAgD;EAChD,+BAA+B;EAE/B,kBAAkB;EAClB,sDAA8C;EAA9C,8CAA8C;AvDkkMhD;;AuD/jMA;EACE,WpD2jC4B;EoD1jC5B,YpD0jC4B;EoDzjC5B,mBpD2jC4B;AHugK9B;;AuD3jMA;EACE;IACE,2BAAmB;IAAnB,mBAAmB;EvD8jMrB;EuD5jMA;IACE,UAAU;EvD8jMZ;AACF;;AuDpkMA;EACE;IACE,2BAAmB;IAAnB,mBAAmB;EvD8jMrB;EuD5jMA;IACE,UAAU;EvD8jMZ;AACF;;AuD3jMA;EACE,qBAAqB;EACrB,WpDmiC0B;EoDliC1B,YpDkiC0B;EoDjiC1B,2BAA2B;EAC3B,8BAA8B;EAE9B,kBAAkB;EAClB,UAAU;EACV,oDAA4C;EAA5C,4CAA4C;AvD6jM9C;;AuD1jMA;EACE,WpD4hC4B;EoD3hC5B,YpD2hC4B;AHkiK9B;;AwDhnMA;EAAqB,mCAAmC;AxDonMxD;;AwDnnMA;EAAqB,8BAA8B;AxDunMnD;;AwDtnMA;EAAqB,iCAAiC;AxD0nMtD;;AwDznMA;EAAqB,iCAAiC;AxD6nMtD;;AwD5nMA;EAAqB,sCAAsC;AxDgoM3D;;AwD/nMA;EAAqB,mCAAmC;AxDmoMxD;;AyDroME;EACE,oCAAmC;AzDwoMvC;;AK9nME;;;EoDLI,oCAAgD;AzDyoMtD;;AyD/oME;EACE,oCAAmC;AzDkpMvC;;AKxoME;;;EoDLI,oCAAgD;AzDmpMtD;;AyDzpME;EACE,oCAAmC;AzD4pMvC;;AKlpME;;;EoDLI,oCAAgD;AzD6pMtD;;AyDnqME;EACE,oCAAmC;AzDsqMvC;;AK5pME;;;EoDLI,oCAAgD;AzDuqMtD;;AyD7qME;EACE,oCAAmC;AzDgrMvC;;AKtqME;;;EoDLI,oCAAgD;AzDirMtD;;AyDvrME;EACE,oCAAmC;AzD0rMvC;;AKhrME;;;EoDLI,oCAAgD;AzD2rMtD;;AyDjsME;EACE,oCAAmC;AzDosMvC;;AK1rME;;;EoDLI,oCAAgD;AzDqsMtD;;AyD3sME;EACE,oCAAmC;AzD8sMvC;;AKpsME;;;EoDLI,oCAAgD;AzD+sMtD;;A0D9sMA;EACE,iCAAmC;A1DitMrC;;A0D9sMA;EACE,wCAAwC;A1DitM1C;;A2D5tMA;EAAkB,oCAAoD;A3DguMtE;;A2D/tMA;EAAkB,wCAAwD;A3DmuM1E;;A2DluMA;EAAkB,0CAA0D;A3DsuM5E;;A2DruMA;EAAkB,2CAA2D;A3DyuM7E;;A2DxuMA;EAAkB,yCAAyD;A3D4uM3E;;A2D1uMA;EAAmB,oBAAoB;A3D8uMvC;;A2D7uMA;EAAmB,wBAAwB;A3DivM3C;;A2DhvMA;EAAmB,0BAA0B;A3DovM7C;;A2DnvMA;EAAmB,2BAA2B;A3DuvM9C;;A2DtvMA;EAAmB,yBAAyB;A3D0vM5C;;A2DvvME;EACE,gCAA+B;A3D0vMnC;;A2D3vME;EACE,gCAA+B;A3D8vMnC;;A2D/vME;EACE,gCAA+B;A3DkwMnC;;A2DnwME;EACE,gCAA+B;A3DswMnC;;A2DvwME;EACE,gCAA+B;A3D0wMnC;;A2D3wME;EACE,gCAA+B;A3D8wMnC;;A2D/wME;EACE,gCAA+B;A3DkxMnC;;A2DnxME;EACE,gCAA+B;A3DsxMnC;;A2DlxMA;EACE,6BAA+B;A3DqxMjC;;A2D9wMA;EACE,gCAA2C;A3DixM7C;;A2D9wMA;EACE,iCAAwC;A3DixM1C;;A2D9wMA;EACE,0CAAiD;EACjD,2CAAkD;A3DixMpD;;A2D9wMA;EACE,2CAAkD;EAClD,8CAAqD;A3DixMvD;;A2D9wMA;EACE,8CAAqD;EACrD,6CAAoD;A3DixMtD;;A2D9wMA;EACE,0CAAiD;EACjD,6CAAoD;A3DixMtD;;A2D9wMA;EACE,gCAA2C;A3DixM7C;;A2D9wMA;EACE,6BAA6B;A3DixM/B;;A2D9wMA;EACE,+BAAuC;A3DixMzC;;A2D9wMA;EACE,2BAA2B;A3DixM7B;;AsDz1ME;EACE,cAAc;EACd,WAAW;EACX,WAAW;AtD41Mf;;A4Dr1MM;EAAwB,wBAA0B;A5Dy1MxD;;A4Dz1MM;EAAwB,0BAA0B;A5D61MxD;;A4D71MM;EAAwB,gCAA0B;A5Di2MxD;;A4Dj2MM;EAAwB,yBAA0B;A5Dq2MxD;;A4Dr2MM;EAAwB,yBAA0B;A5Dy2MxD;;A4Dz2MM;EAAwB,6BAA0B;A5D62MxD;;A4D72MM;EAAwB,8BAA0B;A5Di3MxD;;A4Dj3MM;EAAwB,+BAA0B;EAA1B,wBAA0B;A5Dq3MxD;;A4Dr3MM;EAAwB,sCAA0B;EAA1B,+BAA0B;A5Dy3MxD;;Acx0MI;E8CjDE;IAAwB,wBAA0B;E5D83MtD;E4D93MI;IAAwB,0BAA0B;E5Di4MtD;E4Dj4MI;IAAwB,gCAA0B;E5Do4MtD;E4Dp4MI;IAAwB,yBAA0B;E5Du4MtD;E4Dv4MI;IAAwB,yBAA0B;E5D04MtD;E4D14MI;IAAwB,6BAA0B;E5D64MtD;E4D74MI;IAAwB,8BAA0B;E5Dg5MtD;E4Dh5MI;IAAwB,+BAA0B;IAA1B,wBAA0B;E5Dm5MtD;E4Dn5MI;IAAwB,sCAA0B;IAA1B,+BAA0B;E5Ds5MtD;AACF;;Act2MI;E8CjDE;IAAwB,wBAA0B;E5D45MtD;E4D55MI;IAAwB,0BAA0B;E5D+5MtD;E4D/5MI;IAAwB,gCAA0B;E5Dk6MtD;E4Dl6MI;IAAwB,yBAA0B;E5Dq6MtD;E4Dr6MI;IAAwB,yBAA0B;E5Dw6MtD;E4Dx6MI;IAAwB,6BAA0B;E5D26MtD;E4D36MI;IAAwB,8BAA0B;E5D86MtD;E4D96MI;IAAwB,+BAA0B;IAA1B,wBAA0B;E5Di7MtD;E4Dj7MI;IAAwB,sCAA0B;IAA1B,+BAA0B;E5Do7MtD;AACF;;Acp4MI;E8CjDE;IAAwB,wBAA0B;E5D07MtD;E4D17MI;IAAwB,0BAA0B;E5D67MtD;E4D77MI;IAAwB,gCAA0B;E5Dg8MtD;E4Dh8MI;IAAwB,yBAA0B;E5Dm8MtD;E4Dn8MI;IAAwB,yBAA0B;E5Ds8MtD;E4Dt8MI;IAAwB,6BAA0B;E5Dy8MtD;E4Dz8MI;IAAwB,8BAA0B;E5D48MtD;E4D58MI;IAAwB,+BAA0B;IAA1B,wBAA0B;E5D+8MtD;E4D/8MI;IAAwB,sCAA0B;IAA1B,+BAA0B;E5Dk9MtD;AACF;;Acl6MI;E8CjDE;IAAwB,wBAA0B;E5Dw9MtD;E4Dx9MI;IAAwB,0BAA0B;E5D29MtD;E4D39MI;IAAwB,gCAA0B;E5D89MtD;E4D99MI;IAAwB,yBAA0B;E5Di+MtD;E4Dj+MI;IAAwB,yBAA0B;E5Do+MtD;E4Dp+MI;IAAwB,6BAA0B;E5Du+MtD;E4Dv+MI;IAAwB,8BAA0B;E5D0+MtD;E4D1+MI;IAAwB,+BAA0B;IAA1B,wBAA0B;E5D6+MtD;E4D7+MI;IAAwB,sCAA0B;IAA1B,+BAA0B;E5Dg/MtD;AACF;;A4Dv+MA;EAEI;IAAqB,wBAA0B;E5D0+MjD;E4D1+ME;IAAqB,0BAA0B;E5D6+MjD;E4D7+ME;IAAqB,gCAA0B;E5Dg/MjD;E4Dh/ME;IAAqB,yBAA0B;E5Dm/MjD;E4Dn/ME;IAAqB,yBAA0B;E5Ds/MjD;E4Dt/ME;IAAqB,6BAA0B;E5Dy/MjD;E4Dz/ME;IAAqB,8BAA0B;E5D4/MjD;E4D5/ME;IAAqB,+BAA0B;IAA1B,wBAA0B;E5D+/MjD;E4D//ME;IAAqB,sCAA0B;IAA1B,+BAA0B;E5DkgNjD;AACF;;A6DxhNA;EACE,kBAAkB;EAClB,cAAc;EACd,WAAW;EACX,UAAU;EACV,gBAAgB;A7D2hNlB;;A6DhiNA;EAQI,cAAc;EACd,WAAW;A7D4hNf;;A6DriNA;;;;;EAiBI,kBAAkB;EAClB,MAAM;EACN,SAAS;EACT,OAAO;EACP,WAAW;EACX,YAAY;EACZ,SAAS;A7D4hNb;;A6DphNE;EAEI,uBAA4F;A7DshNlG;;A6DxhNE;EAEI,mBAA4F;A7D0hNlG;;A6D5hNE;EAEI,gBAA4F;A7D8hNlG;;A6DhiNE;EAEI,iBAA4F;A7DkiNlG;;A8D3jNI;EAAgC,kCAA8B;EAA9B,8BAA8B;A9D+jNlE;;A8D9jNI;EAAgC,qCAAiC;EAAjC,iCAAiC;A9DkkNrE;;A8DjkNI;EAAgC,0CAAsC;EAAtC,sCAAsC;A9DqkN1E;;A8DpkNI;EAAgC,6CAAyC;EAAzC,yCAAyC;A9DwkN7E;;A8DtkNI;EAA8B,8BAA0B;EAA1B,0BAA0B;A9D0kN5D;;A8DzkNI;EAA8B,gCAA4B;EAA5B,4BAA4B;A9D6kN9D;;A8D5kNI;EAA8B,sCAAkC;EAAlC,kCAAkC;A9DglNpE;;A8D/kNI;EAA8B,6BAAyB;EAAzB,yBAAyB;A9DmlN3D;;A8DllNI;EAA8B,+BAAuB;EAAvB,uBAAuB;A9DslNzD;;A8DrlNI;EAA8B,+BAAuB;EAAvB,uBAAuB;A9DylNzD;;A8DxlNI;EAA8B,+BAAyB;EAAzB,yBAAyB;A9D4lN3D;;A8D3lNI;EAA8B,+BAAyB;EAAzB,yBAAyB;A9D+lN3D;;A8D7lNI;EAAoC,+BAAsC;EAAtC,sCAAsC;A9DimN9E;;A8DhmNI;EAAoC,6BAAoC;EAApC,oCAAoC;A9DomN5E;;A8DnmNI;EAAoC,gCAAkC;EAAlC,kCAAkC;A9DumN1E;;A8DtmNI;EAAoC,iCAAyC;EAAzC,yCAAyC;A9D0mNjF;;A8DzmNI;EAAoC,oCAAwC;EAAxC,wCAAwC;A9D6mNhF;;A8D3mNI;EAAiC,gCAAkC;EAAlC,kCAAkC;A9D+mNvE;;A8D9mNI;EAAiC,8BAAgC;EAAhC,gCAAgC;A9DknNrE;;A8DjnNI;EAAiC,iCAA8B;EAA9B,8BAA8B;A9DqnNnE;;A8DpnNI;EAAiC,mCAAgC;EAAhC,gCAAgC;A9DwnNrE;;A8DvnNI;EAAiC,kCAA+B;EAA/B,+BAA+B;A9D2nNpE;;A8DznNI;EAAkC,oCAAoC;EAApC,oCAAoC;A9D6nN1E;;A8D5nNI;EAAkC,kCAAkC;EAAlC,kCAAkC;A9DgoNxE;;A8D/nNI;EAAkC,qCAAgC;EAAhC,gCAAgC;A9DmoNtE;;A8DloNI;EAAkC,sCAAuC;EAAvC,uCAAuC;A9DsoN7E;;A8DroNI;EAAkC,yCAAsC;EAAtC,sCAAsC;A9DyoN5E;;A8DxoNI;EAAkC,sCAAiC;EAAjC,iCAAiC;A9D4oNvE;;A8D1oNI;EAAgC,oCAA2B;EAA3B,2BAA2B;A9D8oN/D;;A8D7oNI;EAAgC,qCAAiC;EAAjC,iCAAiC;A9DipNrE;;A8DhpNI;EAAgC,mCAA+B;EAA/B,+BAA+B;A9DopNnE;;A8DnpNI;EAAgC,sCAA6B;EAA7B,6BAA6B;A9DupNjE;;A8DtpNI;EAAgC,wCAA+B;EAA/B,+BAA+B;A9D0pNnE;;A8DzpNI;EAAgC,uCAA8B;EAA9B,8BAA8B;A9D6pNlE;;AcjpNI;EgDlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;E9DwsNhE;E8DvsNE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9D0sNnE;E8DzsNE;IAAgC,0CAAsC;IAAtC,sCAAsC;E9D4sNxE;E8D3sNE;IAAgC,6CAAyC;IAAzC,yCAAyC;E9D8sN3E;E8D5sNE;IAA8B,8BAA0B;IAA1B,0BAA0B;E9D+sN1D;E8D9sNE;IAA8B,gCAA4B;IAA5B,4BAA4B;E9DitN5D;E8DhtNE;IAA8B,sCAAkC;IAAlC,kCAAkC;E9DmtNlE;E8DltNE;IAA8B,6BAAyB;IAAzB,yBAAyB;E9DqtNzD;E8DptNE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9DutNvD;E8DttNE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9DytNvD;E8DxtNE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9D2tNzD;E8D1tNE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9D6tNzD;E8D3tNE;IAAoC,+BAAsC;IAAtC,sCAAsC;E9D8tN5E;E8D7tNE;IAAoC,6BAAoC;IAApC,oCAAoC;E9DguN1E;E8D/tNE;IAAoC,gCAAkC;IAAlC,kCAAkC;E9DkuNxE;E8DjuNE;IAAoC,iCAAyC;IAAzC,yCAAyC;E9DouN/E;E8DnuNE;IAAoC,oCAAwC;IAAxC,wCAAwC;E9DsuN9E;E8DpuNE;IAAiC,gCAAkC;IAAlC,kCAAkC;E9DuuNrE;E8DtuNE;IAAiC,8BAAgC;IAAhC,gCAAgC;E9DyuNnE;E8DxuNE;IAAiC,iCAA8B;IAA9B,8BAA8B;E9D2uNjE;E8D1uNE;IAAiC,mCAAgC;IAAhC,gCAAgC;E9D6uNnE;E8D5uNE;IAAiC,kCAA+B;IAA/B,+BAA+B;E9D+uNlE;E8D7uNE;IAAkC,oCAAoC;IAApC,oCAAoC;E9DgvNxE;E8D/uNE;IAAkC,kCAAkC;IAAlC,kCAAkC;E9DkvNtE;E8DjvNE;IAAkC,qCAAgC;IAAhC,gCAAgC;E9DovNpE;E8DnvNE;IAAkC,sCAAuC;IAAvC,uCAAuC;E9DsvN3E;E8DrvNE;IAAkC,yCAAsC;IAAtC,sCAAsC;E9DwvN1E;E8DvvNE;IAAkC,sCAAiC;IAAjC,iCAAiC;E9D0vNrE;E8DxvNE;IAAgC,oCAA2B;IAA3B,2BAA2B;E9D2vN7D;E8D1vNE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9D6vNnE;E8D5vNE;IAAgC,mCAA+B;IAA/B,+BAA+B;E9D+vNjE;E8D9vNE;IAAgC,sCAA6B;IAA7B,6BAA6B;E9DiwN/D;E8DhwNE;IAAgC,wCAA+B;IAA/B,+BAA+B;E9DmwNjE;E8DlwNE;IAAgC,uCAA8B;IAA9B,8BAA8B;E9DqwNhE;AACF;;Ac1vNI;EgDlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;E9DizNhE;E8DhzNE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9DmzNnE;E8DlzNE;IAAgC,0CAAsC;IAAtC,sCAAsC;E9DqzNxE;E8DpzNE;IAAgC,6CAAyC;IAAzC,yCAAyC;E9DuzN3E;E8DrzNE;IAA8B,8BAA0B;IAA1B,0BAA0B;E9DwzN1D;E8DvzNE;IAA8B,gCAA4B;IAA5B,4BAA4B;E9D0zN5D;E8DzzNE;IAA8B,sCAAkC;IAAlC,kCAAkC;E9D4zNlE;E8D3zNE;IAA8B,6BAAyB;IAAzB,yBAAyB;E9D8zNzD;E8D7zNE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9Dg0NvD;E8D/zNE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9Dk0NvD;E8Dj0NE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9Do0NzD;E8Dn0NE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9Ds0NzD;E8Dp0NE;IAAoC,+BAAsC;IAAtC,sCAAsC;E9Du0N5E;E8Dt0NE;IAAoC,6BAAoC;IAApC,oCAAoC;E9Dy0N1E;E8Dx0NE;IAAoC,gCAAkC;IAAlC,kCAAkC;E9D20NxE;E8D10NE;IAAoC,iCAAyC;IAAzC,yCAAyC;E9D60N/E;E8D50NE;IAAoC,oCAAwC;IAAxC,wCAAwC;E9D+0N9E;E8D70NE;IAAiC,gCAAkC;IAAlC,kCAAkC;E9Dg1NrE;E8D/0NE;IAAiC,8BAAgC;IAAhC,gCAAgC;E9Dk1NnE;E8Dj1NE;IAAiC,iCAA8B;IAA9B,8BAA8B;E9Do1NjE;E8Dn1NE;IAAiC,mCAAgC;IAAhC,gCAAgC;E9Ds1NnE;E8Dr1NE;IAAiC,kCAA+B;IAA/B,+BAA+B;E9Dw1NlE;E8Dt1NE;IAAkC,oCAAoC;IAApC,oCAAoC;E9Dy1NxE;E8Dx1NE;IAAkC,kCAAkC;IAAlC,kCAAkC;E9D21NtE;E8D11NE;IAAkC,qCAAgC;IAAhC,gCAAgC;E9D61NpE;E8D51NE;IAAkC,sCAAuC;IAAvC,uCAAuC;E9D+1N3E;E8D91NE;IAAkC,yCAAsC;IAAtC,sCAAsC;E9Di2N1E;E8Dh2NE;IAAkC,sCAAiC;IAAjC,iCAAiC;E9Dm2NrE;E8Dj2NE;IAAgC,oCAA2B;IAA3B,2BAA2B;E9Do2N7D;E8Dn2NE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9Ds2NnE;E8Dr2NE;IAAgC,mCAA+B;IAA/B,+BAA+B;E9Dw2NjE;E8Dv2NE;IAAgC,sCAA6B;IAA7B,6BAA6B;E9D02N/D;E8Dz2NE;IAAgC,wCAA+B;IAA/B,+BAA+B;E9D42NjE;E8D32NE;IAAgC,uCAA8B;IAA9B,8BAA8B;E9D82NhE;AACF;;Acn2NI;EgDlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;E9D05NhE;E8Dz5NE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9D45NnE;E8D35NE;IAAgC,0CAAsC;IAAtC,sCAAsC;E9D85NxE;E8D75NE;IAAgC,6CAAyC;IAAzC,yCAAyC;E9Dg6N3E;E8D95NE;IAA8B,8BAA0B;IAA1B,0BAA0B;E9Di6N1D;E8Dh6NE;IAA8B,gCAA4B;IAA5B,4BAA4B;E9Dm6N5D;E8Dl6NE;IAA8B,sCAAkC;IAAlC,kCAAkC;E9Dq6NlE;E8Dp6NE;IAA8B,6BAAyB;IAAzB,yBAAyB;E9Du6NzD;E8Dt6NE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9Dy6NvD;E8Dx6NE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9D26NvD;E8D16NE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9D66NzD;E8D56NE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9D+6NzD;E8D76NE;IAAoC,+BAAsC;IAAtC,sCAAsC;E9Dg7N5E;E8D/6NE;IAAoC,6BAAoC;IAApC,oCAAoC;E9Dk7N1E;E8Dj7NE;IAAoC,gCAAkC;IAAlC,kCAAkC;E9Do7NxE;E8Dn7NE;IAAoC,iCAAyC;IAAzC,yCAAyC;E9Ds7N/E;E8Dr7NE;IAAoC,oCAAwC;IAAxC,wCAAwC;E9Dw7N9E;E8Dt7NE;IAAiC,gCAAkC;IAAlC,kCAAkC;E9Dy7NrE;E8Dx7NE;IAAiC,8BAAgC;IAAhC,gCAAgC;E9D27NnE;E8D17NE;IAAiC,iCAA8B;IAA9B,8BAA8B;E9D67NjE;E8D57NE;IAAiC,mCAAgC;IAAhC,gCAAgC;E9D+7NnE;E8D97NE;IAAiC,kCAA+B;IAA/B,+BAA+B;E9Di8NlE;E8D/7NE;IAAkC,oCAAoC;IAApC,oCAAoC;E9Dk8NxE;E8Dj8NE;IAAkC,kCAAkC;IAAlC,kCAAkC;E9Do8NtE;E8Dn8NE;IAAkC,qCAAgC;IAAhC,gCAAgC;E9Ds8NpE;E8Dr8NE;IAAkC,sCAAuC;IAAvC,uCAAuC;E9Dw8N3E;E8Dv8NE;IAAkC,yCAAsC;IAAtC,sCAAsC;E9D08N1E;E8Dz8NE;IAAkC,sCAAiC;IAAjC,iCAAiC;E9D48NrE;E8D18NE;IAAgC,oCAA2B;IAA3B,2BAA2B;E9D68N7D;E8D58NE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9D+8NnE;E8D98NE;IAAgC,mCAA+B;IAA/B,+BAA+B;E9Di9NjE;E8Dh9NE;IAAgC,sCAA6B;IAA7B,6BAA6B;E9Dm9N/D;E8Dl9NE;IAAgC,wCAA+B;IAA/B,+BAA+B;E9Dq9NjE;E8Dp9NE;IAAgC,uCAA8B;IAA9B,8BAA8B;E9Du9NhE;AACF;;Ac58NI;EgDlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;E9DmgOhE;E8DlgOE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9DqgOnE;E8DpgOE;IAAgC,0CAAsC;IAAtC,sCAAsC;E9DugOxE;E8DtgOE;IAAgC,6CAAyC;IAAzC,yCAAyC;E9DygO3E;E8DvgOE;IAA8B,8BAA0B;IAA1B,0BAA0B;E9D0gO1D;E8DzgOE;IAA8B,gCAA4B;IAA5B,4BAA4B;E9D4gO5D;E8D3gOE;IAA8B,sCAAkC;IAAlC,kCAAkC;E9D8gOlE;E8D7gOE;IAA8B,6BAAyB;IAAzB,yBAAyB;E9DghOzD;E8D/gOE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9DkhOvD;E8DjhOE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9DohOvD;E8DnhOE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9DshOzD;E8DrhOE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9DwhOzD;E8DthOE;IAAoC,+BAAsC;IAAtC,sCAAsC;E9DyhO5E;E8DxhOE;IAAoC,6BAAoC;IAApC,oCAAoC;E9D2hO1E;E8D1hOE;IAAoC,gCAAkC;IAAlC,kCAAkC;E9D6hOxE;E8D5hOE;IAAoC,iCAAyC;IAAzC,yCAAyC;E9D+hO/E;E8D9hOE;IAAoC,oCAAwC;IAAxC,wCAAwC;E9DiiO9E;E8D/hOE;IAAiC,gCAAkC;IAAlC,kCAAkC;E9DkiOrE;E8DjiOE;IAAiC,8BAAgC;IAAhC,gCAAgC;E9DoiOnE;E8DniOE;IAAiC,iCAA8B;IAA9B,8BAA8B;E9DsiOjE;E8DriOE;IAAiC,mCAAgC;IAAhC,gCAAgC;E9DwiOnE;E8DviOE;IAAiC,kCAA+B;IAA/B,+BAA+B;E9D0iOlE;E8DxiOE;IAAkC,oCAAoC;IAApC,oCAAoC;E9D2iOxE;E8D1iOE;IAAkC,kCAAkC;IAAlC,kCAAkC;E9D6iOtE;E8D5iOE;IAAkC,qCAAgC;IAAhC,gCAAgC;E9D+iOpE;E8D9iOE;IAAkC,sCAAuC;IAAvC,uCAAuC;E9DijO3E;E8DhjOE;IAAkC,yCAAsC;IAAtC,sCAAsC;E9DmjO1E;E8DljOE;IAAkC,sCAAiC;IAAjC,iCAAiC;E9DqjOrE;E8DnjOE;IAAgC,oCAA2B;IAA3B,2BAA2B;E9DsjO7D;E8DrjOE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9DwjOnE;E8DvjOE;IAAgC,mCAA+B;IAA/B,+BAA+B;E9D0jOjE;E8DzjOE;IAAgC,sCAA6B;IAA7B,6BAA6B;E9D4jO/D;E8D3jOE;IAAgC,wCAA+B;IAA/B,+BAA+B;E9D8jOjE;E8D7jOE;IAAgC,uCAA8B;IAA9B,8BAA8B;E9DgkOhE;AACF;;A+D3mOI;EAAwB,sBAAsB;A/D+mOlD;;A+D9mOI;EAAwB,uBAAuB;A/DknOnD;;A+DjnOI;EAAwB,sBAAsB;A/DqnOlD;;AcjkOI;EiDtDA;IAAwB,sBAAsB;E/D4nOhD;E+D3nOE;IAAwB,uBAAuB;E/D8nOjD;E+D7nOE;IAAwB,sBAAsB;E/DgoOhD;AACF;;Ac7kOI;EiDtDA;IAAwB,sBAAsB;E/DwoOhD;E+DvoOE;IAAwB,uBAAuB;E/D0oOjD;E+DzoOE;IAAwB,sBAAsB;E/D4oOhD;AACF;;AczlOI;EiDtDA;IAAwB,sBAAsB;E/DopOhD;E+DnpOE;IAAwB,uBAAuB;E/DspOjD;E+DrpOE;IAAwB,sBAAsB;E/DwpOhD;AACF;;AcrmOI;EiDtDA;IAAwB,sBAAsB;E/DgqOhD;E+D/pOE;IAAwB,uBAAuB;E/DkqOjD;E+DjqOE;IAAwB,sBAAsB;E/DoqOhD;AACF;;AgE1qOE;EAAsB,yBAA2B;AhE8qOnD;;AgE9qOE;EAAsB,2BAA2B;AhEkrOnD;;AiEjrOE;EAAyB,2BAA8B;AjEqrOzD;;AiErrOE;EAAyB,6BAA8B;AjEyrOzD;;AiEzrOE;EAAyB,6BAA8B;AjE6rOzD;;AiE7rOE;EAAyB,0BAA8B;AjEisOzD;;AiEjsOE;EAAyB,mCAA8B;EAA9B,2BAA8B;AjEqsOzD;;AiEhsOA;EACE,eAAe;EACf,MAAM;EACN,QAAQ;EACR,OAAO;EACP,a9DoqBsC;AH+hNxC;;AiEhsOA;EACE,eAAe;EACf,QAAQ;EACR,SAAS;EACT,OAAO;EACP,a9D4pBsC;AHuiNxC;;AiE/rO8B;EAD9B;IAEI,wBAAgB;IAAhB,gBAAgB;IAChB,MAAM;IACN,a9DopBoC;EH+iNtC;AACF;;AkE7tOA;ECEE,kBAAkB;EAClB,UAAU;EACV,WAAW;EACX,UAAU;EACV,YAAY;EACZ,gBAAgB;EAChB,sBAAsB;EACtB,mBAAmB;EACnB,SAAS;AnE+tOX;;AmErtOE;EAEE,gBAAgB;EAChB,WAAW;EACX,YAAY;EACZ,iBAAiB;EACjB,UAAU;EACV,mBAAmB;AnEutOvB;;AoEpvOA;EAAa,8DAAqC;ApEwvOlD;;AoEvvOA;EAAU,wDAAkC;ApE2vO5C;;AoE1vOA;EAAa,uDAAqC;ApE8vOlD;;AoE7vOA;EAAe,2BAA2B;ApEiwO1C;;AqEhwOI;EAAuB,qBAA4B;ArEowOvD;;AqEpwOI;EAAuB,qBAA4B;ArEwwOvD;;AqExwOI;EAAuB,qBAA4B;ArE4wOvD;;AqE5wOI;EAAuB,sBAA4B;ArEgxOvD;;AqEhxOI;EAAuB,sBAA4B;ArEoxOvD;;AqEpxOI;EAAuB,sBAA4B;ArEwxOvD;;AqExxOI;EAAuB,sBAA4B;ArE4xOvD;;AqE5xOI;EAAuB,sBAA4B;ArEgyOvD;;AqEhyOI;EAAuB,uBAA4B;ArEoyOvD;;AqEpyOI;EAAuB,uBAA4B;ArEwyOvD;;AqEpyOA;EAAU,0BAA0B;ArEwyOpC;;AqEvyOA;EAAU,2BAA2B;ArE2yOrC;;AqEvyOA;EAAc,2BAA2B;ArE2yOzC;;AqE1yOA;EAAc,4BAA4B;ArE8yO1C;;AqE5yOA;EAAU,uBAAuB;ArEgzOjC;;AqE/yOA;EAAU,wBAAwB;ArEmzOlC;;AsEl0OA;EAEI,kBAAkB;EAClB,MAAM;EACN,QAAQ;EACR,SAAS;EACT,OAAO;EACP,UAAU;EAEV,oBAAoB;EACpB,WAAW;EAEX,kCAAkC;AtEk0OtC;;AuEx0OQ;EAAgC,oBAA4B;AvE40OpE;;AuE30OQ;;EAEE,wBAAoC;AvE80O9C;;AuE50OQ;;EAEE,0BAAwC;AvE+0OlD;;AuE70OQ;;EAEE,2BAA0C;AvEg1OpD;;AuE90OQ;;EAEE,yBAAsC;AvEi1OhD;;AuEh2OQ;EAAgC,0BAA4B;AvEo2OpE;;AuEn2OQ;;EAEE,8BAAoC;AvEs2O9C;;AuEp2OQ;;EAEE,gCAAwC;AvEu2OlD;;AuEr2OQ;;EAEE,iCAA0C;AvEw2OpD;;AuEt2OQ;;EAEE,+BAAsC;AvEy2OhD;;AuEx3OQ;EAAgC,yBAA4B;AvE43OpE;;AuE33OQ;;EAEE,6BAAoC;AvE83O9C;;AuE53OQ;;EAEE,+BAAwC;AvE+3OlD;;AuE73OQ;;EAEE,gCAA0C;AvEg4OpD;;AuE93OQ;;EAEE,8BAAsC;AvEi4OhD;;AuEh5OQ;EAAgC,uBAA4B;AvEo5OpE;;AuEn5OQ;;EAEE,2BAAoC;AvEs5O9C;;AuEp5OQ;;EAEE,6BAAwC;AvEu5OlD;;AuEr5OQ;;EAEE,8BAA0C;AvEw5OpD;;AuEt5OQ;;EAEE,4BAAsC;AvEy5OhD;;AuEx6OQ;EAAgC,yBAA4B;AvE46OpE;;AuE36OQ;;EAEE,6BAAoC;AvE86O9C;;AuE56OQ;;EAEE,+BAAwC;AvE+6OlD;;AuE76OQ;;EAEE,gCAA0C;AvEg7OpD;;AuE96OQ;;EAEE,8BAAsC;AvEi7OhD;;AuEh8OQ;EAAgC,uBAA4B;AvEo8OpE;;AuEn8OQ;;EAEE,2BAAoC;AvEs8O9C;;AuEp8OQ;;EAEE,6BAAwC;AvEu8OlD;;AuEr8OQ;;EAEE,8BAA0C;AvEw8OpD;;AuEt8OQ;;EAEE,4BAAsC;AvEy8OhD;;AuEx9OQ;EAAgC,qBAA4B;AvE49OpE;;AuE39OQ;;EAEE,yBAAoC;AvE89O9C;;AuE59OQ;;EAEE,2BAAwC;AvE+9OlD;;AuE79OQ;;EAEE,4BAA0C;AvEg+OpD;;AuE99OQ;;EAEE,0BAAsC;AvEi+OhD;;AuEh/OQ;EAAgC,2BAA4B;AvEo/OpE;;AuEn/OQ;;EAEE,+BAAoC;AvEs/O9C;;AuEp/OQ;;EAEE,iCAAwC;AvEu/OlD;;AuEr/OQ;;EAEE,kCAA0C;AvEw/OpD;;AuEt/OQ;;EAEE,gCAAsC;AvEy/OhD;;AuExgPQ;EAAgC,0BAA4B;AvE4gPpE;;AuE3gPQ;;EAEE,8BAAoC;AvE8gP9C;;AuE5gPQ;;EAEE,gCAAwC;AvE+gPlD;;AuE7gPQ;;EAEE,iCAA0C;AvEghPpD;;AuE9gPQ;;EAEE,+BAAsC;AvEihPhD;;AuEhiPQ;EAAgC,wBAA4B;AvEoiPpE;;AuEniPQ;;EAEE,4BAAoC;AvEsiP9C;;AuEpiPQ;;EAEE,8BAAwC;AvEuiPlD;;AuEriPQ;;EAEE,+BAA0C;AvEwiPpD;;AuEtiPQ;;EAEE,6BAAsC;AvEyiPhD;;AuExjPQ;EAAgC,0BAA4B;AvE4jPpE;;AuE3jPQ;;EAEE,8BAAoC;AvE8jP9C;;AuE5jPQ;;EAEE,gCAAwC;AvE+jPlD;;AuE7jPQ;;EAEE,iCAA0C;AvEgkPpD;;AuE9jPQ;;EAEE,+BAAsC;AvEikPhD;;AuEhlPQ;EAAgC,wBAA4B;AvEolPpE;;AuEnlPQ;;EAEE,4BAAoC;AvEslP9C;;AuEplPQ;;EAEE,8BAAwC;AvEulPlD;;AuErlPQ;;EAEE,+BAA0C;AvEwlPpD;;AuEtlPQ;;EAEE,6BAAsC;AvEylPhD;;AuEjlPQ;EAAwB,2BAA2B;AvEqlP3D;;AuEplPQ;;EAEE,+BAA+B;AvEulPzC;;AuErlPQ;;EAEE,iCAAiC;AvEwlP3C;;AuEtlPQ;;EAEE,kCAAkC;AvEylP5C;;AuEvlPQ;;EAEE,gCAAgC;AvE0lP1C;;AuEzmPQ;EAAwB,0BAA2B;AvE6mP3D;;AuE5mPQ;;EAEE,8BAA+B;AvE+mPzC;;AuE7mPQ;;EAEE,gCAAiC;AvEgnP3C;;AuE9mPQ;;EAEE,iCAAkC;AvEinP5C;;AuE/mPQ;;EAEE,+BAAgC;AvEknP1C;;AuEjoPQ;EAAwB,wBAA2B;AvEqoP3D;;AuEpoPQ;;EAEE,4BAA+B;AvEuoPzC;;AuEroPQ;;EAEE,8BAAiC;AvEwoP3C;;AuEtoPQ;;EAEE,+BAAkC;AvEyoP5C;;AuEvoPQ;;EAEE,6BAAgC;AvE0oP1C;;AuEzpPQ;EAAwB,0BAA2B;AvE6pP3D;;AuE5pPQ;;EAEE,8BAA+B;AvE+pPzC;;AuE7pPQ;;EAEE,gCAAiC;AvEgqP3C;;AuE9pPQ;;EAEE,iCAAkC;AvEiqP5C;;AuE/pPQ;;EAEE,+BAAgC;AvEkqP1C;;AuEjrPQ;EAAwB,wBAA2B;AvEqrP3D;;AuEprPQ;;EAEE,4BAA+B;AvEurPzC;;AuErrPQ;;EAEE,8BAAiC;AvEwrP3C;;AuEtrPQ;;EAEE,+BAAkC;AvEyrP5C;;AuEvrPQ;;EAEE,6BAAgC;AvE0rP1C;;AuEprPI;EAAmB,uBAAuB;AvEwrP9C;;AuEvrPI;;EAEE,2BAA2B;AvE0rPjC;;AuExrPI;;EAEE,6BAA6B;AvE2rPnC;;AuEzrPI;;EAEE,8BAA8B;AvE4rPpC;;AuE1rPI;;EAEE,4BAA4B;AvE6rPlC;;ActsPI;EyDlDI;IAAgC,oBAA4B;EvE6vPlE;EuE5vPM;;IAEE,wBAAoC;EvE8vP5C;EuE5vPM;;IAEE,0BAAwC;EvE8vPhD;EuE5vPM;;IAEE,2BAA0C;EvE8vPlD;EuE5vPM;;IAEE,yBAAsC;EvE8vP9C;EuE7wPM;IAAgC,0BAA4B;EvEgxPlE;EuE/wPM;;IAEE,8BAAoC;EvEixP5C;EuE/wPM;;IAEE,gCAAwC;EvEixPhD;EuE/wPM;;IAEE,iCAA0C;EvEixPlD;EuE/wPM;;IAEE,+BAAsC;EvEixP9C;EuEhyPM;IAAgC,yBAA4B;EvEmyPlE;EuElyPM;;IAEE,6BAAoC;EvEoyP5C;EuElyPM;;IAEE,+BAAwC;EvEoyPhD;EuElyPM;;IAEE,gCAA0C;EvEoyPlD;EuElyPM;;IAEE,8BAAsC;EvEoyP9C;EuEnzPM;IAAgC,uBAA4B;EvEszPlE;EuErzPM;;IAEE,2BAAoC;EvEuzP5C;EuErzPM;;IAEE,6BAAwC;EvEuzPhD;EuErzPM;;IAEE,8BAA0C;EvEuzPlD;EuErzPM;;IAEE,4BAAsC;EvEuzP9C;EuEt0PM;IAAgC,yBAA4B;EvEy0PlE;EuEx0PM;;IAEE,6BAAoC;EvE00P5C;EuEx0PM;;IAEE,+BAAwC;EvE00PhD;EuEx0PM;;IAEE,gCAA0C;EvE00PlD;EuEx0PM;;IAEE,8BAAsC;EvE00P9C;EuEz1PM;IAAgC,uBAA4B;EvE41PlE;EuE31PM;;IAEE,2BAAoC;EvE61P5C;EuE31PM;;IAEE,6BAAwC;EvE61PhD;EuE31PM;;IAEE,8BAA0C;EvE61PlD;EuE31PM;;IAEE,4BAAsC;EvE61P9C;EuE52PM;IAAgC,qBAA4B;EvE+2PlE;EuE92PM;;IAEE,yBAAoC;EvEg3P5C;EuE92PM;;IAEE,2BAAwC;EvEg3PhD;EuE92PM;;IAEE,4BAA0C;EvEg3PlD;EuE92PM;;IAEE,0BAAsC;EvEg3P9C;EuE/3PM;IAAgC,2BAA4B;EvEk4PlE;EuEj4PM;;IAEE,+BAAoC;EvEm4P5C;EuEj4PM;;IAEE,iCAAwC;EvEm4PhD;EuEj4PM;;IAEE,kCAA0C;EvEm4PlD;EuEj4PM;;IAEE,gCAAsC;EvEm4P9C;EuEl5PM;IAAgC,0BAA4B;EvEq5PlE;EuEp5PM;;IAEE,8BAAoC;EvEs5P5C;EuEp5PM;;IAEE,gCAAwC;EvEs5PhD;EuEp5PM;;IAEE,iCAA0C;EvEs5PlD;EuEp5PM;;IAEE,+BAAsC;EvEs5P9C;EuEr6PM;IAAgC,wBAA4B;EvEw6PlE;EuEv6PM;;IAEE,4BAAoC;EvEy6P5C;EuEv6PM;;IAEE,8BAAwC;EvEy6PhD;EuEv6PM;;IAEE,+BAA0C;EvEy6PlD;EuEv6PM;;IAEE,6BAAsC;EvEy6P9C;EuEx7PM;IAAgC,0BAA4B;EvE27PlE;EuE17PM;;IAEE,8BAAoC;EvE47P5C;EuE17PM;;IAEE,gCAAwC;EvE47PhD;EuE17PM;;IAEE,iCAA0C;EvE47PlD;EuE17PM;;IAEE,+BAAsC;EvE47P9C;EuE38PM;IAAgC,wBAA4B;EvE88PlE;EuE78PM;;IAEE,4BAAoC;EvE+8P5C;EuE78PM;;IAEE,8BAAwC;EvE+8PhD;EuE78PM;;IAEE,+BAA0C;EvE+8PlD;EuE78PM;;IAEE,6BAAsC;EvE+8P9C;EuEv8PM;IAAwB,2BAA2B;EvE08PzD;EuEz8PM;;IAEE,+BAA+B;EvE28PvC;EuEz8PM;;IAEE,iCAAiC;EvE28PzC;EuEz8PM;;IAEE,kCAAkC;EvE28P1C;EuEz8PM;;IAEE,gCAAgC;EvE28PxC;EuE19PM;IAAwB,0BAA2B;EvE69PzD;EuE59PM;;IAEE,8BAA+B;EvE89PvC;EuE59PM;;IAEE,gCAAiC;EvE89PzC;EuE59PM;;IAEE,iCAAkC;EvE89P1C;EuE59PM;;IAEE,+BAAgC;EvE89PxC;EuE7+PM;IAAwB,wBAA2B;EvEg/PzD;EuE/+PM;;IAEE,4BAA+B;EvEi/PvC;EuE/+PM;;IAEE,8BAAiC;EvEi/PzC;EuE/+PM;;IAEE,+BAAkC;EvEi/P1C;EuE/+PM;;IAEE,6BAAgC;EvEi/PxC;EuEhgQM;IAAwB,0BAA2B;EvEmgQzD;EuElgQM;;IAEE,8BAA+B;EvEogQvC;EuElgQM;;IAEE,gCAAiC;EvEogQzC;EuElgQM;;IAEE,iCAAkC;EvEogQ1C;EuElgQM;;IAEE,+BAAgC;EvEogQxC;EuEnhQM;IAAwB,wBAA2B;EvEshQzD;EuErhQM;;IAEE,4BAA+B;EvEuhQvC;EuErhQM;;IAEE,8BAAiC;EvEuhQzC;EuErhQM;;IAEE,+BAAkC;EvEuhQ1C;EuErhQM;;IAEE,6BAAgC;EvEuhQxC;EuEjhQE;IAAmB,uBAAuB;EvEohQ5C;EuEnhQE;;IAEE,2BAA2B;EvEqhQ/B;EuEnhQE;;IAEE,6BAA6B;EvEqhQjC;EuEnhQE;;IAEE,8BAA8B;EvEqhQlC;EuEnhQE;;IAEE,4BAA4B;EvEqhQhC;AACF;;Ac/hQI;EyDlDI;IAAgC,oBAA4B;EvEslQlE;EuErlQM;;IAEE,wBAAoC;EvEulQ5C;EuErlQM;;IAEE,0BAAwC;EvEulQhD;EuErlQM;;IAEE,2BAA0C;EvEulQlD;EuErlQM;;IAEE,yBAAsC;EvEulQ9C;EuEtmQM;IAAgC,0BAA4B;EvEymQlE;EuExmQM;;IAEE,8BAAoC;EvE0mQ5C;EuExmQM;;IAEE,gCAAwC;EvE0mQhD;EuExmQM;;IAEE,iCAA0C;EvE0mQlD;EuExmQM;;IAEE,+BAAsC;EvE0mQ9C;EuEznQM;IAAgC,yBAA4B;EvE4nQlE;EuE3nQM;;IAEE,6BAAoC;EvE6nQ5C;EuE3nQM;;IAEE,+BAAwC;EvE6nQhD;EuE3nQM;;IAEE,gCAA0C;EvE6nQlD;EuE3nQM;;IAEE,8BAAsC;EvE6nQ9C;EuE5oQM;IAAgC,uBAA4B;EvE+oQlE;EuE9oQM;;IAEE,2BAAoC;EvEgpQ5C;EuE9oQM;;IAEE,6BAAwC;EvEgpQhD;EuE9oQM;;IAEE,8BAA0C;EvEgpQlD;EuE9oQM;;IAEE,4BAAsC;EvEgpQ9C;EuE/pQM;IAAgC,yBAA4B;EvEkqQlE;EuEjqQM;;IAEE,6BAAoC;EvEmqQ5C;EuEjqQM;;IAEE,+BAAwC;EvEmqQhD;EuEjqQM;;IAEE,gCAA0C;EvEmqQlD;EuEjqQM;;IAEE,8BAAsC;EvEmqQ9C;EuElrQM;IAAgC,uBAA4B;EvEqrQlE;EuEprQM;;IAEE,2BAAoC;EvEsrQ5C;EuEprQM;;IAEE,6BAAwC;EvEsrQhD;EuEprQM;;IAEE,8BAA0C;EvEsrQlD;EuEprQM;;IAEE,4BAAsC;EvEsrQ9C;EuErsQM;IAAgC,qBAA4B;EvEwsQlE;EuEvsQM;;IAEE,yBAAoC;EvEysQ5C;EuEvsQM;;IAEE,2BAAwC;EvEysQhD;EuEvsQM;;IAEE,4BAA0C;EvEysQlD;EuEvsQM;;IAEE,0BAAsC;EvEysQ9C;EuExtQM;IAAgC,2BAA4B;EvE2tQlE;EuE1tQM;;IAEE,+BAAoC;EvE4tQ5C;EuE1tQM;;IAEE,iCAAwC;EvE4tQhD;EuE1tQM;;IAEE,kCAA0C;EvE4tQlD;EuE1tQM;;IAEE,gCAAsC;EvE4tQ9C;EuE3uQM;IAAgC,0BAA4B;EvE8uQlE;EuE7uQM;;IAEE,8BAAoC;EvE+uQ5C;EuE7uQM;;IAEE,gCAAwC;EvE+uQhD;EuE7uQM;;IAEE,iCAA0C;EvE+uQlD;EuE7uQM;;IAEE,+BAAsC;EvE+uQ9C;EuE9vQM;IAAgC,wBAA4B;EvEiwQlE;EuEhwQM;;IAEE,4BAAoC;EvEkwQ5C;EuEhwQM;;IAEE,8BAAwC;EvEkwQhD;EuEhwQM;;IAEE,+BAA0C;EvEkwQlD;EuEhwQM;;IAEE,6BAAsC;EvEkwQ9C;EuEjxQM;IAAgC,0BAA4B;EvEoxQlE;EuEnxQM;;IAEE,8BAAoC;EvEqxQ5C;EuEnxQM;;IAEE,gCAAwC;EvEqxQhD;EuEnxQM;;IAEE,iCAA0C;EvEqxQlD;EuEnxQM;;IAEE,+BAAsC;EvEqxQ9C;EuEpyQM;IAAgC,wBAA4B;EvEuyQlE;EuEtyQM;;IAEE,4BAAoC;EvEwyQ5C;EuEtyQM;;IAEE,8BAAwC;EvEwyQhD;EuEtyQM;;IAEE,+BAA0C;EvEwyQlD;EuEtyQM;;IAEE,6BAAsC;EvEwyQ9C;EuEhyQM;IAAwB,2BAA2B;EvEmyQzD;EuElyQM;;IAEE,+BAA+B;EvEoyQvC;EuElyQM;;IAEE,iCAAiC;EvEoyQzC;EuElyQM;;IAEE,kCAAkC;EvEoyQ1C;EuElyQM;;IAEE,gCAAgC;EvEoyQxC;EuEnzQM;IAAwB,0BAA2B;EvEszQzD;EuErzQM;;IAEE,8BAA+B;EvEuzQvC;EuErzQM;;IAEE,gCAAiC;EvEuzQzC;EuErzQM;;IAEE,iCAAkC;EvEuzQ1C;EuErzQM;;IAEE,+BAAgC;EvEuzQxC;EuEt0QM;IAAwB,wBAA2B;EvEy0QzD;EuEx0QM;;IAEE,4BAA+B;EvE00QvC;EuEx0QM;;IAEE,8BAAiC;EvE00QzC;EuEx0QM;;IAEE,+BAAkC;EvE00Q1C;EuEx0QM;;IAEE,6BAAgC;EvE00QxC;EuEz1QM;IAAwB,0BAA2B;EvE41QzD;EuE31QM;;IAEE,8BAA+B;EvE61QvC;EuE31QM;;IAEE,gCAAiC;EvE61QzC;EuE31QM;;IAEE,iCAAkC;EvE61Q1C;EuE31QM;;IAEE,+BAAgC;EvE61QxC;EuE52QM;IAAwB,wBAA2B;EvE+2QzD;EuE92QM;;IAEE,4BAA+B;EvEg3QvC;EuE92QM;;IAEE,8BAAiC;EvEg3QzC;EuE92QM;;IAEE,+BAAkC;EvEg3Q1C;EuE92QM;;IAEE,6BAAgC;EvEg3QxC;EuE12QE;IAAmB,uBAAuB;EvE62Q5C;EuE52QE;;IAEE,2BAA2B;EvE82Q/B;EuE52QE;;IAEE,6BAA6B;EvE82QjC;EuE52QE;;IAEE,8BAA8B;EvE82QlC;EuE52QE;;IAEE,4BAA4B;EvE82QhC;AACF;;Acx3QI;EyDlDI;IAAgC,oBAA4B;EvE+6QlE;EuE96QM;;IAEE,wBAAoC;EvEg7Q5C;EuE96QM;;IAEE,0BAAwC;EvEg7QhD;EuE96QM;;IAEE,2BAA0C;EvEg7QlD;EuE96QM;;IAEE,yBAAsC;EvEg7Q9C;EuE/7QM;IAAgC,0BAA4B;EvEk8QlE;EuEj8QM;;IAEE,8BAAoC;EvEm8Q5C;EuEj8QM;;IAEE,gCAAwC;EvEm8QhD;EuEj8QM;;IAEE,iCAA0C;EvEm8QlD;EuEj8QM;;IAEE,+BAAsC;EvEm8Q9C;EuEl9QM;IAAgC,yBAA4B;EvEq9QlE;EuEp9QM;;IAEE,6BAAoC;EvEs9Q5C;EuEp9QM;;IAEE,+BAAwC;EvEs9QhD;EuEp9QM;;IAEE,gCAA0C;EvEs9QlD;EuEp9QM;;IAEE,8BAAsC;EvEs9Q9C;EuEr+QM;IAAgC,uBAA4B;EvEw+QlE;EuEv+QM;;IAEE,2BAAoC;EvEy+Q5C;EuEv+QM;;IAEE,6BAAwC;EvEy+QhD;EuEv+QM;;IAEE,8BAA0C;EvEy+QlD;EuEv+QM;;IAEE,4BAAsC;EvEy+Q9C;EuEx/QM;IAAgC,yBAA4B;EvE2/QlE;EuE1/QM;;IAEE,6BAAoC;EvE4/Q5C;EuE1/QM;;IAEE,+BAAwC;EvE4/QhD;EuE1/QM;;IAEE,gCAA0C;EvE4/QlD;EuE1/QM;;IAEE,8BAAsC;EvE4/Q9C;EuE3gRM;IAAgC,uBAA4B;EvE8gRlE;EuE7gRM;;IAEE,2BAAoC;EvE+gR5C;EuE7gRM;;IAEE,6BAAwC;EvE+gRhD;EuE7gRM;;IAEE,8BAA0C;EvE+gRlD;EuE7gRM;;IAEE,4BAAsC;EvE+gR9C;EuE9hRM;IAAgC,qBAA4B;EvEiiRlE;EuEhiRM;;IAEE,yBAAoC;EvEkiR5C;EuEhiRM;;IAEE,2BAAwC;EvEkiRhD;EuEhiRM;;IAEE,4BAA0C;EvEkiRlD;EuEhiRM;;IAEE,0BAAsC;EvEkiR9C;EuEjjRM;IAAgC,2BAA4B;EvEojRlE;EuEnjRM;;IAEE,+BAAoC;EvEqjR5C;EuEnjRM;;IAEE,iCAAwC;EvEqjRhD;EuEnjRM;;IAEE,kCAA0C;EvEqjRlD;EuEnjRM;;IAEE,gCAAsC;EvEqjR9C;EuEpkRM;IAAgC,0BAA4B;EvEukRlE;EuEtkRM;;IAEE,8BAAoC;EvEwkR5C;EuEtkRM;;IAEE,gCAAwC;EvEwkRhD;EuEtkRM;;IAEE,iCAA0C;EvEwkRlD;EuEtkRM;;IAEE,+BAAsC;EvEwkR9C;EuEvlRM;IAAgC,wBAA4B;EvE0lRlE;EuEzlRM;;IAEE,4BAAoC;EvE2lR5C;EuEzlRM;;IAEE,8BAAwC;EvE2lRhD;EuEzlRM;;IAEE,+BAA0C;EvE2lRlD;EuEzlRM;;IAEE,6BAAsC;EvE2lR9C;EuE1mRM;IAAgC,0BAA4B;EvE6mRlE;EuE5mRM;;IAEE,8BAAoC;EvE8mR5C;EuE5mRM;;IAEE,gCAAwC;EvE8mRhD;EuE5mRM;;IAEE,iCAA0C;EvE8mRlD;EuE5mRM;;IAEE,+BAAsC;EvE8mR9C;EuE7nRM;IAAgC,wBAA4B;EvEgoRlE;EuE/nRM;;IAEE,4BAAoC;EvEioR5C;EuE/nRM;;IAEE,8BAAwC;EvEioRhD;EuE/nRM;;IAEE,+BAA0C;EvEioRlD;EuE/nRM;;IAEE,6BAAsC;EvEioR9C;EuEznRM;IAAwB,2BAA2B;EvE4nRzD;EuE3nRM;;IAEE,+BAA+B;EvE6nRvC;EuE3nRM;;IAEE,iCAAiC;EvE6nRzC;EuE3nRM;;IAEE,kCAAkC;EvE6nR1C;EuE3nRM;;IAEE,gCAAgC;EvE6nRxC;EuE5oRM;IAAwB,0BAA2B;EvE+oRzD;EuE9oRM;;IAEE,8BAA+B;EvEgpRvC;EuE9oRM;;IAEE,gCAAiC;EvEgpRzC;EuE9oRM;;IAEE,iCAAkC;EvEgpR1C;EuE9oRM;;IAEE,+BAAgC;EvEgpRxC;EuE/pRM;IAAwB,wBAA2B;EvEkqRzD;EuEjqRM;;IAEE,4BAA+B;EvEmqRvC;EuEjqRM;;IAEE,8BAAiC;EvEmqRzC;EuEjqRM;;IAEE,+BAAkC;EvEmqR1C;EuEjqRM;;IAEE,6BAAgC;EvEmqRxC;EuElrRM;IAAwB,0BAA2B;EvEqrRzD;EuEprRM;;IAEE,8BAA+B;EvEsrRvC;EuEprRM;;IAEE,gCAAiC;EvEsrRzC;EuEprRM;;IAEE,iCAAkC;EvEsrR1C;EuEprRM;;IAEE,+BAAgC;EvEsrRxC;EuErsRM;IAAwB,wBAA2B;EvEwsRzD;EuEvsRM;;IAEE,4BAA+B;EvEysRvC;EuEvsRM;;IAEE,8BAAiC;EvEysRzC;EuEvsRM;;IAEE,+BAAkC;EvEysR1C;EuEvsRM;;IAEE,6BAAgC;EvEysRxC;EuEnsRE;IAAmB,uBAAuB;EvEssR5C;EuErsRE;;IAEE,2BAA2B;EvEusR/B;EuErsRE;;IAEE,6BAA6B;EvEusRjC;EuErsRE;;IAEE,8BAA8B;EvEusRlC;EuErsRE;;IAEE,4BAA4B;EvEusRhC;AACF;;AcjtRI;EyDlDI;IAAgC,oBAA4B;EvEwwRlE;EuEvwRM;;IAEE,wBAAoC;EvEywR5C;EuEvwRM;;IAEE,0BAAwC;EvEywRhD;EuEvwRM;;IAEE,2BAA0C;EvEywRlD;EuEvwRM;;IAEE,yBAAsC;EvEywR9C;EuExxRM;IAAgC,0BAA4B;EvE2xRlE;EuE1xRM;;IAEE,8BAAoC;EvE4xR5C;EuE1xRM;;IAEE,gCAAwC;EvE4xRhD;EuE1xRM;;IAEE,iCAA0C;EvE4xRlD;EuE1xRM;;IAEE,+BAAsC;EvE4xR9C;EuE3yRM;IAAgC,yBAA4B;EvE8yRlE;EuE7yRM;;IAEE,6BAAoC;EvE+yR5C;EuE7yRM;;IAEE,+BAAwC;EvE+yRhD;EuE7yRM;;IAEE,gCAA0C;EvE+yRlD;EuE7yRM;;IAEE,8BAAsC;EvE+yR9C;EuE9zRM;IAAgC,uBAA4B;EvEi0RlE;EuEh0RM;;IAEE,2BAAoC;EvEk0R5C;EuEh0RM;;IAEE,6BAAwC;EvEk0RhD;EuEh0RM;;IAEE,8BAA0C;EvEk0RlD;EuEh0RM;;IAEE,4BAAsC;EvEk0R9C;EuEj1RM;IAAgC,yBAA4B;EvEo1RlE;EuEn1RM;;IAEE,6BAAoC;EvEq1R5C;EuEn1RM;;IAEE,+BAAwC;EvEq1RhD;EuEn1RM;;IAEE,gCAA0C;EvEq1RlD;EuEn1RM;;IAEE,8BAAsC;EvEq1R9C;EuEp2RM;IAAgC,uBAA4B;EvEu2RlE;EuEt2RM;;IAEE,2BAAoC;EvEw2R5C;EuEt2RM;;IAEE,6BAAwC;EvEw2RhD;EuEt2RM;;IAEE,8BAA0C;EvEw2RlD;EuEt2RM;;IAEE,4BAAsC;EvEw2R9C;EuEv3RM;IAAgC,qBAA4B;EvE03RlE;EuEz3RM;;IAEE,yBAAoC;EvE23R5C;EuEz3RM;;IAEE,2BAAwC;EvE23RhD;EuEz3RM;;IAEE,4BAA0C;EvE23RlD;EuEz3RM;;IAEE,0BAAsC;EvE23R9C;EuE14RM;IAAgC,2BAA4B;EvE64RlE;EuE54RM;;IAEE,+BAAoC;EvE84R5C;EuE54RM;;IAEE,iCAAwC;EvE84RhD;EuE54RM;;IAEE,kCAA0C;EvE84RlD;EuE54RM;;IAEE,gCAAsC;EvE84R9C;EuE75RM;IAAgC,0BAA4B;EvEg6RlE;EuE/5RM;;IAEE,8BAAoC;EvEi6R5C;EuE/5RM;;IAEE,gCAAwC;EvEi6RhD;EuE/5RM;;IAEE,iCAA0C;EvEi6RlD;EuE/5RM;;IAEE,+BAAsC;EvEi6R9C;EuEh7RM;IAAgC,wBAA4B;EvEm7RlE;EuEl7RM;;IAEE,4BAAoC;EvEo7R5C;EuEl7RM;;IAEE,8BAAwC;EvEo7RhD;EuEl7RM;;IAEE,+BAA0C;EvEo7RlD;EuEl7RM;;IAEE,6BAAsC;EvEo7R9C;EuEn8RM;IAAgC,0BAA4B;EvEs8RlE;EuEr8RM;;IAEE,8BAAoC;EvEu8R5C;EuEr8RM;;IAEE,gCAAwC;EvEu8RhD;EuEr8RM;;IAEE,iCAA0C;EvEu8RlD;EuEr8RM;;IAEE,+BAAsC;EvEu8R9C;EuEt9RM;IAAgC,wBAA4B;EvEy9RlE;EuEx9RM;;IAEE,4BAAoC;EvE09R5C;EuEx9RM;;IAEE,8BAAwC;EvE09RhD;EuEx9RM;;IAEE,+BAA0C;EvE09RlD;EuEx9RM;;IAEE,6BAAsC;EvE09R9C;EuEl9RM;IAAwB,2BAA2B;EvEq9RzD;EuEp9RM;;IAEE,+BAA+B;EvEs9RvC;EuEp9RM;;IAEE,iCAAiC;EvEs9RzC;EuEp9RM;;IAEE,kCAAkC;EvEs9R1C;EuEp9RM;;IAEE,gCAAgC;EvEs9RxC;EuEr+RM;IAAwB,0BAA2B;EvEw+RzD;EuEv+RM;;IAEE,8BAA+B;EvEy+RvC;EuEv+RM;;IAEE,gCAAiC;EvEy+RzC;EuEv+RM;;IAEE,iCAAkC;EvEy+R1C;EuEv+RM;;IAEE,+BAAgC;EvEy+RxC;EuEx/RM;IAAwB,wBAA2B;EvE2/RzD;EuE1/RM;;IAEE,4BAA+B;EvE4/RvC;EuE1/RM;;IAEE,8BAAiC;EvE4/RzC;EuE1/RM;;IAEE,+BAAkC;EvE4/R1C;EuE1/RM;;IAEE,6BAAgC;EvE4/RxC;EuE3gSM;IAAwB,0BAA2B;EvE8gSzD;EuE7gSM;;IAEE,8BAA+B;EvE+gSvC;EuE7gSM;;IAEE,gCAAiC;EvE+gSzC;EuE7gSM;;IAEE,iCAAkC;EvE+gS1C;EuE7gSM;;IAEE,+BAAgC;EvE+gSxC;EuE9hSM;IAAwB,wBAA2B;EvEiiSzD;EuEhiSM;;IAEE,4BAA+B;EvEkiSvC;EuEhiSM;;IAEE,8BAAiC;EvEkiSzC;EuEhiSM;;IAEE,+BAAkC;EvEkiS1C;EuEhiSM;;IAEE,6BAAgC;EvEkiSxC;EuE5hSE;IAAmB,uBAAuB;EvE+hS5C;EuE9hSE;;IAEE,2BAA2B;EvEgiS/B;EuE9hSE;;IAEE,6BAA6B;EvEgiSjC;EuE9hSE;;IAEE,8BAA8B;EvEgiSlC;EuE9hSE;;IAEE,4BAA4B;EvEgiShC;AACF;;AwEhmSA;EAAkB,4GAA8C;AxEomShE;;AwEhmSA;EAAiB,8BAA8B;AxEomS/C;;AwEnmSA;EAAiB,8BAA8B;AxEumS/C;;AwEtmSA;EAAiB,8BAA8B;AxE0mS/C;;AwEzmSA;ECTE,gBAAgB;EAChB,uBAAuB;EACvB,mBAAmB;AzEsnSrB;;AwEvmSI;EAAwB,2BAA2B;AxE2mSvD;;AwE1mSI;EAAwB,4BAA4B;AxE8mSxD;;AwE7mSI;EAAwB,6BAA6B;AxEinSzD;;Ac5kSI;E0DvCA;IAAwB,2BAA2B;ExEwnSrD;EwEvnSE;IAAwB,4BAA4B;ExE0nStD;EwEznSE;IAAwB,6BAA6B;ExE4nSvD;AACF;;AcxlSI;E0DvCA;IAAwB,2BAA2B;ExEooSrD;EwEnoSE;IAAwB,4BAA4B;ExEsoStD;EwEroSE;IAAwB,6BAA6B;ExEwoSvD;AACF;;AcpmSI;E0DvCA;IAAwB,2BAA2B;ExEgpSrD;EwE/oSE;IAAwB,4BAA4B;ExEkpStD;EwEjpSE;IAAwB,6BAA6B;ExEopSvD;AACF;;AchnSI;E0DvCA;IAAwB,2BAA2B;ExE4pSrD;EwE3pSE;IAAwB,4BAA4B;ExE8pStD;EwE7pSE;IAAwB,6BAA6B;ExEgqSvD;AACF;;AwE3pSA;EAAmB,oCAAoC;AxE+pSvD;;AwE9pSA;EAAmB,oCAAoC;AxEkqSvD;;AwEjqSA;EAAmB,qCAAqC;AxEqqSxD;;AwEjqSA;EAAuB,2BAA0C;AxEqqSjE;;AwEpqSA;EAAuB,+BAA4C;AxEwqSnE;;AwEvqSA;EAAuB,2BAA2C;AxE2qSlE;;AwE1qSA;EAAuB,2BAAyC;AxE8qShE;;AwE7qSA;EAAuB,8BAA2C;AxEirSlE;;AwEhrSA;EAAuB,6BAA6B;AxEorSpD;;AwEhrSA;EAAc,sBAAwB;AxEorStC;;A0E3tSE;EACE,yBAAwB;A1E8tS5B;;AKptSE;EqELM,yBAA0E;A1E6tSlF;;A0EnuSE;EACE,yBAAwB;A1EsuS5B;;AK5tSE;EqELM,yBAA0E;A1EquSlF;;A0E3uSE;EACE,yBAAwB;A1E8uS5B;;AKpuSE;EqELM,yBAA0E;A1E6uSlF;;A0EnvSE;EACE,yBAAwB;A1EsvS5B;;AK5uSE;EqELM,yBAA0E;A1EqvSlF;;A0E3vSE;EACE,yBAAwB;A1E8vS5B;;AKpvSE;EqELM,yBAA0E;A1E6vSlF;;A0EnwSE;EACE,yBAAwB;A1EswS5B;;AK5vSE;EqELM,yBAA0E;A1EqwSlF;;A0E3wSE;EACE,yBAAwB;A1E8wS5B;;AKpwSE;EqELM,yBAA0E;A1E6wSlF;;A0EnxSE;EACE,yBAAwB;A1EsxS5B;;AK5wSE;EqELM,yBAA0E;A1EqxSlF;;AwE9uSA;EAAa,yBAA6B;AxEkvS1C;;AwEjvSA;EAAc,yBAA6B;AxEqvS3C;;AwEnvSA;EAAiB,oCAAkC;AxEuvSnD;;AwEtvSA;EAAiB,0CAAkC;AxE0vSnD;;AwEtvSA;EGvDE,WAAW;EACX,kBAAkB;EAClB,iBAAiB;EACjB,6BAA6B;EAC7B,SAAS;A3EizSX;;AwE1vSA;EAAwB,gCAAgC;AxE8vSxD;;AwE5vSA;EACE,iCAAiC;EACjC,oCAAoC;AxE+vStC;;AwE1vSA;EAAc,yBAAyB;AxE8vSvC;;A4E/zSA;EACE,8BAA8B;A5Ek0ShC;;A4E/zSA;EACE,6BAA6B;A5Ek0S/B;;A6El0SE;E3EOF;;;I2EDM,4BAA4B;IAE5B,2BAA2B;E7Ek0S/B;E6E/zSE;IAEI,0BAA0B;E7Eg0ShC;E6EvzSE;IACE,6BAA6B;E7EyzSjC;EE3nSF;I2E/KM,gCAAgC;E7E6ySpC;E6E3ySE;;IAEE,yB1EzCY;I0E0CZ,wBAAwB;E7E6yS5B;E6ErySE;IACE,2BAA2B;E7EuyS/B;E6EpySE;;IAEE,wBAAwB;E7EsyS5B;E6EnySE;;;IAGE,UAAU;IACV,SAAS;E7EqySb;E6ElySE;;IAEE,uBAAuB;E7EoyS3B;E6E5xSE;IACE,Q1E4hCgC;EHkwQpC;EE10SF;I2E+CM,2BAA2C;E7E8xS/C;EYp3SA;IiEyFI,2BAA2C;E7E8xS/C;EiC52SF;I4CmFM,aAAa;E7E4xSjB;EsC33SF;IuCkGM,sB1EtFS;EHk3Sb;EgB/3SF;I6DuGM,oCAAoC;E7E2xSxC;E6E5xSE;;IAKI,iCAAmC;E7E2xSzC;EgB91SF;;I6D0EQ,oCAAsC;E7EwxS5C;EgB7wSF;I6DNM,cAAc;E7EsxSlB;EiB54SA;;;;I4D4HM,qB1EvHU;EH64ShB;EgBxySF;I6DuBM,cAAc;IACd,qB1E7HY;EHi5ShB;AACF","file":"bootstrap.css","sourcesContent":["/*!\n * Bootstrap v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"root\";\n@import \"reboot\";\n@import \"type\";\n@import \"images\";\n@import \"code\";\n@import \"grid\";\n@import \"tables\";\n@import \"forms\";\n@import \"buttons\";\n@import \"transitions\";\n@import \"dropdown\";\n@import \"button-group\";\n@import \"input-group\";\n@import \"custom-forms\";\n@import \"nav\";\n@import \"navbar\";\n@import \"card\";\n@import \"breadcrumb\";\n@import \"pagination\";\n@import \"badge\";\n@import \"jumbotron\";\n@import \"alert\";\n@import \"progress\";\n@import \"media\";\n@import \"list-group\";\n@import \"close\";\n@import \"toasts\";\n@import \"modal\";\n@import \"tooltip\";\n@import \"popover\";\n@import \"carousel\";\n@import \"spinners\";\n@import \"utilities\";\n@import \"print\";\n","/*!\n * Bootstrap v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n:root {\n --blue: #007bff;\n --indigo: #6610f2;\n --purple: #6f42c1;\n --pink: #e83e8c;\n --red: #dc3545;\n --orange: #fd7e14;\n --yellow: #ffc107;\n --green: #28a745;\n --teal: #20c997;\n --cyan: #17a2b8;\n --white: #fff;\n --gray: #6c757d;\n --gray-dark: #343a40;\n --primary: #007bff;\n --secondary: #6c757d;\n --success: #28a745;\n --info: #17a2b8;\n --warning: #ffc107;\n --danger: #dc3545;\n --light: #f8f9fa;\n --dark: #343a40;\n --breakpoint-xs: 0;\n --breakpoint-sm: 576px;\n --breakpoint-md: 768px;\n --breakpoint-lg: 992px;\n --breakpoint-xl: 1200px;\n --font-family-sans-serif: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg {\n overflow: hidden;\n vertical-align: middle;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nselect {\n word-wrap: normal;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton:not(:disabled),\n[type=\"button\"]:not(:disabled),\n[type=\"reset\"]:not(:disabled),\n[type=\"submit\"]:not(:disabled) {\n cursor: pointer;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n margin-bottom: 0.5rem;\n font-weight: 500;\n line-height: 1.2;\n}\n\nh1, .h1 {\n font-size: 2.5rem;\n}\n\nh2, .h2 {\n font-size: 2rem;\n}\n\nh3, .h3 {\n font-size: 1.75rem;\n}\n\nh4, .h4 {\n font-size: 1.5rem;\n}\n\nh5, .h5 {\n font-size: 1.25rem;\n}\n\nh6, .h6 {\n font-size: 1rem;\n}\n\n.lead {\n font-size: 1.25rem;\n font-weight: 300;\n}\n\n.display-1 {\n font-size: 6rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-2 {\n font-size: 5.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-3 {\n font-size: 4.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-4 {\n font-size: 3.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\nhr {\n margin-top: 1rem;\n margin-bottom: 1rem;\n border: 0;\n border-top: 1px solid rgba(0, 0, 0, 0.1);\n}\n\nsmall,\n.small {\n font-size: 80%;\n font-weight: 400;\n}\n\nmark,\n.mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline-item {\n display: inline-block;\n}\n\n.list-inline-item:not(:last-child) {\n margin-right: 0.5rem;\n}\n\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\n\n.blockquote {\n margin-bottom: 1rem;\n font-size: 1.25rem;\n}\n\n.blockquote-footer {\n display: block;\n font-size: 80%;\n color: #6c757d;\n}\n\n.blockquote-footer::before {\n content: \"\\2014\\00A0\";\n}\n\n.img-fluid {\n max-width: 100%;\n height: auto;\n}\n\n.img-thumbnail {\n padding: 0.25rem;\n background-color: #fff;\n border: 1px solid #dee2e6;\n border-radius: 0.25rem;\n max-width: 100%;\n height: auto;\n}\n\n.figure {\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: 0.5rem;\n line-height: 1;\n}\n\n.figure-caption {\n font-size: 90%;\n color: #6c757d;\n}\n\ncode {\n font-size: 87.5%;\n color: #e83e8c;\n word-wrap: break-word;\n}\n\na > code {\n color: inherit;\n}\n\nkbd {\n padding: 0.2rem 0.4rem;\n font-size: 87.5%;\n color: #fff;\n background-color: #212529;\n border-radius: 0.2rem;\n}\n\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n}\n\npre {\n display: block;\n font-size: 87.5%;\n color: #212529;\n}\n\npre code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n}\n\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n\n.container {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container {\n max-width: 1140px;\n }\n}\n\n.container-fluid, .container-sm, .container-md, .container-lg, .container-xl {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container, .container-sm {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container, .container-sm, .container-md {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container, .container-sm, .container-md, .container-lg {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container, .container-sm, .container-md, .container-lg, .container-xl {\n max-width: 1140px;\n }\n}\n\n.row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n\n.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,\n.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm,\n.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md,\n.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg,\n.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl,\n.col-xl-auto {\n position: relative;\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n}\n\n.col {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n}\n\n.row-cols-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n}\n\n.col-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n}\n\n.col-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-3 {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.col-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.col-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n}\n\n.col-6 {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.col-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n}\n\n.col-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n}\n\n.col-9 {\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.col-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n}\n\n.col-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n}\n\n.col-12 {\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.order-first {\n order: -1;\n}\n\n.order-last {\n order: 13;\n}\n\n.order-0 {\n order: 0;\n}\n\n.order-1 {\n order: 1;\n}\n\n.order-2 {\n order: 2;\n}\n\n.order-3 {\n order: 3;\n}\n\n.order-4 {\n order: 4;\n}\n\n.order-5 {\n order: 5;\n}\n\n.order-6 {\n order: 6;\n}\n\n.order-7 {\n order: 7;\n}\n\n.order-8 {\n order: 8;\n}\n\n.order-9 {\n order: 9;\n}\n\n.order-10 {\n order: 10;\n}\n\n.order-11 {\n order: 11;\n}\n\n.order-12 {\n order: 12;\n}\n\n.offset-1 {\n margin-left: 8.333333%;\n}\n\n.offset-2 {\n margin-left: 16.666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.333333%;\n}\n\n.offset-5 {\n margin-left: 41.666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.333333%;\n}\n\n.offset-8 {\n margin-left: 66.666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.333333%;\n}\n\n.offset-11 {\n margin-left: 91.666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-sm-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-sm-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-sm-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-sm-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-sm-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-sm-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-sm-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-sm-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-sm-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-sm-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-sm-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-sm-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-sm-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-sm-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-sm-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-sm-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-sm-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-sm-first {\n order: -1;\n }\n .order-sm-last {\n order: 13;\n }\n .order-sm-0 {\n order: 0;\n }\n .order-sm-1 {\n order: 1;\n }\n .order-sm-2 {\n order: 2;\n }\n .order-sm-3 {\n order: 3;\n }\n .order-sm-4 {\n order: 4;\n }\n .order-sm-5 {\n order: 5;\n }\n .order-sm-6 {\n order: 6;\n }\n .order-sm-7 {\n order: 7;\n }\n .order-sm-8 {\n order: 8;\n }\n .order-sm-9 {\n order: 9;\n }\n .order-sm-10 {\n order: 10;\n }\n .order-sm-11 {\n order: 11;\n }\n .order-sm-12 {\n order: 12;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.333333%;\n }\n .offset-sm-2 {\n margin-left: 16.666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.333333%;\n }\n .offset-sm-5 {\n margin-left: 41.666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.333333%;\n }\n .offset-sm-8 {\n margin-left: 66.666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.333333%;\n }\n .offset-sm-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 768px) {\n .col-md {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-md-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-md-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-md-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-md-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-md-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-md-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-md-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-md-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-md-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-md-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-md-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-md-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-md-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-md-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-md-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-md-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-md-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-md-first {\n order: -1;\n }\n .order-md-last {\n order: 13;\n }\n .order-md-0 {\n order: 0;\n }\n .order-md-1 {\n order: 1;\n }\n .order-md-2 {\n order: 2;\n }\n .order-md-3 {\n order: 3;\n }\n .order-md-4 {\n order: 4;\n }\n .order-md-5 {\n order: 5;\n }\n .order-md-6 {\n order: 6;\n }\n .order-md-7 {\n order: 7;\n }\n .order-md-8 {\n order: 8;\n }\n .order-md-9 {\n order: 9;\n }\n .order-md-10 {\n order: 10;\n }\n .order-md-11 {\n order: 11;\n }\n .order-md-12 {\n order: 12;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.333333%;\n }\n .offset-md-2 {\n margin-left: 16.666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.333333%;\n }\n .offset-md-5 {\n margin-left: 41.666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.333333%;\n }\n .offset-md-8 {\n margin-left: 66.666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.333333%;\n }\n .offset-md-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 992px) {\n .col-lg {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-lg-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-lg-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-lg-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-lg-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-lg-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-lg-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-lg-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-lg-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-lg-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-lg-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-lg-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-lg-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-lg-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-lg-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-lg-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-lg-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-lg-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-lg-first {\n order: -1;\n }\n .order-lg-last {\n order: 13;\n }\n .order-lg-0 {\n order: 0;\n }\n .order-lg-1 {\n order: 1;\n }\n .order-lg-2 {\n order: 2;\n }\n .order-lg-3 {\n order: 3;\n }\n .order-lg-4 {\n order: 4;\n }\n .order-lg-5 {\n order: 5;\n }\n .order-lg-6 {\n order: 6;\n }\n .order-lg-7 {\n order: 7;\n }\n .order-lg-8 {\n order: 8;\n }\n .order-lg-9 {\n order: 9;\n }\n .order-lg-10 {\n order: 10;\n }\n .order-lg-11 {\n order: 11;\n }\n .order-lg-12 {\n order: 12;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.333333%;\n }\n .offset-lg-2 {\n margin-left: 16.666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.333333%;\n }\n .offset-lg-5 {\n margin-left: 41.666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.333333%;\n }\n .offset-lg-8 {\n margin-left: 66.666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.333333%;\n }\n .offset-lg-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 1200px) {\n .col-xl {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-xl-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-xl-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-xl-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-xl-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-xl-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-xl-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-xl-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-xl-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-xl-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-xl-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-xl-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-xl-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-xl-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-xl-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-xl-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-xl-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-xl-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-xl-first {\n order: -1;\n }\n .order-xl-last {\n order: 13;\n }\n .order-xl-0 {\n order: 0;\n }\n .order-xl-1 {\n order: 1;\n }\n .order-xl-2 {\n order: 2;\n }\n .order-xl-3 {\n order: 3;\n }\n .order-xl-4 {\n order: 4;\n }\n .order-xl-5 {\n order: 5;\n }\n .order-xl-6 {\n order: 6;\n }\n .order-xl-7 {\n order: 7;\n }\n .order-xl-8 {\n order: 8;\n }\n .order-xl-9 {\n order: 9;\n }\n .order-xl-10 {\n order: 10;\n }\n .order-xl-11 {\n order: 11;\n }\n .order-xl-12 {\n order: 12;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.333333%;\n }\n .offset-xl-2 {\n margin-left: 16.666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.333333%;\n }\n .offset-xl-5 {\n margin-left: 41.666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.333333%;\n }\n .offset-xl-8 {\n margin-left: 66.666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.333333%;\n }\n .offset-xl-11 {\n margin-left: 91.666667%;\n }\n}\n\n.table {\n width: 100%;\n margin-bottom: 1rem;\n color: #212529;\n}\n\n.table th,\n.table td {\n padding: 0.75rem;\n vertical-align: top;\n border-top: 1px solid #dee2e6;\n}\n\n.table thead th {\n vertical-align: bottom;\n border-bottom: 2px solid #dee2e6;\n}\n\n.table tbody + tbody {\n border-top: 2px solid #dee2e6;\n}\n\n.table-sm th,\n.table-sm td {\n padding: 0.3rem;\n}\n\n.table-bordered {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered th,\n.table-bordered td {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered thead th,\n.table-bordered thead td {\n border-bottom-width: 2px;\n}\n\n.table-borderless th,\n.table-borderless td,\n.table-borderless thead th,\n.table-borderless tbody + tbody {\n border: 0;\n}\n\n.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(0, 0, 0, 0.05);\n}\n\n.table-hover tbody tr:hover {\n color: #212529;\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-primary,\n.table-primary > th,\n.table-primary > td {\n background-color: #b8daff;\n}\n\n.table-primary th,\n.table-primary td,\n.table-primary thead th,\n.table-primary tbody + tbody {\n border-color: #7abaff;\n}\n\n.table-hover .table-primary:hover {\n background-color: #9fcdff;\n}\n\n.table-hover .table-primary:hover > td,\n.table-hover .table-primary:hover > th {\n background-color: #9fcdff;\n}\n\n.table-secondary,\n.table-secondary > th,\n.table-secondary > td {\n background-color: #d6d8db;\n}\n\n.table-secondary th,\n.table-secondary td,\n.table-secondary thead th,\n.table-secondary tbody + tbody {\n border-color: #b3b7bb;\n}\n\n.table-hover .table-secondary:hover {\n background-color: #c8cbcf;\n}\n\n.table-hover .table-secondary:hover > td,\n.table-hover .table-secondary:hover > th {\n background-color: #c8cbcf;\n}\n\n.table-success,\n.table-success > th,\n.table-success > td {\n background-color: #c3e6cb;\n}\n\n.table-success th,\n.table-success td,\n.table-success thead th,\n.table-success tbody + tbody {\n border-color: #8fd19e;\n}\n\n.table-hover .table-success:hover {\n background-color: #b1dfbb;\n}\n\n.table-hover .table-success:hover > td,\n.table-hover .table-success:hover > th {\n background-color: #b1dfbb;\n}\n\n.table-info,\n.table-info > th,\n.table-info > td {\n background-color: #bee5eb;\n}\n\n.table-info th,\n.table-info td,\n.table-info thead th,\n.table-info tbody + tbody {\n border-color: #86cfda;\n}\n\n.table-hover .table-info:hover {\n background-color: #abdde5;\n}\n\n.table-hover .table-info:hover > td,\n.table-hover .table-info:hover > th {\n background-color: #abdde5;\n}\n\n.table-warning,\n.table-warning > th,\n.table-warning > td {\n background-color: #ffeeba;\n}\n\n.table-warning th,\n.table-warning td,\n.table-warning thead th,\n.table-warning tbody + tbody {\n border-color: #ffdf7e;\n}\n\n.table-hover .table-warning:hover {\n background-color: #ffe8a1;\n}\n\n.table-hover .table-warning:hover > td,\n.table-hover .table-warning:hover > th {\n background-color: #ffe8a1;\n}\n\n.table-danger,\n.table-danger > th,\n.table-danger > td {\n background-color: #f5c6cb;\n}\n\n.table-danger th,\n.table-danger td,\n.table-danger thead th,\n.table-danger tbody + tbody {\n border-color: #ed969e;\n}\n\n.table-hover .table-danger:hover {\n background-color: #f1b0b7;\n}\n\n.table-hover .table-danger:hover > td,\n.table-hover .table-danger:hover > th {\n background-color: #f1b0b7;\n}\n\n.table-light,\n.table-light > th,\n.table-light > td {\n background-color: #fdfdfe;\n}\n\n.table-light th,\n.table-light td,\n.table-light thead th,\n.table-light tbody + tbody {\n border-color: #fbfcfc;\n}\n\n.table-hover .table-light:hover {\n background-color: #ececf6;\n}\n\n.table-hover .table-light:hover > td,\n.table-hover .table-light:hover > th {\n background-color: #ececf6;\n}\n\n.table-dark,\n.table-dark > th,\n.table-dark > td {\n background-color: #c6c8ca;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th,\n.table-dark tbody + tbody {\n border-color: #95999c;\n}\n\n.table-hover .table-dark:hover {\n background-color: #b9bbbe;\n}\n\n.table-hover .table-dark:hover > td,\n.table-hover .table-dark:hover > th {\n background-color: #b9bbbe;\n}\n\n.table-active,\n.table-active > th,\n.table-active > td {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover > td,\n.table-hover .table-active:hover > th {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table .thead-dark th {\n color: #fff;\n background-color: #343a40;\n border-color: #454d55;\n}\n\n.table .thead-light th {\n color: #495057;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.table-dark {\n color: #fff;\n background-color: #343a40;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th {\n border-color: #454d55;\n}\n\n.table-dark.table-bordered {\n border: 0;\n}\n\n.table-dark.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(255, 255, 255, 0.05);\n}\n\n.table-dark.table-hover tbody tr:hover {\n color: #fff;\n background-color: rgba(255, 255, 255, 0.075);\n}\n\n@media (max-width: 575.98px) {\n .table-responsive-sm {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-sm > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 767.98px) {\n .table-responsive-md {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-md > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 991.98px) {\n .table-responsive-lg {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-lg > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 1199.98px) {\n .table-responsive-xl {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-xl > .table-bordered {\n border: 0;\n }\n}\n\n.table-responsive {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n}\n\n.table-responsive > .table-bordered {\n border: 0;\n}\n\n.form-control {\n display: block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .form-control {\n transition: none;\n }\n}\n\n.form-control::-ms-expand {\n background-color: transparent;\n border: 0;\n}\n\n.form-control:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 #495057;\n}\n\n.form-control:focus {\n color: #495057;\n background-color: #fff;\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.form-control::placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control:disabled, .form-control[readonly] {\n background-color: #e9ecef;\n opacity: 1;\n}\n\nselect.form-control:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.form-control-file,\n.form-control-range {\n display: block;\n width: 100%;\n}\n\n.col-form-label {\n padding-top: calc(0.375rem + 1px);\n padding-bottom: calc(0.375rem + 1px);\n margin-bottom: 0;\n font-size: inherit;\n line-height: 1.5;\n}\n\n.col-form-label-lg {\n padding-top: calc(0.5rem + 1px);\n padding-bottom: calc(0.5rem + 1px);\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.col-form-label-sm {\n padding-top: calc(0.25rem + 1px);\n padding-bottom: calc(0.25rem + 1px);\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding: 0.375rem 0;\n margin-bottom: 0;\n font-size: 1rem;\n line-height: 1.5;\n color: #212529;\n background-color: transparent;\n border: solid transparent;\n border-width: 1px 0;\n}\n\n.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {\n padding-right: 0;\n padding-left: 0;\n}\n\n.form-control-sm {\n height: calc(1.5em + 0.5rem + 2px);\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.form-control-lg {\n height: calc(1.5em + 1rem + 2px);\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\nselect.form-control[size], select.form-control[multiple] {\n height: auto;\n}\n\ntextarea.form-control {\n height: auto;\n}\n\n.form-group {\n margin-bottom: 1rem;\n}\n\n.form-text {\n display: block;\n margin-top: 0.25rem;\n}\n\n.form-row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -5px;\n margin-left: -5px;\n}\n\n.form-row > .col,\n.form-row > [class*=\"col-\"] {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.form-check {\n position: relative;\n display: block;\n padding-left: 1.25rem;\n}\n\n.form-check-input {\n position: absolute;\n margin-top: 0.3rem;\n margin-left: -1.25rem;\n}\n\n.form-check-input[disabled] ~ .form-check-label,\n.form-check-input:disabled ~ .form-check-label {\n color: #6c757d;\n}\n\n.form-check-label {\n margin-bottom: 0;\n}\n\n.form-check-inline {\n display: inline-flex;\n align-items: center;\n padding-left: 0;\n margin-right: 0.75rem;\n}\n\n.form-check-inline .form-check-input {\n position: static;\n margin-top: 0;\n margin-right: 0.3125rem;\n margin-left: 0;\n}\n\n.valid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #28a745;\n}\n\n.valid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: .1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: #fff;\n background-color: rgba(40, 167, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated :valid ~ .valid-feedback,\n.was-validated :valid ~ .valid-tooltip,\n.is-valid ~ .valid-feedback,\n.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:valid, .form-control.is-valid {\n border-color: #28a745;\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .form-control:valid:focus, .form-control.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated textarea.form-control:valid, textarea.form-control.is-valid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .custom-select:valid, .custom-select.is-valid {\n border-color: #28a745;\n padding-right: calc(0.75em + 2.3125rem);\n background: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px, url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .custom-select:valid:focus, .custom-select.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {\n color: #28a745;\n}\n\n.was-validated .form-check-input:valid ~ .valid-feedback,\n.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback,\n.form-check-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label {\n color: #28a745;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before {\n border-color: #28a745;\n}\n\n.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before {\n border-color: #34ce57;\n background-color: #34ce57;\n}\n\n.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.invalid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #dc3545;\n}\n\n.invalid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: .1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: #fff;\n background-color: rgba(220, 53, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated :invalid ~ .invalid-feedback,\n.was-validated :invalid ~ .invalid-tooltip,\n.is-invalid ~ .invalid-feedback,\n.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:invalid, .form-control.is-invalid {\n border-color: #dc3545;\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .custom-select:invalid, .custom-select.is-invalid {\n border-color: #dc3545;\n padding-right: calc(0.75em + 2.3125rem);\n background: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px, url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {\n color: #dc3545;\n}\n\n.was-validated .form-check-input:invalid ~ .invalid-feedback,\n.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback,\n.form-check-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label {\n color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before {\n border-color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before {\n border-color: #e4606d;\n background-color: #e4606d;\n}\n\n.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.form-inline {\n display: flex;\n flex-flow: row wrap;\n align-items: center;\n}\n\n.form-inline .form-check {\n width: 100%;\n}\n\n@media (min-width: 576px) {\n .form-inline label {\n display: flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 0;\n }\n .form-inline .form-group {\n display: flex;\n flex: 0 0 auto;\n flex-flow: row wrap;\n align-items: center;\n margin-bottom: 0;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-plaintext {\n display: inline-block;\n }\n .form-inline .input-group,\n .form-inline .custom-select {\n width: auto;\n }\n .form-inline .form-check {\n display: flex;\n align-items: center;\n justify-content: center;\n width: auto;\n padding-left: 0;\n }\n .form-inline .form-check-input {\n position: relative;\n flex-shrink: 0;\n margin-top: 0;\n margin-right: 0.25rem;\n margin-left: 0;\n }\n .form-inline .custom-control {\n align-items: center;\n justify-content: center;\n }\n .form-inline .custom-control-label {\n margin-bottom: 0;\n }\n}\n\n.btn {\n display: inline-block;\n font-weight: 400;\n color: #212529;\n text-align: center;\n vertical-align: middle;\n cursor: pointer;\n user-select: none;\n background-color: transparent;\n border: 1px solid transparent;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n line-height: 1.5;\n border-radius: 0.25rem;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .btn {\n transition: none;\n }\n}\n\n.btn:hover {\n color: #212529;\n text-decoration: none;\n}\n\n.btn:focus, .btn.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.btn.disabled, .btn:disabled {\n opacity: 0.65;\n}\n\na.btn.disabled,\nfieldset:disabled a.btn {\n pointer-events: none;\n}\n\n.btn-primary {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:hover {\n color: #fff;\n background-color: #0069d9;\n border-color: #0062cc;\n}\n\n.btn-primary:focus, .btn-primary.focus {\n color: #fff;\n background-color: #0069d9;\n border-color: #0062cc;\n box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);\n}\n\n.btn-primary.disabled, .btn-primary:disabled {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active,\n.show > .btn-primary.dropdown-toggle {\n color: #fff;\n background-color: #0062cc;\n border-color: #005cbf;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);\n}\n\n.btn-secondary {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:hover {\n color: #fff;\n background-color: #5a6268;\n border-color: #545b62;\n}\n\n.btn-secondary:focus, .btn-secondary.focus {\n color: #fff;\n background-color: #5a6268;\n border-color: #545b62;\n box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);\n}\n\n.btn-secondary.disabled, .btn-secondary:disabled {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-secondary.dropdown-toggle {\n color: #fff;\n background-color: #545b62;\n border-color: #4e555b;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);\n}\n\n.btn-success {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:hover {\n color: #fff;\n background-color: #218838;\n border-color: #1e7e34;\n}\n\n.btn-success:focus, .btn-success.focus {\n color: #fff;\n background-color: #218838;\n border-color: #1e7e34;\n box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);\n}\n\n.btn-success.disabled, .btn-success:disabled {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active,\n.show > .btn-success.dropdown-toggle {\n color: #fff;\n background-color: #1e7e34;\n border-color: #1c7430;\n}\n\n.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);\n}\n\n.btn-info {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:hover {\n color: #fff;\n background-color: #138496;\n border-color: #117a8b;\n}\n\n.btn-info:focus, .btn-info.focus {\n color: #fff;\n background-color: #138496;\n border-color: #117a8b;\n box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);\n}\n\n.btn-info.disabled, .btn-info:disabled {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active,\n.show > .btn-info.dropdown-toggle {\n color: #fff;\n background-color: #117a8b;\n border-color: #10707f;\n}\n\n.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);\n}\n\n.btn-warning {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:hover {\n color: #212529;\n background-color: #e0a800;\n border-color: #d39e00;\n}\n\n.btn-warning:focus, .btn-warning.focus {\n color: #212529;\n background-color: #e0a800;\n border-color: #d39e00;\n box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);\n}\n\n.btn-warning.disabled, .btn-warning:disabled {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active,\n.show > .btn-warning.dropdown-toggle {\n color: #212529;\n background-color: #d39e00;\n border-color: #c69500;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);\n}\n\n.btn-danger {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:hover {\n color: #fff;\n background-color: #c82333;\n border-color: #bd2130;\n}\n\n.btn-danger:focus, .btn-danger.focus {\n color: #fff;\n background-color: #c82333;\n border-color: #bd2130;\n box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);\n}\n\n.btn-danger.disabled, .btn-danger:disabled {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active,\n.show > .btn-danger.dropdown-toggle {\n color: #fff;\n background-color: #bd2130;\n border-color: #b21f2d;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);\n}\n\n.btn-light {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:hover {\n color: #212529;\n background-color: #e2e6ea;\n border-color: #dae0e5;\n}\n\n.btn-light:focus, .btn-light.focus {\n color: #212529;\n background-color: #e2e6ea;\n border-color: #dae0e5;\n box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);\n}\n\n.btn-light.disabled, .btn-light:disabled {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active,\n.show > .btn-light.dropdown-toggle {\n color: #212529;\n background-color: #dae0e5;\n border-color: #d3d9df;\n}\n\n.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);\n}\n\n.btn-dark {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:hover {\n color: #fff;\n background-color: #23272b;\n border-color: #1d2124;\n}\n\n.btn-dark:focus, .btn-dark.focus {\n color: #fff;\n background-color: #23272b;\n border-color: #1d2124;\n box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);\n}\n\n.btn-dark.disabled, .btn-dark:disabled {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active,\n.show > .btn-dark.dropdown-toggle {\n color: #fff;\n background-color: #1d2124;\n border-color: #171a1d;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);\n}\n\n.btn-outline-primary {\n color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:hover {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:focus, .btn-outline-primary.focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-primary.disabled, .btn-outline-primary:disabled {\n color: #007bff;\n background-color: transparent;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-primary.dropdown-toggle {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-secondary {\n color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:hover {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:focus, .btn-outline-secondary.focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-secondary.disabled, .btn-outline-secondary:disabled {\n color: #6c757d;\n background-color: transparent;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-secondary.dropdown-toggle {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-success {\n color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:hover {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:focus, .btn-outline-success.focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-success.disabled, .btn-outline-success:disabled {\n color: #28a745;\n background-color: transparent;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active,\n.show > .btn-outline-success.dropdown-toggle {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-info {\n color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:hover {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:focus, .btn-outline-info.focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-info.disabled, .btn-outline-info:disabled {\n color: #17a2b8;\n background-color: transparent;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active,\n.show > .btn-outline-info.dropdown-toggle {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-warning {\n color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:hover {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:focus, .btn-outline-warning.focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-warning.disabled, .btn-outline-warning:disabled {\n color: #ffc107;\n background-color: transparent;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active,\n.show > .btn-outline-warning.dropdown-toggle {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-danger {\n color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:hover {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:focus, .btn-outline-danger.focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-danger.disabled, .btn-outline-danger:disabled {\n color: #dc3545;\n background-color: transparent;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active,\n.show > .btn-outline-danger.dropdown-toggle {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-light {\n color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:hover {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:focus, .btn-outline-light.focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-light.disabled, .btn-outline-light:disabled {\n color: #f8f9fa;\n background-color: transparent;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active,\n.show > .btn-outline-light.dropdown-toggle {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-dark {\n color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:hover {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:focus, .btn-outline-dark.focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-outline-dark.disabled, .btn-outline-dark:disabled {\n color: #343a40;\n background-color: transparent;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active,\n.show > .btn-outline-dark.dropdown-toggle {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-link {\n font-weight: 400;\n color: #007bff;\n text-decoration: none;\n}\n\n.btn-link:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\n.btn-link:focus, .btn-link.focus {\n text-decoration: underline;\n box-shadow: none;\n}\n\n.btn-link:disabled, .btn-link.disabled {\n color: #6c757d;\n pointer-events: none;\n}\n\n.btn-lg, .btn-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.btn-sm, .btn-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.btn-block {\n display: block;\n width: 100%;\n}\n\n.btn-block + .btn-block {\n margin-top: 0.5rem;\n}\n\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n\n.fade {\n transition: opacity 0.15s linear;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fade {\n transition: none;\n }\n}\n\n.fade:not(.show) {\n opacity: 0;\n}\n\n.collapse:not(.show) {\n display: none;\n}\n\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n transition: height 0.35s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .collapsing {\n transition: none;\n }\n}\n\n.dropup,\n.dropright,\n.dropdown,\n.dropleft {\n position: relative;\n}\n\n.dropdown-toggle {\n white-space: nowrap;\n}\n\n.dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-bottom: 0;\n border-left: 0.3em solid transparent;\n}\n\n.dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 10rem;\n padding: 0.5rem 0;\n margin: 0.125rem 0 0;\n font-size: 1rem;\n color: #212529;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 0.25rem;\n}\n\n.dropdown-menu-left {\n right: auto;\n left: 0;\n}\n\n.dropdown-menu-right {\n right: 0;\n left: auto;\n}\n\n@media (min-width: 576px) {\n .dropdown-menu-sm-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-sm-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 768px) {\n .dropdown-menu-md-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-md-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 992px) {\n .dropdown-menu-lg-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-lg-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 1200px) {\n .dropdown-menu-xl-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-xl-right {\n right: 0;\n left: auto;\n }\n}\n\n.dropup .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: 0.125rem;\n}\n\n.dropup .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0;\n border-right: 0.3em solid transparent;\n border-bottom: 0.3em solid;\n border-left: 0.3em solid transparent;\n}\n\n.dropup .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-menu {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: 0.125rem;\n}\n\n.dropright .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0;\n border-bottom: 0.3em solid transparent;\n border-left: 0.3em solid;\n}\n\n.dropright .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-toggle::after {\n vertical-align: 0;\n}\n\n.dropleft .dropdown-menu {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: 0.125rem;\n}\n\n.dropleft .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n}\n\n.dropleft .dropdown-toggle::after {\n display: none;\n}\n\n.dropleft .dropdown-toggle::before {\n display: inline-block;\n margin-right: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0.3em solid;\n border-bottom: 0.3em solid transparent;\n}\n\n.dropleft .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle::before {\n vertical-align: 0;\n}\n\n.dropdown-menu[x-placement^=\"top\"], .dropdown-menu[x-placement^=\"right\"], .dropdown-menu[x-placement^=\"bottom\"], .dropdown-menu[x-placement^=\"left\"] {\n right: auto;\n bottom: auto;\n}\n\n.dropdown-divider {\n height: 0;\n margin: 0.5rem 0;\n overflow: hidden;\n border-top: 1px solid #e9ecef;\n}\n\n.dropdown-item {\n display: block;\n width: 100%;\n padding: 0.25rem 1.5rem;\n clear: both;\n font-weight: 400;\n color: #212529;\n text-align: inherit;\n white-space: nowrap;\n background-color: transparent;\n border: 0;\n}\n\n.dropdown-item:hover, .dropdown-item:focus {\n color: #16181b;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.dropdown-item.active, .dropdown-item:active {\n color: #fff;\n text-decoration: none;\n background-color: #007bff;\n}\n\n.dropdown-item.disabled, .dropdown-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: transparent;\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n.dropdown-header {\n display: block;\n padding: 0.5rem 1.5rem;\n margin-bottom: 0;\n font-size: 0.875rem;\n color: #6c757d;\n white-space: nowrap;\n}\n\n.dropdown-item-text {\n display: block;\n padding: 0.25rem 1.5rem;\n color: #212529;\n}\n\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-flex;\n vertical-align: middle;\n}\n\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n flex: 1 1 auto;\n}\n\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover {\n z-index: 1;\n}\n\n.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active,\n.btn-group-vertical > .btn:focus,\n.btn-group-vertical > .btn:active,\n.btn-group-vertical > .btn.active {\n z-index: 1;\n}\n\n.btn-toolbar {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n}\n\n.btn-toolbar .input-group {\n width: auto;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) {\n margin-left: -1px;\n}\n\n.btn-group > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.dropdown-toggle-split {\n padding-right: 0.5625rem;\n padding-left: 0.5625rem;\n}\n\n.dropdown-toggle-split::after,\n.dropup .dropdown-toggle-split::after,\n.dropright .dropdown-toggle-split::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle-split::before {\n margin-right: 0;\n}\n\n.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {\n padding-right: 0.375rem;\n padding-left: 0.375rem;\n}\n\n.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {\n padding-right: 0.75rem;\n padding-left: 0.75rem;\n}\n\n.btn-group-vertical {\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group {\n width: 100%;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) {\n margin-top: -1px;\n}\n\n.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group-vertical > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.btn-group-toggle > .btn,\n.btn-group-toggle > .btn-group > .btn {\n margin-bottom: 0;\n}\n\n.btn-group-toggle > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn input[type=\"checkbox\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n\n.input-group {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: stretch;\n width: 100%;\n}\n\n.input-group > .form-control,\n.input-group > .form-control-plaintext,\n.input-group > .custom-select,\n.input-group > .custom-file {\n position: relative;\n flex: 1 1 0%;\n min-width: 0;\n margin-bottom: 0;\n}\n\n.input-group > .form-control + .form-control,\n.input-group > .form-control + .custom-select,\n.input-group > .form-control + .custom-file,\n.input-group > .form-control-plaintext + .form-control,\n.input-group > .form-control-plaintext + .custom-select,\n.input-group > .form-control-plaintext + .custom-file,\n.input-group > .custom-select + .form-control,\n.input-group > .custom-select + .custom-select,\n.input-group > .custom-select + .custom-file,\n.input-group > .custom-file + .form-control,\n.input-group > .custom-file + .custom-select,\n.input-group > .custom-file + .custom-file {\n margin-left: -1px;\n}\n\n.input-group > .form-control:focus,\n.input-group > .custom-select:focus,\n.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label {\n z-index: 3;\n}\n\n.input-group > .custom-file .custom-file-input:focus {\n z-index: 4;\n}\n\n.input-group > .form-control:not(:last-child),\n.input-group > .custom-select:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .form-control:not(:first-child),\n.input-group > .custom-select:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group > .custom-file {\n display: flex;\n align-items: center;\n}\n\n.input-group > .custom-file:not(:last-child) .custom-file-label,\n.input-group > .custom-file:not(:last-child) .custom-file-label::after {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .custom-file:not(:first-child) .custom-file-label {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group-prepend,\n.input-group-append {\n display: flex;\n}\n\n.input-group-prepend .btn,\n.input-group-append .btn {\n position: relative;\n z-index: 2;\n}\n\n.input-group-prepend .btn:focus,\n.input-group-append .btn:focus {\n z-index: 3;\n}\n\n.input-group-prepend .btn + .btn,\n.input-group-prepend .btn + .input-group-text,\n.input-group-prepend .input-group-text + .input-group-text,\n.input-group-prepend .input-group-text + .btn,\n.input-group-append .btn + .btn,\n.input-group-append .btn + .input-group-text,\n.input-group-append .input-group-text + .input-group-text,\n.input-group-append .input-group-text + .btn {\n margin-left: -1px;\n}\n\n.input-group-prepend {\n margin-right: -1px;\n}\n\n.input-group-append {\n margin-left: -1px;\n}\n\n.input-group-text {\n display: flex;\n align-items: center;\n padding: 0.375rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n text-align: center;\n white-space: nowrap;\n background-color: #e9ecef;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.input-group-text input[type=\"radio\"],\n.input-group-text input[type=\"checkbox\"] {\n margin-top: 0;\n}\n\n.input-group-lg > .form-control:not(textarea),\n.input-group-lg > .custom-select {\n height: calc(1.5em + 1rem + 2px);\n}\n\n.input-group-lg > .form-control,\n.input-group-lg > .custom-select,\n.input-group-lg > .input-group-prepend > .input-group-text,\n.input-group-lg > .input-group-append > .input-group-text,\n.input-group-lg > .input-group-prepend > .btn,\n.input-group-lg > .input-group-append > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.input-group-sm > .form-control:not(textarea),\n.input-group-sm > .custom-select {\n height: calc(1.5em + 0.5rem + 2px);\n}\n\n.input-group-sm > .form-control,\n.input-group-sm > .custom-select,\n.input-group-sm > .input-group-prepend > .input-group-text,\n.input-group-sm > .input-group-append > .input-group-text,\n.input-group-sm > .input-group-prepend > .btn,\n.input-group-sm > .input-group-append > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.input-group-lg > .custom-select,\n.input-group-sm > .custom-select {\n padding-right: 1.75rem;\n}\n\n.input-group > .input-group-prepend > .btn,\n.input-group > .input-group-prepend > .input-group-text,\n.input-group > .input-group-append:not(:last-child) > .btn,\n.input-group > .input-group-append:not(:last-child) > .input-group-text,\n.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .input-group-append > .btn,\n.input-group > .input-group-append > .input-group-text,\n.input-group > .input-group-prepend:not(:first-child) > .btn,\n.input-group > .input-group-prepend:not(:first-child) > .input-group-text,\n.input-group > .input-group-prepend:first-child > .btn:not(:first-child),\n.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.custom-control {\n position: relative;\n display: block;\n min-height: 1.5rem;\n padding-left: 1.5rem;\n}\n\n.custom-control-inline {\n display: inline-flex;\n margin-right: 1rem;\n}\n\n.custom-control-input {\n position: absolute;\n left: 0;\n z-index: -1;\n width: 1rem;\n height: 1.25rem;\n opacity: 0;\n}\n\n.custom-control-input:checked ~ .custom-control-label::before {\n color: #fff;\n border-color: #007bff;\n background-color: #007bff;\n}\n\n.custom-control-input:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-control-input:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #80bdff;\n}\n\n.custom-control-input:not(:disabled):active ~ .custom-control-label::before {\n color: #fff;\n background-color: #b3d7ff;\n border-color: #b3d7ff;\n}\n\n.custom-control-input[disabled] ~ .custom-control-label, .custom-control-input:disabled ~ .custom-control-label {\n color: #6c757d;\n}\n\n.custom-control-input[disabled] ~ .custom-control-label::before, .custom-control-input:disabled ~ .custom-control-label::before {\n background-color: #e9ecef;\n}\n\n.custom-control-label {\n position: relative;\n margin-bottom: 0;\n vertical-align: top;\n}\n\n.custom-control-label::before {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n pointer-events: none;\n content: \"\";\n background-color: #fff;\n border: #adb5bd solid 1px;\n}\n\n.custom-control-label::after {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n content: \"\";\n background: no-repeat 50% / 50% 50%;\n}\n\n.custom-checkbox .custom-control-label::before {\n border-radius: 0.25rem;\n}\n\n.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e\");\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before {\n border-color: #007bff;\n background-color: #007bff;\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e\");\n}\n\n.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-radio .custom-control-label::before {\n border-radius: 50%;\n}\n\n.custom-radio .custom-control-input:checked ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e\");\n}\n\n.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-switch {\n padding-left: 2.25rem;\n}\n\n.custom-switch .custom-control-label::before {\n left: -2.25rem;\n width: 1.75rem;\n pointer-events: all;\n border-radius: 0.5rem;\n}\n\n.custom-switch .custom-control-label::after {\n top: calc(0.25rem + 2px);\n left: calc(-2.25rem + 2px);\n width: calc(1rem - 4px);\n height: calc(1rem - 4px);\n background-color: #adb5bd;\n border-radius: 0.5rem;\n transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-switch .custom-control-label::after {\n transition: none;\n }\n}\n\n.custom-switch .custom-control-input:checked ~ .custom-control-label::after {\n background-color: #fff;\n transform: translateX(0.75rem);\n}\n\n.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-select {\n display: inline-block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 1.75rem 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n vertical-align: middle;\n background: #fff url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n appearance: none;\n}\n\n.custom-select:focus {\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-select:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.custom-select[multiple], .custom-select[size]:not([size=\"1\"]) {\n height: auto;\n padding-right: 0.75rem;\n background-image: none;\n}\n\n.custom-select:disabled {\n color: #6c757d;\n background-color: #e9ecef;\n}\n\n.custom-select::-ms-expand {\n display: none;\n}\n\n.custom-select:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 #495057;\n}\n\n.custom-select-sm {\n height: calc(1.5em + 0.5rem + 2px);\n padding-top: 0.25rem;\n padding-bottom: 0.25rem;\n padding-left: 0.5rem;\n font-size: 0.875rem;\n}\n\n.custom-select-lg {\n height: calc(1.5em + 1rem + 2px);\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n padding-left: 1rem;\n font-size: 1.25rem;\n}\n\n.custom-file {\n position: relative;\n display: inline-block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n margin-bottom: 0;\n}\n\n.custom-file-input {\n position: relative;\n z-index: 2;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n margin: 0;\n opacity: 0;\n}\n\n.custom-file-input:focus ~ .custom-file-label {\n border-color: #80bdff;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-file-input[disabled] ~ .custom-file-label,\n.custom-file-input:disabled ~ .custom-file-label {\n background-color: #e9ecef;\n}\n\n.custom-file-input:lang(en) ~ .custom-file-label::after {\n content: \"Browse\";\n}\n\n.custom-file-input ~ .custom-file-label[data-browse]::after {\n content: attr(data-browse);\n}\n\n.custom-file-label {\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 0.75rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.custom-file-label::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n z-index: 3;\n display: block;\n height: calc(1.5em + 0.75rem);\n padding: 0.375rem 0.75rem;\n line-height: 1.5;\n color: #495057;\n content: \"Browse\";\n background-color: #e9ecef;\n border-left: inherit;\n border-radius: 0 0.25rem 0.25rem 0;\n}\n\n.custom-range {\n width: 100%;\n height: 1.4rem;\n padding: 0;\n background-color: transparent;\n appearance: none;\n}\n\n.custom-range:focus {\n outline: none;\n}\n\n.custom-range:focus::-webkit-slider-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range:focus::-moz-range-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range:focus::-ms-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range::-moz-focus-outer {\n border: 0;\n}\n\n.custom-range::-webkit-slider-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: -0.25rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-webkit-slider-thumb {\n transition: none;\n }\n}\n\n.custom-range::-webkit-slider-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-webkit-slider-runnable-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-moz-range-thumb {\n width: 1rem;\n height: 1rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-moz-range-thumb {\n transition: none;\n }\n}\n\n.custom-range::-moz-range-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-moz-range-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: 0;\n margin-right: 0.2rem;\n margin-left: 0.2rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-ms-thumb {\n transition: none;\n }\n}\n\n.custom-range::-ms-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-ms-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: transparent;\n border-color: transparent;\n border-width: 0.5rem;\n}\n\n.custom-range::-ms-fill-lower {\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-fill-upper {\n margin-right: 15px;\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range:disabled::-webkit-slider-thumb {\n background-color: #adb5bd;\n}\n\n.custom-range:disabled::-webkit-slider-runnable-track {\n cursor: default;\n}\n\n.custom-range:disabled::-moz-range-thumb {\n background-color: #adb5bd;\n}\n\n.custom-range:disabled::-moz-range-track {\n cursor: default;\n}\n\n.custom-range:disabled::-ms-thumb {\n background-color: #adb5bd;\n}\n\n.custom-control-label::before,\n.custom-file-label,\n.custom-select {\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-control-label::before,\n .custom-file-label,\n .custom-select {\n transition: none;\n }\n}\n\n.nav {\n display: flex;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: 0.5rem 1rem;\n}\n\n.nav-link:hover, .nav-link:focus {\n text-decoration: none;\n}\n\n.nav-link.disabled {\n color: #6c757d;\n pointer-events: none;\n cursor: default;\n}\n\n.nav-tabs {\n border-bottom: 1px solid #dee2e6;\n}\n\n.nav-tabs .nav-item {\n margin-bottom: -1px;\n}\n\n.nav-tabs .nav-link {\n border: 1px solid transparent;\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {\n border-color: #e9ecef #e9ecef #dee2e6;\n}\n\n.nav-tabs .nav-link.disabled {\n color: #6c757d;\n background-color: transparent;\n border-color: transparent;\n}\n\n.nav-tabs .nav-link.active,\n.nav-tabs .nav-item.show .nav-link {\n color: #495057;\n background-color: #fff;\n border-color: #dee2e6 #dee2e6 #fff;\n}\n\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav-pills .nav-link {\n border-radius: 0.25rem;\n}\n\n.nav-pills .nav-link.active,\n.nav-pills .show > .nav-link {\n color: #fff;\n background-color: #007bff;\n}\n\n.nav-fill .nav-item {\n flex: 1 1 auto;\n text-align: center;\n}\n\n.nav-justified .nav-item {\n flex-basis: 0;\n flex-grow: 1;\n text-align: center;\n}\n\n.tab-content > .tab-pane {\n display: none;\n}\n\n.tab-content > .active {\n display: block;\n}\n\n.navbar {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n padding: 0.5rem 1rem;\n}\n\n.navbar .container,\n.navbar .container-fluid, .navbar .container-sm, .navbar .container-md, .navbar .container-lg, .navbar .container-xl {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n}\n\n.navbar-brand {\n display: inline-block;\n padding-top: 0.3125rem;\n padding-bottom: 0.3125rem;\n margin-right: 1rem;\n font-size: 1.25rem;\n line-height: inherit;\n white-space: nowrap;\n}\n\n.navbar-brand:hover, .navbar-brand:focus {\n text-decoration: none;\n}\n\n.navbar-nav {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.navbar-nav .nav-link {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-nav .dropdown-menu {\n position: static;\n float: none;\n}\n\n.navbar-text {\n display: inline-block;\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n\n.navbar-collapse {\n flex-basis: 100%;\n flex-grow: 1;\n align-items: center;\n}\n\n.navbar-toggler {\n padding: 0.25rem 0.75rem;\n font-size: 1.25rem;\n line-height: 1;\n background-color: transparent;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.navbar-toggler:hover, .navbar-toggler:focus {\n text-decoration: none;\n}\n\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n content: \"\";\n background: no-repeat center center;\n background-size: 100% 100%;\n}\n\n@media (max-width: 575.98px) {\n .navbar-expand-sm > .container,\n .navbar-expand-sm > .container-fluid, .navbar-expand-sm > .container-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .navbar-expand-sm > .container-xl {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 576px) {\n .navbar-expand-sm {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-sm .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-sm .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-sm .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-sm > .container,\n .navbar-expand-sm > .container-fluid, .navbar-expand-sm > .container-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .navbar-expand-sm > .container-xl {\n flex-wrap: nowrap;\n }\n .navbar-expand-sm .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-sm .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 767.98px) {\n .navbar-expand-md > .container,\n .navbar-expand-md > .container-fluid, .navbar-expand-md > .container-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .navbar-expand-md > .container-xl {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 768px) {\n .navbar-expand-md {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-md .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-md .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-md .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-md > .container,\n .navbar-expand-md > .container-fluid, .navbar-expand-md > .container-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .navbar-expand-md > .container-xl {\n flex-wrap: nowrap;\n }\n .navbar-expand-md .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-md .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 991.98px) {\n .navbar-expand-lg > .container,\n .navbar-expand-lg > .container-fluid, .navbar-expand-lg > .container-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .navbar-expand-lg > .container-xl {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 992px) {\n .navbar-expand-lg {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-lg .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-lg .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-lg .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-lg > .container,\n .navbar-expand-lg > .container-fluid, .navbar-expand-lg > .container-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .navbar-expand-lg > .container-xl {\n flex-wrap: nowrap;\n }\n .navbar-expand-lg .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-lg .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 1199.98px) {\n .navbar-expand-xl > .container,\n .navbar-expand-xl > .container-fluid, .navbar-expand-xl > .container-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .navbar-expand-xl > .container-xl {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 1200px) {\n .navbar-expand-xl {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-xl .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-xl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xl .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-xl > .container,\n .navbar-expand-xl > .container-fluid, .navbar-expand-xl > .container-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .navbar-expand-xl > .container-xl {\n flex-wrap: nowrap;\n }\n .navbar-expand-xl .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-xl .navbar-toggler {\n display: none;\n }\n}\n\n.navbar-expand {\n flex-flow: row nowrap;\n justify-content: flex-start;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-expand .navbar-nav {\n flex-direction: row;\n}\n\n.navbar-expand .navbar-nav .dropdown-menu {\n position: absolute;\n}\n\n.navbar-expand .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl {\n flex-wrap: nowrap;\n}\n\n.navbar-expand .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n}\n\n.navbar-expand .navbar-toggler {\n display: none;\n}\n\n.navbar-light .navbar-brand {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-nav .nav-link {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus {\n color: rgba(0, 0, 0, 0.7);\n}\n\n.navbar-light .navbar-nav .nav-link.disabled {\n color: rgba(0, 0, 0, 0.3);\n}\n\n.navbar-light .navbar-nav .show > .nav-link,\n.navbar-light .navbar-nav .active > .nav-link,\n.navbar-light .navbar-nav .nav-link.show,\n.navbar-light .navbar-nav .nav-link.active {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-toggler {\n color: rgba(0, 0, 0, 0.5);\n border-color: rgba(0, 0, 0, 0.1);\n}\n\n.navbar-light .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n.navbar-light .navbar-text {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-text a {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-dark .navbar-brand {\n color: #fff;\n}\n\n.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus {\n color: #fff;\n}\n\n.navbar-dark .navbar-nav .nav-link {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {\n color: rgba(255, 255, 255, 0.75);\n}\n\n.navbar-dark .navbar-nav .nav-link.disabled {\n color: rgba(255, 255, 255, 0.25);\n}\n\n.navbar-dark .navbar-nav .show > .nav-link,\n.navbar-dark .navbar-nav .active > .nav-link,\n.navbar-dark .navbar-nav .nav-link.show,\n.navbar-dark .navbar-nav .nav-link.active {\n color: #fff;\n}\n\n.navbar-dark .navbar-toggler {\n color: rgba(255, 255, 255, 0.5);\n border-color: rgba(255, 255, 255, 0.1);\n}\n\n.navbar-dark .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n.navbar-dark .navbar-text {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-text a {\n color: #fff;\n}\n\n.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus {\n color: #fff;\n}\n\n.card {\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: border-box;\n border: 1px solid rgba(0, 0, 0, 0.125);\n border-radius: 0.25rem;\n}\n\n.card > hr {\n margin-right: 0;\n margin-left: 0;\n}\n\n.card > .list-group:first-child .list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.card > .list-group:last-child .list-group-item:last-child {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.card-body {\n flex: 1 1 auto;\n min-height: 1px;\n padding: 1.25rem;\n}\n\n.card-title {\n margin-bottom: 0.75rem;\n}\n\n.card-subtitle {\n margin-top: -0.375rem;\n margin-bottom: 0;\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link:hover {\n text-decoration: none;\n}\n\n.card-link + .card-link {\n margin-left: 1.25rem;\n}\n\n.card-header {\n padding: 0.75rem 1.25rem;\n margin-bottom: 0;\n background-color: rgba(0, 0, 0, 0.03);\n border-bottom: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-header:first-child {\n border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0;\n}\n\n.card-header + .list-group .list-group-item:first-child {\n border-top: 0;\n}\n\n.card-footer {\n padding: 0.75rem 1.25rem;\n background-color: rgba(0, 0, 0, 0.03);\n border-top: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-footer:last-child {\n border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px);\n}\n\n.card-header-tabs {\n margin-right: -0.625rem;\n margin-bottom: -0.75rem;\n margin-left: -0.625rem;\n border-bottom: 0;\n}\n\n.card-header-pills {\n margin-right: -0.625rem;\n margin-left: -0.625rem;\n}\n\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: 1.25rem;\n}\n\n.card-img,\n.card-img-top,\n.card-img-bottom {\n flex-shrink: 0;\n width: 100%;\n}\n\n.card-img,\n.card-img-top {\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.card-img,\n.card-img-bottom {\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n\n.card-deck .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-deck {\n display: flex;\n flex-flow: row wrap;\n margin-right: -15px;\n margin-left: -15px;\n }\n .card-deck .card {\n flex: 1 0 0%;\n margin-right: 15px;\n margin-bottom: 0;\n margin-left: 15px;\n }\n}\n\n.card-group > .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-group {\n display: flex;\n flex-flow: row wrap;\n }\n .card-group > .card {\n flex: 1 0 0%;\n margin-bottom: 0;\n }\n .card-group > .card + .card {\n margin-left: 0;\n border-left: 0;\n }\n .card-group > .card:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-top,\n .card-group > .card:not(:last-child) .card-header {\n border-top-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-bottom,\n .card-group > .card:not(:last-child) .card-footer {\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-top,\n .card-group > .card:not(:first-child) .card-header {\n border-top-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-bottom,\n .card-group > .card:not(:first-child) .card-footer {\n border-bottom-left-radius: 0;\n }\n}\n\n.card-columns .card {\n margin-bottom: 0.75rem;\n}\n\n@media (min-width: 576px) {\n .card-columns {\n column-count: 3;\n column-gap: 1.25rem;\n orphans: 1;\n widows: 1;\n }\n .card-columns .card {\n display: inline-block;\n width: 100%;\n }\n}\n\n.accordion > .card {\n overflow: hidden;\n}\n\n.accordion > .card:not(:last-of-type) {\n border-bottom: 0;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.accordion > .card:not(:first-of-type) {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.accordion > .card > .card-header {\n border-radius: 0;\n margin-bottom: -1px;\n}\n\n.breadcrumb {\n display: flex;\n flex-wrap: wrap;\n padding: 0.75rem 1rem;\n margin-bottom: 1rem;\n list-style: none;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.breadcrumb-item + .breadcrumb-item {\n padding-left: 0.5rem;\n}\n\n.breadcrumb-item + .breadcrumb-item::before {\n display: inline-block;\n padding-right: 0.5rem;\n color: #6c757d;\n content: \"/\";\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: underline;\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: none;\n}\n\n.breadcrumb-item.active {\n color: #6c757d;\n}\n\n.pagination {\n display: flex;\n padding-left: 0;\n list-style: none;\n border-radius: 0.25rem;\n}\n\n.page-link {\n position: relative;\n display: block;\n padding: 0.5rem 0.75rem;\n margin-left: -1px;\n line-height: 1.25;\n color: #007bff;\n background-color: #fff;\n border: 1px solid #dee2e6;\n}\n\n.page-link:hover {\n z-index: 2;\n color: #0056b3;\n text-decoration: none;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.page-link:focus {\n z-index: 3;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.page-item:first-child .page-link {\n margin-left: 0;\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.page-item:last-child .page-link {\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n}\n\n.page-item.active .page-link {\n z-index: 3;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.page-item.disabled .page-link {\n color: #6c757d;\n pointer-events: none;\n cursor: auto;\n background-color: #fff;\n border-color: #dee2e6;\n}\n\n.pagination-lg .page-link {\n padding: 0.75rem 1.5rem;\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.pagination-lg .page-item:first-child .page-link {\n border-top-left-radius: 0.3rem;\n border-bottom-left-radius: 0.3rem;\n}\n\n.pagination-lg .page-item:last-child .page-link {\n border-top-right-radius: 0.3rem;\n border-bottom-right-radius: 0.3rem;\n}\n\n.pagination-sm .page-link {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.pagination-sm .page-item:first-child .page-link {\n border-top-left-radius: 0.2rem;\n border-bottom-left-radius: 0.2rem;\n}\n\n.pagination-sm .page-item:last-child .page-link {\n border-top-right-radius: 0.2rem;\n border-bottom-right-radius: 0.2rem;\n}\n\n.badge {\n display: inline-block;\n padding: 0.25em 0.4em;\n font-size: 75%;\n font-weight: 700;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25rem;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .badge {\n transition: none;\n }\n}\n\na.badge:hover, a.badge:focus {\n text-decoration: none;\n}\n\n.badge:empty {\n display: none;\n}\n\n.btn .badge {\n position: relative;\n top: -1px;\n}\n\n.badge-pill {\n padding-right: 0.6em;\n padding-left: 0.6em;\n border-radius: 10rem;\n}\n\n.badge-primary {\n color: #fff;\n background-color: #007bff;\n}\n\na.badge-primary:hover, a.badge-primary:focus {\n color: #fff;\n background-color: #0062cc;\n}\n\na.badge-primary:focus, a.badge-primary.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.badge-secondary {\n color: #fff;\n background-color: #6c757d;\n}\n\na.badge-secondary:hover, a.badge-secondary:focus {\n color: #fff;\n background-color: #545b62;\n}\n\na.badge-secondary:focus, a.badge-secondary.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.badge-success {\n color: #fff;\n background-color: #28a745;\n}\n\na.badge-success:hover, a.badge-success:focus {\n color: #fff;\n background-color: #1e7e34;\n}\n\na.badge-success:focus, a.badge-success.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.badge-info {\n color: #fff;\n background-color: #17a2b8;\n}\n\na.badge-info:hover, a.badge-info:focus {\n color: #fff;\n background-color: #117a8b;\n}\n\na.badge-info:focus, a.badge-info.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.badge-warning {\n color: #212529;\n background-color: #ffc107;\n}\n\na.badge-warning:hover, a.badge-warning:focus {\n color: #212529;\n background-color: #d39e00;\n}\n\na.badge-warning:focus, a.badge-warning.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.badge-danger {\n color: #fff;\n background-color: #dc3545;\n}\n\na.badge-danger:hover, a.badge-danger:focus {\n color: #fff;\n background-color: #bd2130;\n}\n\na.badge-danger:focus, a.badge-danger.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.badge-light {\n color: #212529;\n background-color: #f8f9fa;\n}\n\na.badge-light:hover, a.badge-light:focus {\n color: #212529;\n background-color: #dae0e5;\n}\n\na.badge-light:focus, a.badge-light.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.badge-dark {\n color: #fff;\n background-color: #343a40;\n}\n\na.badge-dark:hover, a.badge-dark:focus {\n color: #fff;\n background-color: #1d2124;\n}\n\na.badge-dark:focus, a.badge-dark.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.jumbotron {\n padding: 2rem 1rem;\n margin-bottom: 2rem;\n background-color: #e9ecef;\n border-radius: 0.3rem;\n}\n\n@media (min-width: 576px) {\n .jumbotron {\n padding: 4rem 2rem;\n }\n}\n\n.jumbotron-fluid {\n padding-right: 0;\n padding-left: 0;\n border-radius: 0;\n}\n\n.alert {\n position: relative;\n padding: 0.75rem 1.25rem;\n margin-bottom: 1rem;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.alert-heading {\n color: inherit;\n}\n\n.alert-link {\n font-weight: 700;\n}\n\n.alert-dismissible {\n padding-right: 4rem;\n}\n\n.alert-dismissible .close {\n position: absolute;\n top: 0;\n right: 0;\n padding: 0.75rem 1.25rem;\n color: inherit;\n}\n\n.alert-primary {\n color: #004085;\n background-color: #cce5ff;\n border-color: #b8daff;\n}\n\n.alert-primary hr {\n border-top-color: #9fcdff;\n}\n\n.alert-primary .alert-link {\n color: #002752;\n}\n\n.alert-secondary {\n color: #383d41;\n background-color: #e2e3e5;\n border-color: #d6d8db;\n}\n\n.alert-secondary hr {\n border-top-color: #c8cbcf;\n}\n\n.alert-secondary .alert-link {\n color: #202326;\n}\n\n.alert-success {\n color: #155724;\n background-color: #d4edda;\n border-color: #c3e6cb;\n}\n\n.alert-success hr {\n border-top-color: #b1dfbb;\n}\n\n.alert-success .alert-link {\n color: #0b2e13;\n}\n\n.alert-info {\n color: #0c5460;\n background-color: #d1ecf1;\n border-color: #bee5eb;\n}\n\n.alert-info hr {\n border-top-color: #abdde5;\n}\n\n.alert-info .alert-link {\n color: #062c33;\n}\n\n.alert-warning {\n color: #856404;\n background-color: #fff3cd;\n border-color: #ffeeba;\n}\n\n.alert-warning hr {\n border-top-color: #ffe8a1;\n}\n\n.alert-warning .alert-link {\n color: #533f03;\n}\n\n.alert-danger {\n color: #721c24;\n background-color: #f8d7da;\n border-color: #f5c6cb;\n}\n\n.alert-danger hr {\n border-top-color: #f1b0b7;\n}\n\n.alert-danger .alert-link {\n color: #491217;\n}\n\n.alert-light {\n color: #818182;\n background-color: #fefefe;\n border-color: #fdfdfe;\n}\n\n.alert-light hr {\n border-top-color: #ececf6;\n}\n\n.alert-light .alert-link {\n color: #686868;\n}\n\n.alert-dark {\n color: #1b1e21;\n background-color: #d6d8d9;\n border-color: #c6c8ca;\n}\n\n.alert-dark hr {\n border-top-color: #b9bbbe;\n}\n\n.alert-dark .alert-link {\n color: #040505;\n}\n\n@keyframes progress-bar-stripes {\n from {\n background-position: 1rem 0;\n }\n to {\n background-position: 0 0;\n }\n}\n\n.progress {\n display: flex;\n height: 1rem;\n overflow: hidden;\n font-size: 0.75rem;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.progress-bar {\n display: flex;\n flex-direction: column;\n justify-content: center;\n overflow: hidden;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n background-color: #007bff;\n transition: width 0.6s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .progress-bar {\n transition: none;\n }\n}\n\n.progress-bar-striped {\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 1rem 1rem;\n}\n\n.progress-bar-animated {\n animation: progress-bar-stripes 1s linear infinite;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .progress-bar-animated {\n animation: none;\n }\n}\n\n.media {\n display: flex;\n align-items: flex-start;\n}\n\n.media-body {\n flex: 1;\n}\n\n.list-group {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n}\n\n.list-group-item-action {\n width: 100%;\n color: #495057;\n text-align: inherit;\n}\n\n.list-group-item-action:hover, .list-group-item-action:focus {\n z-index: 1;\n color: #495057;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.list-group-item-action:active {\n color: #212529;\n background-color: #e9ecef;\n}\n\n.list-group-item {\n position: relative;\n display: block;\n padding: 0.75rem 1.25rem;\n background-color: #fff;\n border: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.list-group-item:last-child {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.list-group-item.disabled, .list-group-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: #fff;\n}\n\n.list-group-item.active {\n z-index: 2;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.list-group-item + .list-group-item {\n border-top-width: 0;\n}\n\n.list-group-item + .list-group-item.active {\n margin-top: -1px;\n border-top-width: 1px;\n}\n\n.list-group-horizontal {\n flex-direction: row;\n}\n\n.list-group-horizontal .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n}\n\n.list-group-horizontal .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n}\n\n.list-group-horizontal .list-group-item.active {\n margin-top: 0;\n}\n\n.list-group-horizontal .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n}\n\n.list-group-horizontal .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n}\n\n@media (min-width: 576px) {\n .list-group-horizontal-sm {\n flex-direction: row;\n }\n .list-group-horizontal-sm .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-sm .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-sm .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-sm .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-sm .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n@media (min-width: 768px) {\n .list-group-horizontal-md {\n flex-direction: row;\n }\n .list-group-horizontal-md .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-md .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-md .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-md .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-md .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n@media (min-width: 992px) {\n .list-group-horizontal-lg {\n flex-direction: row;\n }\n .list-group-horizontal-lg .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-lg .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-lg .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-lg .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-lg .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n@media (min-width: 1200px) {\n .list-group-horizontal-xl {\n flex-direction: row;\n }\n .list-group-horizontal-xl .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-xl .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-xl .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-xl .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-xl .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n.list-group-flush .list-group-item {\n border-right-width: 0;\n border-left-width: 0;\n border-radius: 0;\n}\n\n.list-group-flush .list-group-item:first-child {\n border-top-width: 0;\n}\n\n.list-group-flush:last-child .list-group-item:last-child {\n border-bottom-width: 0;\n}\n\n.list-group-item-primary {\n color: #004085;\n background-color: #b8daff;\n}\n\n.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus {\n color: #004085;\n background-color: #9fcdff;\n}\n\n.list-group-item-primary.list-group-item-action.active {\n color: #fff;\n background-color: #004085;\n border-color: #004085;\n}\n\n.list-group-item-secondary {\n color: #383d41;\n background-color: #d6d8db;\n}\n\n.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus {\n color: #383d41;\n background-color: #c8cbcf;\n}\n\n.list-group-item-secondary.list-group-item-action.active {\n color: #fff;\n background-color: #383d41;\n border-color: #383d41;\n}\n\n.list-group-item-success {\n color: #155724;\n background-color: #c3e6cb;\n}\n\n.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus {\n color: #155724;\n background-color: #b1dfbb;\n}\n\n.list-group-item-success.list-group-item-action.active {\n color: #fff;\n background-color: #155724;\n border-color: #155724;\n}\n\n.list-group-item-info {\n color: #0c5460;\n background-color: #bee5eb;\n}\n\n.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {\n color: #0c5460;\n background-color: #abdde5;\n}\n\n.list-group-item-info.list-group-item-action.active {\n color: #fff;\n background-color: #0c5460;\n border-color: #0c5460;\n}\n\n.list-group-item-warning {\n color: #856404;\n background-color: #ffeeba;\n}\n\n.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus {\n color: #856404;\n background-color: #ffe8a1;\n}\n\n.list-group-item-warning.list-group-item-action.active {\n color: #fff;\n background-color: #856404;\n border-color: #856404;\n}\n\n.list-group-item-danger {\n color: #721c24;\n background-color: #f5c6cb;\n}\n\n.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus {\n color: #721c24;\n background-color: #f1b0b7;\n}\n\n.list-group-item-danger.list-group-item-action.active {\n color: #fff;\n background-color: #721c24;\n border-color: #721c24;\n}\n\n.list-group-item-light {\n color: #818182;\n background-color: #fdfdfe;\n}\n\n.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus {\n color: #818182;\n background-color: #ececf6;\n}\n\n.list-group-item-light.list-group-item-action.active {\n color: #fff;\n background-color: #818182;\n border-color: #818182;\n}\n\n.list-group-item-dark {\n color: #1b1e21;\n background-color: #c6c8ca;\n}\n\n.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus {\n color: #1b1e21;\n background-color: #b9bbbe;\n}\n\n.list-group-item-dark.list-group-item-action.active {\n color: #fff;\n background-color: #1b1e21;\n border-color: #1b1e21;\n}\n\n.close {\n float: right;\n font-size: 1.5rem;\n font-weight: 700;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n opacity: .5;\n}\n\n.close:hover {\n color: #000;\n text-decoration: none;\n}\n\n.close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus {\n opacity: .75;\n}\n\nbutton.close {\n padding: 0;\n background-color: transparent;\n border: 0;\n appearance: none;\n}\n\na.close.disabled {\n pointer-events: none;\n}\n\n.toast {\n max-width: 350px;\n overflow: hidden;\n font-size: 0.875rem;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.1);\n box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1);\n backdrop-filter: blur(10px);\n opacity: 0;\n border-radius: 0.25rem;\n}\n\n.toast:not(:last-child) {\n margin-bottom: 0.75rem;\n}\n\n.toast.showing {\n opacity: 1;\n}\n\n.toast.show {\n display: block;\n opacity: 1;\n}\n\n.toast.hide {\n display: none;\n}\n\n.toast-header {\n display: flex;\n align-items: center;\n padding: 0.25rem 0.75rem;\n color: #6c757d;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n}\n\n.toast-body {\n padding: 0.75rem;\n}\n\n.modal-open {\n overflow: hidden;\n}\n\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n.modal {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1050;\n display: none;\n width: 100%;\n height: 100%;\n overflow: hidden;\n outline: 0;\n}\n\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 0.5rem;\n pointer-events: none;\n}\n\n.modal.fade .modal-dialog {\n transition: transform 0.3s ease-out;\n transform: translate(0, -50px);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .modal.fade .modal-dialog {\n transition: none;\n }\n}\n\n.modal.show .modal-dialog {\n transform: none;\n}\n\n.modal.modal-static .modal-dialog {\n transform: scale(1.02);\n}\n\n.modal-dialog-scrollable {\n display: flex;\n max-height: calc(100% - 1rem);\n}\n\n.modal-dialog-scrollable .modal-content {\n max-height: calc(100vh - 1rem);\n overflow: hidden;\n}\n\n.modal-dialog-scrollable .modal-header,\n.modal-dialog-scrollable .modal-footer {\n flex-shrink: 0;\n}\n\n.modal-dialog-scrollable .modal-body {\n overflow-y: auto;\n}\n\n.modal-dialog-centered {\n display: flex;\n align-items: center;\n min-height: calc(100% - 1rem);\n}\n\n.modal-dialog-centered::before {\n display: block;\n height: calc(100vh - 1rem);\n content: \"\";\n}\n\n.modal-dialog-centered.modal-dialog-scrollable {\n flex-direction: column;\n justify-content: center;\n height: 100%;\n}\n\n.modal-dialog-centered.modal-dialog-scrollable .modal-content {\n max-height: none;\n}\n\n.modal-dialog-centered.modal-dialog-scrollable::before {\n content: none;\n}\n\n.modal-content {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n pointer-events: auto;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n outline: 0;\n}\n\n.modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1040;\n width: 100vw;\n height: 100vh;\n background-color: #000;\n}\n\n.modal-backdrop.fade {\n opacity: 0;\n}\n\n.modal-backdrop.show {\n opacity: 0.5;\n}\n\n.modal-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n padding: 1rem 1rem;\n border-bottom: 1px solid #dee2e6;\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n\n.modal-header .close {\n padding: 1rem 1rem;\n margin: -1rem -1rem -1rem auto;\n}\n\n.modal-title {\n margin-bottom: 0;\n line-height: 1.5;\n}\n\n.modal-body {\n position: relative;\n flex: 1 1 auto;\n padding: 1rem;\n}\n\n.modal-footer {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: flex-end;\n padding: 0.75rem;\n border-top: 1px solid #dee2e6;\n border-bottom-right-radius: calc(0.3rem - 1px);\n border-bottom-left-radius: calc(0.3rem - 1px);\n}\n\n.modal-footer > * {\n margin: 0.25rem;\n}\n\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n\n@media (min-width: 576px) {\n .modal-dialog {\n max-width: 500px;\n margin: 1.75rem auto;\n }\n .modal-dialog-scrollable {\n max-height: calc(100% - 3.5rem);\n }\n .modal-dialog-scrollable .modal-content {\n max-height: calc(100vh - 3.5rem);\n }\n .modal-dialog-centered {\n min-height: calc(100% - 3.5rem);\n }\n .modal-dialog-centered::before {\n height: calc(100vh - 3.5rem);\n }\n .modal-sm {\n max-width: 300px;\n }\n}\n\n@media (min-width: 992px) {\n .modal-lg,\n .modal-xl {\n max-width: 800px;\n }\n}\n\n@media (min-width: 1200px) {\n .modal-xl {\n max-width: 1140px;\n }\n}\n\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n opacity: 0;\n}\n\n.tooltip.show {\n opacity: 0.9;\n}\n\n.tooltip .arrow {\n position: absolute;\n display: block;\n width: 0.8rem;\n height: 0.4rem;\n}\n\n.tooltip .arrow::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-tooltip-top, .bs-tooltip-auto[x-placement^=\"top\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^=\"top\"] .arrow {\n bottom: 0;\n}\n\n.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^=\"top\"] .arrow::before {\n top: 0;\n border-width: 0.4rem 0.4rem 0;\n border-top-color: #000;\n}\n\n.bs-tooltip-right, .bs-tooltip-auto[x-placement^=\"right\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^=\"right\"] .arrow {\n left: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^=\"right\"] .arrow::before {\n right: 0;\n border-width: 0.4rem 0.4rem 0.4rem 0;\n border-right-color: #000;\n}\n\n.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^=\"bottom\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow {\n top: 0;\n}\n\n.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow::before {\n bottom: 0;\n border-width: 0 0.4rem 0.4rem;\n border-bottom-color: #000;\n}\n\n.bs-tooltip-left, .bs-tooltip-auto[x-placement^=\"left\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^=\"left\"] .arrow {\n right: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^=\"left\"] .arrow::before {\n left: 0;\n border-width: 0.4rem 0 0.4rem 0.4rem;\n border-left-color: #000;\n}\n\n.tooltip-inner {\n max-width: 200px;\n padding: 0.25rem 0.5rem;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 0.25rem;\n}\n\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: block;\n max-width: 276px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n}\n\n.popover .arrow {\n position: absolute;\n display: block;\n width: 1rem;\n height: 0.5rem;\n margin: 0 0.3rem;\n}\n\n.popover .arrow::before, .popover .arrow::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-popover-top, .bs-popover-auto[x-placement^=\"top\"] {\n margin-bottom: 0.5rem;\n}\n\n.bs-popover-top > .arrow, .bs-popover-auto[x-placement^=\"top\"] > .arrow {\n bottom: calc(-0.5rem - 1px);\n}\n\n.bs-popover-top > .arrow::before, .bs-popover-auto[x-placement^=\"top\"] > .arrow::before {\n bottom: 0;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-top > .arrow::after, .bs-popover-auto[x-placement^=\"top\"] > .arrow::after {\n bottom: 1px;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: #fff;\n}\n\n.bs-popover-right, .bs-popover-auto[x-placement^=\"right\"] {\n margin-left: 0.5rem;\n}\n\n.bs-popover-right > .arrow, .bs-popover-auto[x-placement^=\"right\"] > .arrow {\n left: calc(-0.5rem - 1px);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-right > .arrow::before, .bs-popover-auto[x-placement^=\"right\"] > .arrow::before {\n left: 0;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-right > .arrow::after, .bs-popover-auto[x-placement^=\"right\"] > .arrow::after {\n left: 1px;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: #fff;\n}\n\n.bs-popover-bottom, .bs-popover-auto[x-placement^=\"bottom\"] {\n margin-top: 0.5rem;\n}\n\n.bs-popover-bottom > .arrow, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow {\n top: calc(-0.5rem - 1px);\n}\n\n.bs-popover-bottom > .arrow::before, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow::before {\n top: 0;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-bottom > .arrow::after, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow::after {\n top: 1px;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: #fff;\n}\n\n.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^=\"bottom\"] .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: 1rem;\n margin-left: -0.5rem;\n content: \"\";\n border-bottom: 1px solid #f7f7f7;\n}\n\n.bs-popover-left, .bs-popover-auto[x-placement^=\"left\"] {\n margin-right: 0.5rem;\n}\n\n.bs-popover-left > .arrow, .bs-popover-auto[x-placement^=\"left\"] > .arrow {\n right: calc(-0.5rem - 1px);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-left > .arrow::before, .bs-popover-auto[x-placement^=\"left\"] > .arrow::before {\n right: 0;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-left > .arrow::after, .bs-popover-auto[x-placement^=\"left\"] > .arrow::after {\n right: 1px;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: #fff;\n}\n\n.popover-header {\n padding: 0.5rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n\n.popover-header:empty {\n display: none;\n}\n\n.popover-body {\n padding: 0.5rem 0.75rem;\n color: #212529;\n}\n\n.carousel {\n position: relative;\n}\n\n.carousel.pointer-event {\n touch-action: pan-y;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n\n.carousel-inner::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.carousel-item {\n position: relative;\n display: none;\n float: left;\n width: 100%;\n margin-right: -100%;\n backface-visibility: hidden;\n transition: transform 0.6s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-item {\n transition: none;\n }\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n.carousel-item-next:not(.carousel-item-left),\n.active.carousel-item-right {\n transform: translateX(100%);\n}\n\n.carousel-item-prev:not(.carousel-item-right),\n.active.carousel-item-left {\n transform: translateX(-100%);\n}\n\n.carousel-fade .carousel-item {\n opacity: 0;\n transition-property: opacity;\n transform: none;\n}\n\n.carousel-fade .carousel-item.active,\n.carousel-fade .carousel-item-next.carousel-item-left,\n.carousel-fade .carousel-item-prev.carousel-item-right {\n z-index: 1;\n opacity: 1;\n}\n\n.carousel-fade .active.carousel-item-left,\n.carousel-fade .active.carousel-item-right {\n z-index: 0;\n opacity: 0;\n transition: opacity 0s 0.6s;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-fade .active.carousel-item-left,\n .carousel-fade .active.carousel-item-right {\n transition: none;\n }\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 15%;\n color: #fff;\n text-align: center;\n opacity: 0.5;\n transition: opacity 0.15s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-control-prev,\n .carousel-control-next {\n transition: none;\n }\n}\n\n.carousel-control-prev:hover, .carousel-control-prev:focus,\n.carousel-control-next:hover,\n.carousel-control-next:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n opacity: 0.9;\n}\n\n.carousel-control-prev {\n left: 0;\n}\n\n.carousel-control-next {\n right: 0;\n}\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: 20px;\n height: 20px;\n background: no-repeat 50% / 100% 100%;\n}\n\n.carousel-control-prev-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e\");\n}\n\n.carousel-control-next-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e\");\n}\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 15;\n display: flex;\n justify-content: center;\n padding-left: 0;\n margin-right: 15%;\n margin-left: 15%;\n list-style: none;\n}\n\n.carousel-indicators li {\n box-sizing: content-box;\n flex: 0 1 auto;\n width: 30px;\n height: 3px;\n margin-right: 3px;\n margin-left: 3px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #fff;\n background-clip: padding-box;\n border-top: 10px solid transparent;\n border-bottom: 10px solid transparent;\n opacity: .5;\n transition: opacity 0.6s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-indicators li {\n transition: none;\n }\n}\n\n.carousel-indicators .active {\n opacity: 1;\n}\n\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 20px;\n left: 15%;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n}\n\n@keyframes spinner-border {\n to {\n transform: rotate(360deg);\n }\n}\n\n.spinner-border {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: text-bottom;\n border: 0.25em solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n animation: spinner-border .75s linear infinite;\n}\n\n.spinner-border-sm {\n width: 1rem;\n height: 1rem;\n border-width: 0.2em;\n}\n\n@keyframes spinner-grow {\n 0% {\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n }\n}\n\n.spinner-grow {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: text-bottom;\n background-color: currentColor;\n border-radius: 50%;\n opacity: 0;\n animation: spinner-grow .75s linear infinite;\n}\n\n.spinner-grow-sm {\n width: 1rem;\n height: 1rem;\n}\n\n.align-baseline {\n vertical-align: baseline !important;\n}\n\n.align-top {\n vertical-align: top !important;\n}\n\n.align-middle {\n vertical-align: middle !important;\n}\n\n.align-bottom {\n vertical-align: bottom !important;\n}\n\n.align-text-bottom {\n vertical-align: text-bottom !important;\n}\n\n.align-text-top {\n vertical-align: text-top !important;\n}\n\n.bg-primary {\n background-color: #007bff !important;\n}\n\na.bg-primary:hover, a.bg-primary:focus,\nbutton.bg-primary:hover,\nbutton.bg-primary:focus {\n background-color: #0062cc !important;\n}\n\n.bg-secondary {\n background-color: #6c757d !important;\n}\n\na.bg-secondary:hover, a.bg-secondary:focus,\nbutton.bg-secondary:hover,\nbutton.bg-secondary:focus {\n background-color: #545b62 !important;\n}\n\n.bg-success {\n background-color: #28a745 !important;\n}\n\na.bg-success:hover, a.bg-success:focus,\nbutton.bg-success:hover,\nbutton.bg-success:focus {\n background-color: #1e7e34 !important;\n}\n\n.bg-info {\n background-color: #17a2b8 !important;\n}\n\na.bg-info:hover, a.bg-info:focus,\nbutton.bg-info:hover,\nbutton.bg-info:focus {\n background-color: #117a8b !important;\n}\n\n.bg-warning {\n background-color: #ffc107 !important;\n}\n\na.bg-warning:hover, a.bg-warning:focus,\nbutton.bg-warning:hover,\nbutton.bg-warning:focus {\n background-color: #d39e00 !important;\n}\n\n.bg-danger {\n background-color: #dc3545 !important;\n}\n\na.bg-danger:hover, a.bg-danger:focus,\nbutton.bg-danger:hover,\nbutton.bg-danger:focus {\n background-color: #bd2130 !important;\n}\n\n.bg-light {\n background-color: #f8f9fa !important;\n}\n\na.bg-light:hover, a.bg-light:focus,\nbutton.bg-light:hover,\nbutton.bg-light:focus {\n background-color: #dae0e5 !important;\n}\n\n.bg-dark {\n background-color: #343a40 !important;\n}\n\na.bg-dark:hover, a.bg-dark:focus,\nbutton.bg-dark:hover,\nbutton.bg-dark:focus {\n background-color: #1d2124 !important;\n}\n\n.bg-white {\n background-color: #fff !important;\n}\n\n.bg-transparent {\n background-color: transparent !important;\n}\n\n.border {\n border: 1px solid #dee2e6 !important;\n}\n\n.border-top {\n border-top: 1px solid #dee2e6 !important;\n}\n\n.border-right {\n border-right: 1px solid #dee2e6 !important;\n}\n\n.border-bottom {\n border-bottom: 1px solid #dee2e6 !important;\n}\n\n.border-left {\n border-left: 1px solid #dee2e6 !important;\n}\n\n.border-0 {\n border: 0 !important;\n}\n\n.border-top-0 {\n border-top: 0 !important;\n}\n\n.border-right-0 {\n border-right: 0 !important;\n}\n\n.border-bottom-0 {\n border-bottom: 0 !important;\n}\n\n.border-left-0 {\n border-left: 0 !important;\n}\n\n.border-primary {\n border-color: #007bff !important;\n}\n\n.border-secondary {\n border-color: #6c757d !important;\n}\n\n.border-success {\n border-color: #28a745 !important;\n}\n\n.border-info {\n border-color: #17a2b8 !important;\n}\n\n.border-warning {\n border-color: #ffc107 !important;\n}\n\n.border-danger {\n border-color: #dc3545 !important;\n}\n\n.border-light {\n border-color: #f8f9fa !important;\n}\n\n.border-dark {\n border-color: #343a40 !important;\n}\n\n.border-white {\n border-color: #fff !important;\n}\n\n.rounded-sm {\n border-radius: 0.2rem !important;\n}\n\n.rounded {\n border-radius: 0.25rem !important;\n}\n\n.rounded-top {\n border-top-left-radius: 0.25rem !important;\n border-top-right-radius: 0.25rem !important;\n}\n\n.rounded-right {\n border-top-right-radius: 0.25rem !important;\n border-bottom-right-radius: 0.25rem !important;\n}\n\n.rounded-bottom {\n border-bottom-right-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-left {\n border-top-left-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-lg {\n border-radius: 0.3rem !important;\n}\n\n.rounded-circle {\n border-radius: 50% !important;\n}\n\n.rounded-pill {\n border-radius: 50rem !important;\n}\n\n.rounded-0 {\n border-radius: 0 !important;\n}\n\n.clearfix::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.d-none {\n display: none !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-none {\n display: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 768px) {\n .d-md-none {\n display: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: flex !important;\n }\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 992px) {\n .d-lg-none {\n display: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 1200px) {\n .d-xl-none {\n display: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media print {\n .d-print-none {\n display: none !important;\n }\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: flex !important;\n }\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n}\n\n.embed-responsive {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n overflow: hidden;\n}\n\n.embed-responsive::before {\n display: block;\n content: \"\";\n}\n\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n}\n\n.embed-responsive-21by9::before {\n padding-top: 42.857143%;\n}\n\n.embed-responsive-16by9::before {\n padding-top: 56.25%;\n}\n\n.embed-responsive-4by3::before {\n padding-top: 75%;\n}\n\n.embed-responsive-1by1::before {\n padding-top: 100%;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n .flex-sm-row {\n flex-direction: row !important;\n }\n .flex-sm-column {\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n justify-content: center !important;\n }\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n align-items: center !important;\n }\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n align-content: center !important;\n }\n .align-content-sm-between {\n align-content: space-between !important;\n }\n .align-content-sm-around {\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n align-self: auto !important;\n }\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n align-self: center !important;\n }\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 768px) {\n .flex-md-row {\n flex-direction: row !important;\n }\n .flex-md-column {\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n justify-content: center !important;\n }\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n .align-items-md-start {\n align-items: flex-start !important;\n }\n .align-items-md-end {\n align-items: flex-end !important;\n }\n .align-items-md-center {\n align-items: center !important;\n }\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n .align-content-md-start {\n align-content: flex-start !important;\n }\n .align-content-md-end {\n align-content: flex-end !important;\n }\n .align-content-md-center {\n align-content: center !important;\n }\n .align-content-md-between {\n align-content: space-between !important;\n }\n .align-content-md-around {\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n .align-self-md-auto {\n align-self: auto !important;\n }\n .align-self-md-start {\n align-self: flex-start !important;\n }\n .align-self-md-end {\n align-self: flex-end !important;\n }\n .align-self-md-center {\n align-self: center !important;\n }\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 992px) {\n .flex-lg-row {\n flex-direction: row !important;\n }\n .flex-lg-column {\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n justify-content: center !important;\n }\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n align-items: center !important;\n }\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n align-content: center !important;\n }\n .align-content-lg-between {\n align-content: space-between !important;\n }\n .align-content-lg-around {\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n align-self: auto !important;\n }\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n align-self: center !important;\n }\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 1200px) {\n .flex-xl-row {\n flex-direction: row !important;\n }\n .flex-xl-column {\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n justify-content: center !important;\n }\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n align-items: center !important;\n }\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n align-content: center !important;\n }\n .align-content-xl-between {\n align-content: space-between !important;\n }\n .align-content-xl-around {\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n align-self: auto !important;\n }\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n align-self: center !important;\n }\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n}\n\n.float-left {\n float: left !important;\n}\n\n.float-right {\n float: right !important;\n}\n\n.float-none {\n float: none !important;\n}\n\n@media (min-width: 576px) {\n .float-sm-left {\n float: left !important;\n }\n .float-sm-right {\n float: right !important;\n }\n .float-sm-none {\n float: none !important;\n }\n}\n\n@media (min-width: 768px) {\n .float-md-left {\n float: left !important;\n }\n .float-md-right {\n float: right !important;\n }\n .float-md-none {\n float: none !important;\n }\n}\n\n@media (min-width: 992px) {\n .float-lg-left {\n float: left !important;\n }\n .float-lg-right {\n float: right !important;\n }\n .float-lg-none {\n float: none !important;\n }\n}\n\n@media (min-width: 1200px) {\n .float-xl-left {\n float: left !important;\n }\n .float-xl-right {\n float: right !important;\n }\n .float-xl-none {\n float: none !important;\n }\n}\n\n.overflow-auto {\n overflow: auto !important;\n}\n\n.overflow-hidden {\n overflow: hidden !important;\n}\n\n.position-static {\n position: static !important;\n}\n\n.position-relative {\n position: relative !important;\n}\n\n.position-absolute {\n position: absolute !important;\n}\n\n.position-fixed {\n position: fixed !important;\n}\n\n.position-sticky {\n position: sticky !important;\n}\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1030;\n}\n\n@supports (position: sticky) {\n .sticky-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n.sr-only-focusable:active, .sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n overflow: visible;\n clip: auto;\n white-space: normal;\n}\n\n.shadow-sm {\n box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;\n}\n\n.shadow {\n box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;\n}\n\n.shadow-lg {\n box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;\n}\n\n.shadow-none {\n box-shadow: none !important;\n}\n\n.w-25 {\n width: 25% !important;\n}\n\n.w-50 {\n width: 50% !important;\n}\n\n.w-75 {\n width: 75% !important;\n}\n\n.w-100 {\n width: 100% !important;\n}\n\n.w-auto {\n width: auto !important;\n}\n\n.h-25 {\n height: 25% !important;\n}\n\n.h-50 {\n height: 50% !important;\n}\n\n.h-75 {\n height: 75% !important;\n}\n\n.h-100 {\n height: 100% !important;\n}\n\n.h-auto {\n height: auto !important;\n}\n\n.mw-100 {\n max-width: 100% !important;\n}\n\n.mh-100 {\n max-height: 100% !important;\n}\n\n.min-vw-100 {\n min-width: 100vw !important;\n}\n\n.min-vh-100 {\n min-height: 100vh !important;\n}\n\n.vw-100 {\n width: 100vw !important;\n}\n\n.vh-100 {\n height: 100vh !important;\n}\n\n.stretched-link::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1;\n pointer-events: auto;\n content: \"\";\n background-color: rgba(0, 0, 0, 0);\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n margin-left: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n margin-left: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n margin-left: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.mt-3,\n.my-3 {\n margin-top: 1rem !important;\n}\n\n.mr-3,\n.mx-3 {\n margin-right: 1rem !important;\n}\n\n.mb-3,\n.my-3 {\n margin-bottom: 1rem !important;\n}\n\n.ml-3,\n.mx-3 {\n margin-left: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.mt-4,\n.my-4 {\n margin-top: 1.5rem !important;\n}\n\n.mr-4,\n.mx-4 {\n margin-right: 1.5rem !important;\n}\n\n.mb-4,\n.my-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.ml-4,\n.mx-4 {\n margin-left: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.mt-5,\n.my-5 {\n margin-top: 3rem !important;\n}\n\n.mr-5,\n.mx-5 {\n margin-right: 3rem !important;\n}\n\n.mb-5,\n.my-5 {\n margin-bottom: 3rem !important;\n}\n\n.ml-5,\n.mx-5 {\n margin-left: 3rem !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n padding-left: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n padding-left: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n padding-left: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.pt-3,\n.py-3 {\n padding-top: 1rem !important;\n}\n\n.pr-3,\n.px-3 {\n padding-right: 1rem !important;\n}\n\n.pb-3,\n.py-3 {\n padding-bottom: 1rem !important;\n}\n\n.pl-3,\n.px-3 {\n padding-left: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.pt-4,\n.py-4 {\n padding-top: 1.5rem !important;\n}\n\n.pr-4,\n.px-4 {\n padding-right: 1.5rem !important;\n}\n\n.pb-4,\n.py-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pl-4,\n.px-4 {\n padding-left: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.pt-5,\n.py-5 {\n padding-top: 3rem !important;\n}\n\n.pr-5,\n.px-5 {\n padding-right: 3rem !important;\n}\n\n.pb-5,\n.py-5 {\n padding-bottom: 3rem !important;\n}\n\n.pl-5,\n.px-5 {\n padding-left: 3rem !important;\n}\n\n.m-n1 {\n margin: -0.25rem !important;\n}\n\n.mt-n1,\n.my-n1 {\n margin-top: -0.25rem !important;\n}\n\n.mr-n1,\n.mx-n1 {\n margin-right: -0.25rem !important;\n}\n\n.mb-n1,\n.my-n1 {\n margin-bottom: -0.25rem !important;\n}\n\n.ml-n1,\n.mx-n1 {\n margin-left: -0.25rem !important;\n}\n\n.m-n2 {\n margin: -0.5rem !important;\n}\n\n.mt-n2,\n.my-n2 {\n margin-top: -0.5rem !important;\n}\n\n.mr-n2,\n.mx-n2 {\n margin-right: -0.5rem !important;\n}\n\n.mb-n2,\n.my-n2 {\n margin-bottom: -0.5rem !important;\n}\n\n.ml-n2,\n.mx-n2 {\n margin-left: -0.5rem !important;\n}\n\n.m-n3 {\n margin: -1rem !important;\n}\n\n.mt-n3,\n.my-n3 {\n margin-top: -1rem !important;\n}\n\n.mr-n3,\n.mx-n3 {\n margin-right: -1rem !important;\n}\n\n.mb-n3,\n.my-n3 {\n margin-bottom: -1rem !important;\n}\n\n.ml-n3,\n.mx-n3 {\n margin-left: -1rem !important;\n}\n\n.m-n4 {\n margin: -1.5rem !important;\n}\n\n.mt-n4,\n.my-n4 {\n margin-top: -1.5rem !important;\n}\n\n.mr-n4,\n.mx-n4 {\n margin-right: -1.5rem !important;\n}\n\n.mb-n4,\n.my-n4 {\n margin-bottom: -1.5rem !important;\n}\n\n.ml-n4,\n.mx-n4 {\n margin-left: -1.5rem !important;\n}\n\n.m-n5 {\n margin: -3rem !important;\n}\n\n.mt-n5,\n.my-n5 {\n margin-top: -3rem !important;\n}\n\n.mr-n5,\n.mx-n5 {\n margin-right: -3rem !important;\n}\n\n.mb-n5,\n.my-n5 {\n margin-bottom: -3rem !important;\n}\n\n.ml-n5,\n.mx-n5 {\n margin-left: -3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n .m-sm-0 {\n margin: 0 !important;\n }\n .mt-sm-0,\n .my-sm-0 {\n margin-top: 0 !important;\n }\n .mr-sm-0,\n .mx-sm-0 {\n margin-right: 0 !important;\n }\n .mb-sm-0,\n .my-sm-0 {\n margin-bottom: 0 !important;\n }\n .ml-sm-0,\n .mx-sm-0 {\n margin-left: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .mt-sm-1,\n .my-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mr-sm-1,\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n }\n .mb-sm-1,\n .my-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-sm-1,\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .mt-sm-2,\n .my-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mr-sm-2,\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n }\n .mb-sm-2,\n .my-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-sm-2,\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .mt-sm-3,\n .my-sm-3 {\n margin-top: 1rem !important;\n }\n .mr-sm-3,\n .mx-sm-3 {\n margin-right: 1rem !important;\n }\n .mb-sm-3,\n .my-sm-3 {\n margin-bottom: 1rem !important;\n }\n .ml-sm-3,\n .mx-sm-3 {\n margin-left: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .mt-sm-4,\n .my-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mr-sm-4,\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n }\n .mb-sm-4,\n .my-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-sm-4,\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .mt-sm-5,\n .my-sm-5 {\n margin-top: 3rem !important;\n }\n .mr-sm-5,\n .mx-sm-5 {\n margin-right: 3rem !important;\n }\n .mb-sm-5,\n .my-sm-5 {\n margin-bottom: 3rem !important;\n }\n .ml-sm-5,\n .mx-sm-5 {\n margin-left: 3rem !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .pt-sm-0,\n .py-sm-0 {\n padding-top: 0 !important;\n }\n .pr-sm-0,\n .px-sm-0 {\n padding-right: 0 !important;\n }\n .pb-sm-0,\n .py-sm-0 {\n padding-bottom: 0 !important;\n }\n .pl-sm-0,\n .px-sm-0 {\n padding-left: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .pt-sm-1,\n .py-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pr-sm-1,\n .px-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pb-sm-1,\n .py-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-sm-1,\n .px-sm-1 {\n padding-left: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .pt-sm-2,\n .py-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pr-sm-2,\n .px-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pb-sm-2,\n .py-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-sm-2,\n .px-sm-2 {\n padding-left: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .pt-sm-3,\n .py-sm-3 {\n padding-top: 1rem !important;\n }\n .pr-sm-3,\n .px-sm-3 {\n padding-right: 1rem !important;\n }\n .pb-sm-3,\n .py-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pl-sm-3,\n .px-sm-3 {\n padding-left: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .pt-sm-4,\n .py-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pr-sm-4,\n .px-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pb-sm-4,\n .py-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-sm-4,\n .px-sm-4 {\n padding-left: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .pt-sm-5,\n .py-sm-5 {\n padding-top: 3rem !important;\n }\n .pr-sm-5,\n .px-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-5,\n .py-sm-5 {\n padding-bottom: 3rem !important;\n }\n .pl-sm-5,\n .px-sm-5 {\n padding-left: 3rem !important;\n }\n .m-sm-n1 {\n margin: -0.25rem !important;\n }\n .mt-sm-n1,\n .my-sm-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-sm-n1,\n .mx-sm-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-sm-n1,\n .my-sm-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-sm-n1,\n .mx-sm-n1 {\n margin-left: -0.25rem !important;\n }\n .m-sm-n2 {\n margin: -0.5rem !important;\n }\n .mt-sm-n2,\n .my-sm-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-sm-n2,\n .mx-sm-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-sm-n2,\n .my-sm-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-sm-n2,\n .mx-sm-n2 {\n margin-left: -0.5rem !important;\n }\n .m-sm-n3 {\n margin: -1rem !important;\n }\n .mt-sm-n3,\n .my-sm-n3 {\n margin-top: -1rem !important;\n }\n .mr-sm-n3,\n .mx-sm-n3 {\n margin-right: -1rem !important;\n }\n .mb-sm-n3,\n .my-sm-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-sm-n3,\n .mx-sm-n3 {\n margin-left: -1rem !important;\n }\n .m-sm-n4 {\n margin: -1.5rem !important;\n }\n .mt-sm-n4,\n .my-sm-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-sm-n4,\n .mx-sm-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-sm-n4,\n .my-sm-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-sm-n4,\n .mx-sm-n4 {\n margin-left: -1.5rem !important;\n }\n .m-sm-n5 {\n margin: -3rem !important;\n }\n .mt-sm-n5,\n .my-sm-n5 {\n margin-top: -3rem !important;\n }\n .mr-sm-n5,\n .mx-sm-n5 {\n margin-right: -3rem !important;\n }\n .mb-sm-n5,\n .my-sm-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-sm-n5,\n .mx-sm-n5 {\n margin-left: -3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mt-sm-auto,\n .my-sm-auto {\n margin-top: auto !important;\n }\n .mr-sm-auto,\n .mx-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-auto,\n .my-sm-auto {\n margin-bottom: auto !important;\n }\n .ml-sm-auto,\n .mx-sm-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 768px) {\n .m-md-0 {\n margin: 0 !important;\n }\n .mt-md-0,\n .my-md-0 {\n margin-top: 0 !important;\n }\n .mr-md-0,\n .mx-md-0 {\n margin-right: 0 !important;\n }\n .mb-md-0,\n .my-md-0 {\n margin-bottom: 0 !important;\n }\n .ml-md-0,\n .mx-md-0 {\n margin-left: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .mt-md-1,\n .my-md-1 {\n margin-top: 0.25rem !important;\n }\n .mr-md-1,\n .mx-md-1 {\n margin-right: 0.25rem !important;\n }\n .mb-md-1,\n .my-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-md-1,\n .mx-md-1 {\n margin-left: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .mt-md-2,\n .my-md-2 {\n margin-top: 0.5rem !important;\n }\n .mr-md-2,\n .mx-md-2 {\n margin-right: 0.5rem !important;\n }\n .mb-md-2,\n .my-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-md-2,\n .mx-md-2 {\n margin-left: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .mt-md-3,\n .my-md-3 {\n margin-top: 1rem !important;\n }\n .mr-md-3,\n .mx-md-3 {\n margin-right: 1rem !important;\n }\n .mb-md-3,\n .my-md-3 {\n margin-bottom: 1rem !important;\n }\n .ml-md-3,\n .mx-md-3 {\n margin-left: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .mt-md-4,\n .my-md-4 {\n margin-top: 1.5rem !important;\n }\n .mr-md-4,\n .mx-md-4 {\n margin-right: 1.5rem !important;\n }\n .mb-md-4,\n .my-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-md-4,\n .mx-md-4 {\n margin-left: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .mt-md-5,\n .my-md-5 {\n margin-top: 3rem !important;\n }\n .mr-md-5,\n .mx-md-5 {\n margin-right: 3rem !important;\n }\n .mb-md-5,\n .my-md-5 {\n margin-bottom: 3rem !important;\n }\n .ml-md-5,\n .mx-md-5 {\n margin-left: 3rem !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .pt-md-0,\n .py-md-0 {\n padding-top: 0 !important;\n }\n .pr-md-0,\n .px-md-0 {\n padding-right: 0 !important;\n }\n .pb-md-0,\n .py-md-0 {\n padding-bottom: 0 !important;\n }\n .pl-md-0,\n .px-md-0 {\n padding-left: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .pt-md-1,\n .py-md-1 {\n padding-top: 0.25rem !important;\n }\n .pr-md-1,\n .px-md-1 {\n padding-right: 0.25rem !important;\n }\n .pb-md-1,\n .py-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-md-1,\n .px-md-1 {\n padding-left: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .pt-md-2,\n .py-md-2 {\n padding-top: 0.5rem !important;\n }\n .pr-md-2,\n .px-md-2 {\n padding-right: 0.5rem !important;\n }\n .pb-md-2,\n .py-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-md-2,\n .px-md-2 {\n padding-left: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .pt-md-3,\n .py-md-3 {\n padding-top: 1rem !important;\n }\n .pr-md-3,\n .px-md-3 {\n padding-right: 1rem !important;\n }\n .pb-md-3,\n .py-md-3 {\n padding-bottom: 1rem !important;\n }\n .pl-md-3,\n .px-md-3 {\n padding-left: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .pt-md-4,\n .py-md-4 {\n padding-top: 1.5rem !important;\n }\n .pr-md-4,\n .px-md-4 {\n padding-right: 1.5rem !important;\n }\n .pb-md-4,\n .py-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-md-4,\n .px-md-4 {\n padding-left: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .pt-md-5,\n .py-md-5 {\n padding-top: 3rem !important;\n }\n .pr-md-5,\n .px-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-5,\n .py-md-5 {\n padding-bottom: 3rem !important;\n }\n .pl-md-5,\n .px-md-5 {\n padding-left: 3rem !important;\n }\n .m-md-n1 {\n margin: -0.25rem !important;\n }\n .mt-md-n1,\n .my-md-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-md-n1,\n .mx-md-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-md-n1,\n .my-md-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-md-n1,\n .mx-md-n1 {\n margin-left: -0.25rem !important;\n }\n .m-md-n2 {\n margin: -0.5rem !important;\n }\n .mt-md-n2,\n .my-md-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-md-n2,\n .mx-md-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-md-n2,\n .my-md-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-md-n2,\n .mx-md-n2 {\n margin-left: -0.5rem !important;\n }\n .m-md-n3 {\n margin: -1rem !important;\n }\n .mt-md-n3,\n .my-md-n3 {\n margin-top: -1rem !important;\n }\n .mr-md-n3,\n .mx-md-n3 {\n margin-right: -1rem !important;\n }\n .mb-md-n3,\n .my-md-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-md-n3,\n .mx-md-n3 {\n margin-left: -1rem !important;\n }\n .m-md-n4 {\n margin: -1.5rem !important;\n }\n .mt-md-n4,\n .my-md-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-md-n4,\n .mx-md-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-md-n4,\n .my-md-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-md-n4,\n .mx-md-n4 {\n margin-left: -1.5rem !important;\n }\n .m-md-n5 {\n margin: -3rem !important;\n }\n .mt-md-n5,\n .my-md-n5 {\n margin-top: -3rem !important;\n }\n .mr-md-n5,\n .mx-md-n5 {\n margin-right: -3rem !important;\n }\n .mb-md-n5,\n .my-md-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-md-n5,\n .mx-md-n5 {\n margin-left: -3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mt-md-auto,\n .my-md-auto {\n margin-top: auto !important;\n }\n .mr-md-auto,\n .mx-md-auto {\n margin-right: auto !important;\n }\n .mb-md-auto,\n .my-md-auto {\n margin-bottom: auto !important;\n }\n .ml-md-auto,\n .mx-md-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 992px) {\n .m-lg-0 {\n margin: 0 !important;\n }\n .mt-lg-0,\n .my-lg-0 {\n margin-top: 0 !important;\n }\n .mr-lg-0,\n .mx-lg-0 {\n margin-right: 0 !important;\n }\n .mb-lg-0,\n .my-lg-0 {\n margin-bottom: 0 !important;\n }\n .ml-lg-0,\n .mx-lg-0 {\n margin-left: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .mt-lg-1,\n .my-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mr-lg-1,\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n }\n .mb-lg-1,\n .my-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-lg-1,\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .mt-lg-2,\n .my-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mr-lg-2,\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n }\n .mb-lg-2,\n .my-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-lg-2,\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .mt-lg-3,\n .my-lg-3 {\n margin-top: 1rem !important;\n }\n .mr-lg-3,\n .mx-lg-3 {\n margin-right: 1rem !important;\n }\n .mb-lg-3,\n .my-lg-3 {\n margin-bottom: 1rem !important;\n }\n .ml-lg-3,\n .mx-lg-3 {\n margin-left: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .mt-lg-4,\n .my-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mr-lg-4,\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n }\n .mb-lg-4,\n .my-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-lg-4,\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .mt-lg-5,\n .my-lg-5 {\n margin-top: 3rem !important;\n }\n .mr-lg-5,\n .mx-lg-5 {\n margin-right: 3rem !important;\n }\n .mb-lg-5,\n .my-lg-5 {\n margin-bottom: 3rem !important;\n }\n .ml-lg-5,\n .mx-lg-5 {\n margin-left: 3rem !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .pt-lg-0,\n .py-lg-0 {\n padding-top: 0 !important;\n }\n .pr-lg-0,\n .px-lg-0 {\n padding-right: 0 !important;\n }\n .pb-lg-0,\n .py-lg-0 {\n padding-bottom: 0 !important;\n }\n .pl-lg-0,\n .px-lg-0 {\n padding-left: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .pt-lg-1,\n .py-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pr-lg-1,\n .px-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pb-lg-1,\n .py-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-lg-1,\n .px-lg-1 {\n padding-left: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .pt-lg-2,\n .py-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pr-lg-2,\n .px-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pb-lg-2,\n .py-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-lg-2,\n .px-lg-2 {\n padding-left: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .pt-lg-3,\n .py-lg-3 {\n padding-top: 1rem !important;\n }\n .pr-lg-3,\n .px-lg-3 {\n padding-right: 1rem !important;\n }\n .pb-lg-3,\n .py-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pl-lg-3,\n .px-lg-3 {\n padding-left: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .pt-lg-4,\n .py-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pr-lg-4,\n .px-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pb-lg-4,\n .py-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-lg-4,\n .px-lg-4 {\n padding-left: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .pt-lg-5,\n .py-lg-5 {\n padding-top: 3rem !important;\n }\n .pr-lg-5,\n .px-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-5,\n .py-lg-5 {\n padding-bottom: 3rem !important;\n }\n .pl-lg-5,\n .px-lg-5 {\n padding-left: 3rem !important;\n }\n .m-lg-n1 {\n margin: -0.25rem !important;\n }\n .mt-lg-n1,\n .my-lg-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-lg-n1,\n .mx-lg-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-lg-n1,\n .my-lg-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-lg-n1,\n .mx-lg-n1 {\n margin-left: -0.25rem !important;\n }\n .m-lg-n2 {\n margin: -0.5rem !important;\n }\n .mt-lg-n2,\n .my-lg-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-lg-n2,\n .mx-lg-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-lg-n2,\n .my-lg-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-lg-n2,\n .mx-lg-n2 {\n margin-left: -0.5rem !important;\n }\n .m-lg-n3 {\n margin: -1rem !important;\n }\n .mt-lg-n3,\n .my-lg-n3 {\n margin-top: -1rem !important;\n }\n .mr-lg-n3,\n .mx-lg-n3 {\n margin-right: -1rem !important;\n }\n .mb-lg-n3,\n .my-lg-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-lg-n3,\n .mx-lg-n3 {\n margin-left: -1rem !important;\n }\n .m-lg-n4 {\n margin: -1.5rem !important;\n }\n .mt-lg-n4,\n .my-lg-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-lg-n4,\n .mx-lg-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-lg-n4,\n .my-lg-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-lg-n4,\n .mx-lg-n4 {\n margin-left: -1.5rem !important;\n }\n .m-lg-n5 {\n margin: -3rem !important;\n }\n .mt-lg-n5,\n .my-lg-n5 {\n margin-top: -3rem !important;\n }\n .mr-lg-n5,\n .mx-lg-n5 {\n margin-right: -3rem !important;\n }\n .mb-lg-n5,\n .my-lg-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-lg-n5,\n .mx-lg-n5 {\n margin-left: -3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mt-lg-auto,\n .my-lg-auto {\n margin-top: auto !important;\n }\n .mr-lg-auto,\n .mx-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-auto,\n .my-lg-auto {\n margin-bottom: auto !important;\n }\n .ml-lg-auto,\n .mx-lg-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 1200px) {\n .m-xl-0 {\n margin: 0 !important;\n }\n .mt-xl-0,\n .my-xl-0 {\n margin-top: 0 !important;\n }\n .mr-xl-0,\n .mx-xl-0 {\n margin-right: 0 !important;\n }\n .mb-xl-0,\n .my-xl-0 {\n margin-bottom: 0 !important;\n }\n .ml-xl-0,\n .mx-xl-0 {\n margin-left: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .mt-xl-1,\n .my-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mr-xl-1,\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n }\n .mb-xl-1,\n .my-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-xl-1,\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .mt-xl-2,\n .my-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mr-xl-2,\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n }\n .mb-xl-2,\n .my-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-xl-2,\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .mt-xl-3,\n .my-xl-3 {\n margin-top: 1rem !important;\n }\n .mr-xl-3,\n .mx-xl-3 {\n margin-right: 1rem !important;\n }\n .mb-xl-3,\n .my-xl-3 {\n margin-bottom: 1rem !important;\n }\n .ml-xl-3,\n .mx-xl-3 {\n margin-left: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .mt-xl-4,\n .my-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mr-xl-4,\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n }\n .mb-xl-4,\n .my-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-xl-4,\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .mt-xl-5,\n .my-xl-5 {\n margin-top: 3rem !important;\n }\n .mr-xl-5,\n .mx-xl-5 {\n margin-right: 3rem !important;\n }\n .mb-xl-5,\n .my-xl-5 {\n margin-bottom: 3rem !important;\n }\n .ml-xl-5,\n .mx-xl-5 {\n margin-left: 3rem !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .pt-xl-0,\n .py-xl-0 {\n padding-top: 0 !important;\n }\n .pr-xl-0,\n .px-xl-0 {\n padding-right: 0 !important;\n }\n .pb-xl-0,\n .py-xl-0 {\n padding-bottom: 0 !important;\n }\n .pl-xl-0,\n .px-xl-0 {\n padding-left: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .pt-xl-1,\n .py-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pr-xl-1,\n .px-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pb-xl-1,\n .py-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-xl-1,\n .px-xl-1 {\n padding-left: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .pt-xl-2,\n .py-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pr-xl-2,\n .px-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pb-xl-2,\n .py-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-xl-2,\n .px-xl-2 {\n padding-left: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .pt-xl-3,\n .py-xl-3 {\n padding-top: 1rem !important;\n }\n .pr-xl-3,\n .px-xl-3 {\n padding-right: 1rem !important;\n }\n .pb-xl-3,\n .py-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pl-xl-3,\n .px-xl-3 {\n padding-left: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .pt-xl-4,\n .py-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pr-xl-4,\n .px-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pb-xl-4,\n .py-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-xl-4,\n .px-xl-4 {\n padding-left: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .pt-xl-5,\n .py-xl-5 {\n padding-top: 3rem !important;\n }\n .pr-xl-5,\n .px-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-5,\n .py-xl-5 {\n padding-bottom: 3rem !important;\n }\n .pl-xl-5,\n .px-xl-5 {\n padding-left: 3rem !important;\n }\n .m-xl-n1 {\n margin: -0.25rem !important;\n }\n .mt-xl-n1,\n .my-xl-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-xl-n1,\n .mx-xl-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-xl-n1,\n .my-xl-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-xl-n1,\n .mx-xl-n1 {\n margin-left: -0.25rem !important;\n }\n .m-xl-n2 {\n margin: -0.5rem !important;\n }\n .mt-xl-n2,\n .my-xl-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-xl-n2,\n .mx-xl-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-xl-n2,\n .my-xl-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-xl-n2,\n .mx-xl-n2 {\n margin-left: -0.5rem !important;\n }\n .m-xl-n3 {\n margin: -1rem !important;\n }\n .mt-xl-n3,\n .my-xl-n3 {\n margin-top: -1rem !important;\n }\n .mr-xl-n3,\n .mx-xl-n3 {\n margin-right: -1rem !important;\n }\n .mb-xl-n3,\n .my-xl-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-xl-n3,\n .mx-xl-n3 {\n margin-left: -1rem !important;\n }\n .m-xl-n4 {\n margin: -1.5rem !important;\n }\n .mt-xl-n4,\n .my-xl-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-xl-n4,\n .mx-xl-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-xl-n4,\n .my-xl-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-xl-n4,\n .mx-xl-n4 {\n margin-left: -1.5rem !important;\n }\n .m-xl-n5 {\n margin: -3rem !important;\n }\n .mt-xl-n5,\n .my-xl-n5 {\n margin-top: -3rem !important;\n }\n .mr-xl-n5,\n .mx-xl-n5 {\n margin-right: -3rem !important;\n }\n .mb-xl-n5,\n .my-xl-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-xl-n5,\n .mx-xl-n5 {\n margin-left: -3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mt-xl-auto,\n .my-xl-auto {\n margin-top: auto !important;\n }\n .mr-xl-auto,\n .mx-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-auto,\n .my-xl-auto {\n margin-bottom: auto !important;\n }\n .ml-xl-auto,\n .mx-xl-auto {\n margin-left: auto !important;\n }\n}\n\n.text-monospace {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !important;\n}\n\n.text-justify {\n text-align: justify !important;\n}\n\n.text-wrap {\n white-space: normal !important;\n}\n\n.text-nowrap {\n white-space: nowrap !important;\n}\n\n.text-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.text-left {\n text-align: left !important;\n}\n\n.text-right {\n text-align: right !important;\n}\n\n.text-center {\n text-align: center !important;\n}\n\n@media (min-width: 576px) {\n .text-sm-left {\n text-align: left !important;\n }\n .text-sm-right {\n text-align: right !important;\n }\n .text-sm-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 768px) {\n .text-md-left {\n text-align: left !important;\n }\n .text-md-right {\n text-align: right !important;\n }\n .text-md-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 992px) {\n .text-lg-left {\n text-align: left !important;\n }\n .text-lg-right {\n text-align: right !important;\n }\n .text-lg-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 1200px) {\n .text-xl-left {\n text-align: left !important;\n }\n .text-xl-right {\n text-align: right !important;\n }\n .text-xl-center {\n text-align: center !important;\n }\n}\n\n.text-lowercase {\n text-transform: lowercase !important;\n}\n\n.text-uppercase {\n text-transform: uppercase !important;\n}\n\n.text-capitalize {\n text-transform: capitalize !important;\n}\n\n.font-weight-light {\n font-weight: 300 !important;\n}\n\n.font-weight-lighter {\n font-weight: lighter !important;\n}\n\n.font-weight-normal {\n font-weight: 400 !important;\n}\n\n.font-weight-bold {\n font-weight: 700 !important;\n}\n\n.font-weight-bolder {\n font-weight: bolder !important;\n}\n\n.font-italic {\n font-style: italic !important;\n}\n\n.text-white {\n color: #fff !important;\n}\n\n.text-primary {\n color: #007bff !important;\n}\n\na.text-primary:hover, a.text-primary:focus {\n color: #0056b3 !important;\n}\n\n.text-secondary {\n color: #6c757d !important;\n}\n\na.text-secondary:hover, a.text-secondary:focus {\n color: #494f54 !important;\n}\n\n.text-success {\n color: #28a745 !important;\n}\n\na.text-success:hover, a.text-success:focus {\n color: #19692c !important;\n}\n\n.text-info {\n color: #17a2b8 !important;\n}\n\na.text-info:hover, a.text-info:focus {\n color: #0f6674 !important;\n}\n\n.text-warning {\n color: #ffc107 !important;\n}\n\na.text-warning:hover, a.text-warning:focus {\n color: #ba8b00 !important;\n}\n\n.text-danger {\n color: #dc3545 !important;\n}\n\na.text-danger:hover, a.text-danger:focus {\n color: #a71d2a !important;\n}\n\n.text-light {\n color: #f8f9fa !important;\n}\n\na.text-light:hover, a.text-light:focus {\n color: #cbd3da !important;\n}\n\n.text-dark {\n color: #343a40 !important;\n}\n\na.text-dark:hover, a.text-dark:focus {\n color: #121416 !important;\n}\n\n.text-body {\n color: #212529 !important;\n}\n\n.text-muted {\n color: #6c757d !important;\n}\n\n.text-black-50 {\n color: rgba(0, 0, 0, 0.5) !important;\n}\n\n.text-white-50 {\n color: rgba(255, 255, 255, 0.5) !important;\n}\n\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n\n.text-decoration-none {\n text-decoration: none !important;\n}\n\n.text-break {\n word-break: break-word !important;\n overflow-wrap: break-word !important;\n}\n\n.text-reset {\n color: inherit !important;\n}\n\n.visible {\n visibility: visible !important;\n}\n\n.invisible {\n visibility: hidden !important;\n}\n\n@media print {\n *,\n *::before,\n *::after {\n text-shadow: none !important;\n box-shadow: none !important;\n }\n a:not(.btn) {\n text-decoration: underline;\n }\n abbr[title]::after {\n content: \" (\" attr(title) \")\";\n }\n pre {\n white-space: pre-wrap !important;\n }\n pre,\n blockquote {\n border: 1px solid #adb5bd;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n @page {\n size: a3;\n }\n body {\n min-width: 992px !important;\n }\n .container {\n min-width: 992px !important;\n }\n .navbar {\n display: none;\n }\n .badge {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #dee2e6 !important;\n }\n .table-dark {\n color: inherit;\n }\n .table-dark th,\n .table-dark td,\n .table-dark thead th,\n .table-dark tbody + tbody {\n border-color: #dee2e6;\n }\n .table .thead-dark th {\n color: inherit;\n border-color: #dee2e6;\n }\n}\n\n/*# sourceMappingURL=bootstrap.css.map */","// Do not forget to update getting-started/theming.md!\n:root {\n // Custom variable values only support SassScript inside `#{}`.\n @each $color, $value in $colors {\n --#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$color}: #{$value};\n }\n\n @each $bp, $value in $grid-breakpoints {\n --breakpoint-#{$bp}: #{$value};\n }\n\n // Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --font-family-sans-serif: #{inspect($font-family-sans-serif)};\n --font-family-monospace: #{inspect($font-family-monospace)};\n}\n","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -webkit-tap-highlight-color: rgba($black, 0); // 5\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\n// TODO: remove in v5\n// stylelint-disable-next-line selector-list-comma-newline-after\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use\n// the `inherit` value on things like `` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline\n// on elements that programmatically receive focus but wouldn't normally show a visible\n// focus outline. In general, this would mean that the outline is only applied if the\n// interaction that led to the element receiving programmatic focus was a keyboard interaction,\n// or the browser has somehow determined that the user is primarily a keyboard user and/or\n// wants focus outlines to always be presented.\n//\n// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible\n// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `

    `-`

    ` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

    `s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n\n @include hover() {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `` alignment by inheriting from the ``, or the\n // closest parent with a set `text-align`.\n text-align: inherit;\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n // stylelint-disable-next-line property-blacklist\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// Remove the inheritance of word-wrap in Safari.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24990\nselect {\n word-wrap: normal;\n}\n\n\n// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n// controls in Android 4.\n// 2. Correct the inability to style clickable types in iOS and Safari.\nbutton,\n[type=\"button\"], // 1\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button; // 2\n}\n\n// Opinionated: add \"hand\" cursor to non-disabled button elements.\n@if $enable-pointer-cursor-for-buttons {\n button,\n [type=\"button\"],\n [type=\"reset\"],\n [type=\"submit\"] {\n &:not(:disabled) {\n cursor: pointer;\n }\n }\n}\n\n// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box; // 1. Add the correct box sizing in IE 10-\n padding: 0; // 2. Remove the padding in IE 10-\n}\n\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n // Remove the default appearance of temporal inputs to avoid a Mobile Safari\n // bug where setting a custom line-height prevents text from being vertically\n // centered within the input.\n // See https://bugs.webkit.org/show_bug.cgi?id=139848\n // and https://github.com/twbs/bootstrap/issues/11266\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto; // Remove the default vertical scrollbar in IE.\n // Textareas should really only resize vertically so they don't break their (horizontal) containers.\n resize: vertical;\n}\n\nfieldset {\n // Browsers set a default `min-width: min-content;` on fieldsets,\n // unlike e.g. `

    `s, which have `min-width: 0;` by default.\n // So we reset that to ensure fieldsets behave more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359\n // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements\n min-width: 0;\n // Reset the default outline behavior of fieldsets so they don't affect page layout.\n padding: 0;\n margin: 0;\n border: 0;\n}\n\n// 1. Correct the text wrapping in Edge and IE.\n// 2. Correct the color inheritance from `fieldset` elements in IE.\nlegend {\n display: block;\n width: 100%;\n max-width: 100%; // 1\n padding: 0;\n margin-bottom: .5rem;\n @include font-size(1.5rem);\n line-height: inherit;\n color: inherit; // 2\n white-space: normal; // 1\n}\n\nprogress {\n vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.\n}\n\n// Correct the cursor style of increment and decrement buttons in Chrome.\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n outline-offset: -2px; // 2. Correct the outline style in Safari.\n -webkit-appearance: none;\n}\n\n//\n// Remove the inner padding in Chrome and Safari on macOS.\n//\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// 1. Correct the inability to style clickable types in iOS and Safari.\n// 2. Change font properties to `inherit` in Safari.\n//\n\n::-webkit-file-upload-button {\n font: inherit; // 2\n -webkit-appearance: button; // 1\n}\n\n//\n// Correct element displays\n//\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item; // Add the correct display in all browsers\n cursor: pointer;\n}\n\ntemplate {\n display: none; // Add the correct display in IE\n}\n\n// Always hide an element with the `hidden` HTML attribute (from PureCSS).\n// Needed for proper display in IE 10-.\n[hidden] {\n display: none !important;\n}\n","// Variables\n//\n// Variables should follow the `$component-state-property-size` formula for\n// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.\n\n// Color system\n\n$white: #fff !default;\n$gray-100: #f8f9fa !default;\n$gray-200: #e9ecef !default;\n$gray-300: #dee2e6 !default;\n$gray-400: #ced4da !default;\n$gray-500: #adb5bd !default;\n$gray-600: #6c757d !default;\n$gray-700: #495057 !default;\n$gray-800: #343a40 !default;\n$gray-900: #212529 !default;\n$black: #000 !default;\n\n$grays: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$grays: map-merge(\n (\n \"100\": $gray-100,\n \"200\": $gray-200,\n \"300\": $gray-300,\n \"400\": $gray-400,\n \"500\": $gray-500,\n \"600\": $gray-600,\n \"700\": $gray-700,\n \"800\": $gray-800,\n \"900\": $gray-900\n ),\n $grays\n);\n\n$blue: #007bff !default;\n$indigo: #6610f2 !default;\n$purple: #6f42c1 !default;\n$pink: #e83e8c !default;\n$red: #dc3545 !default;\n$orange: #fd7e14 !default;\n$yellow: #ffc107 !default;\n$green: #28a745 !default;\n$teal: #20c997 !default;\n$cyan: #17a2b8 !default;\n\n$colors: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$colors: map-merge(\n (\n \"blue\": $blue,\n \"indigo\": $indigo,\n \"purple\": $purple,\n \"pink\": $pink,\n \"red\": $red,\n \"orange\": $orange,\n \"yellow\": $yellow,\n \"green\": $green,\n \"teal\": $teal,\n \"cyan\": $cyan,\n \"white\": $white,\n \"gray\": $gray-600,\n \"gray-dark\": $gray-800\n ),\n $colors\n);\n\n$primary: $blue !default;\n$secondary: $gray-600 !default;\n$success: $green !default;\n$info: $cyan !default;\n$warning: $yellow !default;\n$danger: $red !default;\n$light: $gray-100 !default;\n$dark: $gray-800 !default;\n\n$theme-colors: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$theme-colors: map-merge(\n (\n \"primary\": $primary,\n \"secondary\": $secondary,\n \"success\": $success,\n \"info\": $info,\n \"warning\": $warning,\n \"danger\": $danger,\n \"light\": $light,\n \"dark\": $dark\n ),\n $theme-colors\n);\n\n// Set a specific jump point for requesting color jumps\n$theme-color-interval: 8% !default;\n\n// The yiq lightness value that determines when the lightness of color changes from \"dark\" to \"light\". Acceptable values are between 0 and 255.\n$yiq-contrasted-threshold: 150 !default;\n\n// Customize the light and dark text colors for use in our YIQ color contrast function.\n$yiq-text-dark: $gray-900 !default;\n$yiq-text-light: $white !default;\n\n// Characters which are escaped by the escape-svg function\n$escaped-characters: (\n (\"<\",\"%3c\"),\n (\">\",\"%3e\"),\n (\"#\",\"%23\"),\n) !default;\n\n\n// Options\n//\n// Quickly modify global styling by enabling or disabling optional features.\n\n$enable-caret: true !default;\n$enable-rounded: true !default;\n$enable-shadows: false !default;\n$enable-gradients: false !default;\n$enable-transitions: true !default;\n$enable-prefers-reduced-motion-media-query: true !default;\n$enable-hover-media-query: false !default; // Deprecated, no longer affects any compiled CSS\n$enable-grid-classes: true !default;\n$enable-pointer-cursor-for-buttons: true !default;\n$enable-print-styles: true !default;\n$enable-responsive-font-sizes: false !default;\n$enable-validation-icons: true !default;\n$enable-deprecation-messages: true !default;\n\n\n// Spacing\n//\n// Control the default styling of most Bootstrap elements by modifying these\n// variables. Mostly focused on spacing.\n// You can add more entries to the $spacers map, should you need more variation.\n\n$spacer: 1rem !default;\n$spacers: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$spacers: map-merge(\n (\n 0: 0,\n 1: ($spacer * .25),\n 2: ($spacer * .5),\n 3: $spacer,\n 4: ($spacer * 1.5),\n 5: ($spacer * 3)\n ),\n $spacers\n);\n\n// This variable affects the `.h-*` and `.w-*` classes.\n$sizes: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$sizes: map-merge(\n (\n 25: 25%,\n 50: 50%,\n 75: 75%,\n 100: 100%,\n auto: auto\n ),\n $sizes\n);\n\n\n// Body\n//\n// Settings for the `` element.\n\n$body-bg: $white !default;\n$body-color: $gray-900 !default;\n\n\n// Links\n//\n// Style anchor elements.\n\n$link-color: theme-color(\"primary\") !default;\n$link-decoration: none !default;\n$link-hover-color: darken($link-color, 15%) !default;\n$link-hover-decoration: underline !default;\n// Darken percentage for links with `.text-*` class (e.g. `.text-success`)\n$emphasized-link-hover-darken-percentage: 15% !default;\n\n// Paragraphs\n//\n// Style p element.\n\n$paragraph-margin-bottom: 1rem !default;\n\n\n// Grid breakpoints\n//\n// Define the minimum dimensions at which your layout will change,\n// adapting to different screen sizes, for use in media queries.\n\n$grid-breakpoints: (\n xs: 0,\n sm: 576px,\n md: 768px,\n lg: 992px,\n xl: 1200px\n) !default;\n\n@include _assert-ascending($grid-breakpoints, \"$grid-breakpoints\");\n@include _assert-starts-at-zero($grid-breakpoints, \"$grid-breakpoints\");\n\n\n// Grid containers\n//\n// Define the maximum width of `.container` for different screen sizes.\n\n$container-max-widths: (\n sm: 540px,\n md: 720px,\n lg: 960px,\n xl: 1140px\n) !default;\n\n@include _assert-ascending($container-max-widths, \"$container-max-widths\");\n\n\n// Grid columns\n//\n// Set the number of columns and specify the width of the gutters.\n\n$grid-columns: 12 !default;\n$grid-gutter-width: 30px !default;\n$grid-row-columns: 6 !default;\n\n\n// Components\n//\n// Define common padding and border radius sizes and more.\n\n$line-height-lg: 1.5 !default;\n$line-height-sm: 1.5 !default;\n\n$border-width: 1px !default;\n$border-color: $gray-300 !default;\n\n$border-radius: .25rem !default;\n$border-radius-lg: .3rem !default;\n$border-radius-sm: .2rem !default;\n\n$rounded-pill: 50rem !default;\n\n$box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default;\n$box-shadow: 0 .5rem 1rem rgba($black, .15) !default;\n$box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default;\n\n$component-active-color: $white !default;\n$component-active-bg: theme-color(\"primary\") !default;\n\n$caret-width: .3em !default;\n$caret-vertical-align: $caret-width * .85 !default;\n$caret-spacing: $caret-width * .85 !default;\n\n$transition-base: all .2s ease-in-out !default;\n$transition-fade: opacity .15s linear !default;\n$transition-collapse: height .35s ease !default;\n\n$embed-responsive-aspect-ratios: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$embed-responsive-aspect-ratios: join(\n (\n (21 9),\n (16 9),\n (4 3),\n (1 1),\n ),\n $embed-responsive-aspect-ratios\n);\n\n// Typography\n//\n// Font, line-height, and color for body text, headings, and more.\n\n// stylelint-disable value-keyword-case\n$font-family-sans-serif: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\" !default;\n$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !default;\n$font-family-base: $font-family-sans-serif !default;\n// stylelint-enable value-keyword-case\n\n$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`\n$font-size-lg: $font-size-base * 1.25 !default;\n$font-size-sm: $font-size-base * .875 !default;\n\n$font-weight-lighter: lighter !default;\n$font-weight-light: 300 !default;\n$font-weight-normal: 400 !default;\n$font-weight-bold: 700 !default;\n$font-weight-bolder: bolder !default;\n\n$font-weight-base: $font-weight-normal !default;\n$line-height-base: 1.5 !default;\n\n$h1-font-size: $font-size-base * 2.5 !default;\n$h2-font-size: $font-size-base * 2 !default;\n$h3-font-size: $font-size-base * 1.75 !default;\n$h4-font-size: $font-size-base * 1.5 !default;\n$h5-font-size: $font-size-base * 1.25 !default;\n$h6-font-size: $font-size-base !default;\n\n$headings-margin-bottom: $spacer / 2 !default;\n$headings-font-family: null !default;\n$headings-font-weight: 500 !default;\n$headings-line-height: 1.2 !default;\n$headings-color: null !default;\n\n$display1-size: 6rem !default;\n$display2-size: 5.5rem !default;\n$display3-size: 4.5rem !default;\n$display4-size: 3.5rem !default;\n\n$display1-weight: 300 !default;\n$display2-weight: 300 !default;\n$display3-weight: 300 !default;\n$display4-weight: 300 !default;\n$display-line-height: $headings-line-height !default;\n\n$lead-font-size: $font-size-base * 1.25 !default;\n$lead-font-weight: 300 !default;\n\n$small-font-size: 80% !default;\n\n$text-muted: $gray-600 !default;\n\n$blockquote-small-color: $gray-600 !default;\n$blockquote-small-font-size: $small-font-size !default;\n$blockquote-font-size: $font-size-base * 1.25 !default;\n\n$hr-border-color: rgba($black, .1) !default;\n$hr-border-width: $border-width !default;\n\n$mark-padding: .2em !default;\n\n$dt-font-weight: $font-weight-bold !default;\n\n$kbd-box-shadow: inset 0 -.1rem 0 rgba($black, .25) !default;\n$nested-kbd-font-weight: $font-weight-bold !default;\n\n$list-inline-padding: .5rem !default;\n\n$mark-bg: #fcf8e3 !default;\n\n$hr-margin-y: $spacer !default;\n\n\n// Tables\n//\n// Customizes the `.table` component with basic values, each used across all table variations.\n\n$table-cell-padding: .75rem !default;\n$table-cell-padding-sm: .3rem !default;\n\n$table-color: $body-color !default;\n$table-bg: null !default;\n$table-accent-bg: rgba($black, .05) !default;\n$table-hover-color: $table-color !default;\n$table-hover-bg: rgba($black, .075) !default;\n$table-active-bg: $table-hover-bg !default;\n\n$table-border-width: $border-width !default;\n$table-border-color: $border-color !default;\n\n$table-head-bg: $gray-200 !default;\n$table-head-color: $gray-700 !default;\n\n$table-dark-color: $white !default;\n$table-dark-bg: $gray-800 !default;\n$table-dark-accent-bg: rgba($white, .05) !default;\n$table-dark-hover-color: $table-dark-color !default;\n$table-dark-hover-bg: rgba($white, .075) !default;\n$table-dark-border-color: lighten($table-dark-bg, 7.5%) !default;\n\n$table-striped-order: odd !default;\n\n$table-caption-color: $text-muted !default;\n\n$table-bg-level: -9 !default;\n$table-border-level: -6 !default;\n\n\n// Buttons + Forms\n//\n// Shared variables that are reassigned to `$input-` and `$btn-` specific variables.\n\n$input-btn-padding-y: .375rem !default;\n$input-btn-padding-x: .75rem !default;\n$input-btn-font-family: null !default;\n$input-btn-font-size: $font-size-base !default;\n$input-btn-line-height: $line-height-base !default;\n\n$input-btn-focus-width: .2rem !default;\n$input-btn-focus-color: rgba($component-active-bg, .25) !default;\n$input-btn-focus-box-shadow: 0 0 0 $input-btn-focus-width $input-btn-focus-color !default;\n\n$input-btn-padding-y-sm: .25rem !default;\n$input-btn-padding-x-sm: .5rem !default;\n$input-btn-font-size-sm: $font-size-sm !default;\n$input-btn-line-height-sm: $line-height-sm !default;\n\n$input-btn-padding-y-lg: .5rem !default;\n$input-btn-padding-x-lg: 1rem !default;\n$input-btn-font-size-lg: $font-size-lg !default;\n$input-btn-line-height-lg: $line-height-lg !default;\n\n$input-btn-border-width: $border-width !default;\n\n\n// Buttons\n//\n// For each of Bootstrap's buttons, define text, background, and border color.\n\n$btn-padding-y: $input-btn-padding-y !default;\n$btn-padding-x: $input-btn-padding-x !default;\n$btn-font-family: $input-btn-font-family !default;\n$btn-font-size: $input-btn-font-size !default;\n$btn-line-height: $input-btn-line-height !default;\n$btn-white-space: null !default; // Set to `nowrap` to prevent text wrapping\n\n$btn-padding-y-sm: $input-btn-padding-y-sm !default;\n$btn-padding-x-sm: $input-btn-padding-x-sm !default;\n$btn-font-size-sm: $input-btn-font-size-sm !default;\n$btn-line-height-sm: $input-btn-line-height-sm !default;\n\n$btn-padding-y-lg: $input-btn-padding-y-lg !default;\n$btn-padding-x-lg: $input-btn-padding-x-lg !default;\n$btn-font-size-lg: $input-btn-font-size-lg !default;\n$btn-line-height-lg: $input-btn-line-height-lg !default;\n\n$btn-border-width: $input-btn-border-width !default;\n\n$btn-font-weight: $font-weight-normal !default;\n$btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default;\n$btn-focus-width: $input-btn-focus-width !default;\n$btn-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$btn-disabled-opacity: .65 !default;\n$btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default;\n\n$btn-link-disabled-color: $gray-600 !default;\n\n$btn-block-spacing-y: .5rem !default;\n\n// Allows for customizing button radius independently from global border radius\n$btn-border-radius: $border-radius !default;\n$btn-border-radius-lg: $border-radius-lg !default;\n$btn-border-radius-sm: $border-radius-sm !default;\n\n$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n\n// Forms\n\n$label-margin-bottom: .5rem !default;\n\n$input-padding-y: $input-btn-padding-y !default;\n$input-padding-x: $input-btn-padding-x !default;\n$input-font-family: $input-btn-font-family !default;\n$input-font-size: $input-btn-font-size !default;\n$input-font-weight: $font-weight-base !default;\n$input-line-height: $input-btn-line-height !default;\n\n$input-padding-y-sm: $input-btn-padding-y-sm !default;\n$input-padding-x-sm: $input-btn-padding-x-sm !default;\n$input-font-size-sm: $input-btn-font-size-sm !default;\n$input-line-height-sm: $input-btn-line-height-sm !default;\n\n$input-padding-y-lg: $input-btn-padding-y-lg !default;\n$input-padding-x-lg: $input-btn-padding-x-lg !default;\n$input-font-size-lg: $input-btn-font-size-lg !default;\n$input-line-height-lg: $input-btn-line-height-lg !default;\n\n$input-bg: $white !default;\n$input-disabled-bg: $gray-200 !default;\n\n$input-color: $gray-700 !default;\n$input-border-color: $gray-400 !default;\n$input-border-width: $input-btn-border-width !default;\n$input-box-shadow: inset 0 1px 1px rgba($black, .075) !default;\n\n$input-border-radius: $border-radius !default;\n$input-border-radius-lg: $border-radius-lg !default;\n$input-border-radius-sm: $border-radius-sm !default;\n\n$input-focus-bg: $input-bg !default;\n$input-focus-border-color: lighten($component-active-bg, 25%) !default;\n$input-focus-color: $input-color !default;\n$input-focus-width: $input-btn-focus-width !default;\n$input-focus-box-shadow: $input-btn-focus-box-shadow !default;\n\n$input-placeholder-color: $gray-600 !default;\n$input-plaintext-color: $body-color !default;\n\n$input-height-border: $input-border-width * 2 !default;\n\n$input-height-inner: add($input-line-height * 1em, $input-padding-y * 2) !default;\n$input-height-inner-half: add($input-line-height * .5em, $input-padding-y) !default;\n$input-height-inner-quarter: add($input-line-height * .25em, $input-padding-y / 2) !default;\n\n$input-height: add($input-line-height * 1em, add($input-padding-y * 2, $input-height-border, false)) !default;\n$input-height-sm: add($input-line-height-sm * 1em, add($input-padding-y-sm * 2, $input-height-border, false)) !default;\n$input-height-lg: add($input-line-height-lg * 1em, add($input-padding-y-lg * 2, $input-height-border, false)) !default;\n\n$input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$form-text-margin-top: .25rem !default;\n\n$form-check-input-gutter: 1.25rem !default;\n$form-check-input-margin-y: .3rem !default;\n$form-check-input-margin-x: .25rem !default;\n\n$form-check-inline-margin-x: .75rem !default;\n$form-check-inline-input-margin-x: .3125rem !default;\n\n$form-grid-gutter-width: 10px !default;\n$form-group-margin-bottom: 1rem !default;\n\n$input-group-addon-color: $input-color !default;\n$input-group-addon-bg: $gray-200 !default;\n$input-group-addon-border-color: $input-border-color !default;\n\n$custom-forms-transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$custom-control-gutter: .5rem !default;\n$custom-control-spacer-x: 1rem !default;\n$custom-control-cursor: null !default;\n\n$custom-control-indicator-size: 1rem !default;\n$custom-control-indicator-bg: $input-bg !default;\n\n$custom-control-indicator-bg-size: 50% 50% !default;\n$custom-control-indicator-box-shadow: $input-box-shadow !default;\n$custom-control-indicator-border-color: $gray-500 !default;\n$custom-control-indicator-border-width: $input-border-width !default;\n\n$custom-control-label-color: null !default;\n\n$custom-control-indicator-disabled-bg: $input-disabled-bg !default;\n$custom-control-label-disabled-color: $gray-600 !default;\n\n$custom-control-indicator-checked-color: $component-active-color !default;\n$custom-control-indicator-checked-bg: $component-active-bg !default;\n$custom-control-indicator-checked-disabled-bg: rgba(theme-color(\"primary\"), .5) !default;\n$custom-control-indicator-checked-box-shadow: none !default;\n$custom-control-indicator-checked-border-color: $custom-control-indicator-checked-bg !default;\n\n$custom-control-indicator-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-control-indicator-focus-border-color: $input-focus-border-color !default;\n\n$custom-control-indicator-active-color: $component-active-color !default;\n$custom-control-indicator-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-control-indicator-active-box-shadow: none !default;\n$custom-control-indicator-active-border-color: $custom-control-indicator-active-bg !default;\n\n$custom-checkbox-indicator-border-radius: $border-radius !default;\n$custom-checkbox-indicator-icon-checked: url(\"data:image/svg+xml,\") !default;\n\n$custom-checkbox-indicator-indeterminate-bg: $component-active-bg !default;\n$custom-checkbox-indicator-indeterminate-color: $custom-control-indicator-checked-color !default;\n$custom-checkbox-indicator-icon-indeterminate: url(\"data:image/svg+xml,\") !default;\n$custom-checkbox-indicator-indeterminate-box-shadow: none !default;\n$custom-checkbox-indicator-indeterminate-border-color: $custom-checkbox-indicator-indeterminate-bg !default;\n\n$custom-radio-indicator-border-radius: 50% !default;\n$custom-radio-indicator-icon-checked: url(\"data:image/svg+xml,\") !default;\n\n$custom-switch-width: $custom-control-indicator-size * 1.75 !default;\n$custom-switch-indicator-border-radius: $custom-control-indicator-size / 2 !default;\n$custom-switch-indicator-size: subtract($custom-control-indicator-size, $custom-control-indicator-border-width * 4) !default;\n\n$custom-select-padding-y: $input-padding-y !default;\n$custom-select-padding-x: $input-padding-x !default;\n$custom-select-font-family: $input-font-family !default;\n$custom-select-font-size: $input-font-size !default;\n$custom-select-height: $input-height !default;\n$custom-select-indicator-padding: 1rem !default; // Extra padding to account for the presence of the background-image based indicator\n$custom-select-font-weight: $input-font-weight !default;\n$custom-select-line-height: $input-line-height !default;\n$custom-select-color: $input-color !default;\n$custom-select-disabled-color: $gray-600 !default;\n$custom-select-bg: $input-bg !default;\n$custom-select-disabled-bg: $gray-200 !default;\n$custom-select-bg-size: 8px 10px !default; // In pixels because image dimensions\n$custom-select-indicator-color: $gray-800 !default;\n$custom-select-indicator: url(\"data:image/svg+xml,\") !default;\n$custom-select-background: escape-svg($custom-select-indicator) no-repeat right $custom-select-padding-x center / $custom-select-bg-size !default; // Used so we can have multiple background elements (e.g., arrow and feedback icon)\n\n$custom-select-feedback-icon-padding-right: add(1em * .75, (2 * $custom-select-padding-y * .75) + $custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-position: center right ($custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-size: $input-height-inner-half $input-height-inner-half !default;\n\n$custom-select-border-width: $input-border-width !default;\n$custom-select-border-color: $input-border-color !default;\n$custom-select-border-radius: $border-radius !default;\n$custom-select-box-shadow: inset 0 1px 2px rgba($black, .075) !default;\n\n$custom-select-focus-border-color: $input-focus-border-color !default;\n$custom-select-focus-width: $input-focus-width !default;\n$custom-select-focus-box-shadow: 0 0 0 $custom-select-focus-width $input-btn-focus-color !default;\n\n$custom-select-padding-y-sm: $input-padding-y-sm !default;\n$custom-select-padding-x-sm: $input-padding-x-sm !default;\n$custom-select-font-size-sm: $input-font-size-sm !default;\n$custom-select-height-sm: $input-height-sm !default;\n\n$custom-select-padding-y-lg: $input-padding-y-lg !default;\n$custom-select-padding-x-lg: $input-padding-x-lg !default;\n$custom-select-font-size-lg: $input-font-size-lg !default;\n$custom-select-height-lg: $input-height-lg !default;\n\n$custom-range-track-width: 100% !default;\n$custom-range-track-height: .5rem !default;\n$custom-range-track-cursor: pointer !default;\n$custom-range-track-bg: $gray-300 !default;\n$custom-range-track-border-radius: 1rem !default;\n$custom-range-track-box-shadow: inset 0 .25rem .25rem rgba($black, .1) !default;\n\n$custom-range-thumb-width: 1rem !default;\n$custom-range-thumb-height: $custom-range-thumb-width !default;\n$custom-range-thumb-bg: $component-active-bg !default;\n$custom-range-thumb-border: 0 !default;\n$custom-range-thumb-border-radius: 1rem !default;\n$custom-range-thumb-box-shadow: 0 .1rem .25rem rgba($black, .1) !default;\n$custom-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow !default;\n$custom-range-thumb-focus-box-shadow-width: $input-focus-width !default; // For focus box shadow issue in IE/Edge\n$custom-range-thumb-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-range-thumb-disabled-bg: $gray-500 !default;\n\n$custom-file-height: $input-height !default;\n$custom-file-height-inner: $input-height-inner !default;\n$custom-file-focus-border-color: $input-focus-border-color !default;\n$custom-file-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-file-disabled-bg: $input-disabled-bg !default;\n\n$custom-file-padding-y: $input-padding-y !default;\n$custom-file-padding-x: $input-padding-x !default;\n$custom-file-line-height: $input-line-height !default;\n$custom-file-font-family: $input-font-family !default;\n$custom-file-font-weight: $input-font-weight !default;\n$custom-file-color: $input-color !default;\n$custom-file-bg: $input-bg !default;\n$custom-file-border-width: $input-border-width !default;\n$custom-file-border-color: $input-border-color !default;\n$custom-file-border-radius: $input-border-radius !default;\n$custom-file-box-shadow: $input-box-shadow !default;\n$custom-file-button-color: $custom-file-color !default;\n$custom-file-button-bg: $input-group-addon-bg !default;\n$custom-file-text: (\n en: \"Browse\"\n) !default;\n\n\n// Form validation\n\n$form-feedback-margin-top: $form-text-margin-top !default;\n$form-feedback-font-size: $small-font-size !default;\n$form-feedback-valid-color: theme-color(\"success\") !default;\n$form-feedback-invalid-color: theme-color(\"danger\") !default;\n\n$form-feedback-icon-valid-color: $form-feedback-valid-color !default;\n$form-feedback-icon-valid: url(\"data:image/svg+xml,\") !default;\n$form-feedback-icon-invalid-color: $form-feedback-invalid-color !default;\n$form-feedback-icon-invalid: url(\"data:image/svg+xml,\") !default;\n\n$form-validation-states: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$form-validation-states: map-merge(\n (\n \"valid\": (\n \"color\": $form-feedback-valid-color,\n \"icon\": $form-feedback-icon-valid\n ),\n \"invalid\": (\n \"color\": $form-feedback-invalid-color,\n \"icon\": $form-feedback-icon-invalid\n ),\n ),\n $form-validation-states\n);\n\n// Z-index master list\n//\n// Warning: Avoid customizing these values. They're used for a bird's eye view\n// of components dependent on the z-axis and are designed to all work together.\n\n$zindex-dropdown: 1000 !default;\n$zindex-sticky: 1020 !default;\n$zindex-fixed: 1030 !default;\n$zindex-modal-backdrop: 1040 !default;\n$zindex-modal: 1050 !default;\n$zindex-popover: 1060 !default;\n$zindex-tooltip: 1070 !default;\n\n\n// Navs\n\n$nav-link-padding-y: .5rem !default;\n$nav-link-padding-x: 1rem !default;\n$nav-link-disabled-color: $gray-600 !default;\n\n$nav-tabs-border-color: $gray-300 !default;\n$nav-tabs-border-width: $border-width !default;\n$nav-tabs-border-radius: $border-radius !default;\n$nav-tabs-link-hover-border-color: $gray-200 $gray-200 $nav-tabs-border-color !default;\n$nav-tabs-link-active-color: $gray-700 !default;\n$nav-tabs-link-active-bg: $body-bg !default;\n$nav-tabs-link-active-border-color: $gray-300 $gray-300 $nav-tabs-link-active-bg !default;\n\n$nav-pills-border-radius: $border-radius !default;\n$nav-pills-link-active-color: $component-active-color !default;\n$nav-pills-link-active-bg: $component-active-bg !default;\n\n$nav-divider-color: $gray-200 !default;\n$nav-divider-margin-y: $spacer / 2 !default;\n\n\n// Navbar\n\n$navbar-padding-y: $spacer / 2 !default;\n$navbar-padding-x: $spacer !default;\n\n$navbar-nav-link-padding-x: .5rem !default;\n\n$navbar-brand-font-size: $font-size-lg !default;\n// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link\n$nav-link-height: $font-size-base * $line-height-base + $nav-link-padding-y * 2 !default;\n$navbar-brand-height: $navbar-brand-font-size * $line-height-base !default;\n$navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) / 2 !default;\n\n$navbar-toggler-padding-y: .25rem !default;\n$navbar-toggler-padding-x: .75rem !default;\n$navbar-toggler-font-size: $font-size-lg !default;\n$navbar-toggler-border-radius: $btn-border-radius !default;\n\n$navbar-dark-color: rgba($white, .5) !default;\n$navbar-dark-hover-color: rgba($white, .75) !default;\n$navbar-dark-active-color: $white !default;\n$navbar-dark-disabled-color: rgba($white, .25) !default;\n$navbar-dark-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-dark-toggler-border-color: rgba($white, .1) !default;\n\n$navbar-light-color: rgba($black, .5) !default;\n$navbar-light-hover-color: rgba($black, .7) !default;\n$navbar-light-active-color: rgba($black, .9) !default;\n$navbar-light-disabled-color: rgba($black, .3) !default;\n$navbar-light-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-light-toggler-border-color: rgba($black, .1) !default;\n\n$navbar-light-brand-color: $navbar-light-active-color !default;\n$navbar-light-brand-hover-color: $navbar-light-active-color !default;\n$navbar-dark-brand-color: $navbar-dark-active-color !default;\n$navbar-dark-brand-hover-color: $navbar-dark-active-color !default;\n\n\n// Dropdowns\n//\n// Dropdown menu container and contents.\n\n$dropdown-min-width: 10rem !default;\n$dropdown-padding-y: .5rem !default;\n$dropdown-spacer: .125rem !default;\n$dropdown-font-size: $font-size-base !default;\n$dropdown-color: $body-color !default;\n$dropdown-bg: $white !default;\n$dropdown-border-color: rgba($black, .15) !default;\n$dropdown-border-radius: $border-radius !default;\n$dropdown-border-width: $border-width !default;\n$dropdown-inner-border-radius: subtract($dropdown-border-radius, $dropdown-border-width) !default;\n$dropdown-divider-bg: $gray-200 !default;\n$dropdown-divider-margin-y: $nav-divider-margin-y !default;\n$dropdown-box-shadow: 0 .5rem 1rem rgba($black, .175) !default;\n\n$dropdown-link-color: $gray-900 !default;\n$dropdown-link-hover-color: darken($gray-900, 5%) !default;\n$dropdown-link-hover-bg: $gray-100 !default;\n\n$dropdown-link-active-color: $component-active-color !default;\n$dropdown-link-active-bg: $component-active-bg !default;\n\n$dropdown-link-disabled-color: $gray-600 !default;\n\n$dropdown-item-padding-y: .25rem !default;\n$dropdown-item-padding-x: 1.5rem !default;\n\n$dropdown-header-color: $gray-600 !default;\n\n\n// Pagination\n\n$pagination-padding-y: .5rem !default;\n$pagination-padding-x: .75rem !default;\n$pagination-padding-y-sm: .25rem !default;\n$pagination-padding-x-sm: .5rem !default;\n$pagination-padding-y-lg: .75rem !default;\n$pagination-padding-x-lg: 1.5rem !default;\n$pagination-line-height: 1.25 !default;\n\n$pagination-color: $link-color !default;\n$pagination-bg: $white !default;\n$pagination-border-width: $border-width !default;\n$pagination-border-color: $gray-300 !default;\n\n$pagination-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$pagination-focus-outline: 0 !default;\n\n$pagination-hover-color: $link-hover-color !default;\n$pagination-hover-bg: $gray-200 !default;\n$pagination-hover-border-color: $gray-300 !default;\n\n$pagination-active-color: $component-active-color !default;\n$pagination-active-bg: $component-active-bg !default;\n$pagination-active-border-color: $pagination-active-bg !default;\n\n$pagination-disabled-color: $gray-600 !default;\n$pagination-disabled-bg: $white !default;\n$pagination-disabled-border-color: $gray-300 !default;\n\n\n// Jumbotron\n\n$jumbotron-padding: 2rem !default;\n$jumbotron-color: null !default;\n$jumbotron-bg: $gray-200 !default;\n\n\n// Cards\n\n$card-spacer-y: .75rem !default;\n$card-spacer-x: 1.25rem !default;\n$card-border-width: $border-width !default;\n$card-border-radius: $border-radius !default;\n$card-border-color: rgba($black, .125) !default;\n$card-inner-border-radius: subtract($card-border-radius, $card-border-width) !default;\n$card-cap-bg: rgba($black, .03) !default;\n$card-cap-color: null !default;\n$card-height: null !default;\n$card-color: null !default;\n$card-bg: $white !default;\n\n$card-img-overlay-padding: 1.25rem !default;\n\n$card-group-margin: $grid-gutter-width / 2 !default;\n$card-deck-margin: $card-group-margin !default;\n\n$card-columns-count: 3 !default;\n$card-columns-gap: 1.25rem !default;\n$card-columns-margin: $card-spacer-y !default;\n\n\n// Tooltips\n\n$tooltip-font-size: $font-size-sm !default;\n$tooltip-max-width: 200px !default;\n$tooltip-color: $white !default;\n$tooltip-bg: $black !default;\n$tooltip-border-radius: $border-radius !default;\n$tooltip-opacity: .9 !default;\n$tooltip-padding-y: .25rem !default;\n$tooltip-padding-x: .5rem !default;\n$tooltip-margin: 0 !default;\n\n$tooltip-arrow-width: .8rem !default;\n$tooltip-arrow-height: .4rem !default;\n$tooltip-arrow-color: $tooltip-bg !default;\n\n// Form tooltips must come after regular tooltips\n$form-feedback-tooltip-padding-y: $tooltip-padding-y !default;\n$form-feedback-tooltip-padding-x: $tooltip-padding-x !default;\n$form-feedback-tooltip-font-size: $tooltip-font-size !default;\n$form-feedback-tooltip-line-height: $line-height-base !default;\n$form-feedback-tooltip-opacity: $tooltip-opacity !default;\n$form-feedback-tooltip-border-radius: $tooltip-border-radius !default;\n\n\n// Popovers\n\n$popover-font-size: $font-size-sm !default;\n$popover-bg: $white !default;\n$popover-max-width: 276px !default;\n$popover-border-width: $border-width !default;\n$popover-border-color: rgba($black, .2) !default;\n$popover-border-radius: $border-radius-lg !default;\n$popover-inner-border-radius: subtract($popover-border-radius, $popover-border-width) !default;\n$popover-box-shadow: 0 .25rem .5rem rgba($black, .2) !default;\n\n$popover-header-bg: darken($popover-bg, 3%) !default;\n$popover-header-color: $headings-color !default;\n$popover-header-padding-y: .5rem !default;\n$popover-header-padding-x: .75rem !default;\n\n$popover-body-color: $body-color !default;\n$popover-body-padding-y: $popover-header-padding-y !default;\n$popover-body-padding-x: $popover-header-padding-x !default;\n\n$popover-arrow-width: 1rem !default;\n$popover-arrow-height: .5rem !default;\n$popover-arrow-color: $popover-bg !default;\n\n$popover-arrow-outer-color: fade-in($popover-border-color, .05) !default;\n\n\n// Toasts\n\n$toast-max-width: 350px !default;\n$toast-padding-x: .75rem !default;\n$toast-padding-y: .25rem !default;\n$toast-font-size: .875rem !default;\n$toast-color: null !default;\n$toast-background-color: rgba($white, .85) !default;\n$toast-border-width: 1px !default;\n$toast-border-color: rgba(0, 0, 0, .1) !default;\n$toast-border-radius: .25rem !default;\n$toast-box-shadow: 0 .25rem .75rem rgba($black, .1) !default;\n\n$toast-header-color: $gray-600 !default;\n$toast-header-background-color: rgba($white, .85) !default;\n$toast-header-border-color: rgba(0, 0, 0, .05) !default;\n\n\n// Badges\n\n$badge-font-size: 75% !default;\n$badge-font-weight: $font-weight-bold !default;\n$badge-padding-y: .25em !default;\n$badge-padding-x: .4em !default;\n$badge-border-radius: $border-radius !default;\n\n$badge-transition: $btn-transition !default;\n$badge-focus-width: $input-btn-focus-width !default;\n\n$badge-pill-padding-x: .6em !default;\n// Use a higher than normal value to ensure completely rounded edges when\n// customizing padding or font-size on labels.\n$badge-pill-border-radius: 10rem !default;\n\n\n// Modals\n\n// Padding applied to the modal body\n$modal-inner-padding: 1rem !default;\n\n// Margin between elements in footer, must be lower than or equal to 2 * $modal-inner-padding\n$modal-footer-margin-between: .5rem !default;\n\n$modal-dialog-margin: .5rem !default;\n$modal-dialog-margin-y-sm-up: 1.75rem !default;\n\n$modal-title-line-height: $line-height-base !default;\n\n$modal-content-color: null !default;\n$modal-content-bg: $white !default;\n$modal-content-border-color: rgba($black, .2) !default;\n$modal-content-border-width: $border-width !default;\n$modal-content-border-radius: $border-radius-lg !default;\n$modal-content-inner-border-radius: subtract($modal-content-border-radius, $modal-content-border-width) !default;\n$modal-content-box-shadow-xs: 0 .25rem .5rem rgba($black, .5) !default;\n$modal-content-box-shadow-sm-up: 0 .5rem 1rem rgba($black, .5) !default;\n\n$modal-backdrop-bg: $black !default;\n$modal-backdrop-opacity: .5 !default;\n$modal-header-border-color: $border-color !default;\n$modal-footer-border-color: $modal-header-border-color !default;\n$modal-header-border-width: $modal-content-border-width !default;\n$modal-footer-border-width: $modal-header-border-width !default;\n$modal-header-padding-y: 1rem !default;\n$modal-header-padding-x: 1rem !default;\n$modal-header-padding: $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility\n\n$modal-xl: 1140px !default;\n$modal-lg: 800px !default;\n$modal-md: 500px !default;\n$modal-sm: 300px !default;\n\n$modal-fade-transform: translate(0, -50px) !default;\n$modal-show-transform: none !default;\n$modal-transition: transform .3s ease-out !default;\n$modal-scale-transform: scale(1.02) !default;\n\n\n// Alerts\n//\n// Define alert colors, border radius, and padding.\n\n$alert-padding-y: .75rem !default;\n$alert-padding-x: 1.25rem !default;\n$alert-margin-bottom: 1rem !default;\n$alert-border-radius: $border-radius !default;\n$alert-link-font-weight: $font-weight-bold !default;\n$alert-border-width: $border-width !default;\n\n$alert-bg-level: -10 !default;\n$alert-border-level: -9 !default;\n$alert-color-level: 6 !default;\n\n\n// Progress bars\n\n$progress-height: 1rem !default;\n$progress-font-size: $font-size-base * .75 !default;\n$progress-bg: $gray-200 !default;\n$progress-border-radius: $border-radius !default;\n$progress-box-shadow: inset 0 .1rem .1rem rgba($black, .1) !default;\n$progress-bar-color: $white !default;\n$progress-bar-bg: theme-color(\"primary\") !default;\n$progress-bar-animation-timing: 1s linear infinite !default;\n$progress-bar-transition: width .6s ease !default;\n\n\n// List group\n\n$list-group-color: null !default;\n$list-group-bg: $white !default;\n$list-group-border-color: rgba($black, .125) !default;\n$list-group-border-width: $border-width !default;\n$list-group-border-radius: $border-radius !default;\n\n$list-group-item-padding-y: .75rem !default;\n$list-group-item-padding-x: 1.25rem !default;\n\n$list-group-hover-bg: $gray-100 !default;\n$list-group-active-color: $component-active-color !default;\n$list-group-active-bg: $component-active-bg !default;\n$list-group-active-border-color: $list-group-active-bg !default;\n\n$list-group-disabled-color: $gray-600 !default;\n$list-group-disabled-bg: $list-group-bg !default;\n\n$list-group-action-color: $gray-700 !default;\n$list-group-action-hover-color: $list-group-action-color !default;\n\n$list-group-action-active-color: $body-color !default;\n$list-group-action-active-bg: $gray-200 !default;\n\n\n// Image thumbnails\n\n$thumbnail-padding: .25rem !default;\n$thumbnail-bg: $body-bg !default;\n$thumbnail-border-width: $border-width !default;\n$thumbnail-border-color: $gray-300 !default;\n$thumbnail-border-radius: $border-radius !default;\n$thumbnail-box-shadow: 0 1px 2px rgba($black, .075) !default;\n\n\n// Figures\n\n$figure-caption-font-size: 90% !default;\n$figure-caption-color: $gray-600 !default;\n\n\n// Breadcrumbs\n\n$breadcrumb-font-size: null !default;\n\n$breadcrumb-padding-y: .75rem !default;\n$breadcrumb-padding-x: 1rem !default;\n$breadcrumb-item-padding: .5rem !default;\n\n$breadcrumb-margin-bottom: 1rem !default;\n\n$breadcrumb-bg: $gray-200 !default;\n$breadcrumb-divider-color: $gray-600 !default;\n$breadcrumb-active-color: $gray-600 !default;\n$breadcrumb-divider: quote(\"/\") !default;\n\n$breadcrumb-border-radius: $border-radius !default;\n\n\n// Carousel\n\n$carousel-control-color: $white !default;\n$carousel-control-width: 15% !default;\n$carousel-control-opacity: .5 !default;\n$carousel-control-hover-opacity: .9 !default;\n$carousel-control-transition: opacity .15s ease !default;\n\n$carousel-indicator-width: 30px !default;\n$carousel-indicator-height: 3px !default;\n$carousel-indicator-hit-area-height: 10px !default;\n$carousel-indicator-spacer: 3px !default;\n$carousel-indicator-active-bg: $white !default;\n$carousel-indicator-transition: opacity .6s ease !default;\n\n$carousel-caption-width: 70% !default;\n$carousel-caption-color: $white !default;\n\n$carousel-control-icon-width: 20px !default;\n\n$carousel-control-prev-icon-bg: url(\"data:image/svg+xml,\") !default;\n$carousel-control-next-icon-bg: url(\"data:image/svg+xml,\") !default;\n\n$carousel-transition-duration: .6s !default;\n$carousel-transition: transform $carousel-transition-duration ease-in-out !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`)\n\n\n// Spinners\n\n$spinner-width: 2rem !default;\n$spinner-height: $spinner-width !default;\n$spinner-border-width: .25em !default;\n\n$spinner-width-sm: 1rem !default;\n$spinner-height-sm: $spinner-width-sm !default;\n$spinner-border-width-sm: .2em !default;\n\n\n// Close\n\n$close-font-size: $font-size-base * 1.5 !default;\n$close-font-weight: $font-weight-bold !default;\n$close-color: $black !default;\n$close-text-shadow: 0 1px 0 $white !default;\n\n\n// Code\n\n$code-font-size: 87.5% !default;\n$code-color: $pink !default;\n\n$kbd-padding-y: .2rem !default;\n$kbd-padding-x: .4rem !default;\n$kbd-font-size: $code-font-size !default;\n$kbd-color: $white !default;\n$kbd-bg: $gray-900 !default;\n\n$pre-color: $gray-900 !default;\n$pre-scrollable-max-height: 340px !default;\n\n\n// Utilities\n\n$displays: none, inline, inline-block, block, table, table-row, table-cell, flex, inline-flex !default;\n$overflows: auto, hidden !default;\n$positions: static, relative, absolute, fixed, sticky !default;\n\n\n// Printing\n\n$print-page-size: a3 !default;\n$print-body-min-width: map-get($grid-breakpoints, \"lg\") !default;\n","// stylelint-disable property-blacklist, scss/dollar-variable-default\n\n// SCSS RFS mixin\n//\n// Automated font-resizing\n//\n// See https://github.com/twbs/rfs\n\n// Configuration\n\n// Base font size\n$rfs-base-font-size: 1.25rem !default;\n$rfs-font-size-unit: rem !default;\n\n// Breakpoint at where font-size starts decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n// Resize font-size based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != \"number\" or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-responsive-font-sizes to false\n$enable-responsive-font-sizes: true !default;\n\n// Cache $rfs-base-font-size unit\n$rfs-base-font-size-unit: unit($rfs-base-font-size);\n\n// Remove px-unit from $rfs-base-font-size for calculations\n@if $rfs-base-font-size-unit == \"px\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1);\n}\n@else if $rfs-base-font-size-unit == \"rem\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1 / $rfs-rem-value);\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == \"px\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == \"rem\" or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1 / $rfs-rem-value);\n}\n\n// Responsive font-size mixin\n@mixin rfs($fs, $important: false) {\n // Cache $fs unit\n $fs-unit: if(type-of($fs) == \"number\", unit($fs), false);\n\n // Add !important suffix if needed\n $rfs-suffix: if($important, \" !important\", \"\");\n\n // If $fs isn't a number (like inherit) or $fs has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $fs-unit or $fs-unit != \"\" and $fs-unit != \"px\" and $fs-unit != \"rem\" or $fs == 0 {\n font-size: #{$fs}#{$rfs-suffix};\n }\n @else {\n // Variables for storing static and fluid rescaling\n $rfs-static: null;\n $rfs-fluid: null;\n\n // Remove px-unit from $fs for calculations\n @if $fs-unit == \"px\" {\n $fs: $fs / ($fs * 0 + 1);\n }\n @else if $fs-unit == \"rem\" {\n $fs: $fs / ($fs * 0 + 1 / $rfs-rem-value);\n }\n\n // Set default font-size\n @if $rfs-font-size-unit == rem {\n $rfs-static: #{$fs / $rfs-rem-value}rem#{$rfs-suffix};\n }\n @else if $rfs-font-size-unit == px {\n $rfs-static: #{$fs}px#{$rfs-suffix};\n }\n @else {\n @error \"`#{$rfs-font-size-unit}` is not a valid unit for $rfs-font-size-unit. Use `px` or `rem`.\";\n }\n\n // Only add media query if font-size is bigger as the minimum font-size\n // If $rfs-factor == 1, no rescaling will take place\n @if $fs > $rfs-base-font-size and $enable-responsive-font-sizes {\n $min-width: null;\n $variable-unit: null;\n\n // Calculate minimum font-size for given font-size\n $fs-min: $rfs-base-font-size + ($fs - $rfs-base-font-size) / $rfs-factor;\n\n // Calculate difference between given font-size and minimum font-size for given font-size\n $fs-diff: $fs - $fs-min;\n\n // Base font-size formatting\n // No need to check if the unit is valid, because we did that before\n $min-width: if($rfs-font-size-unit == rem, #{$fs-min / $rfs-rem-value}rem, #{$fs-min}px);\n\n // If two-dimensional, use smallest of screen width and height\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{$fs-diff * 100 / $rfs-breakpoint}#{$variable-unit};\n\n // Set the calculated font-size.\n $rfs-fluid: calc(#{$min-width} + #{$variable-width}) #{$rfs-suffix};\n }\n\n // Rendering\n @if $rfs-fluid == null {\n // Only render static font-size if no fluid font-size is available\n font-size: $rfs-static;\n }\n @else {\n $mq-value: null;\n\n // RFS breakpoint formatting\n @if $rfs-breakpoint-unit == em or $rfs-breakpoint-unit == rem {\n $mq-value: #{$rfs-breakpoint / $rfs-rem-value}#{$rfs-breakpoint-unit};\n }\n @else if $rfs-breakpoint-unit == px {\n $mq-value: #{$rfs-breakpoint}px;\n }\n @else {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n }\n\n @if $rfs-class == \"disable\" {\n // Adding an extra class increases specificity,\n // which prevents the media query to override the font size\n &,\n .disable-responsive-font-size &,\n &.disable-responsive-font-size {\n font-size: $rfs-static;\n }\n }\n @else {\n font-size: $rfs-static;\n }\n\n @if $rfs-two-dimensional {\n @media (max-width: #{$mq-value}), (max-height: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n @else {\n @media (max-width: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n }\n }\n}\n\n// The font-size & responsive-font-size mixin uses RFS to rescale font sizes\n@mixin font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n\n@mixin responsive-font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n","// Hover mixin and `$enable-hover-media-query` are deprecated.\n//\n// Originally added during our alphas and maintained during betas, this mixin was\n// designed to prevent `:hover` stickiness on iOS-an issue where hover styles\n// would persist after initial touch.\n//\n// For backward compatibility, we've kept these mixins and updated them to\n// always return their regular pseudo-classes instead of a shimmed media query.\n//\n// Issue: https://github.com/twbs/bootstrap/issues/25195\n\n@mixin hover() {\n &:hover { @content; }\n}\n\n@mixin hover-focus() {\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin plain-hover-focus() {\n &,\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin hover-focus-active() {\n &:hover,\n &:focus,\n &:active {\n @content;\n }\n}\n","// stylelint-disable declaration-no-important, selector-list-comma-newline-after\n\n//\n// Headings\n//\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1, .h1 { @include font-size($h1-font-size); }\nh2, .h2 { @include font-size($h2-font-size); }\nh3, .h3 { @include font-size($h3-font-size); }\nh4, .h4 { @include font-size($h4-font-size); }\nh5, .h5 { @include font-size($h5-font-size); }\nh6, .h6 { @include font-size($h6-font-size); }\n\n.lead {\n @include font-size($lead-font-size);\n font-weight: $lead-font-weight;\n}\n\n// Type display classes\n.display-1 {\n @include font-size($display1-size);\n font-weight: $display1-weight;\n line-height: $display-line-height;\n}\n.display-2 {\n @include font-size($display2-size);\n font-weight: $display2-weight;\n line-height: $display-line-height;\n}\n.display-3 {\n @include font-size($display3-size);\n font-weight: $display3-weight;\n line-height: $display-line-height;\n}\n.display-4 {\n @include font-size($display4-size);\n font-weight: $display4-weight;\n line-height: $display-line-height;\n}\n\n\n//\n// Horizontal rules\n//\n\nhr {\n margin-top: $hr-margin-y;\n margin-bottom: $hr-margin-y;\n border: 0;\n border-top: $hr-border-width solid $hr-border-color;\n}\n\n\n//\n// Emphasis\n//\n\nsmall,\n.small {\n @include font-size($small-font-size);\n font-weight: $font-weight-normal;\n}\n\nmark,\n.mark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n//\n// Lists\n//\n\n.list-unstyled {\n @include list-unstyled();\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n @include list-unstyled();\n}\n.list-inline-item {\n display: inline-block;\n\n &:not(:last-child) {\n margin-right: $list-inline-padding;\n }\n}\n\n\n//\n// Misc\n//\n\n// Builds on `abbr`\n.initialism {\n @include font-size(90%);\n text-transform: uppercase;\n}\n\n// Blockquotes\n.blockquote {\n margin-bottom: $spacer;\n @include font-size($blockquote-font-size);\n}\n\n.blockquote-footer {\n display: block;\n @include font-size($blockquote-small-font-size);\n color: $blockquote-small-color;\n\n &::before {\n content: \"\\2014\\00A0\"; // em dash, nbsp\n }\n}\n","// Lists\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n@mixin list-unstyled() {\n padding-left: 0;\n list-style: none;\n}\n","// Responsive images (ensure images don't scale beyond their parents)\n//\n// This is purposefully opt-in via an explicit class rather than being the default for all ``s.\n// We previously tried the \"images are responsive by default\" approach in Bootstrap v2,\n// and abandoned it in Bootstrap v3 because it breaks lots of third-party widgets (including Google Maps)\n// which weren't expecting the images within themselves to be involuntarily resized.\n// See also https://github.com/twbs/bootstrap/issues/18178\n.img-fluid {\n @include img-fluid();\n}\n\n\n// Image thumbnails\n.img-thumbnail {\n padding: $thumbnail-padding;\n background-color: $thumbnail-bg;\n border: $thumbnail-border-width solid $thumbnail-border-color;\n @include border-radius($thumbnail-border-radius);\n @include box-shadow($thumbnail-box-shadow);\n\n // Keep them at most 100% wide\n @include img-fluid();\n}\n\n//\n// Figures\n//\n\n.figure {\n // Ensures the caption's text aligns with the image.\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: $spacer / 2;\n line-height: 1;\n}\n\n.figure-caption {\n @include font-size($figure-caption-font-size);\n color: $figure-caption-color;\n}\n","// Image Mixins\n// - Responsive image\n// - Retina image\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n\n@mixin img-fluid() {\n // Part 1: Set a maximum relative to the parent\n max-width: 100%;\n // Part 2: Override the height to auto, otherwise images will be stretched\n // when setting a width and height attribute on the img element.\n height: auto;\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size.\n\n@mixin img-retina($file-1x, $file-2x, $width-1x, $height-1x) {\n background-image: url($file-1x);\n\n // Autoprefixer takes care of adding -webkit-min-device-pixel-ratio and -o-min-device-pixel-ratio,\n // but doesn't convert dppx=>dpi.\n // There's no such thing as unprefixed min-device-pixel-ratio since it's nonstandard.\n // Compatibility info: https://caniuse.com/#feat=css-media-resolution\n @media only screen and (min-resolution: 192dpi), // IE9-11 don't support dppx\n only screen and (min-resolution: 2dppx) { // Standardized\n background-image: url($file-2x);\n background-size: $width-1x $height-1x;\n }\n @include deprecate(\"`img-retina()`\", \"v4.3.0\", \"v5\");\n}\n","// stylelint-disable property-blacklist\n// Single side border-radius\n\n@mixin border-radius($radius: $border-radius, $fallback-border-radius: false) {\n @if $enable-rounded {\n border-radius: $radius;\n }\n @else if $fallback-border-radius != false {\n border-radius: $fallback-border-radius;\n }\n}\n\n@mixin border-top-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: $radius;\n border-top-right-radius: $radius;\n }\n}\n\n@mixin border-right-radius($radius) {\n @if $enable-rounded {\n border-top-right-radius: $radius;\n border-bottom-right-radius: $radius;\n }\n}\n\n@mixin border-bottom-radius($radius) {\n @if $enable-rounded {\n border-bottom-right-radius: $radius;\n border-bottom-left-radius: $radius;\n }\n}\n\n@mixin border-left-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: $radius;\n border-bottom-left-radius: $radius;\n }\n}\n\n@mixin border-top-left-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: $radius;\n }\n}\n\n@mixin border-top-right-radius($radius) {\n @if $enable-rounded {\n border-top-right-radius: $radius;\n }\n}\n\n@mixin border-bottom-right-radius($radius) {\n @if $enable-rounded {\n border-bottom-right-radius: $radius;\n }\n}\n\n@mixin border-bottom-left-radius($radius) {\n @if $enable-rounded {\n border-bottom-left-radius: $radius;\n }\n}\n","// Inline code\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n @include box-shadow($kbd-box-shadow);\n\n kbd {\n padding: 0;\n @include font-size(100%);\n font-weight: $nested-kbd-font-weight;\n @include box-shadow(none);\n }\n}\n\n// Blocks of code\npre {\n display: block;\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: $pre-scrollable-max-height;\n overflow-y: scroll;\n}\n","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n // Single container class with breakpoint max-widths\n .container {\n @include make-container();\n @include make-container-max-widths();\n }\n\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n @each $name, $width in $grid-breakpoints {\n @if ($container-max-width > $width or $breakpoint == $name) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n }\n }\n }\n }\n}\n\n\n// Row\n//\n// Rows contain your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n }\n\n // Remove the negative margin from default .row, then the horizontal padding\n // from all immediate children columns (to prevent runaway style inheritance).\n .no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n > .col,\n > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n }\n}\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","/// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-container($gutter: $grid-gutter-width) {\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n margin-right: auto;\n margin-left: auto;\n}\n\n\n// For each breakpoint, define the maximum width of the container in a media query\n@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {\n @each $breakpoint, $container-max-width in $max-widths {\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n max-width: $container-max-width;\n }\n }\n}\n\n@mixin make-row($gutter: $grid-gutter-width) {\n display: flex;\n flex-wrap: wrap;\n margin-right: -$gutter / 2;\n margin-left: -$gutter / 2;\n}\n\n@mixin make-col-ready($gutter: $grid-gutter-width) {\n position: relative;\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we use `flex` values\n // later on to override this initial width.\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n}\n\n@mixin make-col($size, $columns: $grid-columns) {\n flex: 0 0 percentage($size / $columns);\n // Add a `max-width` to ensure content within each column does not blow out\n // the width of the column. Applies to IE10+ and Firefox. Chrome and Safari\n // do not appear to require this.\n max-width: percentage($size / $columns);\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%; // Reset earlier grid tiers\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: $size / $columns;\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// numberof columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n & > * {\n flex: 0 0 100% / $count;\n max-width: 100% / $count;\n }\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @return if($n != null and $n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width. Null for the largest (last) breakpoint.\n// The maximum value is calculated as the minimum of the next one less 0.02px\n// to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $next: breakpoint-next($name, $breakpoints);\n @return if($next, breakpoint-min($next, $breakpoints) - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $max: breakpoint-max($name, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($name, $breakpoints) {\n @content;\n }\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n // Common properties for all breakpoints\n %grid-column {\n position: relative;\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n }\n\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n // Allow columns to stretch full width below their breakpoints\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @extend %grid-column;\n }\n }\n .col#{$infix},\n .col#{$infix}-auto {\n @extend %grid-column;\n }\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n\n .order#{$infix}-first { order: -1; }\n\n .order#{$infix}-last { order: $columns + 1; }\n\n @for $i from 0 through $columns {\n .order#{$infix}-#{$i} { order: $i; }\n }\n\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n }\n}\n","//\n// Basic Bootstrap table\n//\n\n.table {\n width: 100%;\n margin-bottom: $spacer;\n color: $table-color;\n background-color: $table-bg; // Reset for nesting within parents with `background-color`.\n\n th,\n td {\n padding: $table-cell-padding;\n vertical-align: top;\n border-top: $table-border-width solid $table-border-color;\n }\n\n thead th {\n vertical-align: bottom;\n border-bottom: (2 * $table-border-width) solid $table-border-color;\n }\n\n tbody + tbody {\n border-top: (2 * $table-border-width) solid $table-border-color;\n }\n}\n\n\n//\n// Condensed table w/ half padding\n//\n\n.table-sm {\n th,\n td {\n padding: $table-cell-padding-sm;\n }\n}\n\n\n// Border versions\n//\n// Add or remove borders all around the table and between all the columns.\n\n.table-bordered {\n border: $table-border-width solid $table-border-color;\n\n th,\n td {\n border: $table-border-width solid $table-border-color;\n }\n\n thead {\n th,\n td {\n border-bottom-width: 2 * $table-border-width;\n }\n }\n}\n\n.table-borderless {\n th,\n td,\n thead th,\n tbody + tbody {\n border: 0;\n }\n}\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n tbody tr:nth-of-type(#{$table-striped-order}) {\n background-color: $table-accent-bg;\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n tbody tr {\n @include hover() {\n color: $table-hover-color;\n background-color: $table-hover-bg;\n }\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n@each $color, $value in $theme-colors {\n @include table-row-variant($color, theme-color-level($color, $table-bg-level), theme-color-level($color, $table-border-level));\n}\n\n@include table-row-variant(active, $table-active-bg);\n\n\n// Dark styles\n//\n// Same table markup, but inverted color scheme: dark background and light text.\n\n// stylelint-disable-next-line no-duplicate-selectors\n.table {\n .thead-dark {\n th {\n color: $table-dark-color;\n background-color: $table-dark-bg;\n border-color: $table-dark-border-color;\n }\n }\n\n .thead-light {\n th {\n color: $table-head-color;\n background-color: $table-head-bg;\n border-color: $table-border-color;\n }\n }\n}\n\n.table-dark {\n color: $table-dark-color;\n background-color: $table-dark-bg;\n\n th,\n td,\n thead th {\n border-color: $table-dark-border-color;\n }\n\n &.table-bordered {\n border: 0;\n }\n\n &.table-striped {\n tbody tr:nth-of-type(#{$table-striped-order}) {\n background-color: $table-dark-accent-bg;\n }\n }\n\n &.table-hover {\n tbody tr {\n @include hover() {\n color: $table-dark-hover-color;\n background-color: $table-dark-hover-bg;\n }\n }\n }\n}\n\n\n// Responsive tables\n//\n// Generate series of `.table-responsive-*` classes for configuring the screen\n// size of where your table will overflow.\n\n.table-responsive {\n @each $breakpoint in map-keys($grid-breakpoints) {\n $next: breakpoint-next($breakpoint, $grid-breakpoints);\n $infix: breakpoint-infix($next, $grid-breakpoints);\n\n &#{$infix} {\n @include media-breakpoint-down($breakpoint) {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n\n // Prevent double border on horizontal scroll due to use of `display: block;`\n > .table-bordered {\n border: 0;\n }\n }\n }\n }\n}\n","// Tables\n\n@mixin table-row-variant($state, $background, $border: null) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table-#{$state} {\n &,\n > th,\n > td {\n background-color: $background;\n }\n\n @if $border != null {\n th,\n td,\n thead th,\n tbody + tbody {\n border-color: $border;\n }\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover {\n $hover-background: darken($background, 5%);\n\n .table-#{$state} {\n @include hover() {\n background-color: $hover-background;\n\n > td,\n > th {\n background-color: $hover-background;\n }\n }\n }\n }\n}\n","// Bootstrap functions\n//\n// Utility mixins and functions for evaluating source code across our variables, maps, and mixins.\n\n// Ascending\n// Used to evaluate Sass maps like our grid breakpoints.\n@mixin _assert-ascending($map, $map-name) {\n $prev-key: null;\n $prev-num: null;\n @each $key, $num in $map {\n @if $prev-num == null or unit($num) == \"%\" or unit($prev-num) == \"%\" {\n // Do nothing\n } @else if not comparable($prev-num, $num) {\n @warn \"Potentially invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} whose unit makes it incomparable to #{$prev-num}, the value of the previous key '#{$prev-key}' !\";\n } @else if $prev-num >= $num {\n @warn \"Invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} which isn't greater than #{$prev-num}, the value of the previous key '#{$prev-key}' !\";\n }\n $prev-key: $key;\n $prev-num: $num;\n }\n}\n\n// Starts at zero\n// Used to ensure the min-width of the lowest breakpoint starts at 0.\n@mixin _assert-starts-at-zero($map, $map-name: \"$grid-breakpoints\") {\n $values: map-values($map);\n $first-value: nth($values, 1);\n @if $first-value != 0 {\n @warn \"First breakpoint in #{$map-name} must start at 0, but starts at #{$first-value}.\";\n }\n}\n\n// Replace `$search` with `$replace` in `$string`\n// Used on our SVG icon backgrounds for custom forms.\n//\n// @author Hugo Giraudel\n// @param {String} $string - Initial string\n// @param {String} $search - Substring to replace\n// @param {String} $replace ('') - New value\n// @return {String} - Updated string\n@function str-replace($string, $search, $replace: \"\") {\n $index: str-index($string, $search);\n\n @if $index {\n @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);\n }\n\n @return $string;\n}\n\n// See https://codepen.io/kevinweber/pen/dXWoRw\n@function escape-svg($string) {\n @if str-index($string, \"data:image/svg+xml\") {\n @each $char, $encoded in $escaped-characters {\n $string: str-replace($string, $char, $encoded);\n }\n }\n\n @return $string;\n}\n\n// Color contrast\n@function color-yiq($color, $dark: $yiq-text-dark, $light: $yiq-text-light) {\n $r: red($color);\n $g: green($color);\n $b: blue($color);\n\n $yiq: (($r * 299) + ($g * 587) + ($b * 114)) / 1000;\n\n @if ($yiq >= $yiq-contrasted-threshold) {\n @return $dark;\n } @else {\n @return $light;\n }\n}\n\n// Retrieve color Sass maps\n@function color($key: \"blue\") {\n @return map-get($colors, $key);\n}\n\n@function theme-color($key: \"primary\") {\n @return map-get($theme-colors, $key);\n}\n\n@function gray($key: \"100\") {\n @return map-get($grays, $key);\n}\n\n// Request a theme color level\n@function theme-color-level($color-name: \"primary\", $level: 0) {\n $color: theme-color($color-name);\n $color-base: if($level > 0, $black, $white);\n $level: abs($level);\n\n @return mix($color-base, $color, $level * $theme-color-interval);\n}\n\n// Return valid calc\n@function add($value1, $value2, $return-calc: true) {\n @if $value1 == null {\n @return $value2;\n }\n\n @if $value2 == null {\n @return $value1;\n }\n\n @if type-of($value1) == number and type-of($value2) == number and comparable($value1, $value2) {\n @return $value1 + $value2;\n }\n\n @return if($return-calc == true, calc(#{$value1} + #{$value2}), $value1 + unquote(\" + \") + $value2);\n}\n\n@function subtract($value1, $value2, $return-calc: true) {\n @if $value1 == null and $value2 == null {\n @return null;\n }\n\n @if $value1 == null {\n @return -$value2;\n }\n\n @if $value2 == null {\n @return $value1;\n }\n\n @if type-of($value1) == number and type-of($value2) == number and comparable($value1, $value2) {\n @return $value1 - $value2;\n }\n\n @return if($return-calc == true, calc(#{$value1} - #{$value2}), $value1 + unquote(\" - \") + $value2);\n}\n","// stylelint-disable selector-no-qualifying-type\n\n//\n// Textual form controls\n//\n\n.form-control {\n display: block;\n width: 100%;\n height: $input-height;\n padding: $input-padding-y $input-padding-x;\n font-family: $input-font-family;\n @include font-size($input-font-size);\n font-weight: $input-font-weight;\n line-height: $input-line-height;\n color: $input-color;\n background-color: $input-bg;\n background-clip: padding-box;\n border: $input-border-width solid $input-border-color;\n\n // Note: This has no effect on `s in CSS.\n @include border-radius($input-border-radius, 0);\n\n @include box-shadow($input-box-shadow);\n @include transition($input-transition);\n\n // Unstyle the caret on ` receives focus\n // in IE and (under certain conditions) Edge, as it looks bad and cannot be made to\n // match the appearance of the native widget.\n // See https://github.com/twbs/bootstrap/issues/19398.\n color: $input-color;\n background-color: $input-bg;\n }\n}\n\n// Make file inputs better match text inputs by forcing them to new lines.\n.form-control-file,\n.form-control-range {\n display: block;\n width: 100%;\n}\n\n\n//\n// Labels\n//\n\n// For use with horizontal and inline forms, when you need the label (or legend)\n// text to align with the form controls.\n.col-form-label {\n padding-top: add($input-padding-y, $input-border-width);\n padding-bottom: add($input-padding-y, $input-border-width);\n margin-bottom: 0; // Override the `
    ',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:Se,popperConfig:null},Fe="show",Ue="out",We={HIDE:"hide"+Oe,HIDDEN:"hidden"+Oe,SHOW:"show"+Oe,SHOWN:"shown"+Oe,INSERTED:"inserted"+Oe,CLICK:"click"+Oe,FOCUSIN:"focusin"+Oe,FOCUSOUT:"focusout"+Oe,MOUSEENTER:"mouseenter"+Oe,MOUSELEAVE:"mouseleave"+Oe},qe="fade",Me="show",Ke=".tooltip-inner",Qe=".arrow",Be="hover",Ve="focus",Ye="click",ze="manual",Xe=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Me))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal",this._hideModalHandler),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(qe);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,this._getPopperConfig(a)),g(o).addClass(Me),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===Ue&&e._leave(null,e)};if(g(this.tip).hasClass(qe)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){function e(){n._hoverState!==Fe&&i.parentNode&&i.parentNode.removeChild(i),n._cleanTipClass(),n.element.removeAttribute("aria-describedby"),g(n.element).trigger(n.constructor.Event.HIDDEN),null!==n._popper&&n._popper.destroy(),t&&t()}var n=this,i=this.getTipElement(),o=g.Event(this.constructor.Event.HIDE);if(g(this.element).trigger(o),!o.isDefaultPrevented()){if(g(i).removeClass(Me),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[Ye]=!1,this._activeTrigger[Ve]=!1,this._activeTrigger[Be]=!1,g(this.tip).hasClass(qe)){var r=_.getTransitionDurationFromElement(i);g(i).one(_.TRANSITION_END,e).emulateTransitionEnd(r)}else e();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Pe+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(Ke)),this.getTitle()),g(t).removeClass(qe+" "+Me)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=we(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t=t||("function"==typeof this.config.title?this.config.title.call(this.element):this.config.title)},t._getPopperConfig=function(t){var e=this;return l({},{placement:t,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:Qe},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}},{},this.config.popperConfig)},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,{},e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return Re[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==ze){var e=t===Be?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===Be?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),this._hideModalHandler=function(){i.element&&i.hide()},g(this.element).closest(".modal").on("hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");!this.element.getAttribute("title")&&"string"==t||(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Ve:Be]=!0),g(e.getTipElement()).hasClass(Me)||e._hoverState===Fe?e._hoverState=Fe:(clearTimeout(e._timeout),e._hoverState=Fe,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===Fe&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Ve:Be]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=Ue,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===Ue&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=g(this.element).data();return Object.keys(e).forEach(function(t){-1!==je.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,{},e,{},"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(Ae,t,this.constructor.DefaultType),t.sanitize&&(t.template=we(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Le);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(qe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(Ne),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(Ne,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.4.1"}},{key:"Default",get:function(){return xe}},{key:"NAME",get:function(){return Ae}},{key:"DATA_KEY",get:function(){return Ne}},{key:"Event",get:function(){return We}},{key:"EVENT_KEY",get:function(){return Oe}},{key:"DefaultType",get:function(){return He}}]),i}();g.fn[Ae]=Xe._jQueryInterface,g.fn[Ae].Constructor=Xe,g.fn[Ae].noConflict=function(){return g.fn[Ae]=ke,Xe._jQueryInterface};var $e="popover",Ge="bs.popover",Je="."+Ge,Ze=g.fn[$e],tn="bs-popover",en=new RegExp("(^|\\s)"+tn+"\\S+","g"),nn=l({},Xe.Default,{placement:"right",trigger:"click",content:"",template:''}),on=l({},Xe.DefaultType,{content:"(string|element|function)"}),rn="fade",sn="show",an=".popover-header",ln=".popover-body",cn={HIDE:"hide"+Je,HIDDEN:"hidden"+Je,SHOW:"show"+Je,SHOWN:"shown"+Je,INSERTED:"inserted"+Je,CLICK:"click"+Je,FOCUSIN:"focusin"+Je,FOCUSOUT:"focusout"+Je,MOUSEENTER:"mouseenter"+Je,MOUSELEAVE:"mouseleave"+Je},hn=function(t){function i(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),(t.prototype.constructor=t).__proto__=e}(i,t);var e=i.prototype;return e.isWithContent=function(){return this.getTitle()||this._getContent()},e.addAttachmentClass=function(t){g(this.getTipElement()).addClass(tn+"-"+t)},e.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},e.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(an),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(ln),e),t.removeClass(rn+" "+sn)},e._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},e._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(en);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t {\n called = true\n })\n\n setTimeout(() => {\n if (!called) {\n Util.triggerTransitionEnd(this)\n }\n }, duration)\n\n return this\n}\n\nfunction setTransitionEndSupport() {\n $.fn.emulateTransitionEnd = transitionEndEmulator\n $.event.special[Util.TRANSITION_END] = getSpecialTransitionEndEvent()\n}\n\n/**\n * --------------------------------------------------------------------------\n * Public Util Api\n * --------------------------------------------------------------------------\n */\n\nconst Util = {\n\n TRANSITION_END: 'bsTransitionEnd',\n\n getUID(prefix) {\n do {\n // eslint-disable-next-line no-bitwise\n prefix += ~~(Math.random() * MAX_UID) // \"~~\" acts like a faster Math.floor() here\n } while (document.getElementById(prefix))\n return prefix\n },\n\n getSelectorFromElement(element) {\n let selector = element.getAttribute('data-target')\n\n if (!selector || selector === '#') {\n const hrefAttr = element.getAttribute('href')\n selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : ''\n }\n\n try {\n return document.querySelector(selector) ? selector : null\n } catch (err) {\n return null\n }\n },\n\n getTransitionDurationFromElement(element) {\n if (!element) {\n return 0\n }\n\n // Get transition-duration of the element\n let transitionDuration = $(element).css('transition-duration')\n let transitionDelay = $(element).css('transition-delay')\n\n const floatTransitionDuration = parseFloat(transitionDuration)\n const floatTransitionDelay = parseFloat(transitionDelay)\n\n // Return 0 if element or transition duration is not found\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0\n }\n\n // If multiple durations are defined, take the first\n transitionDuration = transitionDuration.split(',')[0]\n transitionDelay = transitionDelay.split(',')[0]\n\n return (parseFloat(transitionDuration) + parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER\n },\n\n reflow(element) {\n return element.offsetHeight\n },\n\n triggerTransitionEnd(element) {\n $(element).trigger(TRANSITION_END)\n },\n\n // TODO: Remove in v5\n supportsTransitionEnd() {\n return Boolean(TRANSITION_END)\n },\n\n isElement(obj) {\n return (obj[0] || obj).nodeType\n },\n\n typeCheckConfig(componentName, config, configTypes) {\n for (const property in configTypes) {\n if (Object.prototype.hasOwnProperty.call(configTypes, property)) {\n const expectedTypes = configTypes[property]\n const value = config[property]\n const valueType = value && Util.isElement(value)\n ? 'element' : toType(value)\n\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new Error(\n `${componentName.toUpperCase()}: ` +\n `Option \"${property}\" provided type \"${valueType}\" ` +\n `but expected type \"${expectedTypes}\".`)\n }\n }\n }\n },\n\n findShadowRoot(element) {\n if (!document.documentElement.attachShadow) {\n return null\n }\n\n // Can find the shadow root otherwise it'll return the document\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode()\n return root instanceof ShadowRoot ? root : null\n }\n\n if (element instanceof ShadowRoot) {\n return element\n }\n\n // when we don't find a shadow root\n if (!element.parentNode) {\n return null\n }\n\n return Util.findShadowRoot(element.parentNode)\n },\n\n jQueryDetection() {\n if (typeof $ === 'undefined') {\n throw new TypeError('Bootstrap\\'s JavaScript requires jQuery. jQuery must be included before Bootstrap\\'s JavaScript.')\n }\n\n const version = $.fn.jquery.split(' ')[0].split('.')\n const minMajor = 1\n const ltMajor = 2\n const minMinor = 9\n const minPatch = 1\n const maxMajor = 4\n\n if (version[0] < ltMajor && version[1] < minMinor || version[0] === minMajor && version[1] === minMinor && version[2] < minPatch || version[0] >= maxMajor) {\n throw new Error('Bootstrap\\'s JavaScript requires at least jQuery v1.9.1 but less than v4.0.0')\n }\n }\n}\n\nUtil.jQueryDetection()\nsetTransitionEndSupport()\n\nexport default Util\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'alert'\nconst VERSION = '4.4.1'\nconst DATA_KEY = 'bs.alert'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst Selector = {\n DISMISS : '[data-dismiss=\"alert\"]'\n}\n\nconst Event = {\n CLOSE : `close${EVENT_KEY}`,\n CLOSED : `closed${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n ALERT : 'alert',\n FADE : 'fade',\n SHOW : 'show'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Alert {\n constructor(element) {\n this._element = element\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n // Public\n\n close(element) {\n let rootElement = this._element\n if (element) {\n rootElement = this._getRootElement(element)\n }\n\n const customEvent = this._triggerCloseEvent(rootElement)\n\n if (customEvent.isDefaultPrevented()) {\n return\n }\n\n this._removeElement(rootElement)\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n this._element = null\n }\n\n // Private\n\n _getRootElement(element) {\n const selector = Util.getSelectorFromElement(element)\n let parent = false\n\n if (selector) {\n parent = document.querySelector(selector)\n }\n\n if (!parent) {\n parent = $(element).closest(`.${ClassName.ALERT}`)[0]\n }\n\n return parent\n }\n\n _triggerCloseEvent(element) {\n const closeEvent = $.Event(Event.CLOSE)\n\n $(element).trigger(closeEvent)\n return closeEvent\n }\n\n _removeElement(element) {\n $(element).removeClass(ClassName.SHOW)\n\n if (!$(element).hasClass(ClassName.FADE)) {\n this._destroyElement(element)\n return\n }\n\n const transitionDuration = Util.getTransitionDurationFromElement(element)\n\n $(element)\n .one(Util.TRANSITION_END, (event) => this._destroyElement(element, event))\n .emulateTransitionEnd(transitionDuration)\n }\n\n _destroyElement(element) {\n $(element)\n .detach()\n .trigger(Event.CLOSED)\n .remove()\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n const $element = $(this)\n let data = $element.data(DATA_KEY)\n\n if (!data) {\n data = new Alert(this)\n $element.data(DATA_KEY, data)\n }\n\n if (config === 'close') {\n data[config](this)\n }\n })\n }\n\n static _handleDismiss(alertInstance) {\n return function (event) {\n if (event) {\n event.preventDefault()\n }\n\n alertInstance.close(this)\n }\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document).on(\n Event.CLICK_DATA_API,\n Selector.DISMISS,\n Alert._handleDismiss(new Alert())\n)\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Alert._jQueryInterface\n$.fn[NAME].Constructor = Alert\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Alert._jQueryInterface\n}\n\nexport default Alert\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'button'\nconst VERSION = '4.4.1'\nconst DATA_KEY = 'bs.button'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst ClassName = {\n ACTIVE : 'active',\n BUTTON : 'btn',\n FOCUS : 'focus'\n}\n\nconst Selector = {\n DATA_TOGGLE_CARROT : '[data-toggle^=\"button\"]',\n DATA_TOGGLES : '[data-toggle=\"buttons\"]',\n DATA_TOGGLE : '[data-toggle=\"button\"]',\n DATA_TOGGLES_BUTTONS : '[data-toggle=\"buttons\"] .btn',\n INPUT : 'input:not([type=\"hidden\"])',\n ACTIVE : '.active',\n BUTTON : '.btn'\n}\n\nconst Event = {\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`,\n FOCUS_BLUR_DATA_API : `focus${EVENT_KEY}${DATA_API_KEY} ` +\n `blur${EVENT_KEY}${DATA_API_KEY}`,\n LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Button {\n constructor(element) {\n this._element = element\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n // Public\n\n toggle() {\n let triggerChangeEvent = true\n let addAriaPressed = true\n const rootElement = $(this._element).closest(\n Selector.DATA_TOGGLES\n )[0]\n\n if (rootElement) {\n const input = this._element.querySelector(Selector.INPUT)\n\n if (input) {\n if (input.type === 'radio') {\n if (input.checked &&\n this._element.classList.contains(ClassName.ACTIVE)) {\n triggerChangeEvent = false\n } else {\n const activeElement = rootElement.querySelector(Selector.ACTIVE)\n\n if (activeElement) {\n $(activeElement).removeClass(ClassName.ACTIVE)\n }\n }\n } else if (input.type === 'checkbox') {\n if (this._element.tagName === 'LABEL' && input.checked === this._element.classList.contains(ClassName.ACTIVE)) {\n triggerChangeEvent = false\n }\n } else {\n // if it's not a radio button or checkbox don't add a pointless/invalid checked property to the input\n triggerChangeEvent = false\n }\n\n if (triggerChangeEvent) {\n input.checked = !this._element.classList.contains(ClassName.ACTIVE)\n $(input).trigger('change')\n }\n\n input.focus()\n addAriaPressed = false\n }\n }\n\n if (!(this._element.hasAttribute('disabled') || this._element.classList.contains('disabled'))) {\n if (addAriaPressed) {\n this._element.setAttribute('aria-pressed',\n !this._element.classList.contains(ClassName.ACTIVE))\n }\n\n if (triggerChangeEvent) {\n $(this._element).toggleClass(ClassName.ACTIVE)\n }\n }\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n this._element = null\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n\n if (!data) {\n data = new Button(this)\n $(this).data(DATA_KEY, data)\n }\n\n if (config === 'toggle') {\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document)\n .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE_CARROT, (event) => {\n let button = event.target\n\n if (!$(button).hasClass(ClassName.BUTTON)) {\n button = $(button).closest(Selector.BUTTON)[0]\n }\n\n if (!button || button.hasAttribute('disabled') || button.classList.contains('disabled')) {\n event.preventDefault() // work around Firefox bug #1540995\n } else {\n const inputBtn = button.querySelector(Selector.INPUT)\n\n if (inputBtn && (inputBtn.hasAttribute('disabled') || inputBtn.classList.contains('disabled'))) {\n event.preventDefault() // work around Firefox bug #1540995\n return\n }\n\n Button._jQueryInterface.call($(button), 'toggle')\n }\n })\n .on(Event.FOCUS_BLUR_DATA_API, Selector.DATA_TOGGLE_CARROT, (event) => {\n const button = $(event.target).closest(Selector.BUTTON)[0]\n $(button).toggleClass(ClassName.FOCUS, /^focus(in)?$/.test(event.type))\n })\n\n$(window).on(Event.LOAD_DATA_API, () => {\n // ensure correct active class is set to match the controls' actual values/states\n\n // find all checkboxes/readio buttons inside data-toggle groups\n let buttons = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLES_BUTTONS))\n for (let i = 0, len = buttons.length; i < len; i++) {\n const button = buttons[i]\n const input = button.querySelector(Selector.INPUT)\n if (input.checked || input.hasAttribute('checked')) {\n button.classList.add(ClassName.ACTIVE)\n } else {\n button.classList.remove(ClassName.ACTIVE)\n }\n }\n\n // find all button toggles\n buttons = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLE))\n for (let i = 0, len = buttons.length; i < len; i++) {\n const button = buttons[i]\n if (button.getAttribute('aria-pressed') === 'true') {\n button.classList.add(ClassName.ACTIVE)\n } else {\n button.classList.remove(ClassName.ACTIVE)\n }\n }\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Button._jQueryInterface\n$.fn[NAME].Constructor = Button\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Button._jQueryInterface\n}\n\nexport default Button\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'carousel'\nconst VERSION = '4.4.1'\nconst DATA_KEY = 'bs.carousel'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst ARROW_LEFT_KEYCODE = 37 // KeyboardEvent.which value for left arrow key\nconst ARROW_RIGHT_KEYCODE = 39 // KeyboardEvent.which value for right arrow key\nconst TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch\nconst SWIPE_THRESHOLD = 40\n\nconst Default = {\n interval : 5000,\n keyboard : true,\n slide : false,\n pause : 'hover',\n wrap : true,\n touch : true\n}\n\nconst DefaultType = {\n interval : '(number|boolean)',\n keyboard : 'boolean',\n slide : '(boolean|string)',\n pause : '(string|boolean)',\n wrap : 'boolean',\n touch : 'boolean'\n}\n\nconst Direction = {\n NEXT : 'next',\n PREV : 'prev',\n LEFT : 'left',\n RIGHT : 'right'\n}\n\nconst Event = {\n SLIDE : `slide${EVENT_KEY}`,\n SLID : `slid${EVENT_KEY}`,\n KEYDOWN : `keydown${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`,\n TOUCHSTART : `touchstart${EVENT_KEY}`,\n TOUCHMOVE : `touchmove${EVENT_KEY}`,\n TOUCHEND : `touchend${EVENT_KEY}`,\n POINTERDOWN : `pointerdown${EVENT_KEY}`,\n POINTERUP : `pointerup${EVENT_KEY}`,\n DRAG_START : `dragstart${EVENT_KEY}`,\n LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n CAROUSEL : 'carousel',\n ACTIVE : 'active',\n SLIDE : 'slide',\n RIGHT : 'carousel-item-right',\n LEFT : 'carousel-item-left',\n NEXT : 'carousel-item-next',\n PREV : 'carousel-item-prev',\n ITEM : 'carousel-item',\n POINTER_EVENT : 'pointer-event'\n}\n\nconst Selector = {\n ACTIVE : '.active',\n ACTIVE_ITEM : '.active.carousel-item',\n ITEM : '.carousel-item',\n ITEM_IMG : '.carousel-item img',\n NEXT_PREV : '.carousel-item-next, .carousel-item-prev',\n INDICATORS : '.carousel-indicators',\n DATA_SLIDE : '[data-slide], [data-slide-to]',\n DATA_RIDE : '[data-ride=\"carousel\"]'\n}\n\nconst PointerType = {\n TOUCH : 'touch',\n PEN : 'pen'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\nclass Carousel {\n constructor(element, config) {\n this._items = null\n this._interval = null\n this._activeElement = null\n this._isPaused = false\n this._isSliding = false\n this.touchTimeout = null\n this.touchStartX = 0\n this.touchDeltaX = 0\n\n this._config = this._getConfig(config)\n this._element = element\n this._indicatorsElement = this._element.querySelector(Selector.INDICATORS)\n this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0\n this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent)\n\n this._addEventListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n next() {\n if (!this._isSliding) {\n this._slide(Direction.NEXT)\n }\n }\n\n nextWhenVisible() {\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden &&\n ($(this._element).is(':visible') && $(this._element).css('visibility') !== 'hidden')) {\n this.next()\n }\n }\n\n prev() {\n if (!this._isSliding) {\n this._slide(Direction.PREV)\n }\n }\n\n pause(event) {\n if (!event) {\n this._isPaused = true\n }\n\n if (this._element.querySelector(Selector.NEXT_PREV)) {\n Util.triggerTransitionEnd(this._element)\n this.cycle(true)\n }\n\n clearInterval(this._interval)\n this._interval = null\n }\n\n cycle(event) {\n if (!event) {\n this._isPaused = false\n }\n\n if (this._interval) {\n clearInterval(this._interval)\n this._interval = null\n }\n\n if (this._config.interval && !this._isPaused) {\n this._interval = setInterval(\n (document.visibilityState ? this.nextWhenVisible : this.next).bind(this),\n this._config.interval\n )\n }\n }\n\n to(index) {\n this._activeElement = this._element.querySelector(Selector.ACTIVE_ITEM)\n\n const activeIndex = this._getItemIndex(this._activeElement)\n\n if (index > this._items.length - 1 || index < 0) {\n return\n }\n\n if (this._isSliding) {\n $(this._element).one(Event.SLID, () => this.to(index))\n return\n }\n\n if (activeIndex === index) {\n this.pause()\n this.cycle()\n return\n }\n\n const direction = index > activeIndex\n ? Direction.NEXT\n : Direction.PREV\n\n this._slide(direction, this._items[index])\n }\n\n dispose() {\n $(this._element).off(EVENT_KEY)\n $.removeData(this._element, DATA_KEY)\n\n this._items = null\n this._config = null\n this._element = null\n this._interval = null\n this._isPaused = null\n this._isSliding = null\n this._activeElement = null\n this._indicatorsElement = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _handleSwipe() {\n const absDeltax = Math.abs(this.touchDeltaX)\n\n if (absDeltax <= SWIPE_THRESHOLD) {\n return\n }\n\n const direction = absDeltax / this.touchDeltaX\n\n this.touchDeltaX = 0\n\n // swipe left\n if (direction > 0) {\n this.prev()\n }\n\n // swipe right\n if (direction < 0) {\n this.next()\n }\n }\n\n _addEventListeners() {\n if (this._config.keyboard) {\n $(this._element)\n .on(Event.KEYDOWN, (event) => this._keydown(event))\n }\n\n if (this._config.pause === 'hover') {\n $(this._element)\n .on(Event.MOUSEENTER, (event) => this.pause(event))\n .on(Event.MOUSELEAVE, (event) => this.cycle(event))\n }\n\n if (this._config.touch) {\n this._addTouchEventListeners()\n }\n }\n\n _addTouchEventListeners() {\n if (!this._touchSupported) {\n return\n }\n\n const start = (event) => {\n if (this._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {\n this.touchStartX = event.originalEvent.clientX\n } else if (!this._pointerEvent) {\n this.touchStartX = event.originalEvent.touches[0].clientX\n }\n }\n\n const move = (event) => {\n // ensure swiping with one touch and not pinching\n if (event.originalEvent.touches && event.originalEvent.touches.length > 1) {\n this.touchDeltaX = 0\n } else {\n this.touchDeltaX = event.originalEvent.touches[0].clientX - this.touchStartX\n }\n }\n\n const end = (event) => {\n if (this._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {\n this.touchDeltaX = event.originalEvent.clientX - this.touchStartX\n }\n\n this._handleSwipe()\n if (this._config.pause === 'hover') {\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n\n this.pause()\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout)\n }\n this.touchTimeout = setTimeout((event) => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval)\n }\n }\n\n $(this._element.querySelectorAll(Selector.ITEM_IMG)).on(Event.DRAG_START, (e) => e.preventDefault())\n if (this._pointerEvent) {\n $(this._element).on(Event.POINTERDOWN, (event) => start(event))\n $(this._element).on(Event.POINTERUP, (event) => end(event))\n\n this._element.classList.add(ClassName.POINTER_EVENT)\n } else {\n $(this._element).on(Event.TOUCHSTART, (event) => start(event))\n $(this._element).on(Event.TOUCHMOVE, (event) => move(event))\n $(this._element).on(Event.TOUCHEND, (event) => end(event))\n }\n }\n\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return\n }\n\n switch (event.which) {\n case ARROW_LEFT_KEYCODE:\n event.preventDefault()\n this.prev()\n break\n case ARROW_RIGHT_KEYCODE:\n event.preventDefault()\n this.next()\n break\n default:\n }\n }\n\n _getItemIndex(element) {\n this._items = element && element.parentNode\n ? [].slice.call(element.parentNode.querySelectorAll(Selector.ITEM))\n : []\n return this._items.indexOf(element)\n }\n\n _getItemByDirection(direction, activeElement) {\n const isNextDirection = direction === Direction.NEXT\n const isPrevDirection = direction === Direction.PREV\n const activeIndex = this._getItemIndex(activeElement)\n const lastItemIndex = this._items.length - 1\n const isGoingToWrap = isPrevDirection && activeIndex === 0 ||\n isNextDirection && activeIndex === lastItemIndex\n\n if (isGoingToWrap && !this._config.wrap) {\n return activeElement\n }\n\n const delta = direction === Direction.PREV ? -1 : 1\n const itemIndex = (activeIndex + delta) % this._items.length\n\n return itemIndex === -1\n ? this._items[this._items.length - 1] : this._items[itemIndex]\n }\n\n _triggerSlideEvent(relatedTarget, eventDirectionName) {\n const targetIndex = this._getItemIndex(relatedTarget)\n const fromIndex = this._getItemIndex(this._element.querySelector(Selector.ACTIVE_ITEM))\n const slideEvent = $.Event(Event.SLIDE, {\n relatedTarget,\n direction: eventDirectionName,\n from: fromIndex,\n to: targetIndex\n })\n\n $(this._element).trigger(slideEvent)\n\n return slideEvent\n }\n\n _setActiveIndicatorElement(element) {\n if (this._indicatorsElement) {\n const indicators = [].slice.call(this._indicatorsElement.querySelectorAll(Selector.ACTIVE))\n $(indicators)\n .removeClass(ClassName.ACTIVE)\n\n const nextIndicator = this._indicatorsElement.children[\n this._getItemIndex(element)\n ]\n\n if (nextIndicator) {\n $(nextIndicator).addClass(ClassName.ACTIVE)\n }\n }\n }\n\n _slide(direction, element) {\n const activeElement = this._element.querySelector(Selector.ACTIVE_ITEM)\n const activeElementIndex = this._getItemIndex(activeElement)\n const nextElement = element || activeElement &&\n this._getItemByDirection(direction, activeElement)\n const nextElementIndex = this._getItemIndex(nextElement)\n const isCycling = Boolean(this._interval)\n\n let directionalClassName\n let orderClassName\n let eventDirectionName\n\n if (direction === Direction.NEXT) {\n directionalClassName = ClassName.LEFT\n orderClassName = ClassName.NEXT\n eventDirectionName = Direction.LEFT\n } else {\n directionalClassName = ClassName.RIGHT\n orderClassName = ClassName.PREV\n eventDirectionName = Direction.RIGHT\n }\n\n if (nextElement && $(nextElement).hasClass(ClassName.ACTIVE)) {\n this._isSliding = false\n return\n }\n\n const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName)\n if (slideEvent.isDefaultPrevented()) {\n return\n }\n\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n return\n }\n\n this._isSliding = true\n\n if (isCycling) {\n this.pause()\n }\n\n this._setActiveIndicatorElement(nextElement)\n\n const slidEvent = $.Event(Event.SLID, {\n relatedTarget: nextElement,\n direction: eventDirectionName,\n from: activeElementIndex,\n to: nextElementIndex\n })\n\n if ($(this._element).hasClass(ClassName.SLIDE)) {\n $(nextElement).addClass(orderClassName)\n\n Util.reflow(nextElement)\n\n $(activeElement).addClass(directionalClassName)\n $(nextElement).addClass(directionalClassName)\n\n const nextElementInterval = parseInt(nextElement.getAttribute('data-interval'), 10)\n if (nextElementInterval) {\n this._config.defaultInterval = this._config.defaultInterval || this._config.interval\n this._config.interval = nextElementInterval\n } else {\n this._config.interval = this._config.defaultInterval || this._config.interval\n }\n\n const transitionDuration = Util.getTransitionDurationFromElement(activeElement)\n\n $(activeElement)\n .one(Util.TRANSITION_END, () => {\n $(nextElement)\n .removeClass(`${directionalClassName} ${orderClassName}`)\n .addClass(ClassName.ACTIVE)\n\n $(activeElement).removeClass(`${ClassName.ACTIVE} ${orderClassName} ${directionalClassName}`)\n\n this._isSliding = false\n\n setTimeout(() => $(this._element).trigger(slidEvent), 0)\n })\n .emulateTransitionEnd(transitionDuration)\n } else {\n $(activeElement).removeClass(ClassName.ACTIVE)\n $(nextElement).addClass(ClassName.ACTIVE)\n\n this._isSliding = false\n $(this._element).trigger(slidEvent)\n }\n\n if (isCycling) {\n this.cycle()\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n let _config = {\n ...Default,\n ...$(this).data()\n }\n\n if (typeof config === 'object') {\n _config = {\n ..._config,\n ...config\n }\n }\n\n const action = typeof config === 'string' ? config : _config.slide\n\n if (!data) {\n data = new Carousel(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'number') {\n data.to(config)\n } else if (typeof action === 'string') {\n if (typeof data[action] === 'undefined') {\n throw new TypeError(`No method named \"${action}\"`)\n }\n data[action]()\n } else if (_config.interval && _config.ride) {\n data.pause()\n data.cycle()\n }\n })\n }\n\n static _dataApiClickHandler(event) {\n const selector = Util.getSelectorFromElement(this)\n\n if (!selector) {\n return\n }\n\n const target = $(selector)[0]\n\n if (!target || !$(target).hasClass(ClassName.CAROUSEL)) {\n return\n }\n\n const config = {\n ...$(target).data(),\n ...$(this).data()\n }\n const slideIndex = this.getAttribute('data-slide-to')\n\n if (slideIndex) {\n config.interval = false\n }\n\n Carousel._jQueryInterface.call($(target), config)\n\n if (slideIndex) {\n $(target).data(DATA_KEY).to(slideIndex)\n }\n\n event.preventDefault()\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document)\n .on(Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel._dataApiClickHandler)\n\n$(window).on(Event.LOAD_DATA_API, () => {\n const carousels = [].slice.call(document.querySelectorAll(Selector.DATA_RIDE))\n for (let i = 0, len = carousels.length; i < len; i++) {\n const $carousel = $(carousels[i])\n Carousel._jQueryInterface.call($carousel, $carousel.data())\n }\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Carousel._jQueryInterface\n$.fn[NAME].Constructor = Carousel\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Carousel._jQueryInterface\n}\n\nexport default Carousel\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'collapse'\nconst VERSION = '4.4.1'\nconst DATA_KEY = 'bs.collapse'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst Default = {\n toggle : true,\n parent : ''\n}\n\nconst DefaultType = {\n toggle : 'boolean',\n parent : '(string|element)'\n}\n\nconst Event = {\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n SHOW : 'show',\n COLLAPSE : 'collapse',\n COLLAPSING : 'collapsing',\n COLLAPSED : 'collapsed'\n}\n\nconst Dimension = {\n WIDTH : 'width',\n HEIGHT : 'height'\n}\n\nconst Selector = {\n ACTIVES : '.show, .collapsing',\n DATA_TOGGLE : '[data-toggle=\"collapse\"]'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Collapse {\n constructor(element, config) {\n this._isTransitioning = false\n this._element = element\n this._config = this._getConfig(config)\n this._triggerArray = [].slice.call(document.querySelectorAll(\n `[data-toggle=\"collapse\"][href=\"#${element.id}\"],` +\n `[data-toggle=\"collapse\"][data-target=\"#${element.id}\"]`\n ))\n\n const toggleList = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLE))\n for (let i = 0, len = toggleList.length; i < len; i++) {\n const elem = toggleList[i]\n const selector = Util.getSelectorFromElement(elem)\n const filterElement = [].slice.call(document.querySelectorAll(selector))\n .filter((foundElem) => foundElem === element)\n\n if (selector !== null && filterElement.length > 0) {\n this._selector = selector\n this._triggerArray.push(elem)\n }\n }\n\n this._parent = this._config.parent ? this._getParent() : null\n\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._element, this._triggerArray)\n }\n\n if (this._config.toggle) {\n this.toggle()\n }\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n toggle() {\n if ($(this._element).hasClass(ClassName.SHOW)) {\n this.hide()\n } else {\n this.show()\n }\n }\n\n show() {\n if (this._isTransitioning ||\n $(this._element).hasClass(ClassName.SHOW)) {\n return\n }\n\n let actives\n let activesData\n\n if (this._parent) {\n actives = [].slice.call(this._parent.querySelectorAll(Selector.ACTIVES))\n .filter((elem) => {\n if (typeof this._config.parent === 'string') {\n return elem.getAttribute('data-parent') === this._config.parent\n }\n\n return elem.classList.contains(ClassName.COLLAPSE)\n })\n\n if (actives.length === 0) {\n actives = null\n }\n }\n\n if (actives) {\n activesData = $(actives).not(this._selector).data(DATA_KEY)\n if (activesData && activesData._isTransitioning) {\n return\n }\n }\n\n const startEvent = $.Event(Event.SHOW)\n $(this._element).trigger(startEvent)\n if (startEvent.isDefaultPrevented()) {\n return\n }\n\n if (actives) {\n Collapse._jQueryInterface.call($(actives).not(this._selector), 'hide')\n if (!activesData) {\n $(actives).data(DATA_KEY, null)\n }\n }\n\n const dimension = this._getDimension()\n\n $(this._element)\n .removeClass(ClassName.COLLAPSE)\n .addClass(ClassName.COLLAPSING)\n\n this._element.style[dimension] = 0\n\n if (this._triggerArray.length) {\n $(this._triggerArray)\n .removeClass(ClassName.COLLAPSED)\n .attr('aria-expanded', true)\n }\n\n this.setTransitioning(true)\n\n const complete = () => {\n $(this._element)\n .removeClass(ClassName.COLLAPSING)\n .addClass(ClassName.COLLAPSE)\n .addClass(ClassName.SHOW)\n\n this._element.style[dimension] = ''\n\n this.setTransitioning(false)\n\n $(this._element).trigger(Event.SHOWN)\n }\n\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1)\n const scrollSize = `scroll${capitalizedDimension}`\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n\n this._element.style[dimension] = `${this._element[scrollSize]}px`\n }\n\n hide() {\n if (this._isTransitioning ||\n !$(this._element).hasClass(ClassName.SHOW)) {\n return\n }\n\n const startEvent = $.Event(Event.HIDE)\n $(this._element).trigger(startEvent)\n if (startEvent.isDefaultPrevented()) {\n return\n }\n\n const dimension = this._getDimension()\n\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`\n\n Util.reflow(this._element)\n\n $(this._element)\n .addClass(ClassName.COLLAPSING)\n .removeClass(ClassName.COLLAPSE)\n .removeClass(ClassName.SHOW)\n\n const triggerArrayLength = this._triggerArray.length\n if (triggerArrayLength > 0) {\n for (let i = 0; i < triggerArrayLength; i++) {\n const trigger = this._triggerArray[i]\n const selector = Util.getSelectorFromElement(trigger)\n\n if (selector !== null) {\n const $elem = $([].slice.call(document.querySelectorAll(selector)))\n if (!$elem.hasClass(ClassName.SHOW)) {\n $(trigger).addClass(ClassName.COLLAPSED)\n .attr('aria-expanded', false)\n }\n }\n }\n }\n\n this.setTransitioning(true)\n\n const complete = () => {\n this.setTransitioning(false)\n $(this._element)\n .removeClass(ClassName.COLLAPSING)\n .addClass(ClassName.COLLAPSE)\n .trigger(Event.HIDDEN)\n }\n\n this._element.style[dimension] = ''\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n }\n\n setTransitioning(isTransitioning) {\n this._isTransitioning = isTransitioning\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n\n this._config = null\n this._parent = null\n this._element = null\n this._triggerArray = null\n this._isTransitioning = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n config.toggle = Boolean(config.toggle) // Coerce string values\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _getDimension() {\n const hasWidth = $(this._element).hasClass(Dimension.WIDTH)\n return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT\n }\n\n _getParent() {\n let parent\n\n if (Util.isElement(this._config.parent)) {\n parent = this._config.parent\n\n // It's a jQuery object\n if (typeof this._config.parent.jquery !== 'undefined') {\n parent = this._config.parent[0]\n }\n } else {\n parent = document.querySelector(this._config.parent)\n }\n\n const selector =\n `[data-toggle=\"collapse\"][data-parent=\"${this._config.parent}\"]`\n\n const children = [].slice.call(parent.querySelectorAll(selector))\n $(children).each((i, element) => {\n this._addAriaAndCollapsedClass(\n Collapse._getTargetFromElement(element),\n [element]\n )\n })\n\n return parent\n }\n\n _addAriaAndCollapsedClass(element, triggerArray) {\n const isOpen = $(element).hasClass(ClassName.SHOW)\n\n if (triggerArray.length) {\n $(triggerArray)\n .toggleClass(ClassName.COLLAPSED, !isOpen)\n .attr('aria-expanded', isOpen)\n }\n }\n\n // Static\n\n static _getTargetFromElement(element) {\n const selector = Util.getSelectorFromElement(element)\n return selector ? document.querySelector(selector) : null\n }\n\n static _jQueryInterface(config) {\n return this.each(function () {\n const $this = $(this)\n let data = $this.data(DATA_KEY)\n const _config = {\n ...Default,\n ...$this.data(),\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (!data && _config.toggle && /show|hide/.test(config)) {\n _config.toggle = false\n }\n\n if (!data) {\n data = new Collapse(this, _config)\n $this.data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {\n // preventDefault only for elements (which change the URL) not inside the collapsible element\n if (event.currentTarget.tagName === 'A') {\n event.preventDefault()\n }\n\n const $trigger = $(this)\n const selector = Util.getSelectorFromElement(this)\n const selectors = [].slice.call(document.querySelectorAll(selector))\n\n $(selectors).each(function () {\n const $target = $(this)\n const data = $target.data(DATA_KEY)\n const config = data ? 'toggle' : $trigger.data()\n Collapse._jQueryInterface.call($target, config)\n })\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Collapse._jQueryInterface\n$.fn[NAME].Constructor = Collapse\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Collapse._jQueryInterface\n}\n\nexport default Collapse\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Popper from 'popper.js'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'dropdown'\nconst VERSION = '4.4.1'\nconst DATA_KEY = 'bs.dropdown'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key\nconst SPACE_KEYCODE = 32 // KeyboardEvent.which value for space key\nconst TAB_KEYCODE = 9 // KeyboardEvent.which value for tab key\nconst ARROW_UP_KEYCODE = 38 // KeyboardEvent.which value for up arrow key\nconst ARROW_DOWN_KEYCODE = 40 // KeyboardEvent.which value for down arrow key\nconst RIGHT_MOUSE_BUTTON_WHICH = 3 // MouseEvent.which value for the right button (assuming a right-handed mouse)\nconst REGEXP_KEYDOWN = new RegExp(`${ARROW_UP_KEYCODE}|${ARROW_DOWN_KEYCODE}|${ESCAPE_KEYCODE}`)\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`,\n KEYDOWN_DATA_API : `keydown${EVENT_KEY}${DATA_API_KEY}`,\n KEYUP_DATA_API : `keyup${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n DISABLED : 'disabled',\n SHOW : 'show',\n DROPUP : 'dropup',\n DROPRIGHT : 'dropright',\n DROPLEFT : 'dropleft',\n MENURIGHT : 'dropdown-menu-right',\n MENULEFT : 'dropdown-menu-left',\n POSITION_STATIC : 'position-static'\n}\n\nconst Selector = {\n DATA_TOGGLE : '[data-toggle=\"dropdown\"]',\n FORM_CHILD : '.dropdown form',\n MENU : '.dropdown-menu',\n NAVBAR_NAV : '.navbar-nav',\n VISIBLE_ITEMS : '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'\n}\n\nconst AttachmentMap = {\n TOP : 'top-start',\n TOPEND : 'top-end',\n BOTTOM : 'bottom-start',\n BOTTOMEND : 'bottom-end',\n RIGHT : 'right-start',\n RIGHTEND : 'right-end',\n LEFT : 'left-start',\n LEFTEND : 'left-end'\n}\n\nconst Default = {\n offset : 0,\n flip : true,\n boundary : 'scrollParent',\n reference : 'toggle',\n display : 'dynamic',\n popperConfig : null\n}\n\nconst DefaultType = {\n offset : '(number|string|function)',\n flip : 'boolean',\n boundary : '(string|element)',\n reference : '(string|element)',\n display : 'string',\n popperConfig : '(null|object)'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Dropdown {\n constructor(element, config) {\n this._element = element\n this._popper = null\n this._config = this._getConfig(config)\n this._menu = this._getMenuElement()\n this._inNavbar = this._detectNavbar()\n\n this._addEventListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Public\n\n toggle() {\n if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED)) {\n return\n }\n\n const isActive = $(this._menu).hasClass(ClassName.SHOW)\n\n Dropdown._clearMenus()\n\n if (isActive) {\n return\n }\n\n this.show(true)\n }\n\n show(usePopper = false) {\n if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED) || $(this._menu).hasClass(ClassName.SHOW)) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n const showEvent = $.Event(Event.SHOW, relatedTarget)\n const parent = Dropdown._getParentFromElement(this._element)\n\n $(parent).trigger(showEvent)\n\n if (showEvent.isDefaultPrevented()) {\n return\n }\n\n // Disable totally Popper.js for Dropdown in Navbar\n if (!this._inNavbar && usePopper) {\n /**\n * Check for Popper dependency\n * Popper - https://popper.js.org\n */\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper.js (https://popper.js.org/)')\n }\n\n let referenceElement = this._element\n\n if (this._config.reference === 'parent') {\n referenceElement = parent\n } else if (Util.isElement(this._config.reference)) {\n referenceElement = this._config.reference\n\n // Check if it's jQuery element\n if (typeof this._config.reference.jquery !== 'undefined') {\n referenceElement = this._config.reference[0]\n }\n }\n\n // If boundary is not `scrollParent`, then set position to `static`\n // to allow the menu to \"escape\" the scroll parent's boundaries\n // https://github.com/twbs/bootstrap/issues/24251\n if (this._config.boundary !== 'scrollParent') {\n $(parent).addClass(ClassName.POSITION_STATIC)\n }\n this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig())\n }\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement &&\n $(parent).closest(Selector.NAVBAR_NAV).length === 0) {\n $(document.body).children().on('mouseover', null, $.noop)\n }\n\n this._element.focus()\n this._element.setAttribute('aria-expanded', true)\n\n $(this._menu).toggleClass(ClassName.SHOW)\n $(parent)\n .toggleClass(ClassName.SHOW)\n .trigger($.Event(Event.SHOWN, relatedTarget))\n }\n\n hide() {\n if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED) || !$(this._menu).hasClass(ClassName.SHOW)) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n const hideEvent = $.Event(Event.HIDE, relatedTarget)\n const parent = Dropdown._getParentFromElement(this._element)\n\n $(parent).trigger(hideEvent)\n\n if (hideEvent.isDefaultPrevented()) {\n return\n }\n\n if (this._popper) {\n this._popper.destroy()\n }\n\n $(this._menu).toggleClass(ClassName.SHOW)\n $(parent)\n .toggleClass(ClassName.SHOW)\n .trigger($.Event(Event.HIDDEN, relatedTarget))\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n $(this._element).off(EVENT_KEY)\n this._element = null\n this._menu = null\n if (this._popper !== null) {\n this._popper.destroy()\n this._popper = null\n }\n }\n\n update() {\n this._inNavbar = this._detectNavbar()\n if (this._popper !== null) {\n this._popper.scheduleUpdate()\n }\n }\n\n // Private\n\n _addEventListeners() {\n $(this._element).on(Event.CLICK, (event) => {\n event.preventDefault()\n event.stopPropagation()\n this.toggle()\n })\n }\n\n _getConfig(config) {\n config = {\n ...this.constructor.Default,\n ...$(this._element).data(),\n ...config\n }\n\n Util.typeCheckConfig(\n NAME,\n config,\n this.constructor.DefaultType\n )\n\n return config\n }\n\n _getMenuElement() {\n if (!this._menu) {\n const parent = Dropdown._getParentFromElement(this._element)\n\n if (parent) {\n this._menu = parent.querySelector(Selector.MENU)\n }\n }\n return this._menu\n }\n\n _getPlacement() {\n const $parentDropdown = $(this._element.parentNode)\n let placement = AttachmentMap.BOTTOM\n\n // Handle dropup\n if ($parentDropdown.hasClass(ClassName.DROPUP)) {\n placement = AttachmentMap.TOP\n if ($(this._menu).hasClass(ClassName.MENURIGHT)) {\n placement = AttachmentMap.TOPEND\n }\n } else if ($parentDropdown.hasClass(ClassName.DROPRIGHT)) {\n placement = AttachmentMap.RIGHT\n } else if ($parentDropdown.hasClass(ClassName.DROPLEFT)) {\n placement = AttachmentMap.LEFT\n } else if ($(this._menu).hasClass(ClassName.MENURIGHT)) {\n placement = AttachmentMap.BOTTOMEND\n }\n return placement\n }\n\n _detectNavbar() {\n return $(this._element).closest('.navbar').length > 0\n }\n\n _getOffset() {\n const offset = {}\n\n if (typeof this._config.offset === 'function') {\n offset.fn = (data) => {\n data.offsets = {\n ...data.offsets,\n ...this._config.offset(data.offsets, this._element) || {}\n }\n\n return data\n }\n } else {\n offset.offset = this._config.offset\n }\n\n return offset\n }\n\n _getPopperConfig() {\n const popperConfig = {\n placement: this._getPlacement(),\n modifiers: {\n offset: this._getOffset(),\n flip: {\n enabled: this._config.flip\n },\n preventOverflow: {\n boundariesElement: this._config.boundary\n }\n }\n }\n\n // Disable Popper.js if we have a static display\n if (this._config.display === 'static') {\n popperConfig.modifiers.applyStyle = {\n enabled: false\n }\n }\n\n return {\n ...popperConfig,\n ...this._config.popperConfig\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' ? config : null\n\n if (!data) {\n data = new Dropdown(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n\n static _clearMenus(event) {\n if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH ||\n event.type === 'keyup' && event.which !== TAB_KEYCODE)) {\n return\n }\n\n const toggles = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLE))\n\n for (let i = 0, len = toggles.length; i < len; i++) {\n const parent = Dropdown._getParentFromElement(toggles[i])\n const context = $(toggles[i]).data(DATA_KEY)\n const relatedTarget = {\n relatedTarget: toggles[i]\n }\n\n if (event && event.type === 'click') {\n relatedTarget.clickEvent = event\n }\n\n if (!context) {\n continue\n }\n\n const dropdownMenu = context._menu\n if (!$(parent).hasClass(ClassName.SHOW)) {\n continue\n }\n\n if (event && (event.type === 'click' &&\n /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) &&\n $.contains(parent, event.target)) {\n continue\n }\n\n const hideEvent = $.Event(Event.HIDE, relatedTarget)\n $(parent).trigger(hideEvent)\n if (hideEvent.isDefaultPrevented()) {\n continue\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n $(document.body).children().off('mouseover', null, $.noop)\n }\n\n toggles[i].setAttribute('aria-expanded', 'false')\n\n if (context._popper) {\n context._popper.destroy()\n }\n\n $(dropdownMenu).removeClass(ClassName.SHOW)\n $(parent)\n .removeClass(ClassName.SHOW)\n .trigger($.Event(Event.HIDDEN, relatedTarget))\n }\n }\n\n static _getParentFromElement(element) {\n let parent\n const selector = Util.getSelectorFromElement(element)\n\n if (selector) {\n parent = document.querySelector(selector)\n }\n\n return parent || element.parentNode\n }\n\n // eslint-disable-next-line complexity\n static _dataApiKeydownHandler(event) {\n // If not input/textarea:\n // - And not a key in REGEXP_KEYDOWN => not a dropdown command\n // If input/textarea:\n // - If space key => not a dropdown command\n // - If key is other than escape\n // - If key is not up or down => not a dropdown command\n // - If trigger inside the menu => not a dropdown command\n if (/input|textarea/i.test(event.target.tagName)\n ? event.which === SPACE_KEYCODE || event.which !== ESCAPE_KEYCODE &&\n (event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE ||\n $(event.target).closest(Selector.MENU).length) : !REGEXP_KEYDOWN.test(event.which)) {\n return\n }\n\n event.preventDefault()\n event.stopPropagation()\n\n if (this.disabled || $(this).hasClass(ClassName.DISABLED)) {\n return\n }\n\n const parent = Dropdown._getParentFromElement(this)\n const isActive = $(parent).hasClass(ClassName.SHOW)\n\n if (!isActive && event.which === ESCAPE_KEYCODE) {\n return\n }\n\n if (!isActive || isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) {\n if (event.which === ESCAPE_KEYCODE) {\n const toggle = parent.querySelector(Selector.DATA_TOGGLE)\n $(toggle).trigger('focus')\n }\n\n $(this).trigger('click')\n return\n }\n\n const items = [].slice.call(parent.querySelectorAll(Selector.VISIBLE_ITEMS))\n .filter((item) => $(item).is(':visible'))\n\n if (items.length === 0) {\n return\n }\n\n let index = items.indexOf(event.target)\n\n if (event.which === ARROW_UP_KEYCODE && index > 0) { // Up\n index--\n }\n\n if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) { // Down\n index++\n }\n\n if (index < 0) {\n index = 0\n }\n\n items[index].focus()\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document)\n .on(Event.KEYDOWN_DATA_API, Selector.DATA_TOGGLE, Dropdown._dataApiKeydownHandler)\n .on(Event.KEYDOWN_DATA_API, Selector.MENU, Dropdown._dataApiKeydownHandler)\n .on(`${Event.CLICK_DATA_API} ${Event.KEYUP_DATA_API}`, Dropdown._clearMenus)\n .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {\n event.preventDefault()\n event.stopPropagation()\n Dropdown._jQueryInterface.call($(this), 'toggle')\n })\n .on(Event.CLICK_DATA_API, Selector.FORM_CHILD, (e) => {\n e.stopPropagation()\n })\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Dropdown._jQueryInterface\n$.fn[NAME].Constructor = Dropdown\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Dropdown._jQueryInterface\n}\n\n\nexport default Dropdown\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'modal'\nconst VERSION = '4.4.1'\nconst DATA_KEY = 'bs.modal'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key\n\nconst Default = {\n backdrop : true,\n keyboard : true,\n focus : true,\n show : true\n}\n\nconst DefaultType = {\n backdrop : '(boolean|string)',\n keyboard : 'boolean',\n focus : 'boolean',\n show : 'boolean'\n}\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDE_PREVENTED : `hidePrevented${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n RESIZE : `resize${EVENT_KEY}`,\n CLICK_DISMISS : `click.dismiss${EVENT_KEY}`,\n KEYDOWN_DISMISS : `keydown.dismiss${EVENT_KEY}`,\n MOUSEUP_DISMISS : `mouseup.dismiss${EVENT_KEY}`,\n MOUSEDOWN_DISMISS : `mousedown.dismiss${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n SCROLLABLE : 'modal-dialog-scrollable',\n SCROLLBAR_MEASURER : 'modal-scrollbar-measure',\n BACKDROP : 'modal-backdrop',\n OPEN : 'modal-open',\n FADE : 'fade',\n SHOW : 'show',\n STATIC : 'modal-static'\n}\n\nconst Selector = {\n DIALOG : '.modal-dialog',\n MODAL_BODY : '.modal-body',\n DATA_TOGGLE : '[data-toggle=\"modal\"]',\n DATA_DISMISS : '[data-dismiss=\"modal\"]',\n FIXED_CONTENT : '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',\n STICKY_CONTENT : '.sticky-top'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Modal {\n constructor(element, config) {\n this._config = this._getConfig(config)\n this._element = element\n this._dialog = element.querySelector(Selector.DIALOG)\n this._backdrop = null\n this._isShown = false\n this._isBodyOverflowing = false\n this._ignoreBackdropClick = false\n this._isTransitioning = false\n this._scrollbarWidth = 0\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget)\n }\n\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return\n }\n\n if ($(this._element).hasClass(ClassName.FADE)) {\n this._isTransitioning = true\n }\n\n const showEvent = $.Event(Event.SHOW, {\n relatedTarget\n })\n\n $(this._element).trigger(showEvent)\n\n if (this._isShown || showEvent.isDefaultPrevented()) {\n return\n }\n\n this._isShown = true\n\n this._checkScrollbar()\n this._setScrollbar()\n\n this._adjustDialog()\n\n this._setEscapeEvent()\n this._setResizeEvent()\n\n $(this._element).on(\n Event.CLICK_DISMISS,\n Selector.DATA_DISMISS,\n (event) => this.hide(event)\n )\n\n $(this._dialog).on(Event.MOUSEDOWN_DISMISS, () => {\n $(this._element).one(Event.MOUSEUP_DISMISS, (event) => {\n if ($(event.target).is(this._element)) {\n this._ignoreBackdropClick = true\n }\n })\n })\n\n this._showBackdrop(() => this._showElement(relatedTarget))\n }\n\n hide(event) {\n if (event) {\n event.preventDefault()\n }\n\n if (!this._isShown || this._isTransitioning) {\n return\n }\n\n const hideEvent = $.Event(Event.HIDE)\n\n $(this._element).trigger(hideEvent)\n\n if (!this._isShown || hideEvent.isDefaultPrevented()) {\n return\n }\n\n this._isShown = false\n const transition = $(this._element).hasClass(ClassName.FADE)\n\n if (transition) {\n this._isTransitioning = true\n }\n\n this._setEscapeEvent()\n this._setResizeEvent()\n\n $(document).off(Event.FOCUSIN)\n\n $(this._element).removeClass(ClassName.SHOW)\n\n $(this._element).off(Event.CLICK_DISMISS)\n $(this._dialog).off(Event.MOUSEDOWN_DISMISS)\n\n\n if (transition) {\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, (event) => this._hideModal(event))\n .emulateTransitionEnd(transitionDuration)\n } else {\n this._hideModal()\n }\n }\n\n dispose() {\n [window, this._element, this._dialog]\n .forEach((htmlElement) => $(htmlElement).off(EVENT_KEY))\n\n /**\n * `document` has 2 events `Event.FOCUSIN` and `Event.CLICK_DATA_API`\n * Do not move `document` in `htmlElements` array\n * It will remove `Event.CLICK_DATA_API` event that should remain\n */\n $(document).off(Event.FOCUSIN)\n\n $.removeData(this._element, DATA_KEY)\n\n this._config = null\n this._element = null\n this._dialog = null\n this._backdrop = null\n this._isShown = null\n this._isBodyOverflowing = null\n this._ignoreBackdropClick = null\n this._isTransitioning = null\n this._scrollbarWidth = null\n }\n\n handleUpdate() {\n this._adjustDialog()\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _triggerBackdropTransition() {\n if (this._config.backdrop === 'static') {\n const hideEventPrevented = $.Event(Event.HIDE_PREVENTED)\n\n $(this._element).trigger(hideEventPrevented)\n if (hideEventPrevented.defaultPrevented) {\n return\n }\n\n this._element.classList.add(ClassName.STATIC)\n\n const modalTransitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element).one(Util.TRANSITION_END, () => {\n this._element.classList.remove(ClassName.STATIC)\n })\n .emulateTransitionEnd(modalTransitionDuration)\n this._element.focus()\n } else {\n this.hide()\n }\n }\n\n _showElement(relatedTarget) {\n const transition = $(this._element).hasClass(ClassName.FADE)\n const modalBody = this._dialog ? this._dialog.querySelector(Selector.MODAL_BODY) : null\n\n if (!this._element.parentNode ||\n this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {\n // Don't move modal's DOM position\n document.body.appendChild(this._element)\n }\n\n this._element.style.display = 'block'\n this._element.removeAttribute('aria-hidden')\n this._element.setAttribute('aria-modal', true)\n\n if ($(this._dialog).hasClass(ClassName.SCROLLABLE) && modalBody) {\n modalBody.scrollTop = 0\n } else {\n this._element.scrollTop = 0\n }\n\n if (transition) {\n Util.reflow(this._element)\n }\n\n $(this._element).addClass(ClassName.SHOW)\n\n if (this._config.focus) {\n this._enforceFocus()\n }\n\n const shownEvent = $.Event(Event.SHOWN, {\n relatedTarget\n })\n\n const transitionComplete = () => {\n if (this._config.focus) {\n this._element.focus()\n }\n this._isTransitioning = false\n $(this._element).trigger(shownEvent)\n }\n\n if (transition) {\n const transitionDuration = Util.getTransitionDurationFromElement(this._dialog)\n\n $(this._dialog)\n .one(Util.TRANSITION_END, transitionComplete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n transitionComplete()\n }\n }\n\n _enforceFocus() {\n $(document)\n .off(Event.FOCUSIN) // Guard against infinite focus loop\n .on(Event.FOCUSIN, (event) => {\n if (document !== event.target &&\n this._element !== event.target &&\n $(this._element).has(event.target).length === 0) {\n this._element.focus()\n }\n })\n }\n\n _setEscapeEvent() {\n if (this._isShown && this._config.keyboard) {\n $(this._element).on(Event.KEYDOWN_DISMISS, (event) => {\n if (event.which === ESCAPE_KEYCODE) {\n this._triggerBackdropTransition()\n }\n })\n } else if (!this._isShown) {\n $(this._element).off(Event.KEYDOWN_DISMISS)\n }\n }\n\n _setResizeEvent() {\n if (this._isShown) {\n $(window).on(Event.RESIZE, (event) => this.handleUpdate(event))\n } else {\n $(window).off(Event.RESIZE)\n }\n }\n\n _hideModal() {\n this._element.style.display = 'none'\n this._element.setAttribute('aria-hidden', true)\n this._element.removeAttribute('aria-modal')\n this._isTransitioning = false\n this._showBackdrop(() => {\n $(document.body).removeClass(ClassName.OPEN)\n this._resetAdjustments()\n this._resetScrollbar()\n $(this._element).trigger(Event.HIDDEN)\n })\n }\n\n _removeBackdrop() {\n if (this._backdrop) {\n $(this._backdrop).remove()\n this._backdrop = null\n }\n }\n\n _showBackdrop(callback) {\n const animate = $(this._element).hasClass(ClassName.FADE)\n ? ClassName.FADE : ''\n\n if (this._isShown && this._config.backdrop) {\n this._backdrop = document.createElement('div')\n this._backdrop.className = ClassName.BACKDROP\n\n if (animate) {\n this._backdrop.classList.add(animate)\n }\n\n $(this._backdrop).appendTo(document.body)\n\n $(this._element).on(Event.CLICK_DISMISS, (event) => {\n if (this._ignoreBackdropClick) {\n this._ignoreBackdropClick = false\n return\n }\n if (event.target !== event.currentTarget) {\n return\n }\n\n this._triggerBackdropTransition()\n })\n\n if (animate) {\n Util.reflow(this._backdrop)\n }\n\n $(this._backdrop).addClass(ClassName.SHOW)\n\n if (!callback) {\n return\n }\n\n if (!animate) {\n callback()\n return\n }\n\n const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)\n\n $(this._backdrop)\n .one(Util.TRANSITION_END, callback)\n .emulateTransitionEnd(backdropTransitionDuration)\n } else if (!this._isShown && this._backdrop) {\n $(this._backdrop).removeClass(ClassName.SHOW)\n\n const callbackRemove = () => {\n this._removeBackdrop()\n if (callback) {\n callback()\n }\n }\n\n if ($(this._element).hasClass(ClassName.FADE)) {\n const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)\n\n $(this._backdrop)\n .one(Util.TRANSITION_END, callbackRemove)\n .emulateTransitionEnd(backdropTransitionDuration)\n } else {\n callbackRemove()\n }\n } else if (callback) {\n callback()\n }\n }\n\n // ----------------------------------------------------------------------\n // the following methods are used to handle overflowing modals\n // todo (fat): these should probably be refactored out of modal.js\n // ----------------------------------------------------------------------\n\n _adjustDialog() {\n const isModalOverflowing =\n this._element.scrollHeight > document.documentElement.clientHeight\n\n if (!this._isBodyOverflowing && isModalOverflowing) {\n this._element.style.paddingLeft = `${this._scrollbarWidth}px`\n }\n\n if (this._isBodyOverflowing && !isModalOverflowing) {\n this._element.style.paddingRight = `${this._scrollbarWidth}px`\n }\n }\n\n _resetAdjustments() {\n this._element.style.paddingLeft = ''\n this._element.style.paddingRight = ''\n }\n\n _checkScrollbar() {\n const rect = document.body.getBoundingClientRect()\n this._isBodyOverflowing = rect.left + rect.right < window.innerWidth\n this._scrollbarWidth = this._getScrollbarWidth()\n }\n\n _setScrollbar() {\n if (this._isBodyOverflowing) {\n // Note: DOMNode.style.paddingRight returns the actual value or '' if not set\n // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set\n const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))\n const stickyContent = [].slice.call(document.querySelectorAll(Selector.STICKY_CONTENT))\n\n // Adjust fixed content padding\n $(fixedContent).each((index, element) => {\n const actualPadding = element.style.paddingRight\n const calculatedPadding = $(element).css('padding-right')\n $(element)\n .data('padding-right', actualPadding)\n .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)\n })\n\n // Adjust sticky content margin\n $(stickyContent).each((index, element) => {\n const actualMargin = element.style.marginRight\n const calculatedMargin = $(element).css('margin-right')\n $(element)\n .data('margin-right', actualMargin)\n .css('margin-right', `${parseFloat(calculatedMargin) - this._scrollbarWidth}px`)\n })\n\n // Adjust body padding\n const actualPadding = document.body.style.paddingRight\n const calculatedPadding = $(document.body).css('padding-right')\n $(document.body)\n .data('padding-right', actualPadding)\n .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)\n }\n\n $(document.body).addClass(ClassName.OPEN)\n }\n\n _resetScrollbar() {\n // Restore fixed content padding\n const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))\n $(fixedContent).each((index, element) => {\n const padding = $(element).data('padding-right')\n $(element).removeData('padding-right')\n element.style.paddingRight = padding ? padding : ''\n })\n\n // Restore sticky content\n const elements = [].slice.call(document.querySelectorAll(`${Selector.STICKY_CONTENT}`))\n $(elements).each((index, element) => {\n const margin = $(element).data('margin-right')\n if (typeof margin !== 'undefined') {\n $(element).css('margin-right', margin).removeData('margin-right')\n }\n })\n\n // Restore body padding\n const padding = $(document.body).data('padding-right')\n $(document.body).removeData('padding-right')\n document.body.style.paddingRight = padding ? padding : ''\n }\n\n _getScrollbarWidth() { // thx d.walsh\n const scrollDiv = document.createElement('div')\n scrollDiv.className = ClassName.SCROLLBAR_MEASURER\n document.body.appendChild(scrollDiv)\n const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth\n document.body.removeChild(scrollDiv)\n return scrollbarWidth\n }\n\n // Static\n\n static _jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = {\n ...Default,\n ...$(this).data(),\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (!data) {\n data = new Modal(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config](relatedTarget)\n } else if (_config.show) {\n data.show(relatedTarget)\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {\n let target\n const selector = Util.getSelectorFromElement(this)\n\n if (selector) {\n target = document.querySelector(selector)\n }\n\n const config = $(target).data(DATA_KEY)\n ? 'toggle' : {\n ...$(target).data(),\n ...$(this).data()\n }\n\n if (this.tagName === 'A' || this.tagName === 'AREA') {\n event.preventDefault()\n }\n\n const $target = $(target).one(Event.SHOW, (showEvent) => {\n if (showEvent.isDefaultPrevented()) {\n // Only register focus restorer if modal will actually get shown\n return\n }\n\n $target.one(Event.HIDDEN, () => {\n if ($(this).is(':visible')) {\n this.focus()\n }\n })\n })\n\n Modal._jQueryInterface.call($(target), config, this)\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Modal._jQueryInterface\n$.fn[NAME].Constructor = Modal\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Modal._jQueryInterface\n}\n\nexport default Modal\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): tools/sanitizer.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst uriAttrs = [\n 'background',\n 'cite',\n 'href',\n 'itemtype',\n 'longdesc',\n 'poster',\n 'src',\n 'xlink:href'\n]\n\nconst ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i\n\nexport const DefaultWhitelist = {\n // Global attributes allowed on any supplied element below.\n '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n a: ['target', 'href', 'title', 'rel'],\n area: [],\n b: [],\n br: [],\n col: [],\n code: [],\n div: [],\n em: [],\n hr: [],\n h1: [],\n h2: [],\n h3: [],\n h4: [],\n h5: [],\n h6: [],\n i: [],\n img: ['src', 'alt', 'title', 'width', 'height'],\n li: [],\n ol: [],\n p: [],\n pre: [],\n s: [],\n small: [],\n span: [],\n sub: [],\n sup: [],\n strong: [],\n u: [],\n ul: []\n}\n\n/**\n * A pattern that recognizes a commonly useful subset of URLs that are safe.\n *\n * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts\n */\nconst SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi\n\n/**\n * A pattern that matches safe data URLs. Only matches image, video and audio types.\n *\n * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts\n */\nconst DATA_URL_PATTERN = /^data:(?:image\\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\\/(?:mpeg|mp4|ogg|webm)|audio\\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i\n\nfunction allowedAttribute(attr, allowedAttributeList) {\n const attrName = attr.nodeName.toLowerCase()\n\n if (allowedAttributeList.indexOf(attrName) !== -1) {\n if (uriAttrs.indexOf(attrName) !== -1) {\n return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN))\n }\n\n return true\n }\n\n const regExp = allowedAttributeList.filter((attrRegex) => attrRegex instanceof RegExp)\n\n // Check if a regular expression validates the attribute.\n for (let i = 0, l = regExp.length; i < l; i++) {\n if (attrName.match(regExp[i])) {\n return true\n }\n }\n\n return false\n}\n\nexport function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) {\n if (unsafeHtml.length === 0) {\n return unsafeHtml\n }\n\n if (sanitizeFn && typeof sanitizeFn === 'function') {\n return sanitizeFn(unsafeHtml)\n }\n\n const domParser = new window.DOMParser()\n const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html')\n const whitelistKeys = Object.keys(whiteList)\n const elements = [].slice.call(createdDocument.body.querySelectorAll('*'))\n\n for (let i = 0, len = elements.length; i < len; i++) {\n const el = elements[i]\n const elName = el.nodeName.toLowerCase()\n\n if (whitelistKeys.indexOf(el.nodeName.toLowerCase()) === -1) {\n el.parentNode.removeChild(el)\n\n continue\n }\n\n const attributeList = [].slice.call(el.attributes)\n const whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || [])\n\n attributeList.forEach((attr) => {\n if (!allowedAttribute(attr, whitelistedAttributes)) {\n el.removeAttribute(attr.nodeName)\n }\n })\n }\n\n return createdDocument.body.innerHTML\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport {\n DefaultWhitelist,\n sanitizeHtml\n} from './tools/sanitizer'\nimport $ from 'jquery'\nimport Popper from 'popper.js'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'tooltip'\nconst VERSION = '4.4.1'\nconst DATA_KEY = 'bs.tooltip'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst CLASS_PREFIX = 'bs-tooltip'\nconst BSCLS_PREFIX_REGEX = new RegExp(`(^|\\\\s)${CLASS_PREFIX}\\\\S+`, 'g')\nconst DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']\n\nconst DefaultType = {\n animation : 'boolean',\n template : 'string',\n title : '(string|element|function)',\n trigger : 'string',\n delay : '(number|object)',\n html : 'boolean',\n selector : '(string|boolean)',\n placement : '(string|function)',\n offset : '(number|string|function)',\n container : '(string|element|boolean)',\n fallbackPlacement : '(string|array)',\n boundary : '(string|element)',\n sanitize : 'boolean',\n sanitizeFn : '(null|function)',\n whiteList : 'object',\n popperConfig : '(null|object)'\n}\n\nconst AttachmentMap = {\n AUTO : 'auto',\n TOP : 'top',\n RIGHT : 'right',\n BOTTOM : 'bottom',\n LEFT : 'left'\n}\n\nconst Default = {\n animation : true,\n template : '
    ' +\n '
    ' +\n '
    ',\n trigger : 'hover focus',\n title : '',\n delay : 0,\n html : false,\n selector : false,\n placement : 'top',\n offset : 0,\n container : false,\n fallbackPlacement : 'flip',\n boundary : 'scrollParent',\n sanitize : true,\n sanitizeFn : null,\n whiteList : DefaultWhitelist,\n popperConfig : null\n}\n\nconst HoverState = {\n SHOW : 'show',\n OUT : 'out'\n}\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n INSERTED : `inserted${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n FOCUSOUT : `focusout${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`\n}\n\nconst ClassName = {\n FADE : 'fade',\n SHOW : 'show'\n}\n\nconst Selector = {\n TOOLTIP : '.tooltip',\n TOOLTIP_INNER : '.tooltip-inner',\n ARROW : '.arrow'\n}\n\nconst Trigger = {\n HOVER : 'hover',\n FOCUS : 'focus',\n CLICK : 'click',\n MANUAL : 'manual'\n}\n\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Tooltip {\n constructor(element, config) {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper.js (https://popper.js.org/)')\n }\n\n // private\n this._isEnabled = true\n this._timeout = 0\n this._hoverState = ''\n this._activeTrigger = {}\n this._popper = null\n\n // Protected\n this.element = element\n this.config = this._getConfig(config)\n this.tip = null\n\n this._setListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get NAME() {\n return NAME\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n static get Event() {\n return Event\n }\n\n static get EVENT_KEY() {\n return EVENT_KEY\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Public\n\n enable() {\n this._isEnabled = true\n }\n\n disable() {\n this._isEnabled = false\n }\n\n toggleEnabled() {\n this._isEnabled = !this._isEnabled\n }\n\n toggle(event) {\n if (!this._isEnabled) {\n return\n }\n\n if (event) {\n const dataKey = this.constructor.DATA_KEY\n let context = $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n context._activeTrigger.click = !context._activeTrigger.click\n\n if (context._isWithActiveTrigger()) {\n context._enter(null, context)\n } else {\n context._leave(null, context)\n }\n } else {\n if ($(this.getTipElement()).hasClass(ClassName.SHOW)) {\n this._leave(null, this)\n return\n }\n\n this._enter(null, this)\n }\n }\n\n dispose() {\n clearTimeout(this._timeout)\n\n $.removeData(this.element, this.constructor.DATA_KEY)\n\n $(this.element).off(this.constructor.EVENT_KEY)\n $(this.element).closest('.modal').off('hide.bs.modal', this._hideModalHandler)\n\n if (this.tip) {\n $(this.tip).remove()\n }\n\n this._isEnabled = null\n this._timeout = null\n this._hoverState = null\n this._activeTrigger = null\n if (this._popper) {\n this._popper.destroy()\n }\n\n this._popper = null\n this.element = null\n this.config = null\n this.tip = null\n }\n\n show() {\n if ($(this.element).css('display') === 'none') {\n throw new Error('Please use show on visible elements')\n }\n\n const showEvent = $.Event(this.constructor.Event.SHOW)\n if (this.isWithContent() && this._isEnabled) {\n $(this.element).trigger(showEvent)\n\n const shadowRoot = Util.findShadowRoot(this.element)\n const isInTheDom = $.contains(\n shadowRoot !== null ? shadowRoot : this.element.ownerDocument.documentElement,\n this.element\n )\n\n if (showEvent.isDefaultPrevented() || !isInTheDom) {\n return\n }\n\n const tip = this.getTipElement()\n const tipId = Util.getUID(this.constructor.NAME)\n\n tip.setAttribute('id', tipId)\n this.element.setAttribute('aria-describedby', tipId)\n\n this.setContent()\n\n if (this.config.animation) {\n $(tip).addClass(ClassName.FADE)\n }\n\n const placement = typeof this.config.placement === 'function'\n ? this.config.placement.call(this, tip, this.element)\n : this.config.placement\n\n const attachment = this._getAttachment(placement)\n this.addAttachmentClass(attachment)\n\n const container = this._getContainer()\n $(tip).data(this.constructor.DATA_KEY, this)\n\n if (!$.contains(this.element.ownerDocument.documentElement, this.tip)) {\n $(tip).appendTo(container)\n }\n\n $(this.element).trigger(this.constructor.Event.INSERTED)\n\n this._popper = new Popper(this.element, tip, this._getPopperConfig(attachment))\n\n $(tip).addClass(ClassName.SHOW)\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement) {\n $(document.body).children().on('mouseover', null, $.noop)\n }\n\n const complete = () => {\n if (this.config.animation) {\n this._fixTransition()\n }\n const prevHoverState = this._hoverState\n this._hoverState = null\n\n $(this.element).trigger(this.constructor.Event.SHOWN)\n\n if (prevHoverState === HoverState.OUT) {\n this._leave(null, this)\n }\n }\n\n if ($(this.tip).hasClass(ClassName.FADE)) {\n const transitionDuration = Util.getTransitionDurationFromElement(this.tip)\n\n $(this.tip)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n complete()\n }\n }\n }\n\n hide(callback) {\n const tip = this.getTipElement()\n const hideEvent = $.Event(this.constructor.Event.HIDE)\n const complete = () => {\n if (this._hoverState !== HoverState.SHOW && tip.parentNode) {\n tip.parentNode.removeChild(tip)\n }\n\n this._cleanTipClass()\n this.element.removeAttribute('aria-describedby')\n $(this.element).trigger(this.constructor.Event.HIDDEN)\n if (this._popper !== null) {\n this._popper.destroy()\n }\n\n if (callback) {\n callback()\n }\n }\n\n $(this.element).trigger(hideEvent)\n\n if (hideEvent.isDefaultPrevented()) {\n return\n }\n\n $(tip).removeClass(ClassName.SHOW)\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n $(document.body).children().off('mouseover', null, $.noop)\n }\n\n this._activeTrigger[Trigger.CLICK] = false\n this._activeTrigger[Trigger.FOCUS] = false\n this._activeTrigger[Trigger.HOVER] = false\n\n if ($(this.tip).hasClass(ClassName.FADE)) {\n const transitionDuration = Util.getTransitionDurationFromElement(tip)\n\n $(tip)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n complete()\n }\n\n this._hoverState = ''\n }\n\n update() {\n if (this._popper !== null) {\n this._popper.scheduleUpdate()\n }\n }\n\n // Protected\n\n isWithContent() {\n return Boolean(this.getTitle())\n }\n\n addAttachmentClass(attachment) {\n $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)\n }\n\n getTipElement() {\n this.tip = this.tip || $(this.config.template)[0]\n return this.tip\n }\n\n setContent() {\n const tip = this.getTipElement()\n this.setElementContent($(tip.querySelectorAll(Selector.TOOLTIP_INNER)), this.getTitle())\n $(tip).removeClass(`${ClassName.FADE} ${ClassName.SHOW}`)\n }\n\n setElementContent($element, content) {\n if (typeof content === 'object' && (content.nodeType || content.jquery)) {\n // Content is a DOM node or a jQuery\n if (this.config.html) {\n if (!$(content).parent().is($element)) {\n $element.empty().append(content)\n }\n } else {\n $element.text($(content).text())\n }\n\n return\n }\n\n if (this.config.html) {\n if (this.config.sanitize) {\n content = sanitizeHtml(content, this.config.whiteList, this.config.sanitizeFn)\n }\n\n $element.html(content)\n } else {\n $element.text(content)\n }\n }\n\n getTitle() {\n let title = this.element.getAttribute('data-original-title')\n\n if (!title) {\n title = typeof this.config.title === 'function'\n ? this.config.title.call(this.element)\n : this.config.title\n }\n\n return title\n }\n\n // Private\n\n _getPopperConfig(attachment) {\n const defaultBsConfig = {\n placement: attachment,\n modifiers: {\n offset: this._getOffset(),\n flip: {\n behavior: this.config.fallbackPlacement\n },\n arrow: {\n element: Selector.ARROW\n },\n preventOverflow: {\n boundariesElement: this.config.boundary\n }\n },\n onCreate: (data) => {\n if (data.originalPlacement !== data.placement) {\n this._handlePopperPlacementChange(data)\n }\n },\n onUpdate: (data) => this._handlePopperPlacementChange(data)\n }\n\n return {\n ...defaultBsConfig,\n ...this.config.popperConfig\n }\n }\n\n _getOffset() {\n const offset = {}\n\n if (typeof this.config.offset === 'function') {\n offset.fn = (data) => {\n data.offsets = {\n ...data.offsets,\n ...this.config.offset(data.offsets, this.element) || {}\n }\n\n return data\n }\n } else {\n offset.offset = this.config.offset\n }\n\n return offset\n }\n\n _getContainer() {\n if (this.config.container === false) {\n return document.body\n }\n\n if (Util.isElement(this.config.container)) {\n return $(this.config.container)\n }\n\n return $(document).find(this.config.container)\n }\n\n _getAttachment(placement) {\n return AttachmentMap[placement.toUpperCase()]\n }\n\n _setListeners() {\n const triggers = this.config.trigger.split(' ')\n\n triggers.forEach((trigger) => {\n if (trigger === 'click') {\n $(this.element).on(\n this.constructor.Event.CLICK,\n this.config.selector,\n (event) => this.toggle(event)\n )\n } else if (trigger !== Trigger.MANUAL) {\n const eventIn = trigger === Trigger.HOVER\n ? this.constructor.Event.MOUSEENTER\n : this.constructor.Event.FOCUSIN\n const eventOut = trigger === Trigger.HOVER\n ? this.constructor.Event.MOUSELEAVE\n : this.constructor.Event.FOCUSOUT\n\n $(this.element)\n .on(\n eventIn,\n this.config.selector,\n (event) => this._enter(event)\n )\n .on(\n eventOut,\n this.config.selector,\n (event) => this._leave(event)\n )\n }\n })\n\n this._hideModalHandler = () => {\n if (this.element) {\n this.hide()\n }\n }\n\n $(this.element).closest('.modal').on(\n 'hide.bs.modal',\n this._hideModalHandler\n )\n\n if (this.config.selector) {\n this.config = {\n ...this.config,\n trigger: 'manual',\n selector: ''\n }\n } else {\n this._fixTitle()\n }\n }\n\n _fixTitle() {\n const titleType = typeof this.element.getAttribute('data-original-title')\n\n if (this.element.getAttribute('title') || titleType !== 'string') {\n this.element.setAttribute(\n 'data-original-title',\n this.element.getAttribute('title') || ''\n )\n\n this.element.setAttribute('title', '')\n }\n }\n\n _enter(event, context) {\n const dataKey = this.constructor.DATA_KEY\n context = context || $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n if (event) {\n context._activeTrigger[\n event.type === 'focusin' ? Trigger.FOCUS : Trigger.HOVER\n ] = true\n }\n\n if ($(context.getTipElement()).hasClass(ClassName.SHOW) || context._hoverState === HoverState.SHOW) {\n context._hoverState = HoverState.SHOW\n return\n }\n\n clearTimeout(context._timeout)\n\n context._hoverState = HoverState.SHOW\n\n if (!context.config.delay || !context.config.delay.show) {\n context.show()\n return\n }\n\n context._timeout = setTimeout(() => {\n if (context._hoverState === HoverState.SHOW) {\n context.show()\n }\n }, context.config.delay.show)\n }\n\n _leave(event, context) {\n const dataKey = this.constructor.DATA_KEY\n context = context || $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n if (event) {\n context._activeTrigger[\n event.type === 'focusout' ? Trigger.FOCUS : Trigger.HOVER\n ] = false\n }\n\n if (context._isWithActiveTrigger()) {\n return\n }\n\n clearTimeout(context._timeout)\n\n context._hoverState = HoverState.OUT\n\n if (!context.config.delay || !context.config.delay.hide) {\n context.hide()\n return\n }\n\n context._timeout = setTimeout(() => {\n if (context._hoverState === HoverState.OUT) {\n context.hide()\n }\n }, context.config.delay.hide)\n }\n\n _isWithActiveTrigger() {\n for (const trigger in this._activeTrigger) {\n if (this._activeTrigger[trigger]) {\n return true\n }\n }\n\n return false\n }\n\n _getConfig(config) {\n const dataAttributes = $(this.element).data()\n\n Object.keys(dataAttributes)\n .forEach((dataAttr) => {\n if (DISALLOWED_ATTRIBUTES.indexOf(dataAttr) !== -1) {\n delete dataAttributes[dataAttr]\n }\n })\n\n config = {\n ...this.constructor.Default,\n ...dataAttributes,\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n }\n }\n\n if (typeof config.title === 'number') {\n config.title = config.title.toString()\n }\n\n if (typeof config.content === 'number') {\n config.content = config.content.toString()\n }\n\n Util.typeCheckConfig(\n NAME,\n config,\n this.constructor.DefaultType\n )\n\n if (config.sanitize) {\n config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn)\n }\n\n return config\n }\n\n _getDelegateConfig() {\n const config = {}\n\n if (this.config) {\n for (const key in this.config) {\n if (this.constructor.Default[key] !== this.config[key]) {\n config[key] = this.config[key]\n }\n }\n }\n\n return config\n }\n\n _cleanTipClass() {\n const $tip = $(this.getTipElement())\n const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)\n if (tabClass !== null && tabClass.length) {\n $tip.removeClass(tabClass.join(''))\n }\n }\n\n _handlePopperPlacementChange(popperData) {\n const popperInstance = popperData.instance\n this.tip = popperInstance.popper\n this._cleanTipClass()\n this.addAttachmentClass(this._getAttachment(popperData.placement))\n }\n\n _fixTransition() {\n const tip = this.getTipElement()\n const initConfigAnimation = this.config.animation\n\n if (tip.getAttribute('x-placement') !== null) {\n return\n }\n\n $(tip).removeClass(ClassName.FADE)\n this.config.animation = false\n this.hide()\n this.show()\n this.config.animation = initConfigAnimation\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' && config\n\n if (!data && /dispose|hide/.test(config)) {\n return\n }\n\n if (!data) {\n data = new Tooltip(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Tooltip._jQueryInterface\n$.fn[NAME].Constructor = Tooltip\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Tooltip._jQueryInterface\n}\n\nexport default Tooltip\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Tooltip from './tooltip'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'popover'\nconst VERSION = '4.4.1'\nconst DATA_KEY = 'bs.popover'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst CLASS_PREFIX = 'bs-popover'\nconst BSCLS_PREFIX_REGEX = new RegExp(`(^|\\\\s)${CLASS_PREFIX}\\\\S+`, 'g')\n\nconst Default = {\n ...Tooltip.Default,\n placement : 'right',\n trigger : 'click',\n content : '',\n template : '
    ' +\n '
    ' +\n '

    ' +\n '
    '\n}\n\nconst DefaultType = {\n ...Tooltip.DefaultType,\n content : '(string|element|function)'\n}\n\nconst ClassName = {\n FADE : 'fade',\n SHOW : 'show'\n}\n\nconst Selector = {\n TITLE : '.popover-header',\n CONTENT : '.popover-body'\n}\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n INSERTED : `inserted${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n FOCUSOUT : `focusout${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Popover extends Tooltip {\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get NAME() {\n return NAME\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n static get Event() {\n return Event\n }\n\n static get EVENT_KEY() {\n return EVENT_KEY\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Overrides\n\n isWithContent() {\n return this.getTitle() || this._getContent()\n }\n\n addAttachmentClass(attachment) {\n $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)\n }\n\n getTipElement() {\n this.tip = this.tip || $(this.config.template)[0]\n return this.tip\n }\n\n setContent() {\n const $tip = $(this.getTipElement())\n\n // We use append for html objects to maintain js events\n this.setElementContent($tip.find(Selector.TITLE), this.getTitle())\n let content = this._getContent()\n if (typeof content === 'function') {\n content = content.call(this.element)\n }\n this.setElementContent($tip.find(Selector.CONTENT), content)\n\n $tip.removeClass(`${ClassName.FADE} ${ClassName.SHOW}`)\n }\n\n // Private\n\n _getContent() {\n return this.element.getAttribute('data-content') ||\n this.config.content\n }\n\n _cleanTipClass() {\n const $tip = $(this.getTipElement())\n const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)\n if (tabClass !== null && tabClass.length > 0) {\n $tip.removeClass(tabClass.join(''))\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' ? config : null\n\n if (!data && /dispose|hide/.test(config)) {\n return\n }\n\n if (!data) {\n data = new Popover(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Popover._jQueryInterface\n$.fn[NAME].Constructor = Popover\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Popover._jQueryInterface\n}\n\nexport default Popover\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.4.1): scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'scrollspy'\nconst VERSION = '4.4.1'\nconst DATA_KEY = 'bs.scrollspy'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst Default = {\n offset : 10,\n method : 'auto',\n target : ''\n}\n\nconst DefaultType = {\n offset : 'number',\n method : 'string',\n target : '(string|element)'\n}\n\nconst Event = {\n ACTIVATE : `activate${EVENT_KEY}`,\n SCROLL : `scroll${EVENT_KEY}`,\n LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n DROPDOWN_ITEM : 'dropdown-item',\n DROPDOWN_MENU : 'dropdown-menu',\n ACTIVE : 'active'\n}\n\nconst Selector = {\n DATA_SPY : '[data-spy=\"scroll\"]',\n ACTIVE : '.active',\n NAV_LIST_GROUP : '.nav, .list-group',\n NAV_LINKS : '.nav-link',\n NAV_ITEMS : '.nav-item',\n LIST_ITEMS : '.list-group-item',\n DROPDOWN : '.dropdown',\n DROPDOWN_ITEMS : '.dropdown-item',\n DROPDOWN_TOGGLE : '.dropdown-toggle'\n}\n\nconst OffsetMethod = {\n OFFSET : 'offset',\n POSITION : 'position'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass ScrollSpy {\n constructor(element, config) {\n this._element = element\n this._scrollElement = element.tagName === 'BODY' ? window : element\n this._config = this._getConfig(config)\n this._selector = `${this._config.target} ${Selector.NAV_LINKS},` +\n `${this._config.target} ${Selector.LIST_ITEMS},` +\n `${this._config.target} ${Selector.DROPDOWN_ITEMS}`\n this._offsets = []\n this._targets = []\n this._activeTarget = null\n this._scrollHeight = 0\n\n $(this._scrollElement).on(Event.SCROLL, (event) => this._process(event))\n\n this.refresh()\n this._process()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n refresh() {\n const autoMethod = this._scrollElement === this._scrollElement.window\n ? OffsetMethod.OFFSET : OffsetMethod.POSITION\n\n const offsetMethod = this._config.method === 'auto'\n ? autoMethod : this._config.method\n\n const offsetBase = offsetMethod === OffsetMethod.POSITION\n ? this._getScrollTop() : 0\n\n this._offsets = []\n this._targets = []\n\n this._scrollHeight = this._getScrollHeight()\n\n const targets = [].slice.call(document.querySelectorAll(this._selector))\n\n targets\n .map((element) => {\n let target\n const targetSelector = Util.getSelectorFromElement(element)\n\n if (targetSelector) {\n target = document.querySelector(targetSelector)\n }\n\n if (target) {\n const targetBCR = target.getBoundingClientRect()\n if (targetBCR.width || targetBCR.height) {\n // TODO (fat): remove sketch reliance on jQuery position/offset\n return [\n $(target)[offsetMethod]().top + offsetBase,\n targetSelector\n ]\n }\n }\n return null\n })\n .filter((item) => item)\n .sort((a, b) => a[0] - b[0])\n .forEach((item) => {\n this._offsets.push(item[0])\n this._targets.push(item[1])\n })\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n $(this._scrollElement).off(EVENT_KEY)\n\n this._element = null\n this._scrollElement = null\n this._config = null\n this._selector = null\n this._offsets = null\n this._targets = null\n this._activeTarget = null\n this._scrollHeight = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (typeof config.target !== 'string') {\n let id = $(config.target).attr('id')\n if (!id) {\n id = Util.getUID(NAME)\n $(config.target).attr('id', id)\n }\n config.target = `#${id}`\n }\n\n Util.typeCheckConfig(NAME, config, DefaultType)\n\n return config\n }\n\n _getScrollTop() {\n return this._scrollElement === window\n ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop\n }\n\n _getScrollHeight() {\n return this._scrollElement.scrollHeight || Math.max(\n document.body.scrollHeight,\n document.documentElement.scrollHeight\n )\n }\n\n _getOffsetHeight() {\n return this._scrollElement === window\n ? window.innerHeight : this._scrollElement.getBoundingClientRect().height\n }\n\n _process() {\n const scrollTop = this._getScrollTop() + this._config.offset\n const scrollHeight = this._getScrollHeight()\n const maxScroll = this._config.offset +\n scrollHeight -\n this._getOffsetHeight()\n\n if (this._scrollHeight !== scrollHeight) {\n this.refresh()\n }\n\n if (scrollTop >= maxScroll) {\n const target = this._targets[this._targets.length - 1]\n\n if (this._activeTarget !== target) {\n this._activate(target)\n }\n return\n }\n\n if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {\n this._activeTarget = null\n this._clear()\n return\n }\n\n const offsetLength = this._offsets.length\n for (let i = offsetLength; i--;) {\n const isActiveTarget = this._activeTarget !== this._targets[i] &&\n scrollTop >= this._offsets[i] &&\n (typeof this._offsets[i + 1] === 'undefined' ||\n scrollTop < this._offsets[i + 1])\n\n if (isActiveTarget) {\n this._activate(this._targets[i])\n }\n }\n }\n\n _activate(target) {\n this._activeTarget = target\n\n this._clear()\n\n const queries = this._selector\n .split(',')\n .map((selector) => `${selector}[data-target=\"${target}\"],${selector}[href=\"${target}\"]`)\n\n const $link = $([].slice.call(document.querySelectorAll(queries.join(','))))\n\n if ($link.hasClass(ClassName.DROPDOWN_ITEM)) {\n $link.closest(Selector.DROPDOWN).find(Selector.DROPDOWN_TOGGLE).addClass(ClassName.ACTIVE)\n $link.addClass(ClassName.ACTIVE)\n } else {\n // Set triggered link as active\n $link.addClass(ClassName.ACTIVE)\n // Set triggered links parents as active\n // With both
      and