Files
OnboardXAdmin/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/OnboardingContractTemplateService.cs
T
2026-09-10 16:43:11 +08:00

671 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace SimpleAdmin.Application;
/// <summary>
/// 合同模板主档案;具体 DOCX 文件、变量与适用范围均挂在此档案下。
/// </summary>
public class OnboardingContractTemplateService : DbRepository<ObContractTemplate>, IOnboardingContractTemplateService
{
private readonly IFileService _fileService;
private readonly IOnboardingDataPermissionService _dataPermission;
public OnboardingContractTemplateService(
IFileService fileService,
IOnboardingDataPermissionService dataPermission)
{
_fileService = fileService;
_dataPermission = dataPermission;
}
/// <summary>
/// 分页查询合同模板主档案。
/// </summary>
public async Task<SqlSugarPagedList<ObContractTemplate>> Page(OnboardingContractTemplatePageInput input)
{
var query = Context.Queryable<ObContractTemplate>()
.WhereIF(input.CompanyId.HasValue, item => item.CompanyId == input.CompanyId.Value)
.WhereIF(input.DepartmentId.HasValue, item => item.DepartmentId == input.DepartmentId.Value)
.WhereIF(
!string.IsNullOrWhiteSpace(input.TemplateName),
it => it.TemplateName.Contains(input.TemplateName) || it.TemplateCode.Contains(input.TemplateName))
.WhereIF(!string.IsNullOrWhiteSpace(input.ContractType), it => it.ContractType == input.ContractType)
.WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status);
if (input.PositionId.HasValue)
{
var templateIds = await Context.Queryable<ObContractTemplatePosition>()
.Where(item => item.PositionId == input.PositionId.Value)
.Select(item => item.ContractTemplateId)
.ToListAsync();
query = query.Where(item => templateIds.Contains(item.Id));
}
var page = await query
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), $"{input.SortField} {input.SortOrder}")
.OrderByDescending(it => it.CreateTime)
.OrderBy(it => it.TemplateName)
.ToPagedListAsync(input.PageNum, input.PageSize);
await PopulateScopeDisplay(page.List.ToList());
return page;
}
/// <summary>
/// 查询合同模板详情。
/// </summary>
public async Task<ObContractTemplate> Detail(BaseIdInput input)
{
var template = await GetByIdAsync(input.Id) ?? throw Oops.Bah("合同模板不存在");
await PopulateScopeDisplay(new List<ObContractTemplate> { template });
return template;
}
/// <summary>
/// 查询同时覆盖全部已选岗位的启用合同模板。
/// </summary>
public async Task<List<ObContractTemplate>> ListAvailable(
OnboardingContractTemplateAvailableInput input)
{
input.PositionIds = input.PositionIds?.Distinct().ToList() ?? new List<long>();
if (input.PositionIds.Count == 0)
return new List<ObContractTemplate>();
await ValidateBusinessScope(input.CompanyId, input.DepartmentId, input.PositionIds);
var candidates = await Context.Queryable<ObContractTemplate>()
.Where(item => item.CompanyId == input.CompanyId
&& item.DepartmentId == input.DepartmentId
&& item.Status == "enable")
.OrderBy(item => item.TemplateName)
.ToListAsync();
if (candidates.Count == 0)
return candidates;
var candidateIds = candidates.Select(item => item.Id).ToList();
var relations = await Context.Queryable<ObContractTemplatePosition>()
.Where(item => candidateIds.Contains(item.ContractTemplateId))
.ToListAsync();
var availableIds = relations
.GroupBy(item => item.ContractTemplateId)
.Where(group => input.PositionIds.All(positionId =>
group.Any(item => item.PositionId == positionId)))
.Select(group => group.Key)
.ToHashSet();
var availableTemplates = candidates
.Where(item => availableIds.Contains(item.Id))
.ToList();
await PopulateScopeDisplay(availableTemplates);
return availableTemplates;
}
/// <summary>
/// 新增合同模板主档案。
/// </summary>
public async Task Add(OnboardingContractTemplateAddInput input)
{
NormalizeInput(input);
await ValidateBusinessScope(input.CompanyId, input.DepartmentId, input.PositionIds);
await EnsureCodeUnique(input.TemplateCode, 0);
var templateId = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
var entity = BuildEntity(templateId, input);
await Context.Ado.BeginTranAsync();
try
{
await Context.Insertable(entity).ExecuteCommandAsync();
await InsertPositionRelations(templateId, input.PositionIds);
await Context.Ado.CommitTranAsync();
}
catch
{
await Context.Ado.RollbackTranAsync();
throw;
}
}
/// <summary>
/// 编辑合同模板主档案。
/// </summary>
public async Task Edit(OnboardingContractTemplateEditInput input)
{
var template = await Detail(new BaseIdInput { Id = input.Id });
await _dataPermission.EnsureCanManage(
template.CreateUserId,
OnboardingPermissionCodes.ContractManageAll,
"合同模板");
NormalizeInput(input);
await ValidateBusinessScope(input.CompanyId, input.DepartmentId, input.PositionIds);
await ValidateReferencedOnboardingTemplates(input);
await EnsureCodeUnique(input.TemplateCode, input.Id);
var entity = BuildEntity(input.Id, input);
await Context.Ado.BeginTranAsync();
try
{
await Context.Updateable(entity)
.IgnoreColumns(ignoreAllNullColumns: true)
.ExecuteCommandAsync();
await Context.Deleteable<ObContractTemplatePosition>()
.Where(item => item.ContractTemplateId == input.Id)
.ExecuteCommandAsync();
await InsertPositionRelations(input.Id, input.PositionIds);
await Context.Ado.CommitTranAsync();
}
catch
{
await Context.Ado.RollbackTranAsync();
throw;
}
}
/// <summary>
/// 删除未被入职模板或任务合同引用的模板(级联删除未使用的版本和变量)。
/// </summary>
public async Task Delete(BaseIdListInput input)
{
if (input.Ids.Count == 0)
return;
var templates = await Context.Queryable<ObContractTemplate>()
.Where(item => input.Ids.Contains(item.Id))
.ToListAsync();
if (templates.Count != input.Ids.Distinct().Count())
throw Oops.Bah("合同模板不存在");
await _dataPermission.EnsureCanManage(
templates.Select(item => item.CreateUserId),
OnboardingPermissionCodes.ContractManageAll,
"合同模板");
var hasOnboardingTemplates = await Context.Queryable<ObOnboardingTemplateContract>()
.AnyAsync(it => input.Ids.Contains(it.ContractTemplateId));
if (hasOnboardingTemplates)
throw Oops.Bah("模板已被入职模板引用,不能删除,请先在入职模板中移除关联");
var hasContracts = await Context.Queryable<ObTaskContract>()
.AnyAsync(it => input.Ids.Contains(it.TemplateId));
if (hasContracts)
throw Oops.Bah("模板已生成入职任务合同,不能删除,可改为停用");
var versionIds = await Context.Queryable<ObContractTemplateVersion>()
.Where(it => input.Ids.Contains(it.TemplateId))
.Select(it => it.Id)
.ToListAsync();
await Context.Ado.BeginTranAsync();
try
{
if (versionIds.Count > 0)
{
await Context.Deleteable<ObTemplateVariable>()
.Where(it => versionIds.Contains(it.TemplateVersionId))
.ExecuteCommandAsync();
await Context.Deleteable<ObContractTemplateVersion>()
.In(versionIds)
.ExecuteCommandAsync();
}
await Context.Deleteable<ObContractScope>()
.Where(it => input.Ids.Contains(it.TemplateId))
.ExecuteCommandAsync();
await Context.Deleteable<ObContractTemplatePosition>()
.Where(item => input.Ids.Contains(item.ContractTemplateId))
.ExecuteCommandAsync();
await DeleteByIdsAsync(input.Ids.Cast<object>().ToArray());
await Context.Ado.CommitTranAsync();
}
catch
{
await Context.Ado.RollbackTranAsync();
throw;
}
}
/// <summary>
/// 删除未被入职模板或任务使用的合同版本。
/// </summary>
public async Task DeleteVersion(BaseIdInput input)
{
var version = await Context.Queryable<ObContractTemplateVersion>().InSingleAsync(input.Id)
?? throw Oops.Bah("合同版本不存在");
await EnsureTemplateCanManage(version.TemplateId);
var isUsedInOnboardingTemplate = await Context.Queryable<ObOnboardingTemplateContract>()
.AnyAsync(it => it.ContractTemplateVersionId == version.Id);
if (isUsedInOnboardingTemplate)
throw Oops.Bah("该版本已被入职模板引用,不能删除,请先在入职模板中移除关联");
var isUsedInTask = await Context.Queryable<ObTaskContract>()
.AnyAsync(it => it.TemplateVersionId == version.Id);
if (isUsedInTask)
throw Oops.Bah("该版本已用于实际入职任务,不能删除,可改为停用");
await Context.Ado.BeginTranAsync();
try
{
await Context.Deleteable<ObTemplateVariable>()
.Where(it => it.TemplateVersionId == version.Id)
.ExecuteCommandAsync();
await Context.Deleteable<ObContractScope>()
.Where(it => it.TemplateVersionId == version.Id)
.ExecuteCommandAsync();
await Context.Deleteable<ObContractTemplateVersion>()
.In(version.Id)
.ExecuteCommandAsync();
await Context.Ado.CommitTranAsync();
}
catch
{
await Context.Ado.RollbackTranAsync();
throw;
}
}
/// <summary>
/// 分页查询指定合同模板的版本。
/// </summary>
public async Task<SqlSugarPagedList<ObContractTemplateVersion>> VersionPage(OnboardingContractVersionPageInput input)
{
return await Context.Queryable<ObContractTemplateVersion>()
.Where(it => it.TemplateId == input.TemplateId)
.WhereIF(!string.IsNullOrWhiteSpace(input.VersionStatus), it => it.VersionStatus == input.VersionStatus)
.OrderByDescending(it => it.CreateTime)
.ToPagedListAsync(input.PageNum, input.PageSize);
}
/// <summary>
/// 新增合同模板 DOCX 版本。
/// </summary>
public async Task AddVersion(OnboardingContractVersionAddInput input)
{
var template = await Detail(new BaseIdInput { Id = input.TemplateId });
await _dataPermission.EnsureCanManage(
template.CreateUserId,
OnboardingPermissionCodes.ContractManageAll,
"合同模板");
var file = await Context.Queryable<SysFile>().InSingleAsync(input.SourceDocxFileId)
?? throw Oops.Bah("DOCX 源文件不存在");
if (!string.Equals(file.Suffix, "docx", StringComparison.OrdinalIgnoreCase))
throw Oops.Bah("合同模板仅支持 DOCX 文件");
var versionExists = await Context.Queryable<ObContractTemplateVersion>()
.AnyAsync(item => item.TemplateId == input.TemplateId && item.VersionNo == input.VersionNo);
if (versionExists)
throw Oops.Bah("该模板版本号已存在");
var sourceResult = await _fileService.Download(new BaseIdInput { Id = file.Id })
?? throw Oops.Bah("无法读取 DOCX 源文件");
await using var sourceStream = sourceResult.FileStream;
using var sourceBytes = new MemoryStream();
await sourceStream.CopyToAsync(sourceBytes);
var bytes = sourceBytes.ToArray();
var variableCodes = DocxTemplateProcessor.ExtractVariableCodes(bytes);
// 严格比对变量字典库,防止手误和非法占位符
var dictList = await Context.Queryable<ObVariableDefinition>().ToListAsync();
var duplicateDefinition = dictList
.GroupBy(item => item.VariableCode, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault(group => group.Count() > 1);
if (duplicateDefinition != null)
{
throw Oops.Bah(
$"变量字典编码“{duplicateDefinition.Key}”存在重复数据,请先清理后再导入合同版本");
}
var dictMap = dictList.ToDictionary(it => it.VariableCode, it => it, StringComparer.OrdinalIgnoreCase);
var undefinedCodes = variableCodes.Where(code => !dictMap.ContainsKey(code)).ToList();
if (undefinedCodes.Count > 0)
{
throw Oops.Bah($"DOCX 模板包含未在字典库中注册的占位符:{string.Join(",", undefinedCodes)}。请检查拼写或先在【变量字典】中添加该变量。");
}
var versionId = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
var version = input.Adapt<ObContractTemplateVersion>();
version.Id = versionId;
version.VersionStatus = ContractTemplateVersionStatus.Draft;
version.FileHash = string.IsNullOrWhiteSpace(input.FileHash)
? Convert.ToHexString(global::System.Security.Cryptography.SHA256.HashData(bytes))
: input.FileHash;
var variables = variableCodes.Select((code, index) =>
{
var def = dictMap.TryGetValue(code, out var d) ? d : null;
return BuildDetectedVariable(versionId, code, def, index);
}).ToList();
await Context.Ado.BeginTranAsync();
try
{
await Context.Insertable(version).ExecuteCommandAsync();
if (variables.Count > 0)
await Context.Insertable(variables).ExecuteCommandAsync();
await Context.Ado.CommitTranAsync();
}
catch
{
await Context.Ado.RollbackTranAsync();
throw;
}
}
/// <summary>
/// 启用或停用合同模板版本。
/// </summary>
public async Task SetVersionStatus(OnboardingContractVersionStatusInput input)
{
if (input.VersionStatus != ContractTemplateVersionStatus.Enabled
&& input.VersionStatus != ContractTemplateVersionStatus.Disabled)
{
throw Oops.Bah("版本状态仅支持启用或停用");
}
var version = await Context.Queryable<ObContractTemplateVersion>().InSingleAsync(input.Id)
?? throw Oops.Bah("模板版本不存在");
await EnsureTemplateCanManage(version.TemplateId);
if (input.VersionStatus == ContractTemplateVersionStatus.Enabled)
{
var hasUnresolvedVariables = await Context.Queryable<ObTemplateVariable>()
.AnyAsync(item => item.TemplateVersionId == input.Id
&& item.VariableType == ContractVariableType.Unresolved);
if (hasUnresolvedVariables)
throw Oops.Bah("仍有未识别来源的合同变量,请逐项确认后再启用");
}
version.VersionStatus = input.VersionStatus;
await Context.Updateable(version).ExecuteCommandAsync();
}
/// <summary>
/// 分页查询指定模板版本的变量定义。
/// </summary>
public async Task<SqlSugarPagedList<ObTemplateVariable>> VariablePage(OnboardingTemplateVariablePageInput input)
{
return await Context.Queryable<ObTemplateVariable>()
.Where(it => it.TemplateVersionId == input.TemplateVersionId)
.OrderBy(it => it.SortCode)
.OrderBy(it => it.VariableCode)
.ToPagedListAsync(input.PageNum, input.PageSize);
}
/// <summary>
/// 编辑模板版本中自动识别出的变量,不允许修改 DOCX 占位符编码。
/// </summary>
public async Task EditVariable(OnboardingTemplateVariableEditInput input)
{
var variable = await Context.Queryable<ObTemplateVariable>().InSingleAsync(input.Id)
?? throw Oops.Bah("模板变量不存在");
if (variable.TemplateVersionId != input.TemplateVersionId)
throw Oops.Bah("不允许变更变量所属版本");
if (!string.Equals(variable.VariableCode, input.VariableCode, StringComparison.Ordinal))
throw Oops.Bah("DOCX 占位符编码由系统识别,不能修改");
var version = await Context.Queryable<ObContractTemplateVersion>()
.InSingleAsync(variable.TemplateVersionId)
?? throw Oops.Bah("模板版本不存在");
await EnsureTemplateCanManage(version.TemplateId);
var isUsedInTask = await Context.Queryable<ObTaskContract>()
.AnyAsync(it => it.TemplateVersionId == input.TemplateVersionId);
if (isUsedInTask)
throw Oops.Bah("该版本已生成实际入职任务合同,变量配置已锁定");
var entity = input.Adapt<ObTemplateVariable>();
entity.VariableCode = variable.VariableCode;
await Context.Updateable(entity).ExecuteCommandAsync();
}
private async Task EnsureTemplateCanManage(long templateId)
{
var template = await Context.Queryable<ObContractTemplate>().InSingleAsync(templateId)
?? throw Oops.Bah("合同模板不存在");
await _dataPermission.EnsureCanManage(
template.CreateUserId,
OnboardingPermissionCodes.ContractManageAll,
"合同模板");
}
private static ObTemplateVariable BuildDetectedVariable(long versionId, string code, ObVariableDefinition def, int index)
{
var separatorIndex = code.IndexOf('.');
var prefix = separatorIndex > 0 ? code[..separatorIndex].ToLowerInvariant() : string.Empty;
var sourceField = separatorIndex > 0 ? code[(separatorIndex + 1)..] : code;
var variableType = prefix switch
{
"employee" => ContractVariableType.Employee,
"company" => ContractVariableType.Company,
"hr" => ContractVariableType.Contract,
"system" => ContractVariableType.System,
_ => ContractVariableType.Unresolved
};
var varName = def != null && !string.IsNullOrWhiteSpace(def.VariableName) ? def.VariableName : sourceField;
var dataType = def != null && !string.IsNullOrWhiteSpace(def.DataType) ? def.DataType : InferDataType(sourceField);
return new ObTemplateVariable
{
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
TemplateVersionId = versionId,
VariableCode = code,
VariableName = varName,
VariableType = variableType,
DataType = dataType,
EmployeeField = variableType is ContractVariableType.Employee or ContractVariableType.Company
? sourceField
: null,
DefaultValue = def?.DefaultValue,
Required = def?.Required ?? true,
HrEditable = variableType == ContractVariableType.Contract,
// 合同版本首次识别时继承全局变量字典排序;未登记排序时才使用识别顺序兜底。
SortCode = def?.SortCode ?? (index + 1) * 10,
Remark = def?.Remark
};
}
private static string InferDataType(string sourceField)
{
var normalized = sourceField.ToLowerInvariant();
if (normalized.Contains("date") || normalized.EndsWith("at"))
return "date";
if (normalized.Contains("salary")
|| normalized.Contains("amount")
|| normalized.Contains("wage")
|| normalized.Contains("copies"))
{
return "decimal";
}
return "string";
}
private async Task EnsureCodeUnique(string code, long id)
{
var exists = await Context.Queryable<ObContractTemplate>()
.AnyAsync(it => it.TemplateCode == code && it.Id != id);
if (exists)
throw Oops.Bah("模板编码已存在");
}
/// <summary>
/// 标准化合同模板保存参数并执行基础业务校验。
/// </summary>
private static void NormalizeInput(OnboardingContractTemplateAddInput input)
{
input.TemplateCode = OnboardingCodeValidator.Normalize(input.TemplateCode, "签署材料模板编码", 50);
input.TemplateName = input.TemplateName?.Trim();
input.ContractType = input.ContractType?.Trim();
input.PositionIds = input.PositionIds?.Distinct().ToList() ?? new List<long>();
if (string.IsNullOrWhiteSpace(input.TemplateName)
|| string.IsNullOrWhiteSpace(input.ContractType))
{
throw Oops.Bah("请填写模板编码、模板名称和合同类型");
}
if (input.PositionIds.Count == 0)
throw Oops.Bah("请至少选择一个适用岗位");
if (input.Status != "enable" && input.Status != "disable")
throw Oops.Bah("合同模板状态不正确");
}
/// <summary>
/// 校验公司、部门、岗位属于同一条启用的业务层级。
/// </summary>
private async Task ValidateBusinessScope(
long companyId,
long departmentId,
List<long> positionIds)
{
var companyEnabled = await Context.Queryable<ObCompany>()
.AnyAsync(item => item.Id == companyId && item.Status == "enable");
if (!companyEnabled)
throw Oops.Bah("所属公司不存在或未启用");
var departmentEnabled = await Context.Queryable<ObDepartment>()
.AnyAsync(item => item.Id == departmentId
&& item.CompanyId == companyId
&& item.Status == "enable");
if (!departmentEnabled)
throw Oops.Bah("所属部门不存在、未启用或不属于该公司");
var positionCount = await Context.Queryable<ObPosition>()
.CountAsync(item => positionIds.Contains(item.Id)
&& item.CompanyId == companyId
&& item.DepartmentId == departmentId
&& item.Status == "enable");
if (positionCount != positionIds.Count)
throw Oops.Bah("存在未启用或不属于该公司、部门的岗位");
}
/// <summary>
/// 防止修改合同模板归属后破坏已经保存的入职模板配置。
/// </summary>
private async Task ValidateReferencedOnboardingTemplates(
OnboardingContractTemplateEditInput input)
{
var relations = await Context.Queryable<ObOnboardingTemplateContract>()
.Where(item => item.ContractTemplateId == input.Id)
.ToListAsync();
if (relations.Count == 0)
return;
var onboardingTemplateIds = relations
.Select(item => item.OnboardingTemplateId)
.Distinct()
.ToList();
var onboardingTemplates = await Context.Queryable<ObOnboardingTemplate>()
.Where(item => onboardingTemplateIds.Contains(item.Id))
.ToListAsync();
var positionRelations = await Context.Queryable<ObOnboardingTemplatePosition>()
.Where(item => onboardingTemplateIds.Contains(item.OnboardingTemplateId))
.ToListAsync();
foreach (var template in onboardingTemplates)
{
var positionIds = positionRelations
.Where(item => item.OnboardingTemplateId == template.Id)
.Select(item => item.PositionId)
.Distinct()
.ToList();
var departmentMatches = !template.DepartmentId.HasValue
|| template.DepartmentId.Value == input.DepartmentId;
var scopeMatches = template.CompanyId == input.CompanyId
&& departmentMatches
&& positionIds.All(input.PositionIds.Contains);
if (!scopeMatches)
{
throw Oops.Bah(
$"合同模板仍被入职模板“{template.TemplateName}”引用,当前归属调整会使其失效,请先调整入职模板");
}
}
}
/// <summary>
/// 构建合同模板主档案实体。
/// </summary>
private static ObContractTemplate BuildEntity(
long templateId,
OnboardingContractTemplateAddInput input)
{
return new ObContractTemplate
{
Id = templateId,
CompanyId = input.CompanyId,
DepartmentId = input.DepartmentId,
TemplateCode = input.TemplateCode,
TemplateName = input.TemplateName,
ContractType = input.ContractType,
Description = input.Description,
Status = input.Status
};
}
/// <summary>
/// 保存合同模板适用岗位关系。
/// </summary>
private async Task InsertPositionRelations(long templateId, List<long> positionIds)
{
var relations = positionIds.Select(positionId => new ObContractTemplatePosition
{
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
ContractTemplateId = templateId,
PositionId = positionId
}).ToList();
await Context.Insertable(relations).ExecuteCommandAsync();
}
/// <summary>
/// 回填合同模板的公司、部门和适用岗位展示信息。
/// </summary>
private async Task PopulateScopeDisplay(List<ObContractTemplate> templates)
{
if (templates.Count == 0)
return;
var templateIds = templates.Select(item => item.Id).ToList();
var companyIds = templates
.Where(item => item.CompanyId.HasValue)
.Select(item => item.CompanyId.Value)
.Distinct()
.ToList();
var departmentIds = templates
.Where(item => item.DepartmentId.HasValue)
.Select(item => item.DepartmentId.Value)
.Distinct()
.ToList();
var companies = companyIds.Count == 0
? new List<ObCompany>()
: await Context.Queryable<ObCompany>()
.Where(item => companyIds.Contains(item.Id))
.ToListAsync();
var departments = departmentIds.Count == 0
? new List<ObDepartment>()
: await Context.Queryable<ObDepartment>()
.Where(item => departmentIds.Contains(item.Id))
.ToListAsync();
var relations = await Context.Queryable<ObContractTemplatePosition>()
.Where(item => templateIds.Contains(item.ContractTemplateId))
.ToListAsync();
var positionIds = relations.Select(item => item.PositionId).Distinct().ToList();
var positions = positionIds.Count == 0
? new List<ObPosition>()
: await Context.Queryable<ObPosition>()
.Where(item => positionIds.Contains(item.Id))
.ToListAsync();
foreach (var template in templates)
{
template.CompanyName = companies
.FirstOrDefault(item => item.Id == template.CompanyId)?.CompanyName;
template.DepartmentName = departments
.FirstOrDefault(item => item.Id == template.DepartmentId)?.DepartmentName;
template.PositionIds = relations
.Where(item => item.ContractTemplateId == template.Id)
.Select(item => item.PositionId)
.ToList();
template.PositionNames = template.PositionIds
.Select(positionId => positions.FirstOrDefault(item => item.Id == positionId)?.PositionName)
.Where(name => !string.IsNullOrWhiteSpace(name))
.ToList();
}
}
}