2026-08-21 19:27:59 +00:00
|
|
|
|
using System;
|
2026-05-04 19:50:21 +00:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
using Microsoft.AspNetCore.Authorization;
|
|
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
2026-08-21 19:27:59 +00:00
|
|
|
|
using Sozsoft.Platform.Entities;
|
|
|
|
|
|
using Sozsoft.Platform.Extensions;
|
2026-05-04 19:50:21 +00:00
|
|
|
|
using Volo.Abp.Domain.Repositories;
|
|
|
|
|
|
using Volo.Abp.Identity;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Sozsoft.Platform.OrgChart;
|
|
|
|
|
|
|
|
|
|
|
|
[Authorize]
|
|
|
|
|
|
[Route("api/app/org-chart")]
|
|
|
|
|
|
public class OrgChartAppService : PlatformAppService
|
|
|
|
|
|
{
|
2026-10-14 21:12:14 +00:00
|
|
|
|
private readonly IRepository<Branch, Guid> _branchRepository;
|
2026-05-04 19:50:21 +00:00
|
|
|
|
private readonly IRepository<Department, Guid> _departmentRepository;
|
|
|
|
|
|
private readonly IRepository<JobPosition, Guid> _jobPositionRepository;
|
|
|
|
|
|
private readonly IIdentityUserRepository _userRepository;
|
|
|
|
|
|
|
|
|
|
|
|
public OrgChartAppService(
|
2026-10-14 21:12:14 +00:00
|
|
|
|
IRepository<Branch, Guid> branchRepository,
|
2026-05-04 19:50:21 +00:00
|
|
|
|
IRepository<Department, Guid> departmentRepository,
|
|
|
|
|
|
IRepository<JobPosition, Guid> jobPositionRepository,
|
|
|
|
|
|
IIdentityUserRepository userRepository)
|
|
|
|
|
|
{
|
2026-10-14 21:12:14 +00:00
|
|
|
|
_branchRepository = branchRepository;
|
2026-05-04 19:50:21 +00:00
|
|
|
|
_departmentRepository = departmentRepository;
|
|
|
|
|
|
_jobPositionRepository = jobPositionRepository;
|
|
|
|
|
|
_userRepository = userRepository;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[HttpGet("departments")]
|
|
|
|
|
|
public async Task<List<OrgChartNodeDto>> GetDepartmentsAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
var departments = await _departmentRepository.GetListAsync();
|
|
|
|
|
|
var jobPositions = await _jobPositionRepository.GetListAsync();
|
|
|
|
|
|
var users = await _userRepository.GetListAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var jobById = jobPositions.ToDictionary(x => x.Id);
|
2026-07-11 20:35:32 +00:00
|
|
|
|
var positionsByDepartment = jobPositions
|
|
|
|
|
|
.GroupBy(position => position.DepartmentId)
|
|
|
|
|
|
.ToDictionary(group => group.Key, group => group.OrderBy(position => position.Name).ToList());
|
|
|
|
|
|
var usersByJobPosition = GroupUsersByJobPosition(users);
|
2026-05-04 19:50:21 +00:00
|
|
|
|
var topPositionByDepartment = new Dictionary<Guid, JobPosition>();
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var department in departments)
|
|
|
|
|
|
{
|
2026-07-11 20:35:32 +00:00
|
|
|
|
if (!positionsByDepartment.TryGetValue(department.Id, out var departmentPositions))
|
2026-05-04 19:50:21 +00:00
|
|
|
|
{
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Top position means: no parent, parent missing, or parent belongs to another department.
|
|
|
|
|
|
var topCandidates = departmentPositions
|
|
|
|
|
|
.Where(position =>
|
|
|
|
|
|
!position.ParentId.HasValue ||
|
|
|
|
|
|
!jobById.ContainsKey(position.ParentId.Value) ||
|
|
|
|
|
|
jobById[position.ParentId.Value].DepartmentId != department.Id)
|
|
|
|
|
|
.OrderBy(position => position.Name)
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
|
|
var selectedTop = topCandidates.FirstOrDefault() ?? departmentPositions
|
|
|
|
|
|
.OrderBy(position => position.Name)
|
|
|
|
|
|
.First();
|
|
|
|
|
|
|
|
|
|
|
|
topPositionByDepartment[department.Id] = selectedTop;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 18:50:39 +00:00
|
|
|
|
var nodes = departments.Select(d =>
|
2026-05-04 19:50:21 +00:00
|
|
|
|
{
|
2026-06-13 18:50:39 +00:00
|
|
|
|
topPositionByDepartment.TryGetValue(d.Id, out var topPosition);
|
|
|
|
|
|
|
|
|
|
|
|
return new OrgChartNodeDto
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = d.Id,
|
|
|
|
|
|
Name = d.Name,
|
2026-10-14 21:12:14 +00:00
|
|
|
|
NodeType = OrgChartNodeType.Department,
|
|
|
|
|
|
ParentId = ResolveParent(d.ParentId, d.BranchId, departments.Select(x => x.Id)),
|
2026-06-13 18:50:39 +00:00
|
|
|
|
JobPositionId = topPosition?.Id,
|
|
|
|
|
|
JobPositionName = topPosition?.Name,
|
2026-07-11 20:35:32 +00:00
|
|
|
|
Users = topPosition != null && usersByJobPosition.TryGetValue(topPosition.Id, out var positionUsers)
|
|
|
|
|
|
? positionUsers
|
|
|
|
|
|
: [],
|
2026-06-13 18:50:39 +00:00
|
|
|
|
};
|
2026-05-04 19:50:21 +00:00
|
|
|
|
}).ToList();
|
|
|
|
|
|
|
2026-10-14 21:12:14 +00:00
|
|
|
|
return await WithBranchRootsAsync(nodes);
|
2026-05-04 19:50:21 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[HttpGet("job-positions")]
|
|
|
|
|
|
public async Task<List<OrgChartNodeDto>> GetJobPositionsAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
var jobPositions = await _jobPositionRepository.GetListAsync();
|
|
|
|
|
|
var departments = await _departmentRepository.GetListAsync();
|
|
|
|
|
|
var users = await _userRepository.GetListAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var deptDict = departments.ToDictionary(d => d.Id, d => d.Name);
|
2026-07-11 20:35:32 +00:00
|
|
|
|
var usersByJobPosition = GroupUsersByJobPosition(users);
|
2026-05-04 19:50:21 +00:00
|
|
|
|
|
|
|
|
|
|
var nodes = jobPositions.Select(jp => new OrgChartNodeDto
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = jp.Id,
|
|
|
|
|
|
Name = jp.Name,
|
2026-10-14 21:12:14 +00:00
|
|
|
|
NodeType = OrgChartNodeType.JobPosition,
|
|
|
|
|
|
ParentId = ResolveParent(jp.ParentId, jp.BranchId, jobPositions.Select(x => x.Id)),
|
2026-05-04 19:50:21 +00:00
|
|
|
|
DepartmentId = jp.DepartmentId,
|
|
|
|
|
|
DepartmentName = deptDict.TryGetValue(jp.DepartmentId, out var name) ? name : null,
|
2026-06-13 18:50:39 +00:00
|
|
|
|
JobPositionId = jp.Id,
|
|
|
|
|
|
JobPositionName = jp.Name,
|
2026-07-11 20:35:32 +00:00
|
|
|
|
Users = usersByJobPosition.TryGetValue(jp.Id, out var positionUsers)
|
|
|
|
|
|
? positionUsers
|
|
|
|
|
|
: [],
|
2026-05-04 19:50:21 +00:00
|
|
|
|
}).ToList();
|
|
|
|
|
|
|
2026-10-14 21:12:14 +00:00
|
|
|
|
return await WithBranchRootsAsync(nodes);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Üstü olmayan (ya da üstü silinmiş) kayıt şubesinin altına bağlanır; şube ağacın köküdür.
|
|
|
|
|
|
private static Guid? ResolveParent(Guid? parentId, Guid branchId, IEnumerable<Guid> knownIds)
|
|
|
|
|
|
=> parentId.HasValue && knownIds.Contains(parentId.Value) ? parentId : branchId;
|
|
|
|
|
|
|
|
|
|
|
|
// Yalnızca altında kayıt bulunan şubeler düğüm olur; Branch IMultiTenant değildir, kiracı süzgeci elle yazılır.
|
|
|
|
|
|
private async Task<List<OrgChartNodeDto>> WithBranchRootsAsync(List<OrgChartNodeDto> nodes)
|
|
|
|
|
|
{
|
|
|
|
|
|
var nodeIds = nodes.Select(n => n.Id).ToHashSet();
|
|
|
|
|
|
var branchIds = nodes
|
|
|
|
|
|
.Where(n => n.ParentId.HasValue && !nodeIds.Contains(n.ParentId.Value))
|
|
|
|
|
|
.Select(n => n.ParentId!.Value)
|
|
|
|
|
|
.ToHashSet();
|
|
|
|
|
|
if (branchIds.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
return nodes;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var tenantId = CurrentTenant.Id;
|
|
|
|
|
|
var branches = await _branchRepository.GetListAsync(b => b.TenantId == tenantId && branchIds.Contains(b.Id));
|
|
|
|
|
|
var branchNodes = branches
|
|
|
|
|
|
.OrderBy(b => b.Name)
|
|
|
|
|
|
.Select(b => new OrgChartNodeDto { Id = b.Id, Name = b.Name, NodeType = OrgChartNodeType.Branch });
|
|
|
|
|
|
|
|
|
|
|
|
return branchNodes.Concat(nodes).ToList();
|
2026-05-04 19:50:21 +00:00
|
|
|
|
}
|
2026-07-11 20:35:32 +00:00
|
|
|
|
|
|
|
|
|
|
private static Dictionary<Guid, List<OrgChartUserDto>> GroupUsersByJobPosition(
|
|
|
|
|
|
IEnumerable<IdentityUser> users)
|
|
|
|
|
|
{
|
|
|
|
|
|
return users
|
|
|
|
|
|
.GroupBy(user => user.GetJobPositionId())
|
|
|
|
|
|
.ToDictionary(
|
|
|
|
|
|
group => group.Key,
|
|
|
|
|
|
group => group.Select(user => new OrgChartUserDto
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = user.Id,
|
|
|
|
|
|
FullName = user.GetFullName(),
|
|
|
|
|
|
Email = user.Email,
|
|
|
|
|
|
UserName = user.UserName,
|
|
|
|
|
|
PhoneNumber = user.PhoneNumber
|
|
|
|
|
|
}).ToList());
|
|
|
|
|
|
}
|
2026-05-04 19:50:21 +00:00
|
|
|
|
}
|