diff --git a/PARR.Core/Common/Helpers/JobGroupUnitFilterSynchronizeHelper.cs b/PARR.Core/Common/Helpers/JobGroupUnitFilterSynchronizeHelper.cs
index 290dff6f..9beadac2 100644
--- a/PARR.Core/Common/Helpers/JobGroupUnitFilterSynchronizeHelper.cs
+++ b/PARR.Core/Common/Helpers/JobGroupUnitFilterSynchronizeHelper.cs
@@ -3,9 +3,20 @@ using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
namespace PARR.Core.Common.Helpers;
+///
+/// Сервис для синхронизации свойств Job внутри JobGroup.
+/// Эталоном считается Job с максимальным MaxValueRelationships.
+/// Синхронизируются: WorkName, маски, IsParentRelationships, AutoControl, UnitFilters.
+/// НЕ синхронизируются: Id, Name, MinValueRelationships, MaxValueRelationships, TnkId.
+///
public class JobGroupUnitFilterSynchronizeHelper
{
private readonly IJobRepository jobRepository;
@@ -25,12 +36,13 @@ public class JobGroupUnitFilterSynchronizeHelper
.ThenInclude(uf => uf.FieldFilters)
.Include(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters)
+ .Include(j => j.AutoControl)
.Where(j => j.GroupId == jobGroupId)
.ToListAsync(ct);
if (jobs == null || jobs.Count < 2)
{
- logger.LogDebug("JobGroup {JobGroupId} содержит менее двух Job. Синхронизация фильтров не требуется.", jobGroupId);
+ logger.LogDebug("JobGroup {JobGroupId} содержит менее двух Job. Синхронизация не требуется.", jobGroupId);
return;
}
@@ -41,7 +53,7 @@ public class JobGroupUnitFilterSynchronizeHelper
return;
}
- logger.LogInformation("Начало синхронизации фильтров для JobGroup {JobGroupId}. Эталонный Job: {ReferenceJobId}.", jobGroupId, referenceJob.Id);
+ logger.LogInformation("Начало синхронизации для JobGroup {JobGroupId}. Эталонный Job: {ReferenceJobId}.", jobGroupId, referenceJob.Id);
foreach (var job in jobs)
{
@@ -49,31 +61,84 @@ public class JobGroupUnitFilterSynchronizeHelper
if (job.Id == referenceJob.Id) continue;
- if (!AreUnitFiltersIdentical(job.UnitFilters, referenceJob.UnitFilters))
+ if (!AreJobsIdentical(job, referenceJob))
{
- logger.LogDebug("Фильтры Job {JobId} отличаются от эталона. Запуск обновления.", job.Id);
- await ApplyFilterCorrectionAsync(job.Id, referenceJob.UnitFilters ?? Enumerable.Empty(), initiator, ct);
+ logger.LogDebug("Свойства Job {JobId} отличаются от эталона. Запуск обновления.", job.Id);
+ await ApplyJobCorrectionAsync(job.Id, referenceJob, initiator, ct);
}
else
{
- logger.LogDebug("Фильтры Job {JobId} идентичны эталону. Пропуск.", job.Id);
+ logger.LogDebug("Свойства Job {JobId} идентичны эталону. Пропуск.", job.Id);
}
}
- logger.LogInformation("Синхронизация фильтров для JobGroup {JobGroupId} завершена.", jobGroupId);
+ logger.LogInformation("Синхронизация для JobGroup {JobGroupId} завершена.", jobGroupId);
}
- private async Task ApplyFilterCorrectionAsync(Guid jobId, IEnumerable referenceFilters, HistoryInitiator initiator, CancellationToken ct)
+ private bool AreJobsIdentical(Job current, Job reference)
+ {
+ // Сравнение скалярных полей (кроме Id, Name, Min/MaxValueRelationships, TnkId)
+ if (current.WorkName != reference.WorkName) return false;
+ if (current.IsParentRelationships != reference.IsParentRelationships) return false;
+ if (current.TemplateNameMask != reference.TemplateNameMask) return false;
+ if (current.WorkGroupMask != reference.WorkGroupMask) return false;
+ if (current.ResponseAreaMask != reference.ResponseAreaMask) return false;
+
+ // Сравнение AutoControl
+ if (current.AutoControl == null && reference.AutoControl != null) return false;
+ if (current.AutoControl != null && reference.AutoControl == null) return false;
+ if (current.AutoControl != null && reference.AutoControl != null)
+ {
+ if (current.AutoControl.InitUsedTemplateState != reference.AutoControl.InitUsedTemplateState) return false;
+ if (current.AutoControl.InitUsedScheduleState != reference.AutoControl.InitUsedScheduleState) return false;
+ }
+
+ // Сравнение фильтров
+ if (!AreUnitFiltersIdentical(current.UnitFilters, reference.UnitFilters)) return false;
+
+ return true;
+ }
+
+ private async Task ApplyJobCorrectionAsync(Guid jobId, Job referenceJob, 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)
+ .Include(j => j.AutoControl)
.FirstOrDefaultAsync(j => j.Id == jobId, ct);
if (job == null) return;
+ // 1. Обновляем скалярные поля
+ job.WorkName = referenceJob.WorkName;
+ job.IsParentRelationships = referenceJob.IsParentRelationships;
+ job.TemplateNameMask = referenceJob.TemplateNameMask;
+ job.WorkGroupMask = referenceJob.WorkGroupMask;
+ job.ResponseAreaMask = referenceJob.ResponseAreaMask;
+ job.DateModified = DateTimeOffset.UtcNow;
+
+ // 2. Обновляем AutoControl
+ if (referenceJob.AutoControl != null)
+ {
+ if (job.AutoControl == null)
+ {
+ job.AutoControl = new JobAutoControl
+ {
+ JobId = job.Id,
+ InitUsedTemplateState = referenceJob.AutoControl.InitUsedTemplateState,
+ InitUsedScheduleState = referenceJob.AutoControl.InitUsedScheduleState
+ };
+ }
+ else
+ {
+ job.AutoControl.InitUsedTemplateState = referenceJob.AutoControl.InitUsedTemplateState;
+ job.AutoControl.InitUsedScheduleState = referenceJob.AutoControl.InitUsedScheduleState;
+ }
+ }
+
+ // 3. Обновляем UnitFilters
if (job.UnitFilters == null)
{
job.UnitFilters = new List();
@@ -83,14 +148,14 @@ public class JobGroupUnitFilterSynchronizeHelper
job.UnitFilters.Clear();
}
- var clonedFilters = CloneFilters(referenceFilters, job.Id);
+ var clonedFilters = CloneFilters(referenceJob.UnitFilters ?? Enumerable.Empty(), job.Id);
foreach (var filter in clonedFilters)
{
job.UnitFilters!.Add(filter);
}
await jobRepository.CommitAsync(initiator);
- logger.LogInformation("Фильтры Job {JobId} успешно обновлены.", jobId);
+ logger.LogInformation("Job {JobId} успешно синхронизирован с эталоном.", jobId);
}
private bool AreUnitFiltersIdentical(IEnumerable? current, IEnumerable? reference)
diff --git a/PARR.Core/Repositories/Interfaces/Unit/IUnitFieldRepository.cs b/PARR.Core/Repositories/Interfaces/Unit/IUnitFieldRepository.cs
index 2a9c0cf3..85050679 100644
--- a/PARR.Core/Repositories/Interfaces/Unit/IUnitFieldRepository.cs
+++ b/PARR.Core/Repositories/Interfaces/Unit/IUnitFieldRepository.cs
@@ -5,6 +5,7 @@ namespace PARR.Core.Repositories.Interfaces.Unit
{
public interface IUnitFieldRepository : IBaseRepository
{
- Task GetByAihitNameAsync(string name);
+ Task GetByCodeAsync(string code, CancellationToken ct = default);
+ Task GetByAihitNameAsync(string aihitName, CancellationToken ct = default);
}
}
diff --git a/PARR.DAL/Repositories/Unit/UnitFieldRepository.cs b/PARR.DAL/Repositories/Unit/UnitFieldRepository.cs
index 814136a8..03be73a1 100644
--- a/PARR.DAL/Repositories/Unit/UnitFieldRepository.cs
+++ b/PARR.DAL/Repositories/Unit/UnitFieldRepository.cs
@@ -10,10 +10,24 @@ namespace PARR.DAL.Repositories.Unit
internal class UnitFieldRepository : BaseRepository, IUnitFieldRepository
{
public UnitFieldRepository(DataContext dataContext, ILogger logger) : base(logger, dataContext) { }
-
- public async Task GetByAihitNameAsync(string name)
+ public async Task GetByCodeAsync(string code, CancellationToken ct = default)
{
- return await EntitySet.FirstOrDefaultAsync(uf => uf.AihitName.ToLower().Trim() == name.ToLower().Trim());
+ if (string.IsNullOrWhiteSpace(code))
+ return null;
+
+ return await Get()
+ .AsNoTracking()
+ .FirstOrDefaultAsync(f => f.Code != null && EF.Functions.ILike(f.Code, code), ct);
+ }
+
+ public async Task GetByAihitNameAsync(string aihitName, CancellationToken ct = default)
+ {
+ if (string.IsNullOrWhiteSpace(aihitName))
+ return null;
+
+ return await Get()
+ .AsNoTracking()
+ .FirstOrDefaultAsync(f => EF.Functions.ILike(f.AihitName, aihitName), ct);
}
}
}
diff --git a/PARR.Domain/Constants/UnitFieldCodes.cs b/PARR.Domain/Constants/UnitFieldCodes.cs
new file mode 100644
index 00000000..29a883b2
--- /dev/null
+++ b/PARR.Domain/Constants/UnitFieldCodes.cs
@@ -0,0 +1,15 @@
+namespace PARR.Domain.Constants
+{
+ public class UnitFieldCodes
+ {
+ public const string IP = "ip";
+ public const string IsActive = "isActive";
+ public const string ResponseArea = "responseArea";
+ public const string WorkGroupResponseArea = "workGroupResponseArea";
+ public const string OS = "os";
+ public const string Responsible = "responsible";
+ public const string Tag = "tag";
+ public const string WorkGroup = "workGroup";
+ public const string EkStatus = "ekStatus";
+ }
+}
diff --git a/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateBuilder.cs b/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateBuilder.cs
index 32a869fc..fb1456fb 100644
--- a/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateBuilder.cs
+++ b/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateBuilder.cs
@@ -1,6 +1,9 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces.Unit;
+using PARR.Core.Services.Shortcodes;
+using PARR.Domain.Constants;
+using PARR.Domain.Entities;
using PARR.Domain.Entities.Job;
using PARR.TemplateMatcher.Models;
using PARR.TemplateMatcher.Services.Interfaces;
@@ -12,42 +15,44 @@ internal class GroupedTemplateBuilder : IGroupedTemplateBuilder
private readonly ILogger logger;
private readonly IUnitInValueRepository unitInValueRepository;
private readonly IUnitFieldRepository unitFieldRepository;
+ private readonly IShortcodesService shortcodesService;
public GroupedTemplateBuilder(
ILogger logger,
IUnitInValueRepository unitInValueRepository,
- IUnitFieldRepository unitFieldRepository)
+ IUnitFieldRepository unitFieldRepository,
+ IShortcodesService shortcodesService
+ )
{
this.logger = logger;
this.unitInValueRepository = unitInValueRepository;
this.unitFieldRepository = unitFieldRepository;
+ this.shortcodesService = shortcodesService;
}
public async Task> BuildAsync(
- Dictionary> initialReverseMapping,
- JobGroup jobGroup,
- Job maxJob,
- CancellationToken ct = default)
+ Dictionary> initialReverseMapping,
+ JobGroup jobGroup,
+ Job maxJob,
+ CancellationToken ct = default)
{
logger.LogDebug("Начало построения структуры групп для JobGroup {JobGroupId}.", jobGroup.Id);
if (!initialReverseMapping.Any())
return new List();
- // 1. Определяем поле для внутренней группировки
- var isGroupByResponsible = jobGroup.IsGroupByResponsible == true;
- var innerGroupingFieldName = isGroupByResponsible ? "ОТВЕТСТВЕННЫЙ_ЗА_ЭК" : "РАБОЧАЯ_ГР_ОТВ_ЗА_ЭК";
+ // 1. Определяем стратегию внутренней группировки
+ // Если IsGroupByResponsible != true, используем WorkGroupMask через ShortcodesService
+ bool useWorkGroupMask = jobGroup.IsGroupByResponsible != true;
- var innerGroupingField = await unitFieldRepository.GetByAihitNameAsync(innerGroupingFieldName)
- ?? throw new InvalidOperationException($"Поле '{innerGroupingFieldName}' не найдено.");
-
- var innerGroupingFieldId = innerGroupingField.Id;
- var groupingFieldId = jobGroup.GroupingUnitFieldId!.Value;
+ logger.LogDebug("Стратегия внутренней группировки: {Strategy}",
+ useWorkGroupMask ? "WorkGroupMask" : "Поле 'Ответственный за ЭК'");
// 2. Собираем все исходные UnitId
var allSourceUnitIds = initialReverseMapping.Values.SelectMany(ids => ids).Distinct().ToList();
- // 3. Загружаем UnitInValue для трансформации
+ // 3. Загружаем UnitInValue для трансформации по полю группировки из настроек JobGroup
+ var groupingFieldId = jobGroup.GroupingUnitFieldId!.Value;
var relevantUnitInValues = await unitInValueRepository.Get()
.AsNoTracking()
.Where(uiv => allSourceUnitIds.Contains(uiv.UnitId) && uiv.FieldId == groupingFieldId)
@@ -58,9 +63,7 @@ internal class GroupedTemplateBuilder : IGroupedTemplateBuilder
var uivLookup = relevantUnitInValues
.GroupBy(x => x.UnitId)
- .ToDictionary(
- g => g.Key,
- g => g.Select(x => x.ValueId).ToList());
+ .ToDictionary(g => g.Key, g => g.Select(x => x.ValueId).ToList());
// 4. Трансформируем в reverseMapping с парами
var reverseMapping = new Dictionary>();
@@ -83,7 +86,7 @@ internal class GroupedTemplateBuilder : IGroupedTemplateBuilder
var uniqueEntries = entries
.GroupBy(e => (e.UnitId, e.UnitFieldValueId))
.Select(g => g.First())
- .OrderBy(e => e.UnitId) // ИСПРАВЛЕНО: стабильная сортировка
+ .OrderBy(e => e.UnitId)
.ThenBy(e => e.UnitFieldValueId)
.ToList();
@@ -94,18 +97,62 @@ internal class GroupedTemplateBuilder : IGroupedTemplateBuilder
if (!reverseMapping.Any())
return new List();
- // 5. Загружаем значения для внутренней группировки
- var allUnitsInTemplatePairs = reverseMapping.Values.SelectMany(list => list).ToList();
- var allUnitIdsForInnerGrouping = allUnitsInTemplatePairs.Select(e => e.UnitId).Distinct().ToList();
+ // 5. Определяем значения для внутренней группировки
+ Dictionary unitIdToGroupingValueMap;
- var innerGroupingValues = await unitInValueRepository.GetByUnitIdsAndFieldIdsAsync(
- allUnitIdsForInnerGrouping,
- new HashSet { innerGroupingFieldId },
- ct);
+ if (useWorkGroupMask)
+ {
+ // Используем ShortcodesService для получения финальных значений WorkGroupMask
+ unitIdToGroupingValueMap = new Dictionary();
- var unitIdToInnerGroupingValueMap = innerGroupingValues
- .Where(uv => uv.Value != null && !string.IsNullOrEmpty(uv.Value.Value))
- .ToDictionary(uv => uv.UnitId, uv => uv.Value!.Value);
+ foreach (var potentialUnitId in reverseMapping.Keys)
+ {
+ var relatedUnitIds = reverseMapping[potentialUnitId].Select(e => e.UnitId).Distinct().ToList();
+
+ foreach (var relatedUnitId in relatedUnitIds)
+ {
+ // Создаем временный Template для применения шорткодов
+ var tempTemplate = new Template
+ {
+ Id = Guid.NewGuid(),
+ Name = "temp",
+ JobId = maxJob.Id,
+ UnitId = potentialUnitId, // Родительский юнит шаблона
+ Job = maxJob,
+ UnitsInTemplate = new List
+ {
+ new UnitsInTemplate { UnitId = relatedUnitId, UnitFieldValueId = Guid.Empty }
+ }
+ };
+
+ var workGroupValue = await shortcodesService.ApplyShortcodesAsync(
+ maxJob.WorkGroupMask,
+ tempTemplate,
+ nameof(GroupedTemplateBuilder));
+
+ unitIdToGroupingValueMap[relatedUnitId] = workGroupValue;
+ }
+ }
+ }
+ else
+ {
+ // Логика разделения по ответственному за ЭК через служебный код поля
+ var innerGroupingField = await unitFieldRepository.GetByCodeAsync(UnitFieldCodes.Responsible, ct)
+ ?? throw new InvalidOperationException($"Поле с кодом '{UnitFieldCodes.Responsible}' не найдено в справочнике UnitField.");
+
+ var innerGroupingFieldId = innerGroupingField.Id;
+ var allUnitsInTemplatePairs = reverseMapping.Values.SelectMany(list => list).ToList();
+ var allUnitIdsForInnerGrouping = allUnitsInTemplatePairs.Select(e => e.UnitId).Distinct().ToList();
+
+ var innerGroupingValues = await unitInValueRepository.GetByUnitIdsAndFieldIdsAsync(
+ allUnitIdsForInnerGrouping,
+ new HashSet { innerGroupingFieldId },
+ ct);
+
+ unitIdToGroupingValueMap = innerGroupingValues
+ .Where(uv => uv.Value != null && uv.Value.Value != null && !string.IsNullOrEmpty(uv.Value.Value))
+ .ToDictionary(uv => uv.UnitId, uv => uv.Value!.Value!);
+ }
// 6. Формируем итоговую структуру
var templateGroups = new List();
@@ -117,7 +164,7 @@ internal class GroupedTemplateBuilder : IGroupedTemplateBuilder
var unitsInTemplateForThisPotentialUnitId = kvp.Value;
var innerGroupedUnits = unitsInTemplateForThisPotentialUnitId
- .GroupBy(entry => unitIdToInnerGroupingValueMap.GetValueOrDefault(entry.UnitId, "Нет данных"))
+ .GroupBy(entry => unitIdToGroupingValueMap.GetValueOrDefault(entry.UnitId, "Нет данных"))
.OrderBy(g => g.Key, StringComparer.Ordinal)
.ToList();