From 5bbf2e2db52c34bbad655e43630bf39c11535ca3 Mon Sep 17 00:00:00 2001 From: Mikhail Kuznetsov Date: Tue, 1 Jul 2025 17:20:48 +1000 Subject: [PATCH] =?UTF-8?q?feat(api):=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B=20JobController,=20JobGrou?= =?UTF-8?q?pController?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PARR.API/Contracts/V1/ApiRoutes.cs | 11 + .../Contracts/V1/Requests/JobGroupRequest.cs | 29 ++ PARR.API/Contracts/V1/Requests/JobRequest.cs | 19 ++ .../V1/Requests/Queries/JobGroupQuery.cs | 17 ++ .../Contracts/V1/Requests/Queries/JobQuery.cs | 15 + .../V1/Responses/ApplicationInWorkResponse.cs | 60 ++-- .../Contracts/V1/Responses/JobBaseResponse.cs | 46 +++ .../V1/Responses/JobGroupResponse.cs | 38 +++ PARR.API/Controllers/V1/JobController.cs | 282 ++++++++++++++++++ PARR.API/Controllers/V1/JobGroupController.cs | 250 ++++++++++++++++ .../DomainToResponseProfile.cs | 13 +- PARR.API/Validators/JobGroupValidator.cs | 26 ++ PARR.API/Validators/JobValidator.cs | 48 +++ PARR.DAL/Models/Job/Job.cs | 8 +- PARR.DAL/Models/Unit/Unit.cs | 3 + PARR.Test/Worker.cs | 34 ++- 16 files changed, 855 insertions(+), 44 deletions(-) create mode 100644 PARR.API/Contracts/V1/Requests/JobGroupRequest.cs create mode 100644 PARR.API/Contracts/V1/Requests/JobRequest.cs create mode 100644 PARR.API/Contracts/V1/Requests/Queries/JobGroupQuery.cs create mode 100644 PARR.API/Contracts/V1/Requests/Queries/JobQuery.cs create mode 100644 PARR.API/Contracts/V1/Responses/JobGroupResponse.cs create mode 100644 PARR.API/Controllers/V1/JobController.cs create mode 100644 PARR.API/Controllers/V1/JobGroupController.cs create mode 100644 PARR.API/Validators/JobGroupValidator.cs create mode 100644 PARR.API/Validators/JobValidator.cs diff --git a/PARR.API/Contracts/V1/ApiRoutes.cs b/PARR.API/Contracts/V1/ApiRoutes.cs index 656c52f5..7a711b8e 100644 --- a/PARR.API/Contracts/V1/ApiRoutes.cs +++ b/PARR.API/Contracts/V1/ApiRoutes.cs @@ -377,6 +377,17 @@ public const string getParam = "{id}"; } + public static class JobGroup + { + public const string GetAll = Base + "/job-groups/"; + public const string Get = Base + "/job-groups/" + getParam; + + public const string Delete = Base + "/job-groups/" + getParam; + public const string Create = Base + "/job-groups/"; + public const string Update = Base + "/job-groups/" + getParam; + + public const string getParam = "{id}"; + } public static class JobAutoControl { diff --git a/PARR.API/Contracts/V1/Requests/JobGroupRequest.cs b/PARR.API/Contracts/V1/Requests/JobGroupRequest.cs new file mode 100644 index 00000000..87fac01a --- /dev/null +++ b/PARR.API/Contracts/V1/Requests/JobGroupRequest.cs @@ -0,0 +1,29 @@ +namespace PARR.API.Contracts.V1.Requests +{ + public class JobGroupRequest + { + public required string Name { get; set; } + + public bool? IsUmbrella { get; set; } + + public required string ShortDescription { get; set; } + + public required string FullDescription { get; set; } + + public required string Solution { get; set; } + + public required string TemplateDuration { get; set; } + + public DateTimeOffset ReferenceDate { get; set; } + + public bool IsAutoDistributionEnabled { get; set; } + + public bool IsAgent { get; set; } + + public string? AgentName { get; set; } + + public int? AgentTimeOutSec { get; set; } + + public string? AgentScript { get; set; } + } +} diff --git a/PARR.API/Contracts/V1/Requests/JobRequest.cs b/PARR.API/Contracts/V1/Requests/JobRequest.cs new file mode 100644 index 00000000..13dc7205 --- /dev/null +++ b/PARR.API/Contracts/V1/Requests/JobRequest.cs @@ -0,0 +1,19 @@ +namespace PARR.API.Contracts.V1.Requests +{ + public class JobRequest + { + public required Guid TnkId { get; set; } + + public required Guid GroupId { get; set; } + + public required string Name { get; set; } + + public int? MinValueRelationships { get; set; } + + public int? MaxValueRelationships { get; set; } + + public string? TemplateNameMask { get; set; } + + public required string WorkName { get; set; } + } +} diff --git a/PARR.API/Contracts/V1/Requests/Queries/JobGroupQuery.cs b/PARR.API/Contracts/V1/Requests/Queries/JobGroupQuery.cs new file mode 100644 index 00000000..03e3f547 --- /dev/null +++ b/PARR.API/Contracts/V1/Requests/Queries/JobGroupQuery.cs @@ -0,0 +1,17 @@ +using PARR.API.Contracts.V1.Requests.BaseRequests; + +namespace PARR.API.Contracts.V1.Requests.Queries +{ + public class JobGroupQuery: FullQuery + { + /// + /// Поиск по имени + /// + public string? Name { get; set; } + + /// + /// Поиск по короткому описанию + /// + public string? ShortDescription { get; set; } + } +} diff --git a/PARR.API/Contracts/V1/Requests/Queries/JobQuery.cs b/PARR.API/Contracts/V1/Requests/Queries/JobQuery.cs new file mode 100644 index 00000000..ba867428 --- /dev/null +++ b/PARR.API/Contracts/V1/Requests/Queries/JobQuery.cs @@ -0,0 +1,15 @@ +namespace PARR.API.Contracts.V1.Requests.Queries +{ + public class JobQuery + { + /// + /// Поиск по имени + /// + public string? Name { get; set; } + + /// + /// Поиск по Id группы работ + /// + public Guid? GroupId { get; set; } + } +} diff --git a/PARR.API/Contracts/V1/Responses/ApplicationInWorkResponse.cs b/PARR.API/Contracts/V1/Responses/ApplicationInWorkResponse.cs index fa0e00fc..37fbf432 100644 --- a/PARR.API/Contracts/V1/Responses/ApplicationInWorkResponse.cs +++ b/PARR.API/Contracts/V1/Responses/ApplicationInWorkResponse.cs @@ -62,41 +62,41 @@ } - public class TemplateStats - { - /// - /// Активированных шаблонов - /// - public int Activated { get; set; } + //public class TemplateStats + //{ + // /// + // /// Активированных шаблонов + // /// + // public int Activated { get; set; } - /// - /// Синхронизировано шаблонов (TaskStatus = 30/Ok) - /// - public int Synchronized { get; set; } + // /// + // /// Синхронизировано шаблонов (TaskStatus = 30/Ok) + // /// + // public int Synchronized { get; set; } - /// - /// Ошибок синхронизации (RobotStatus = 33/Error) - /// - public int Errors { get; set; } - } + // /// + // /// Ошибок синхронизации (RobotStatus = 33/Error) + // /// + // public int Errors { get; set; } + //} - public class ScheduleStats - { - /// - /// Активированных шаблонов - /// - public int Activated { get; set; } + //public class ScheduleStats + //{ + // /// + // /// Активированных шаблонов + // /// + // public int Activated { get; set; } - /// - /// Синхронизировано шаблонов (TaskStatus = 30/Ok) - /// - public int Synchronized { get; set; } + // /// + // /// Синхронизировано шаблонов (TaskStatus = 30/Ok) + // /// + // public int Synchronized { get; set; } - /// - /// Ошибок синхронизации (RobotStatus = 33/Error) - /// - public int Errors { get; set; } - } + // /// + // /// Ошибок синхронизации (RobotStatus = 33/Error) + // /// + // public int Errors { get; set; } + //} } diff --git a/PARR.API/Contracts/V1/Responses/JobBaseResponse.cs b/PARR.API/Contracts/V1/Responses/JobBaseResponse.cs index ad107d11..3f5e2459 100644 --- a/PARR.API/Contracts/V1/Responses/JobBaseResponse.cs +++ b/PARR.API/Contracts/V1/Responses/JobBaseResponse.cs @@ -12,6 +12,52 @@ public class JobResponse : JobBaseResponse { + public int TemplatesCount { get; set; } + #region Статистика + + public TemplateStats? TemplateStatistics { get; set; } + + public ScheduleStats? ScheduleStatistics { get; set; } + + #endregion + } + + + public class TemplateStats + { + /// + /// Активированных шаблонов + /// + public int Activated { get; set; } + + /// + /// Синхронизировано шаблонов (TaskStatus = 30/Ok) + /// + public int Synchronized { get; set; } + + /// + /// Ошибок синхронизации (RobotStatus = 33/Error) + /// + public int Errors { get; set; } + } + + + public class ScheduleStats + { + /// + /// Активированных шаблонов + /// + public int Activated { get; set; } + + /// + /// Синхронизировано шаблонов (TaskStatus = 30/Ok) + /// + public int Synchronized { get; set; } + + /// + /// Ошибок синхронизации (RobotStatus = 33/Error) + /// + public int Errors { get; set; } } } diff --git a/PARR.API/Contracts/V1/Responses/JobGroupResponse.cs b/PARR.API/Contracts/V1/Responses/JobGroupResponse.cs new file mode 100644 index 00000000..454994ca --- /dev/null +++ b/PARR.API/Contracts/V1/Responses/JobGroupResponse.cs @@ -0,0 +1,38 @@ +using PARR.DAL.Models.Job; + +namespace PARR.API.Contracts.V1.Responses +{ + public class JobGroupBaseResponse + { + public Guid Id { get; set; } + + public required string Name { get; set; } + + public bool? IsUmbrella { get; set; } + + public required string ShortDescription { get; set; } + + public required string FullDescription { get; set; } + + public required string Solution { get; set; } + + public required string TemplateDuration { get; set; } + + public DateTimeOffset ReferenceDate { get; set; } + + public bool IsAutoDistributionEnabled { get; set; } + + public bool IsAgent { get; set; } + + public string? AgentName { get; set; } + + public int? AgentTimeOutSec { get; set; } + + public string? AgentScript { get; set; } + } + + public class JobGroupResponse : JobGroupBaseResponse + { + public List? Jobs { get; set; } + } +} diff --git a/PARR.API/Controllers/V1/JobController.cs b/PARR.API/Controllers/V1/JobController.cs new file mode 100644 index 00000000..20366ceb --- /dev/null +++ b/PARR.API/Controllers/V1/JobController.cs @@ -0,0 +1,282 @@ +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.Constants; +using PARR.DAL.Contracts; +using PARR.DAL.DomainModels; +using PARR.DAL.Models.Job; +using PARR.DAL.Services.Interfaces; +using PARR.DAL.Services.Interfaces.Job; + +namespace PARR.API.Controllers.V1 +{ + /// + /// Управление работами + /// + [Authorize(Roles = ParrRoles.Administrator.Role)] + public class JobController : BaseApiController + { + private readonly ILogger logger; + private readonly IMapper mapper; + private readonly IUriService uriService; + private readonly IJobService jobService; + private readonly ITemplateService templateService; + private readonly IValidator validator; + + public JobController( + ILogger logger, + IMapper mapper, + IUriService uriService, + IJobService jobService, + ITemplateService templateService, + IValidator validator + ) + { + this.logger = logger; + this.mapper = mapper; + this.uriService = uriService; + this.jobService = jobService; + this.templateService = templateService; + this.validator = validator; + } + + /// + /// Получить список заданий на выполнение работ(Job) постранично + /// + /// + [HttpGet(ApiRoutes.Job.GetAll)] + public async Task GetAll([FromQuery] PaginationQuery paginationQuery, [FromQuery] JobQuery filter) + { + var paginationFilter = mapper.Map(paginationQuery); + + IQueryable query = jobService.Get().Include(t => t.Tnk); + + query = query.OrderBy(t => t.Name); + + if (!string.IsNullOrEmpty(filter.Name)) + query = query.Where(t => t.Name.ToLower().Contains(filter.Name.ToLower())); + + if (filter.GroupId.HasValue) + query = query.Where(t => t.GroupId == filter.GroupId.Value); + + var jobs = await jobService.GetPage(query, paginationFilter).ToListAsync(); + + if (!jobs.Any()) + return NoContent(); + + var response = mapper.Map>(jobs);//TODO Migration to job + + var paginationResponse = new PagedResponse(response, true).GetPaginatedProps(paginationFilter, query); + + return Ok(paginationResponse); + } + + + /// + /// Получить задание на выполнение работ по id + /// + /// + /// + [HttpGet(ApiRoutes.Job.Get)] + public async Task GetById([FromRoute] Guid id) + { + var job = await jobService.Get().Include(t => t.Tnk).FirstOrDefaultAsync(t => t.Id == id); + + if (job == null) + return NotFound(); + + var response = mapper.Map(job); + response.TemplatesCount = await templateService.Get().CountAsync(t => t.JobId == id); + + var statistics = await GetStatisticsAsync(response.Id); + BindStatistics(response, statistics); + + return Ok(new Response(response, true)); + } + + + /// + /// Создать задание на выполнение работ (Job) + /// + /// + /// + [HttpPost(ApiRoutes.Job.Create)] + public async Task Create([FromBody] JobRequest request) + { + var resultValidate = await validator.ValidateAsync(request); + + if (!resultValidate.IsValid) + return BadRequest(new Response(resultValidate.Errors)); + + var job = new Job + { + Id = Guid.NewGuid(), + Name = request.Name.Trim(), + WorkName = request.WorkName.Trim(), + MinValueRelationships = request.MinValueRelationships, + MaxValueRelationships = request.MaxValueRelationships, + TemplateNameMask = request.TemplateNameMask?.Trim(), + TnkId = request.TnkId, + GroupId = request.GroupId + }; + + + if (!await jobService.CreateAsync(job) || !await jobService.CommitAsync()) + return BadRequest(new Response(false, new List { new ErrorModel { Message = "Ошибка при созании задания на выполнение работ" } })); + + logger.LogInformation($"Пользователь {User.Identity?.Name} добавил задание на выполнение работ: {job.Id}, {job.Name}, {job.WorkName}"); + + + var createdJob = await jobService.Get().Include(t => t.Tnk) + .FirstAsync(t => t.Id == job.Id); + + var locationUri = uriService.GetUri(ApiRoutes.Job.Get, ApiRoutes.Job.getParam, createdJob.Id); + + var response = mapper.Map(createdJob); + // так как мы только что создали Job, то у него нет шаблонов, смело ставим = 0 (ускоряем запрос) + response.TemplatesCount = 0; + + return Created(locationUri, new Response(response, true)); + } + + + /// + /// Обновить задание на выполнение работ (Job) + /// + /// + /// + /// + [HttpPut(ApiRoutes.Job.Update)] + public async Task Update([FromRoute] Guid id, [FromBody] JobRequest request) + { + var resultValidate = await validator.ValidateAsync(request); + if (!resultValidate.IsValid) + return BadRequest(new Response(resultValidate.Errors)); + + var orig = await jobService.Get().Include(t => t.Tnk) + .FirstOrDefaultAsync(t => t.Id == id); + + if (orig == null) + return BadRequest(new Response(false, new List { new ErrorModel { Message = $"Ошибка при изменении задания на выполнение работ. Не найдено задание с Id: {id}" } })); + + orig.Name = request.Name.Trim(); + orig.WorkName = request.WorkName.Trim(); + orig.MinValueRelationships = request.MinValueRelationships; + orig.MaxValueRelationships = request.MaxValueRelationships; + orig.TemplateNameMask = request.TemplateNameMask?.Trim(); + orig.TnkId = request.TnkId; + orig.GroupId = request.GroupId; + + if (!await jobService.CommitAsync()) + return BadRequest(new Response(false, new List { new ErrorModel { Message = "Ошибка при изменении задания на выполнение работ." } })); + + logger.LogInformation($"Пользователь {User.Identity?.Name} обновил задание на выполнение работ: {orig.Id}," + + $" {orig.Name}, {orig.WorkName}, {orig.MinValueRelationships}, {orig.MaxValueRelationships}," + + $" {orig.TemplateNameMask}, {orig.TnkId}, {nameof(orig.GroupId)}"); + + + var updatedApplicationInWork = await jobService.Get().Include(t => t.Tnk) + .FirstAsync(t => t.Id == orig.Id); + + var response = mapper.Map(updatedApplicationInWork); + response.TemplatesCount = await templateService.Get().CountAsync(t => t.JobId == id); + + var statistics = await GetStatisticsAsync(response.Id); + BindStatistics(response, statistics); + + return Ok(new Response(response, true)); + + } + + + /// + /// Удалить задание на выполнение работ (только если нет связанных шаблонов) + /// + /// + /// + [HttpDelete(ApiRoutes.Job.Delete)] + public async Task Delete([FromRoute] Guid id) + { + var job = await jobService.Get().Include(t => t.Tnk) + .FirstOrDefaultAsync(t => t.Id == id); + + if (job == null) + return BadRequest(new Response(false, new List { new ErrorModel { + Message = $"Ошибка при удалении задания на выполнение работ. Не найдено задание на выполнение работ Id: {id}" + } })); + + var templateCount = await templateService.Get().CountAsync(t => t.JobId == id); + + if (templateCount > 0) + return BadRequest(new Response(false, new List { new ErrorModel { + Message = $"Ошибка при удалении задания на выполнение работ. С данным заданием связаны шаблоны: {templateCount} шт." + } })); + + if (!jobService.Delete(job) || !await jobService.CommitAsync()) + return BadRequest(new Response(false, new List { new ErrorModel { + Message = $"Ошибка при удалении задания на выполнение работ" + } })); + + logger.LogInformation($"Пользователь {User.Identity?.Name} удалил задание на выполнение работ: {job.Id},{job.Name}," + + $" {job.WorkName}, {job.MinValueRelationships}, {job.MaxValueRelationships}," + + $" {job.TemplateNameMask}, {job.TnkId}, {job.GroupId}"); + + return NoContent(); + } + + + /// + /// Загрузка статистики + /// + /// + /// + private async Task GetStatisticsAsync(Guid jobId) + { + var statResult = await jobService.Get() + .Include(t => t.Templates) + .ThenInclude(t => t.RobotConfigurations) + .Where(x => x.Id == jobId) + .Select(t => new + { + TemplateActivated = t.Templates.Count(x => x.IsActiveTemplate), + TemplateSynchronized = t.Templates.Count(x => x.RobotConfigurations.Any(c => c.TaskStatusCode == (int)TaskStatusEnum.Ok && c.RobotCode == (int)RobotsEnum.TemplateOrder)), + TemplateErrors = t.Templates.Count(x => x.RobotConfigurations.Any(c => c.RobotStatusCode == (int)RobotStatusEnum.Error && c.RobotCode == (int)RobotsEnum.TemplateOrder)), + ScheduleActivated = t.Templates.Count(x => x.IsActiveSchedule), + ScheduleSynchronized = t.Templates.Count(x => x.RobotConfigurations.Any(c => c.TaskStatusCode == (int)TaskStatusEnum.Ok && c.RobotCode == (int)RobotsEnum.ScheduleOrder)), + ScheduleErrors = t.Templates.Count(x => x.RobotConfigurations.Any(c => c.RobotStatusCode == (int)RobotStatusEnum.Error && c.RobotCode == (int)RobotsEnum.ScheduleOrder)) + + }).FirstOrDefaultAsync(); + + return new JobStatModel + { + ScheduleStatistics = new ScheduleStats { Activated = statResult?.ScheduleActivated ?? 0, Errors = statResult?.ScheduleErrors ?? 0, Synchronized = statResult?.ScheduleSynchronized ?? 0 }, + TemplateStatistics = new TemplateStats { Activated = statResult?.TemplateActivated ?? 0, Errors = statResult?.TemplateErrors ?? 0, Synchronized = statResult?.TemplateSynchronized ?? 0 } + }; + } + + private void BindStatistics(JobResponse job, JobStatModel statistics) + { + job.TemplateStatistics = new TemplateStats { Activated = statistics.TemplateStatistics.Activated, Errors = statistics.TemplateStatistics.Errors, Synchronized = statistics.TemplateStatistics.Synchronized }; + job.ScheduleStatistics = new ScheduleStats { Activated = statistics.ScheduleStatistics.Activated, Errors = statistics.ScheduleStatistics.Errors, Synchronized = statistics.ScheduleStatistics.Synchronized }; + } + + + } + + public class JobStatModel + { + public TemplateStats TemplateStatistics { get; set; } + + public ScheduleStats ScheduleStatistics { get; set; } + } +} diff --git a/PARR.API/Controllers/V1/JobGroupController.cs b/PARR.API/Controllers/V1/JobGroupController.cs new file mode 100644 index 00000000..f834afe3 --- /dev/null +++ b/PARR.API/Controllers/V1/JobGroupController.cs @@ -0,0 +1,250 @@ +using AutoMapper; +using FluentValidation; +using InfluxDB.Client.Api.Domain; +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.Constants; +using PARR.DAL.DomainModels; +using PARR.DAL.Models.Job; +using PARR.DAL.Services.Interfaces.Job; +using System.ComponentModel.DataAnnotations; + +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 IJobGroupService groupService; + private readonly IValidator validator; + + public JobGroupController( + ILogger logger, + IMapper mapper, + IUriService uriService, + IJobGroupService groupService, + IValidator validator + ) + { + this.logger = logger; + this.mapper = mapper; + this.uriService = uriService; + this.groupService = groupService; + this.validator = validator; + } + + + /// + /// Получить список групп заданий на выполнение работ постранично + /// + /// + [HttpGet(ApiRoutes.JobGroup.GetAll)] + public async Task GetAll([FromQuery] PaginationQuery paginationQuery, [FromQuery] JobGroupQuery filter) + { + var paginationFilter = mapper.Map(paginationQuery); + + IQueryable query = groupService.Get(); + + query = query.OrderBy(t => t.GroupName); + + if (!string.IsNullOrEmpty(filter.Name)) + query = query.Where(t => t.GroupName.ToLower().Contains(filter.Name.ToLower())); + + if (!string.IsNullOrEmpty(filter.ShortDescription)) + query = query.Where(t => t.ShortDescription.ToLower().Contains(filter.ShortDescription.ToLower())); + + if (filter.IsFull) + query = query.Include(t => t.Jobs).ThenInclude(t => t.Tnk); + + var jobs = await groupService.GetPage(query, paginationFilter).ToListAsync(); + + if (!jobs.Any()) + return NoContent(); + + if (filter.IsFull == true) + { + var responseFull = mapper.Map>(jobs); + var paginationResponseFull = new PagedResponse(responseFull, true).GetPaginatedProps(paginationFilter, query); + + return Ok(paginationResponseFull); + } + + var response = mapper.Map>(jobs); + + 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 groupService.Get() + .Include(t => t.Jobs).ThenInclude(t => t.Tnk) + .FirstOrDefaultAsync(t => t.Id == id); + + if (jobGroup == null) + return NotFound(); + + var response = mapper.Map(jobGroup); + + return Ok(new Response(response, true)); + } + + + /// + /// Создать группу заданий на выполнение работ (JobGroup) + /// + /// + /// + [HttpPost(ApiRoutes.JobGroup.Create)] + public async Task Create([FromBody] JobGroupRequest request) + { + var resultValidate = await validator.ValidateAsync(request); + + if (!resultValidate.IsValid) + return BadRequest(new Response(resultValidate.Errors)); + + var jobGroup = new JobGroup + { + Id = Guid.NewGuid(), + GroupName = request.Name.Trim(), + IsUmbrella = request.IsUmbrella, + ShortDescription = request.ShortDescription.Trim(), + FullDescription = request.FullDescription.Trim(), + Solution = request.Solution.Trim(), + TemplateDuration = request.TemplateDuration.Trim(), + ReferenceDate = request.ReferenceDate, + IsAutoDistributionEnabled = request.IsAutoDistributionEnabled, + IsAgent = request.IsAgent, + AgentName = request.AgentName, + AgentTimeOutSec = request.AgentTimeOutSec, + AgentScript = request.AgentScript + }; + + + if (!await groupService.CreateAsync(jobGroup) || !await groupService.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 groupService.Get().Include(t=>t.Jobs).ThenInclude(t => t.Tnk) + .FirstAsync(t => t.Id == jobGroup.Id); + + var locationUri = uriService.GetUri(ApiRoutes.JobGroup.Get, ApiRoutes.JobGroup.getParam, createdJobGroup.Id); + + var response = mapper.Map(createdJobGroup); + + return Created(locationUri, new Response(response, true)); + } + + + /// + /// Обновить группу заданий на выполнение работ (JobGroup) + /// + /// + /// + /// + [HttpPut(ApiRoutes.JobGroup.Update)] + public async Task Update([FromRoute] Guid id, [FromBody] JobGroupRequest request) + { + var resultValidate = await validator.ValidateAsync(request); + + if (!resultValidate.IsValid) + return BadRequest(new Response(resultValidate.Errors)); + + var orig = await groupService.Get() + .Include(t=>t.Jobs) + .ThenInclude(t => t.Tnk) + .FirstOrDefaultAsync(t => t.Id == id); + + if (orig == null) + return BadRequest(new Response(false, new List { new ErrorModel { Message = $"Ошибка при изменении группы заданий на выполнение работ. Не найдена группа с Id: {id}" } })); + + orig.GroupName = request.Name.Trim(); + orig.IsUmbrella = request.IsUmbrella; + 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.IsAutoDistributionEnabled = request.IsAutoDistributionEnabled; + orig.IsAgent = request.IsAgent; + orig.AgentName = request.AgentName; + orig.AgentTimeOutSec = request.AgentTimeOutSec; + orig.AgentScript = request.AgentScript; + + if (!await groupService.CommitAsync()) + return BadRequest(new Response(false, new List { new ErrorModel { Message = "Ошибка при изменении группы заданий на выполнение работ." } })); + + logger.LogInformation($"Пользователь {User.Identity?.Name} обновил группу заданий на выполнение работ: {orig.Id}," + + $" {orig.GroupName}, {orig.IsUmbrella}, {orig.ShortDescription}, {orig.FullDescription}," + + $" {orig.Solution}, {orig.TemplateDuration}, {orig.ReferenceDate}, {orig.IsAutoDistributionEnabled}" + + $", {orig.IsAgent}, {orig.AgentName}, {orig.AgentTimeOutSec}, {orig.AgentScript}"); + + + var updatedJobGroup = await groupService.Get() + .Include(t => t.Jobs) + .ThenInclude(t => t.Tnk) + .FirstAsync(t => t.Id == orig.Id); + + var response = mapper.Map(updatedJobGroup); + + return Ok(new Response(response, true)); + } + + + /// + /// Удалить группу заданий на выполнение работ (только если нет связанных шаблонов) + /// + /// + /// + [HttpDelete(ApiRoutes.JobGroup.Delete)] + public async Task Delete([FromRoute] Guid id) + { + var jobGroup = await groupService.Get() + .Include(t => t.Jobs) + .ThenInclude(t => t.Tnk) + .FirstOrDefaultAsync(t => t.Id == id); + + if (jobGroup == null) + return BadRequest(new Response(false, new List { new ErrorModel { + Message = $"Ошибка при удалении группы заданий на выполнение работ. Не найдена группа заданий на выполнение работ Id: {id}" + } })); + + if (!groupService.Delete(jobGroup) || !await groupService.CommitAsync()) + return BadRequest(new Response(false, new List { new ErrorModel { + Message = $"Ошибка при удалении группы заданий на выполнение работ" + } })); + + logger.LogInformation($"Пользователь {User.Identity?.Name} удалил задание на выполнение работ: {jobGroup.Id},{jobGroup.GroupName}," + + $" {jobGroup.IsUmbrella}, {jobGroup.ShortDescription}, {jobGroup.FullDescription}," + + $" {jobGroup.Solution}, {jobGroup.TemplateDuration}, {jobGroup.ReferenceDate}" + + $"{jobGroup.IsAutoDistributionEnabled}, {jobGroup.IsAgent}, {jobGroup.AgentName}" + + $"{jobGroup.AgentTimeOutSec}, {jobGroup.AgentScript}"); + + return NoContent(); + } + } +} diff --git a/PARR.API/MappingProfiles/DomainToResponseProfile.cs b/PARR.API/MappingProfiles/DomainToResponseProfile.cs index f605dfad..2b578b0e 100644 --- a/PARR.API/MappingProfiles/DomainToResponseProfile.cs +++ b/PARR.API/MappingProfiles/DomainToResponseProfile.cs @@ -160,8 +160,8 @@ namespace PARR.API.MappingProfiles .ForMember(d => d.FullDescription, o => o.MapFrom(s => s.Template!.Job!.Group!.FullDescription)) .ForMember(d => d.ShortDescription, o => o.MapFrom(s => s.Template!.Job!.Group!.ShortDescription.ApplyShortcode(ShortcodeEnum.EK, s.Template!.Unit!.Name))) .ForMember(d => d.Solution, o => o.MapFrom(s => s.Template!.Job!.Group!.Solution)) - //.ForMember(d => d.ResponseArea, o => o.MapFrom(s => s.Template!.Host!.ResponseArea!.Name)) - //ЗО берем у группы а не у хоста + //.ForMember(d => d.ResponseArea, o => o.MapFrom(s => s.Template!.Host!.ResponseArea!.Name)) + //ЗО берем у группы а не у хоста .ForMember(d => d.ResponseArea, o => o.MapFrom(s => s.Template!.Unit.BaseFields.ResponseArea)) .ForMember(d => d.TemplateDuration, o => o.MapFrom(s => s.Template!.Job!.Group!.TemplateDuration)) .ForMember(d => d.Initiator, o => o.MapFrom()) @@ -306,6 +306,15 @@ namespace PARR.API.MappingProfiles CreateMap(); #endregion + #region JobGroup + CreateMap() + .Include() + .ForMember(d => d.Name, o => o.MapFrom(s => s.GroupName)); + + CreateMap() + .ForMember(d => d.Jobs, o => o.MapFrom(s => s.Jobs)); + #endregion + #region JobAutoControl CreateMap() diff --git a/PARR.API/Validators/JobGroupValidator.cs b/PARR.API/Validators/JobGroupValidator.cs new file mode 100644 index 00000000..e3392787 --- /dev/null +++ b/PARR.API/Validators/JobGroupValidator.cs @@ -0,0 +1,26 @@ +using FluentValidation; +using PARR.API.Contracts.V1.Requests; + +namespace PARR.API.Validators +{ + public class JobGroupValidator : AbstractValidator + { + public JobGroupValidator() + { + RuleFor(t => t.Name) + .NotNull().NotEmpty(); + + RuleFor(t => t.ShortDescription) + .NotNull().NotEmpty(); + + RuleFor(t => t.FullDescription) + .NotNull().NotEmpty(); + + RuleFor(t => t.Solution) + .NotNull().NotEmpty(); + + RuleFor(t => t.TemplateDuration) + .NotNull().NotEmpty(); + } + } +} diff --git a/PARR.API/Validators/JobValidator.cs b/PARR.API/Validators/JobValidator.cs new file mode 100644 index 00000000..376c138c --- /dev/null +++ b/PARR.API/Validators/JobValidator.cs @@ -0,0 +1,48 @@ +using FluentValidation; +using PARR.API.Contracts.V1.Requests; +using PARR.DAL.Services.Interfaces; +using PARR.DAL.Services.Interfaces.Job; + +namespace PARR.API.Validators +{ + public class JobValidator : AbstractValidator + { + private readonly ITnkService tnkService; + private readonly IJobGroupService jobGroupService; + + public JobValidator( + ITnkService tnkService, + IJobGroupService jobGroupService + ) + { + this.tnkService = tnkService; + this.jobGroupService = jobGroupService; + + RuleFor(t => t.Name) + .NotNull().NotEmpty(); + + RuleFor(t => t.WorkName) + .NotNull().NotEmpty(); + + RuleFor(t => t.TnkId) + .MustAsync(async (entity, value, c) => await IsTnkExist(entity)) + .WithMessage("Указан несуществующий Id ТНК"); + + RuleFor(t => t.GroupId) + .MustAsync(async (entity, value, c) => await IsGroupExist(entity)) + .WithMessage("Указан несуществующий Id группы работ"); + } + + + private async Task IsGroupExist(JobRequest entity) + { + return await jobGroupService.GetAsync(entity.GroupId) != null; + } + + + private async Task IsTnkExist(JobRequest entity) + { + return await tnkService.GetAsync(entity.TnkId) != null; + } + } +} diff --git a/PARR.DAL/Models/Job/Job.cs b/PARR.DAL/Models/Job/Job.cs index 6ddc55e6..4868be28 100644 --- a/PARR.DAL/Models/Job/Job.cs +++ b/PARR.DAL/Models/Job/Job.cs @@ -47,14 +47,16 @@ namespace PARR.DAL.Models.Job public Guid GroupId { get; set; } - + [ForeignKey(nameof(GroupId))] public JobGroup? Group { get; set; } [ForeignKey(nameof(TnkId))] public Tnk? Tnk { get; set; } - - public ICollection UnitFilters { get; set; }= new HashSet(); + + public ICollection UnitFilters { get; set; } = new HashSet(); + + public ICollection