diff --git a/PARR.Core/Common/Helpers/JobGroupCloneHelper.cs b/PARR.Core/Common/Helpers/JobGroupCloneHelper.cs
new file mode 100644
index 00000000..8098d222
--- /dev/null
+++ b/PARR.Core/Common/Helpers/JobGroupCloneHelper.cs
@@ -0,0 +1,182 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using PARR.Core.Repositories.Interfaces.Job;
+using PARR.Domain.Entities.Base.History;
+using PARR.Domain.Entities.Job;
+using PARR.Domain.Entities.Schedule;
+
+namespace PARR.Core.Common.Helpers;
+
+///
+/// Сервис для полного клонирования JobGroup со всеми вложенными Job, фильтрами, AutoControl и EsppSchValues.
+/// Шаблоны (Template) не клонируются. Идентификаторы генерируются стратегией EF Core.
+///
+public class JobGroupCloneHelper
+{
+ private readonly IJobGroupRepository groupRepository;
+ private readonly IJobRepository jobRepository;
+ private readonly ILogger logger;
+
+ public JobGroupCloneHelper(
+ IJobGroupRepository groupRepository,
+ IJobRepository jobRepository,
+ ILogger logger)
+ {
+ this.groupRepository = groupRepository;
+ this.jobRepository = jobRepository;
+ this.logger = logger;
+ }
+
+ public async Task CloneAsync(
+ Guid sourceJobGroupId,
+ string? newGroupName,
+ HistoryInitiator initiator,
+ CancellationToken ct = default)
+ {
+ var sourceGroup = await groupRepository.Get()
+ .AsNoTracking()
+ .Include(g => g.Jobs)
+ .ThenInclude(j => j.UnitFilters)
+ .ThenInclude(uf => uf.FieldFilters)
+ .Include(g => g.Jobs)
+ .ThenInclude(j => j.UnitFilters)
+ .ThenInclude(uf => uf.RelationshipFilters)
+ .Include(g => g.Jobs)
+ .ThenInclude(j => j.AutoControl)
+ .Include(g => g.EsppSchValues)
+ .FirstOrDefaultAsync(g => g.Id == sourceJobGroupId, ct);
+
+ if (sourceGroup == null)
+ {
+ throw new InvalidOperationException($"JobGroup {sourceJobGroupId} not found.");
+ }
+
+ logger.LogInformation("Начало клонирования JobGroup {JobGroupId}.", sourceJobGroupId);
+
+ var newGroup = MapJobGroup(sourceGroup, newGroupName);
+
+ foreach (var sourceJob in sourceGroup.Jobs ?? Enumerable.Empty())
+ {
+ var newJob = MapJob(sourceJob);
+ newGroup.Jobs.Add(newJob);
+
+ CloneJobFilters(sourceJob.UnitFilters, newJob);
+
+ if (sourceJob.AutoControl != null)
+ {
+ newJob.AutoControl = MapAutoControl(sourceJob.AutoControl);
+ }
+ }
+
+ // Клонирование значений расписания
+ foreach (var sourceSchValue in sourceGroup.EsppSchValues ?? Enumerable.Empty())
+ {
+ newGroup.EsppSchValues.Add(new EsppSchValue
+ {
+ TypeValueId = sourceSchValue.TypeValueId,
+ TypeConfigId = sourceSchValue.TypeConfigId
+ });
+ }
+
+ var isSuccess = await groupRepository.CreateAsync(newGroup);
+ if (!isSuccess)
+ {
+ throw new InvalidOperationException("Не удалось сохранить клон JobGroup в базу данных.");
+ }
+
+ await groupRepository.CommitAsync(initiator);
+
+ logger.LogInformation("JobGroup успешно клонирована. Новый ID: {NewGroupId}.", newGroup.Id);
+
+ return newGroup.Id;
+ }
+
+ private JobGroup MapJobGroup(JobGroup source, string? newName)
+ {
+ string finalName = string.IsNullOrWhiteSpace(newName)
+ ? $"{source.GroupName?.TrimEnd()} КЛОН"
+ : newName.Trim();
+
+ return new JobGroup
+ {
+ GroupName = finalName,
+ ShortDescription = source.ShortDescription,
+ FullDescription = source.FullDescription,
+ Solution = source.Solution,
+ TemplateDuration = source.TemplateDuration,
+ ReferenceDate = source.ReferenceDate,
+ UserTimeZoneOffsetMinutes = source.UserTimeZoneOffsetMinutes,
+ IsResponseAreaTimezone = source.IsResponseAreaTimezone,
+ IsAutoDistributionEnabled = source.IsAutoDistributionEnabled,
+ IsAgent = source.IsAgent,
+ AgentName = source.AgentName,
+ AgentTimeOutSec = source.AgentTimeOutSec,
+ AgentScript = source.AgentScript,
+ GroupTypeId = source.GroupTypeId,
+ GroupingUnitFieldId = source.GroupingUnitFieldId,
+ IsGroupByResponsible = source.IsGroupByResponsible,
+ ScheduleExcludeTypeId = source.ScheduleExcludeTypeId,
+ ScheduleExcludeTypeCalendarId = source.ScheduleExcludeTypeCalendarId,
+ DateCreated = DateTimeOffset.UtcNow,
+ Jobs = new HashSet(),
+ EsppSchValues = new HashSet()
+ };
+ }
+
+ private Job MapJob(Job source)
+ {
+ return new Job
+ {
+ Name = source.Name,
+ WorkName = source.WorkName,
+ MinValueRelationships = source.MinValueRelationships,
+ MaxValueRelationships = source.MaxValueRelationships,
+ IsParentRelationships = source.IsParentRelationships,
+ TemplateNameMask = source.TemplateNameMask,
+ WorkGroupMask = source.WorkGroupMask,
+ ResponseAreaMask = source.ResponseAreaMask,
+ TnkId = source.TnkId,
+ DateCreated = DateTimeOffset.UtcNow,
+ UnitFilters = new HashSet()
+ };
+ }
+
+ private JobAutoControl MapAutoControl(JobAutoControl source)
+ {
+ return new JobAutoControl
+ {
+ InitUsedTemplateState = source.InitUsedTemplateState,
+ InitUsedScheduleState = source.InitUsedScheduleState
+ };
+ }
+
+ private void CloneJobFilters(IEnumerable? sourceFilters, Job targetJob)
+ {
+ if (sourceFilters == null) return;
+
+ foreach (var sourceFilter in sourceFilters)
+ {
+ var newFilter = new JobUnitFilter
+ {
+ UnitFilter = sourceFilter.UnitFilter,
+ DateCreated = DateTimeOffset.UtcNow,
+ FieldFilters = sourceFilter.FieldFilters?.Select(f => new JobFieldFilter
+ {
+ FieldId = f.FieldId,
+ ValueMask = f.ValueMask,
+ IsInverse = f.IsInverse,
+ DateCreated = DateTimeOffset.UtcNow
+ }).ToList() ?? new List(),
+ RelationshipFilters = sourceFilter.RelationshipFilters?.Select(r => new JobRelationshipFilter
+ {
+ FieldId = r.FieldId,
+ ValueMask = r.ValueMask,
+ IsInverse = r.IsInverse,
+ IsParent = r.IsParent,
+ IsFullMatch = r.IsFullMatch
+ }).ToList() ?? new List()
+ };
+ targetJob.UnitFilters.Add(newFilter);
+ }
+ }
+}
\ No newline at end of file
diff --git a/PARR.Core/Common/Helpers/JobGroupUnitFilterSynchronizeHelper.cs b/PARR.Core/Common/Helpers/JobGroupUnitFilterSynchronizeHelper.cs
new file mode 100644
index 00000000..290dff6f
--- /dev/null
+++ b/PARR.Core/Common/Helpers/JobGroupUnitFilterSynchronizeHelper.cs
@@ -0,0 +1,188 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using PARR.Core.Repositories.Interfaces.Job;
+using PARR.Domain.Entities.Base.History;
+using PARR.Domain.Entities.Job;
+
+namespace PARR.Core.Common.Helpers;
+
+public class JobGroupUnitFilterSynchronizeHelper
+{
+ private readonly IJobRepository jobRepository;
+ private readonly ILogger logger;
+
+ public JobGroupUnitFilterSynchronizeHelper(IJobRepository jobRepository, ILogger logger)
+ {
+ this.jobRepository = jobRepository;
+ this.logger = logger;
+ }
+
+ public async Task SynchronizeGroupFiltersAsync(Guid jobGroupId, HistoryInitiator initiator, CancellationToken ct = default)
+ {
+ var jobs = await jobRepository.Get()
+ .AsNoTracking()
+ .Include(j => j.UnitFilters)
+ .ThenInclude(uf => uf.FieldFilters)
+ .Include(j => j.UnitFilters)
+ .ThenInclude(uf => uf.RelationshipFilters)
+ .Where(j => j.GroupId == jobGroupId)
+ .ToListAsync(ct);
+
+ if (jobs == null || jobs.Count < 2)
+ {
+ logger.LogDebug("JobGroup {JobGroupId} содержит менее двух Job. Синхронизация фильтров не требуется.", jobGroupId);
+ return;
+ }
+
+ var referenceJob = jobs.OrderByDescending(j => j.MaxValueRelationships).FirstOrDefault();
+ if (referenceJob == null)
+ {
+ logger.LogWarning("Не удалось определить эталонный Job для JobGroup {JobGroupId}.", jobGroupId);
+ return;
+ }
+
+ logger.LogInformation("Начало синхронизации фильтров для JobGroup {JobGroupId}. Эталонный Job: {ReferenceJobId}.", jobGroupId, referenceJob.Id);
+
+ foreach (var job in jobs)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (job.Id == referenceJob.Id) continue;
+
+ if (!AreUnitFiltersIdentical(job.UnitFilters, referenceJob.UnitFilters))
+ {
+ logger.LogDebug("Фильтры Job {JobId} отличаются от эталона. Запуск обновления.", job.Id);
+ await ApplyFilterCorrectionAsync(job.Id, referenceJob.UnitFilters ?? Enumerable.Empty(), initiator, ct);
+ }
+ else
+ {
+ logger.LogDebug("Фильтры Job {JobId} идентичны эталону. Пропуск.", job.Id);
+ }
+ }
+
+ logger.LogInformation("Синхронизация фильтров для JobGroup {JobGroupId} завершена.", jobGroupId);
+ }
+
+ private async Task ApplyFilterCorrectionAsync(Guid jobId, IEnumerable referenceFilters, HistoryInitiator initiator, CancellationToken ct)
+ {
+ var job = await jobRepository.Get()
+ .Include(j => j.UnitFilters)
+ .ThenInclude(uf => uf.FieldFilters)
+ .Include(j => j.UnitFilters)
+ .ThenInclude(uf => uf.RelationshipFilters)
+ .FirstOrDefaultAsync(j => j.Id == jobId, ct);
+
+ if (job == null) return;
+
+ if (job.UnitFilters == null)
+ {
+ job.UnitFilters = new List();
+ }
+ else
+ {
+ job.UnitFilters.Clear();
+ }
+
+ var clonedFilters = CloneFilters(referenceFilters, job.Id);
+ foreach (var filter in clonedFilters)
+ {
+ job.UnitFilters!.Add(filter);
+ }
+
+ await jobRepository.CommitAsync(initiator);
+ logger.LogInformation("Фильтры Job {JobId} успешно обновлены.", jobId);
+ }
+
+ private bool AreUnitFiltersIdentical(IEnumerable? current, IEnumerable? reference)
+ {
+ var currentList = (current ?? Enumerable.Empty()).ToList();
+ var refList = (reference ?? Enumerable.Empty()).ToList();
+
+ if (currentList.Count != refList.Count) return false;
+
+ for (int i = 0; i < currentList.Count; i++)
+ {
+ if (!AreFiltersEqual(currentList[i], refList[i]))
+ return false;
+ }
+ return true;
+ }
+
+ private bool AreFiltersEqual(JobUnitFilter a, JobUnitFilter b)
+ {
+ if (!AreFieldFiltersEqual(a.FieldFilters, b.FieldFilters)) return false;
+ if (!AreRelationshipFiltersEqual(a.RelationshipFilters, b.RelationshipFilters)) return false;
+ return true;
+ }
+
+ private bool AreFieldFiltersEqual(IEnumerable? a, IEnumerable? b)
+ {
+ var listA = (a ?? Enumerable.Empty()).OrderBy(f => f.FieldId).ToList();
+ var listB = (b ?? Enumerable.Empty()).OrderBy(f => f.FieldId).ToList();
+
+ if (listA.Count != listB.Count) return false;
+
+ for (int i = 0; i < listA.Count; i++)
+ {
+ if (listA[i].FieldId != listB[i].FieldId ||
+ !string.Equals(listA[i].ValueMask, listB[i].ValueMask, StringComparison.OrdinalIgnoreCase) ||
+ listA[i].IsInverse != listB[i].IsInverse)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private bool AreRelationshipFiltersEqual(IEnumerable? a, IEnumerable? b)
+ {
+ var listA = (a ?? Enumerable.Empty()).OrderBy(f => f.FieldId).ToList();
+ var listB = (b ?? Enumerable.Empty()).OrderBy(f => f.FieldId).ToList();
+
+ if (listA.Count != listB.Count) return false;
+
+ for (int i = 0; i < listA.Count; i++)
+ {
+ if (listA[i].FieldId != listB[i].FieldId ||
+ !string.Equals(listA[i].ValueMask, listB[i].ValueMask, StringComparison.OrdinalIgnoreCase) ||
+ listA[i].IsInverse != listB[i].IsInverse ||
+ listA[i].IsParent != listB[i].IsParent ||
+ listA[i].IsFullMatch != listB[i].IsFullMatch)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private List CloneFilters(IEnumerable source, Guid targetJobId)
+ {
+ var result = new List();
+ foreach (var src in source ?? Enumerable.Empty())
+ {
+ var newFilter = new JobUnitFilter
+ {
+ JobId = targetJobId,
+ UnitFilter = src.UnitFilter!,
+ DateCreated = DateTimeOffset.UtcNow,
+ FieldFilters = src.FieldFilters?.Select(f => new JobFieldFilter
+ {
+ FieldId = f.FieldId,
+ ValueMask = f.ValueMask,
+ IsInverse = f.IsInverse,
+ DateCreated = DateTimeOffset.UtcNow
+ }).ToList() ?? new List(),
+ RelationshipFilters = src.RelationshipFilters?.Select(r => new JobRelationshipFilter
+ {
+ FieldId = r.FieldId,
+ ValueMask = r.ValueMask,
+ IsInverse = r.IsInverse,
+ IsParent = r.IsParent,
+ IsFullMatch = r.IsFullMatch
+ }).ToList() ?? new List()
+ };
+ result.Add(newFilter);
+ }
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/PARR.Core/DependencyInjection.cs b/PARR.Core/DependencyInjection.cs
index 078c0ea2..de21bc6f 100644
--- a/PARR.Core/DependencyInjection.cs
+++ b/PARR.Core/DependencyInjection.cs
@@ -1,6 +1,7 @@
using FluentValidation;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using PARR.Core.Common.Helpers;
using PARR.Core.Common.Implementations;
using PARR.Core.Common.Interfaces;
using PARR.Core.Services.MatchingStatusService;
@@ -49,6 +50,13 @@ namespace PARR.Core
services.AddTransient();
+ #region Helpers
+
+ services.AddScoped();
+ services.AddScoped();
+
+ #endregion
+
#endregion
#region Task
diff --git a/PARR.Core/Repositories/Base/IBaseRepository.cs b/PARR.Core/Repositories/Base/IBaseRepository.cs
index 74cfce0c..1c1beb04 100644
--- a/PARR.Core/Repositories/Base/IBaseRepository.cs
+++ b/PARR.Core/Repositories/Base/IBaseRepository.cs
@@ -20,7 +20,6 @@ namespace PARR.Core.Repositories.Base
bool Delete(T obj);
- //Task CommitAsync();
Task CommitAsync(IHistoryInitiator? initiator = null);
}
}
diff --git a/PARR.Core/Services/UnitFilterService/UnitFilterService.cs b/PARR.Core/Services/UnitFilterService/UnitFilterService.cs
index e12d2cca..ee02368a 100644
--- a/PARR.Core/Services/UnitFilterService/UnitFilterService.cs
+++ b/PARR.Core/Services/UnitFilterService/UnitFilterService.cs
@@ -16,7 +16,7 @@ namespace PARR.Core.Services.UnitFilterService;
internal class UnitFilterService : IUnitFilterService
{
#if DEBUG
- private readonly Guid debugTargetUnitId = Guid.Parse("f2017292-193c-48e9-b333-3a00e737c6fc");
+ private readonly Guid debugTargetUnitId = Guid.Parse("3494fbec-adb9-4167-8619-b2a8b6b6e36a");
#endif
private const int DebugMaxUnitsToLog = 10;
diff --git a/PARR.Domain/Entities/Schedule/EsppSchValue.cs b/PARR.Domain/Entities/Schedule/EsppSchValue.cs
index 7a9af3f8..1ddfc3ff 100644
--- a/PARR.Domain/Entities/Schedule/EsppSchValue.cs
+++ b/PARR.Domain/Entities/Schedule/EsppSchValue.cs
@@ -6,26 +6,18 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace PARR.Domain.Entities.Schedule
{
///
- /// Расписание ЕСПП: значения заданий для ApplicationsInWorks
+ /// Расписание ЕСПП: значения заданий для JobGroup
///
[Table("EsppSchValues", Schema = DatabaseSchemas.Schedule)]
- //[Index(nameof(ApplicationsInWorkId), nameof(TypeValueId), nameof(TypeConfigId), IsUnique = true)]
- //[PrimaryKey(nameof(ApplicationsInWorkId), nameof(TypeValueId), nameof(TypeConfigId))]
[PrimaryKey(nameof(JobGroupId), nameof(TypeValueId), nameof(TypeConfigId))]
public class EsppSchValue
{
- //public Guid ApplicationsInWorkId { get; set; }
-
public Guid TypeValueId { get; set; }
public Guid TypeConfigId { get; set; }
public Guid JobGroupId { get; set; }
-
- //[ForeignKey(nameof(ApplicationsInWorkId))]
- //public ApplicationsInWork? ApplicationsInWork { get; set; }
-
[ForeignKey(nameof(TypeValueId))]
public EsppSchTypeValue? EsppSchTypeValue { get; set; }