using AutoMapper; using FluentValidation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using PARR.API.Contracts.V1; using PARR.API.Contracts.V1.Requests; using PARR.API.Contracts.V1.Requests.Queries; using PARR.API.Contracts.V1.Responses; using PARR.API.Contracts.V1.Responses.Base; using PARR.API.Controllers.V1.Base; using PARR.API.Extensions; using PARR.API.Services.Interfaces; using PARR.API.Settings; using PARR.Core.Common.Helpers; using PARR.Core.Common.Interfaces.RabbitServices; using PARR.Core.Repositories.Interfaces.Job; using PARR.Core.Repositories.Interfaces.Schedule; using PARR.Core.Services.MatchingStatusService; using PARR.Domain.Common.Pagination; using PARR.Domain.Common.Rabbit.Messages; using PARR.Domain.Common.Roles; using PARR.Domain.Entities.Base.History; using PARR.Domain.Entities.JobGroupEntities; using PARR.Domain.Entities.Schedule; using PARR.Domain.Enums; namespace PARR.API.Controllers.V1 { /// /// Управление группами работам /// [Authorize(Roles = ParrRoles.Administrator.Role)] public class JobGroupController : BaseApiController { private readonly ILogger _logger; private readonly IMapper _mapper; private readonly IUriService _uriService; private readonly IJobGroupRepository _groupRepository; private readonly IJobRepository _jobRepository; private readonly IEsppSchTypeConfigRepository _esppConfigRepository; private readonly IJobGroupTypeRepository _jobGroupTypeRepository; private readonly IMatchingStatusService _matchingStatusRepository; private readonly IScheduleResponseAreaTimeOffsetRepository _scheduleResponseAreaTimeOffsetRepository; private readonly IJobAutoControlRepository _jobAutoControlRepository; private readonly IRabbitService _mqService; private readonly MqSettings _mqSettings; public JobGroupController( ILogger logger, IMapper mapper, IUriService uriService, IJobGroupRepository groupRepository, IJobRepository jobRepository, IEsppSchTypeConfigRepository esppConfigRepository, IJobGroupTypeRepository jobGroupTypeRepository, IMatchingStatusService matchingStatusRepository, IScheduleResponseAreaTimeOffsetRepository scheduleResponseAreaTimeOffsetRepository, IJobAutoControlRepository jobAutoControlRepository, IRabbitService mqService, MqSettings mqSettings ) { this._logger = logger; this._mapper = mapper; this._uriService = uriService; _groupRepository = groupRepository; _jobRepository = jobRepository; _esppConfigRepository = esppConfigRepository; _jobGroupTypeRepository = jobGroupTypeRepository; _matchingStatusRepository = matchingStatusRepository; _scheduleResponseAreaTimeOffsetRepository = scheduleResponseAreaTimeOffsetRepository; _jobAutoControlRepository = jobAutoControlRepository; this._mqService = mqService; this._mqSettings = mqSettings; } /// /// Получить список групп работ постранично /// /// [HttpGet(ApiRoutes.JobGroup.GetAll)] public async Task GetAll([FromQuery] PaginationQuery paginationQuery, [FromQuery] JobGroupQuery filter) { var paginationFilter = _mapper.Map(paginationQuery); IQueryable query = _groupRepository.Get() .Include(t => t.GroupType) .Include(t => t.GroupingUnitField) .Include(t => t.ScheduleExcludeType) .Include(t => t.ScheduleExcludeTypeCalendar) .Include(t => t.AutoControl); query = query.OrderBy(t => t.GroupName); if (!string.IsNullOrEmpty(filter.Name)) { //query = query.Where(t => t.GroupName.ToLower().Contains(filter.Name.ToLower())); query = query.Where(t => EF.Functions.Like(t.GroupName.ToLower(), SqlHelpers.RegexToLike(filter.Name))); } if (filter.IsFull) query = query .Include(t => t.Jobs).ThenInclude(t => t.Tnk) .Include(t => t.DistributionConfig).ThenInclude(t => t.DistributionPeriod); var jobGroups = await _groupRepository.GetPage(query, paginationFilter).ToListAsync(); if (!jobGroups.Any()) return NoContent(); var response = _mapper.Map>(jobGroups); if (filter.IsFull) foreach (var jobGroupResponse in response) await AppendMissingDataAsync(jobGroupResponse); var paginationResponse = new PagedResponse(response, true).GetPaginatedProps(paginationFilter, query); return Ok(paginationResponse); } /// /// Получить группу работ по id /// /// /// [HttpGet(ApiRoutes.JobGroup.Get)] public async Task GetById([FromRoute] Guid id) { var jobGroup = await _groupRepository.Get() .Include(t => t.Jobs).ThenInclude(t => t.Tnk) .Include(t => t.GroupType) .Include(t => t.GroupingUnitField) .Include(t => t.ScheduleExcludeType) .Include(t => t.ScheduleExcludeTypeCalendar) .Include(t => t.DistributionConfig).ThenInclude(t => t.DistributionPeriod) .Include(t => t.AutoControl) .FirstOrDefaultAsync(t => t.Id == id); if (jobGroup == null) return NotFound(); var response = _mapper.Map(jobGroup); await AppendMissingDataAsync(response); return Ok(new Response(response, true)); } /// /// Создать группу работ (JobGroup) /// /// /// [HttpPost(ApiRoutes.JobGroup.Create)] public async Task Create([FromBody] JobGroupRequest request) { var jobGroup = new JobGroup { Id = Guid.NewGuid(), GroupName = request.Name.Trim(), //IsUmbrella = request.IsUmbrella, GroupTypeId = request.GroupTypeId, GroupingUnitFieldId = await GetGroupingUnitFieldIdAsync(request), ShortDescription = request.ShortDescription.Trim(), FullDescription = request.FullDescription.Trim(), Solution = request.Solution.Trim(), TemplateDuration = request.TemplateDuration.Trim(), ReferenceDate = request.ReferenceDate, UserTimeZoneOffsetMinutes = request.UserTimeZoneOffsetMinutes, ScheduleExcludeTypeId = request.ScheduleExcludeTypeId, ScheduleExcludeTypeCalendarId = request.ScheduleExcludeTypeCalendarId, IsAutoDistributionEnabled = request.IsAutoDistributionEnabled, IsResponseAreaTimezone = request.IsWorkGroupTimezone, IsGroupByResponsible = request.IsGroupByResponsible //IsAgent = request.IsAgent, //AgentName = request.AgentName, //AgentTimeOutSec = request.AgentTimeOutSec, //AgentScript = request.AgentScript }; #region Если включено автораспределение, добавляем настройки if (request.IsAutoDistributionEnabled) { // на всякий проверим, но вообще это проверяется в валидаторе if (request.DistributionConfig == null) { _logger.LogError("Ошибка при создании группы работ '{name}', отсутствуют настройки автораспределения", request.Name); return BadRequest(new Response(false, new List { new ErrorModel { Message = "Ошибка при создании группы заданий на выполнение работ" } })); } jobGroup.DistributionConfig = CreateDistributionConfig(request.DistributionConfig, jobGroup.Id); } #endregion #region Настройки автоконтроля // Корректность проверяется в валидаторе if (request.AutoControl != null) { jobGroup.AutoControl = new JobGroupAutoControl { InitUsedScheduleState = request.AutoControl.InitUsedScheduleState, InitUsedTemplateState = request.AutoControl.InitUsedTemplateState, IsEnable = request.AutoControl.IsEnable, JobGroupId = jobGroup.Id }; } #endregion //Добавляем настройки планировщика request.Schedule.ForEach(item => { jobGroup.EsppSchValues.Add(new EsppSchValue { JobGroupId = jobGroup.Id, TypeConfigId = item.TypeConfigId, TypeValueId = item.TypeValueId }); }); if (!await _groupRepository.CreateAsync(jobGroup) || !await _groupRepository.CommitAsync()) return BadRequest(new Response(false, new List { new ErrorModel { Message = "Ошибка при создании группы заданий на выполнение работ" } })); _logger.LogInformation($"Пользователь {User.Identity?.Name} добавил группу заданий на выполнение работ: {jobGroup.Id}, {jobGroup.GroupName}, {jobGroup.ShortDescription}"); var createdJobGroup = await _groupRepository.Get().Include(t => t.Jobs).ThenInclude(t => t.Tnk) .Include(t => t.GroupType) .Include(t => t.GroupingUnitField) .Include(t => t.ScheduleExcludeType) .Include(t => t.ScheduleExcludeTypeCalendar) .Include(t => t.DistributionConfig).ThenInclude(t => t.DistributionPeriod) .Include(t => t.AutoControl) .FirstAsync(t => t.Id == jobGroup.Id); var locationUri = _uriService.GetUri(ApiRoutes.JobGroup.Get, ApiRoutes.JobGroup.getParam, createdJobGroup.Id); var response = _mapper.Map(createdJobGroup); await AppendMissingDataAsync(response); return Created(locationUri, new Response(response, true)); } /// /// Обновить группу работ (JobGroup) /// /// /// /// [HttpPut(ApiRoutes.JobGroup.Update)] public async Task Update([FromRoute] Guid id, [FromBody] JobGroupRequest request) { var orig = await _groupRepository.Get() .Include(t => t.Jobs) .ThenInclude(t => t.Tnk) .Include(t => t.EsppSchValues) .Include(t => t.AutoControl) .Include(t => t.DistributionConfig).ThenInclude(t => t.DistributionPeriod) .FirstOrDefaultAsync(t => t.Id == id); if (orig == null) return BadRequest(new Response(false, new List { new ErrorModel { Message = $"Ошибка при изменении группы заданий на выполнение работ. Не найдена группа с Id: {id}" } })); //Расписание было изменено, ниже добавим задание в очередь на обновление расписаний у связанных шаблонов var isScheduleChanged = IsScheduleChanged(orig, request); orig.GroupName = request.Name.Trim(); //orig.IsUmbrella = request.IsUmbrella; orig.GroupTypeId = request.GroupTypeId; orig.GroupingUnitFieldId = await GetGroupingUnitFieldIdAsync(request); orig.ShortDescription = request.ShortDescription.Trim(); orig.FullDescription = request.FullDescription.Trim(); orig.Solution = request.Solution.Trim(); orig.TemplateDuration = request.TemplateDuration.Trim(); orig.ReferenceDate = request.ReferenceDate; orig.UserTimeZoneOffsetMinutes = request.UserTimeZoneOffsetMinutes; orig.ScheduleExcludeTypeId = request.ScheduleExcludeTypeId; orig.ScheduleExcludeTypeCalendarId = request.ScheduleExcludeTypeCalendarId; orig.IsAutoDistributionEnabled = request.IsAutoDistributionEnabled; orig.IsResponseAreaTimezone = request.IsWorkGroupTimezone; // IsGroupByResponsible - может быть значение, только если тип ГРУППОВОЙ orig.IsGroupByResponsible = await GetIsGroupByResponsibleValue(request); //orig.IsAgent = request.IsAgent; //orig.AgentName = request.AgentName; //orig.AgentTimeOutSec = request.AgentTimeOutSec; //orig.AgentScript = request.AgentScript; #region Обновляем настройки автораспределения if (request.IsAutoDistributionEnabled) { if (request.DistributionConfig == null) { _logger.LogError("Ошибка при изменении группы работ '{name}', отсутствуют настройки автораспределения", request.Name); return BadRequest(new Response(false, new List { new ErrorModel { Message = "Ошибка при изменении группы заданий на выполнение работ" } })); } // если настройки были, меняем, если не было, создаем if (orig.DistributionConfig != null) { orig.DistributionConfig.DistributionPeriodId = request.DistributionConfig.DistributionPeriodId; orig.DistributionConfig.IsExcludeWeekends = request.DistributionConfig.IsExcludeWeekends; orig.DistributionConfig.IsGroupingByWorkGroup = request.DistributionConfig.IsGroupingByWorkGroup; } else { //создаем orig.DistributionConfig = CreateDistributionConfig(request.DistributionConfig, orig.Id); } } else { // удаляем настройки распределения если они были if (orig.DistributionConfig != null) { _groupRepository.DeleteDistributionConfig(orig.DistributionConfig); } } #endregion #region Настройки автоконтроля // валидатор проверяет корректность if (request.AutoControl != null) { // автоконтролем управляет JobGroup if (orig.AutoControl != null) { // обновляем orig.AutoControl.IsEnable = request.AutoControl.IsEnable; orig.AutoControl.InitUsedScheduleState = request.AutoControl.InitUsedScheduleState; orig.AutoControl.InitUsedTemplateState = request.AutoControl.InitUsedTemplateState; // удалить настройки автоконтроля для job await RemoveJobAutoControlSettingsAsync(orig.Id); } else { // создаем новую запись orig.AutoControl = new JobGroupAutoControl { InitUsedScheduleState = request.AutoControl.InitUsedScheduleState, InitUsedTemplateState = request.AutoControl.InitUsedTemplateState, IsEnable = request.AutoControl.IsEnable, JobGroupId = orig.Id }; // удалить настройки автоконтроля для job await RemoveJobAutoControlSettingsAsync(orig.Id); } } else { // автоконтролем управляет каждый job отдельно // удаляем настройки, если они были orig.AutoControl = null; } #endregion //обновляем планировщик orig.EsppSchValues.Clear(); request.Schedule.ForEach(item => { orig.EsppSchValues.Add(new EsppSchValue { JobGroupId = orig.Id, TypeConfigId = item.TypeConfigId, TypeValueId = item.TypeValueId }); }); if (!await _groupRepository.CommitAsync()) return BadRequest(new Response(false, new List { new ErrorModel { Message = "Ошибка при изменении группы заданий на выполнение работ." } })); _logger.LogInformation($"Пользователь {User.Identity?.Name} обновил группу заданий на выполнение работ: {orig.Id}," + $" {orig.GroupName}, {orig.ShortDescription}, {orig.FullDescription}," + $" {orig.Solution}, {orig.TemplateDuration}, {orig.ReferenceDate}, {orig.IsAutoDistributionEnabled}" + $", {orig.IsAgent}, {orig.AgentName}, {orig.AgentTimeOutSec}, {orig.AgentScript}"); #region расписание было изменено, отправим задание на перерасчет nextRun if (isScheduleChanged) { // расписание было изменено, отправим задание на перерасчет nextRun var requestToMq = new NextRunUpdateMq { JobGroupId = id, Initiator = new HistoryInitiator { InitiatorComment = "Изменилось расписание группы работ в ГУИ, отправлен запрос на перерасчет nextRun", InitiatorParrComponentId = ParrComponentsEnum.Api } }; var sendResult = await _mqService.SendAsync(_mqSettings.NextRun, new List { requestToMq }); if (sendResult.IsSuccess) _logger.LogInformation("Задание на перерасчет NextRun успешно отправлено в очередь MQ {queueName}", _mqSettings.NextRun.QueueName); else _logger.LogError("Ошибка при отправке задания на перерасчет NextRun в очередь MQ {queueName}", _mqSettings.NextRun.QueueName); } #endregion var updatedJobGroup = await _groupRepository.Get() .Include(t => t.Jobs) .ThenInclude(t => t.Tnk) .Include(t => t.GroupType) .Include(t => t.GroupingUnitField) .Include(t => t.ScheduleExcludeType) .Include(t => t.ScheduleExcludeTypeCalendar) .Include(t => t.DistributionConfig).ThenInclude(t => t.DistributionPeriod) .Include(t => t.AutoControl) .FirstAsync(t => t.Id == orig.Id); var response = _mapper.Map(updatedJobGroup); await AppendMissingDataAsync(response); return Ok(new Response(response, true)); } /// /// Удалить группу работ (только если нет связанных работ) /// /// /// [HttpDelete(ApiRoutes.JobGroup.Delete)] public async Task Delete([FromRoute] Guid id) { var jobGroup = await _groupRepository.Get() .FirstOrDefaultAsync(t => t.Id == id); if (jobGroup == null) return BadRequest(new Response(false, new List { new ErrorModel { Message = $"Ошибка при удалении группы заданий на выполнение работ. Не найдена группа заданий на выполнение работ Id: {id}" } })); var jobCount = await _jobRepository.Get().CountAsync(t => t.GroupId == id); if (jobCount > 0) return BadRequest(new Response(false, new List { new ErrorModel { Message = $"Ошибка при удалении группы заданий на выполнение работ. С данным группой связаны задания: {jobCount} шт." } })); if (!_groupRepository.Delete(jobGroup) || !await _groupRepository.CommitAsync()) return BadRequest(new Response(false, new List { new ErrorModel { Message = $"Ошибка при удалении группы заданий на выполнение работ" } })); _logger.LogInformation($"Пользователь {User.Identity?.Name} удалил группу заданий на выполнение работ: {jobGroup.Id},{jobGroup.GroupName}," + $" {jobGroup.ShortDescription}, {jobGroup.FullDescription}," + $" {jobGroup.Solution}, {jobGroup.TemplateDuration}, {jobGroup.ReferenceDate}," + $" {jobGroup.IsAutoDistributionEnabled}, {jobGroup.IsAgent}, {jobGroup.AgentName}," + $" {jobGroup.AgentTimeOutSec}, {jobGroup.AgentScript}"); return NoContent(); } /// /// Удалить настройки автоконтроля для связанных Job /// /// /// private async Task RemoveJobAutoControlSettingsAsync(Guid jobGroupId) { var jobAutoControlsToRemove = await _jobAutoControlRepository.Get() .Where(t => t.Job!.GroupId == jobGroupId) .ToListAsync(); if (!jobAutoControlsToRemove.Any()) return; _logger.LogInformation("У группы работ {JobGroupId}, у связанных работ {JobsCount} шт. удалены настройки автоконтроля, так как автоконтролем управляем группа работ.", jobGroupId, jobAutoControlsToRemove.Count); _jobAutoControlRepository.RemoveRange(jobAutoControlsToRemove); } /// /// Проверка, были ли изменения в расписании /// /// /// /// private bool IsScheduleChanged(JobGroup orig, JobGroupRequest request) { var isScheduleChanged = false; if (orig.ReferenceDate != request.ReferenceDate) isScheduleChanged = true; if (request.Schedule.Count() != orig.EsppSchValues.Count()) isScheduleChanged = true; if (request.IsAutoDistributionEnabled != orig.IsAutoDistributionEnabled) isScheduleChanged = true; if (request.DistributionConfig?.DistributionPeriodId != orig.DistributionConfig?.DistributionPeriodId) isScheduleChanged = true; if (request.DistributionConfig?.IsExcludeWeekends != orig.DistributionConfig?.IsExcludeWeekends) isScheduleChanged = true; if (request.DistributionConfig?.IsGroupingByWorkGroup != orig.DistributionConfig?.IsGroupingByWorkGroup) isScheduleChanged = true; if (request.ScheduleExcludeTypeId != orig.ScheduleExcludeTypeId) isScheduleChanged = true; if (request.ScheduleExcludeTypeCalendarId != orig.ScheduleExcludeTypeCalendarId) isScheduleChanged = true; if (request.IsWorkGroupTimezone != orig.IsResponseAreaTimezone) isScheduleChanged = true; if (request.UserTimeZoneOffsetMinutes != orig.UserTimeZoneOffsetMinutes) isScheduleChanged = true; request.Schedule.ForEach(requestSchedule => { var schExist = orig.EsppSchValues.FirstOrDefault(t => t.JobGroupId == orig.Id && t.TypeValueId == requestSchedule.TypeValueId && t.TypeConfigId == requestSchedule.TypeConfigId); if (schExist == null) isScheduleChanged = true; }); return isScheduleChanged; } /// /// Заполнить обязательные поля /// /// /// private async Task AppendMissingDataAsync(JobGroupResponse jobGroupResponse) { var schedule = await _esppConfigRepository.GetEsppScheduleDtoAsync(jobGroupResponse.Id); if (schedule == null) { _logger.LogError($"Не смог замапить расписание, так как оно null. JobGroupId: {jobGroupResponse.Id}"); return; } var scheduleResponse = new JobGroupScheduleResponse { //Timezone = settingsFromDb.ScheduleTimezone, Timezone = _scheduleResponseAreaTimeOffsetRepository.GetDefault.EsppValue, TypeSchedule = _mapper.Map(schedule.TypeSchedule), Values = _mapper.Map>(schedule.Values).OrderBy(t => t.Order).ToList() }; jobGroupResponse.Schedule = scheduleResponse; jobGroupResponse.JobsCount = await _jobRepository.Get().CountAsync(t => t.GroupId == jobGroupResponse.Id); jobGroupResponse.MatchingStatus = await GetMatchingStatusAsync(jobGroupResponse.Id); } /// /// Получить значение IsGroupByResponsible из реквеста /// /// /// private async Task GetIsGroupByResponsibleValue(JobGroupRequest request) { // IsGroupByResponsible - может быть значение, только если тип ГРУППОВОЙ if (!request.IsGroupByResponsible.HasValue) return null; // Если есть значение, смотрим, групповой ли тип работ, и если нет, то вернем null var groupingType = await _jobGroupTypeRepository.Get().FirstAsync(t => t.Code == JobGroupTypesEnum.Group); if (request.GroupTypeId == groupingType.Id) { // это групповой тип работ, все ок return request.IsGroupByResponsible; } else { // Это не сгруппированный тип, обнуляем IsGroupByResponsible _logger.LogInformation("При сохраненни JobGroup, был передан IsGroupByResponsible: {IsGroupByResponsible}, но при этом, тип группы не сгруппированный, а GroupTypeId: {GroupTypeId}, обнулил IsGroupByResponsible", request.IsGroupByResponsible, request.GroupTypeId); return null; } } /// /// Смотрим на тип Группы, и возвращаем GroupingUnitFieldId или обнуляем его /// /// /// private async Task GetGroupingUnitFieldIdAsync(JobGroupRequest request) { // Если request.GroupingUnitFieldId == null, то ничего проверять не будем if (request.GroupingUnitFieldId == null) return request.GroupingUnitFieldId; var groupingType = await _jobGroupTypeRepository.Get().FirstAsync(t => t.Code == JobGroupTypesEnum.Group); if (request.GroupTypeId == groupingType.Id) { // Это сгруппированный тип, все ок return request.GroupingUnitFieldId; } else { // Это не сгруппированный тип, обнуляем GroupingUnitFieldId _logger.LogInformation("При сохраненни JobGroup, был передан GroupingUnitFieldId: {GroupingUnitFieldId}, но при этом, тип группы не сгруппированный, а GroupTypeId: {GroupTypeId}, обнулил GroupingUnitFieldId", request.GroupingUnitFieldId, request.GroupTypeId); return null; } } /// /// Получить статус matching`a /// /// /// private async Task GetMatchingStatusAsync(Guid jobGroupId) { var statusMatching = await _matchingStatusRepository.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); return _mapper.Map(statusMatching); } /// /// Создать конфиг распределения /// /// /// /// private JobGroupDistributionConfig CreateDistributionConfig(DistributionConfigRequest distributionConfigRequest, Guid jobGroupId) { return new JobGroupDistributionConfig { GroupId = jobGroupId, DistributionPeriodId = distributionConfigRequest.DistributionPeriodId, IsExcludeWeekends = distributionConfigRequest.IsExcludeWeekends, IsGroupingByWorkGroup = distributionConfigRequest.IsGroupingByWorkGroup }; } } }