< Summary

Information
Class: FAU.Logica.Services.PersonalService
Assembly: FAU.Logica
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.Logica/Services/PersonalService.cs
Line coverage
66%
Covered lines: 477
Uncovered lines: 237
Coverable lines: 714
Total lines: 1106
Line coverage: 66.8%
Branch coverage
36%
Covered branches: 160
Total branches: 434
Branch coverage: 36.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetPersonalListAsync()50%201468.29%
GetDetalleAsync()75%88100%
CreatePersonalAsync()47.22%853666.36%
UpdatePersonalAsync()36.4%316222861.64%
RegistrarBajaAsync()62.5%252486.44%
RegistrarAscensoAsync()75%333291.34%
ApplyOrdering(...)27.27%2262225%
DeterminarRegimenIdAsync()14.28%481444.44%
ObtenerReporteMovimientosAsync()0%2162460%

File(s)

/home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.Logica/Services/PersonalService.cs

#LineLine coverage
 1using FAU.DataAccess;
 2using FAU.DataAccess.Repositories;
 3using FAU.Entidades;
 4using FAU.Logica.DTOs;
 5using FAU.Logica.Validadores;
 6using Microsoft.EntityFrameworkCore;
 7using Microsoft.Extensions.Logging;
 8
 9namespace FAU.Logica.Services;
 10
 11/// <summary>
 12/// Servicio de gestión de Personal
 13/// </summary>
 14public class PersonalService : IPersonalService
 15{
 16    private readonly IPersonalRepository _personalRepository;
 17    private readonly IMotivoBajaRepository _motivoBajaRepository;
 18    private readonly ApplicationDbContext _context;
 19    private readonly ILogger<PersonalService> _logger;
 20    private readonly IAuditoriaService _auditoriaService;
 21    private readonly ICurrentUserContext _currentUser;
 22
 2023    public PersonalService(
 2024        IPersonalRepository personalRepository,
 2025        IMotivoBajaRepository motivoBajaRepository,
 2026        ApplicationDbContext context,
 2027        ILogger<PersonalService> logger,
 2028        IAuditoriaService auditoriaService,
 2029        ICurrentUserContext currentUser)
 30    {
 2031        _personalRepository = personalRepository;
 2032        _motivoBajaRepository = motivoBajaRepository;
 2033        _context = context;
 2034        _logger = logger;
 2035        _auditoriaService = auditoriaService;
 2036        _currentUser = currentUser;
 2037    }
 38
 39    public async Task<object> GetPersonalListAsync(
 40        string? buscar,
 41        long? cargoId,
 42        string? estado,
 43        string? ley,
 44        int page,
 45        int pageSize,
 46        string? orderBy,
 47        string sort)
 48    {
 149        var query = _context.RelacionesLaborales
 150            .Include(r => r.Persona)
 151            .Include(r => r.Regimen)
 152            .Include(r => r.Escalafon)
 153            .Include(r => r.Grado)
 154            .Include(r => r.Situacion)
 155            .AsQueryable();
 56
 57        // Filtro por búsqueda (CI, nombre o apellido)
 158        if (!string.IsNullOrWhiteSpace(buscar))
 59        {
 060            var buscarLower = buscar.ToLower();
 061            query = query.Where(r =>
 062                r.Persona.Cedula.Contains(buscarLower) ||
 063                r.Persona.PrimerNombre.ToLower().Contains(buscarLower) ||
 064                r.Persona.PrimerApellido.ToLower().Contains(buscarLower) ||
 065                (r.Persona.SegundoNombre != null && r.Persona.SegundoNombre.ToLower().Contains(buscarLower)) ||
 066                (r.Persona.SegundoApellido != null && r.Persona.SegundoApellido.ToLower().Contains(buscarLower)));
 67        }
 68
 69        // Filtro por cargo (grado)
 170        if (cargoId.HasValue)
 71        {
 072            query = query.Where(r => r.GradoId == cargoId.Value);
 73        }
 74
 75        // Filtro por estado (parse string to enum)
 176        if (!string.IsNullOrWhiteSpace(estado))
 77        {
 178            if (Enum.TryParse<EstadoRelacion>(estado, true, out var estadoEnum))
 79            {
 180                query = query.Where(r => r.Estado == estadoEnum);
 81            }
 82            else
 83            {
 84                // Si no se puede parsear, intentar aceptar valores comunes
 085                var estLower = estado.Trim().ToLower();
 086                if (estLower == "activo") query = query.Where(r => r.Estado == EstadoRelacion.Activo);
 087                else if (estLower == "inactivo") query = query.Where(r => r.Estado == EstadoRelacion.Inactivo);
 88            }
 89        }
 90
 91        // Filtro por ley (régimen)
 192        if (!string.IsNullOrWhiteSpace(ley))
 93        {
 094            var leyLower = ley.ToLower();
 095            query = query.Where(r => r.Regimen.NumeroLey.ToLower().Contains(leyLower));
 96        }
 97
 198        var total = await query.CountAsync();
 99
 1100        query = ApplyOrdering(query, orderBy, sort);
 101
 102        // Paginación: obtener entidades y luego proyectar a DTO en memoria para poder convertir enums a string
 1103        var relacionesPage = await query
 1104            .Skip((page - 1) * pageSize)
 1105            .Take(pageSize)
 1106            .ToListAsync();
 107
 2108        var items = relacionesPage.Select(r => new PersonalListDto
 2109        {
 2110            Id = r.Id,
 2111            Ci = r.Persona.Cedula,
 2112            personalId = r.PersonaId,
 2113            PrimerNombre = r.Persona.PrimerNombre,
 2114            SegundoNombre = r.Persona.SegundoNombre,
 2115            PrimerApellido = r.Persona.PrimerApellido,
 2116            SegundoApellido = r.Persona.SegundoApellido,
 2117            Grado = r.Grado.Denominacion,
 2118            Ley = r.Regimen.NumeroLey,
 2119            Estado = r.Estado.ToString(),
 2120            FechaIngreso = r.FechaInicio
 2121        }).ToList();
 122
 1123        return new
 1124        {
 1125            items,
 1126            total,
 1127            page,
 1128            pageSize,
 1129            totalPages = (int)Math.Ceiling((double)total / pageSize)
 1130        };
 1131    }
 132
 133    public async Task<PersonalDetalleDto?> GetDetalleAsync(long id)
 134    {
 135        // id is PersonaId: obtener la relación laboral activa o la última relación laboral
 3136        var relacionLaboral = await _context.RelacionesLaborales
 3137            .Include(r => r.Persona)
 3138            .Include(r => r.Regimen)
 3139            .Include(r => r.Escalafon)
 3140            .Include(r => r.Grado)
 3141            .Include(r => r.Situacion)
 3142            .Include(r => r.Programa)
 3143            .Include(r => r.Unidad)
 3144            .Include(r => r.SubUnidad)
 3145            .Include(r => r.MotivoBaja)
 3146            .FirstOrDefaultAsync(r => r.PersonaId == id && r.Estado == EstadoRelacion.Activo);
 147
 3148        if (relacionLaboral == null)
 149        {
 2150            relacionLaboral = await _context.RelacionesLaborales
 2151                .Include(r => r.Persona)
 2152                .Include(r => r.Regimen)
 2153                .Include(r => r.Escalafon)
 2154                .Include(r => r.Grado)
 2155                .Include(r => r.Situacion)
 2156                .Include(r => r.Programa)
 2157                .Include(r => r.Unidad)
 2158                .Include(r => r.SubUnidad)
 2159                .Include(r => r.MotivoBaja)
 2160                .Where(r => r.PersonaId == id)
 2161                .OrderByDescending(r => r.FechaInicio)
 2162                .FirstOrDefaultAsync();
 163        }
 164
 3165        if (relacionLaboral == null)
 1166            return null;
 167
 168        // Obtener todos los ids de relación laboral de la persona y luego el historial de movimientos para cualquiera d
 2169        var relacionIds = await _context.RelacionesLaborales
 2170            .Where(r => r.PersonaId == relacionLaboral.PersonaId)
 2171            .Select(r => r.Id)
 2172            .ToListAsync();
 173
 2174        var historial = await _context.MovimientosLaborales
 2175            .Include(m => m.TipoMovimiento)
 2176            .Include(m => m.Usuario)
 2177                .ThenInclude(u => u.Persona)
 2178            .Where(m => relacionIds.Contains(m.RelacionLaboralId))
 2179            .OrderByDescending(m => m.Id)
 2180            .Select(m => new HistorialMovimientoDto
 2181            {
 2182                Id = m.Id,
 2183                TipoMovimiento = m.TipoMovimiento.Nombre,
 2184                EsAlta = m.TipoMovimiento.EsAlta,
 2185                FechaMovimiento = m.FechaMovimiento,
 2186                Observaciones = m.Observaciones,
 2187                RegistradoPor = m.Usuario != null ? m.Usuario.Username : null,
 2188                FechaRegistro = m.FechaMovimiento
 2189            })
 2190            .ToListAsync();
 191
 2192        return new PersonalDetalleDto
 2193        {
 2194            Id = relacionLaboral.Id,
 2195            DatosPersonales = new DatosPersonalesDto
 2196            {
 2197                PersonaId = relacionLaboral.PersonaId,
 2198                Ci = relacionLaboral.Persona.Cedula,
 2199                PrimerNombre = relacionLaboral.Persona.PrimerNombre,
 2200                SegundoNombre = relacionLaboral.Persona.SegundoNombre,
 2201                PrimerApellido = relacionLaboral.Persona.PrimerApellido,
 2202                SegundoApellido = relacionLaboral.Persona.SegundoApellido,
 2203                FechaNacimiento = relacionLaboral.Persona.FechaNacimiento,
 2204                Email = relacionLaboral.Persona.Email,
 2205                Telefono = relacionLaboral.Persona.Telefono,
 2206                Direccion = relacionLaboral.Persona.Direccion,
 2207                CodigoPostal = relacionLaboral.Persona.CodigoPostal,
 2208                Departamento = relacionLaboral.Persona.Departamento,
 2209                Localidad = relacionLaboral.Persona.Localidad
 2210            },
 2211            DatosLaborales = new DatosLaboralesDto
 2212            {
 2213                RelacionLaboralId = relacionLaboral.Id,
 2214                Estado = relacionLaboral.Estado.ToString(),
 2215                FechaInicio = relacionLaboral.FechaInicio,
 2216                FechaFin = relacionLaboral.FechaFin,
 2217                RegimenId = relacionLaboral.RegimenId,
 2218                Regimen = relacionLaboral.Regimen.Denominacion,
 2219                Ley = relacionLaboral.Regimen.NumeroLey,
 2220                EscalafonId = relacionLaboral.EscalafonId,
 2221                Escalafon = relacionLaboral.Escalafon.Denominacion,
 2222                GradoId = relacionLaboral.GradoId,
 2223                Grado = relacionLaboral.Grado.Denominacion,
 2224                SituacionId = relacionLaboral.SituacionId,
 2225                Situacion = relacionLaboral.Situacion.Denominacion,
 2226                ProgramaId = relacionLaboral.ProgramaId,
 2227                Programa = relacionLaboral.Programa.Denominacion,
 2228                UnidadId = relacionLaboral.UnidadId,
 2229                Unidad = relacionLaboral.Unidad.Denominacion,
 2230                SubUnidadId = relacionLaboral.SubUnidadId,
 2231                SubUnidad = relacionLaboral.SubUnidad?.Denominacion,
 2232                MotivoBajaId = relacionLaboral.MotivoBajaId,
 2233                MotivoBaja = relacionLaboral.MotivoBaja?.Denominacion,
 2234                RiesgoVuelo = relacionLaboral.RiesgoVuelo,
 2235                PrimaTecnica = relacionLaboral.PrimaTecnica,
 2236                PrimaSolidariaFamiliar = relacionLaboral.PrimaSolidariaFamiliar,
 2237                AniosInactivos = relacionLaboral.AniosInactivos,
 2238                Observaciones = relacionLaboral.Observaciones
 2239            },
 2240            Historial = historial
 2241        };
 3242    }
 243
 244    public async Task<(bool success, long? personalId, string? error)> CreatePersonalAsync(CreatePersonalRequest request
 245    {
 246        try
 247        {
 248            // Validar y limpiar CI
 4249            var cedulaLimpia = CedulaValidator.Limpiar(request.Ci);
 4250            if (!CedulaValidator.Validar(cedulaLimpia))
 251            {
 1252                return (false, null, "La cédula ingresada no es válida.");
 253            }
 254
 255            // Validar que la fecha de ingreso no sea futura
 3256            var errorFechaIngreso = FechaIngresoValidator.ObtenerErrorSiEsFutura(request.FechaIngreso);
 3257            if (errorFechaIngreso != null)
 258            {
 1259                return (false, null, errorFechaIngreso);
 260            }
 261
 262            // Validar que no exista una relación laboral activa para esa cédula
 2263            var validadorRelacion = new RelacionLaboralValidator(_context);
 2264            var errorRelacionActiva = await validadorRelacion.ObtenerErrorSiExisteRelacionActiva(cedulaLimpia);
 2265            if (errorRelacionActiva != null)
 266            {
 1267                return (false, null, errorRelacionActiva);
 268            }
 269
 270            // Validar que el grado pertenece al escalafón indicado
 1271            var escalafonId = request.EscalafonId ?? 1;
 1272            var grado = await _context.Grados.FindAsync(request.GradoId);
 1273            if (grado == null)
 0274                return (false, null, "El grado seleccionado no existe.");
 275            // TODO: reactivar validación cuando el frontend envíe EscalafonId correctamente
 276            // if (grado.EscalafonId != escalafonId)
 277            //     return (false, null, "El grado seleccionado no pertenece al escalafón indicado.");
 278
 279            // Validar fecha de nacimiento
 1280            if (request.FechaNacimiento != default)
 281            {
 1282                var fechaNac = request.FechaNacimiento.Date;
 1283                if (fechaNac > DateTime.UtcNow.Date)
 0284                    return (false, null, "La fecha de nacimiento no puede ser futura.");
 1285                if (fechaNac < new DateTime(1940, 1, 1))
 0286                    return (false, null, "La fecha de nacimiento no puede ser anterior a 01/01/1940.");
 287            }
 288
 289            // Buscar o crear persona
 1290            Persona? persona = await _context.Personas.FirstOrDefaultAsync(p => p.Cedula == cedulaLimpia);
 291
 1292            if (persona == null)
 293            {
 294                // Crear persona - Convertir fechas a UTC
 1295                var fechaNacimientoUtc = request.FechaNacimiento.Kind == DateTimeKind.Unspecified
 1296                    ? DateTime.SpecifyKind(request.FechaNacimiento.Date, DateTimeKind.Utc)
 1297                    : request.FechaNacimiento.Date.ToUniversalTime();
 298
 1299                persona = new Persona
 1300                {
 1301                    Cedula = cedulaLimpia,
 1302                    PrimerNombre = request.PrimerNombre,
 1303                    SegundoNombre = request.SegundoNombre,
 1304                    PrimerApellido = request.PrimerApellido,
 1305                    SegundoApellido = request.SegundoApellido,
 1306                    FechaNacimiento = fechaNacimientoUtc,
 1307                    Email = request.Email,
 1308                    Telefono = request.Telefono,
 1309                    Direccion = request.Direccion,
 1310                    CodigoPostal = request.CodigoPostal,
 1311                    Departamento = request.Departamento,
 1312                    Localidad = request.Localidad
 1313                };
 314
 1315                _context.Personas.Add(persona);
 1316                await _context.SaveChangesAsync();
 317            }
 318            else
 319            {
 320                // Persona ya existe: actualizar campos de dirección si vienen en el request
 0321                bool personaModificada = false;
 0322                if (request.CodigoPostal != null) { persona.CodigoPostal = request.CodigoPostal; personaModificada = tru
 0323                if (request.Departamento != null) { persona.Departamento = request.Departamento; personaModificada = tru
 0324                if (request.Localidad != null) { persona.Localidad = request.Localidad; personaModificada = true; }
 0325                if (personaModificada)
 326                {
 0327                    persona.FechaActualizacion = DateTime.UtcNow;
 0328                    _context.Personas.Update(persona);
 0329                    await _context.SaveChangesAsync();
 330                }
 331            }
 332
 333            // Determinar régimen: explícito si se pasa, sino auto-calcular
 334            long regimenId;
 1335            if (!string.IsNullOrWhiteSpace(request.Ley))
 336            {
 0337                var regimenExplicito = await _context.Regimenes
 0338                    .FirstOrDefaultAsync(r => r.NumeroLey == request.Ley && r.Vigente);
 0339                if (regimenExplicito == null)
 0340                    return (false, null, $"No se encontró el régimen con ley {request.Ley}");
 0341                regimenId = regimenExplicito.Id;
 342            }
 343            else
 344            {
 1345                regimenId = await DeterminarRegimenIdAsync(request.FechaIngreso, request.EscalafonId);
 346            }
 347
 348            // Crear relación laboral - Convertir fechas a UTC
 1349            var fechaIngresoUtc = request.FechaIngreso.Kind == DateTimeKind.Unspecified
 1350                ? DateTime.SpecifyKind(request.FechaIngreso.Date, DateTimeKind.Utc)
 1351                : request.FechaIngreso.Date.ToUniversalTime();
 352
 1353            var relacionLaboral = new RelacionLaboral
 1354            {
 1355                PersonaId = persona.Id,
 1356                RegimenId = regimenId,
 1357                EscalafonId = request.EscalafonId ?? 1,
 1358                GradoId = request.GradoId,
 1359                SituacionId = request.SituacionId ?? 1,
 1360                ProgramaId = request.ProgramaId ?? 1,
 1361                UnidadId = request.UnidadId ?? 1,
 1362                SubUnidadId = request.SubUnidadId,
 1363                Estado = EstadoRelacion.Activo,
 1364                FechaInicio = fechaIngresoUtc,
 1365                Observaciones = request.Observaciones
 1366            };
 367
 1368            _context.RelacionesLaborales.Add(relacionLaboral);
 1369            await _context.SaveChangesAsync();
 370
 371            // Al dar de alta, asignar automáticamente Prima Solidaria Familiar como "Sin hijos"
 1372            var tipoPrimaSolidaria = await _context.TiposBeneficio
 1373                .FirstOrDefaultAsync(t => t.Codigo == "PRIMA_SOLIDARIA_FAMILIAR" && t.Activo);
 1374            if (tipoPrimaSolidaria != null)
 375            {
 0376                var primaSolidaria = new BeneficioSocial
 0377                {
 0378                    PersonaId = persona.Id,
 0379                    TipoBeneficioId = tipoPrimaSolidaria.Id,
 0380                    ClaveEvento = "SIN_HIJOS",
 0381                    VigenteDesde = fechaIngresoUtc,
 0382                    Estado = "activo",
 0383                    RetroactividadPendiente = false,
 0384                    Cantidad = 1,
 0385                    CreadoPor = _currentUser.UserId
 0386                };
 0387                _context.BeneficiosSociales.Add(primaSolidaria);
 0388                await _context.SaveChangesAsync();
 389            }
 390
 1391            await _auditoriaService.LogAuditoriaAsync(
 1392                usuarioId: _currentUser.UserId ?? 0,
 1393                AccionEnum.CrearPersonal,
 1394                ContextoEnum.Personal,
 1395                host: _currentUser.Host,
 1396                entidad: nameof(Persona),
 1397                entidadId: persona.Id,
 1398                detalle: new { relacionLaboralId = relacionLaboral.Id, personaId = persona.Id, cedula = persona.Cedula }
 399
 1400            return (true, relacionLaboral.Id, null);
 401        }
 0402        catch (Exception ex)
 403        {
 0404            _logger.LogError(ex, "Error al crear personal");
 0405            var errorMessage = ex.Message;
 0406            var innerEx = ex.InnerException;
 0407            while (innerEx != null)
 408            {
 0409                errorMessage += $" | {innerEx.Message}";
 0410                innerEx = innerEx.InnerException;
 411            }
 0412            return (false, null, $"Error al crear personal: {errorMessage}");
 413        }
 4414    }
 415
 416    public async Task<(bool success, string? error)> UpdatePersonalAsync(long id, UpdatePersonalRequest request)
 417    {
 418        try
 419        {
 3420            var persona = await _context.Personas.FindAsync(id);
 3421            if (persona == null)
 1422                return (false, "Persona no encontrada");
 423
 424            // Validar campos de compensación si se proporcionan
 2425            var errorCompensacion = CompensacionValidator.ValidarCompensaciones(
 2426                request.RiesgoVuelo,
 2427                request.PrimaTecnica,
 2428                request.PrimaSolidariaFamiliar,
 2429                request.AniosInactivos);
 430
 2431            if (errorCompensacion != null)
 432            {
 0433                return (false, errorCompensacion);
 434            }
 435
 436            // Actualizar datos personales
 2437            if (!string.IsNullOrWhiteSpace(request.PrimerNombre))
 1438                persona.PrimerNombre = request.PrimerNombre.Trim();
 439
 2440            if (request.SegundoNombre != null)
 0441                persona.SegundoNombre = string.IsNullOrWhiteSpace(request.SegundoNombre) ? null : request.SegundoNombre.
 442
 2443            if (!string.IsNullOrWhiteSpace(request.PrimerApellido))
 1444                persona.PrimerApellido = request.PrimerApellido.Trim();
 445
 2446            if (request.SegundoApellido != null)
 0447                persona.SegundoApellido = string.IsNullOrWhiteSpace(request.SegundoApellido) ? null : request.SegundoApe
 448
 2449            if (request.FechaNacimiento.HasValue)
 450            {
 0451                var fechaNac = request.FechaNacimiento.Value;
 0452                persona.FechaNacimiento = fechaNac.Kind == DateTimeKind.Unspecified
 0453                    ? DateTime.SpecifyKind(fechaNac.Date, DateTimeKind.Utc)
 0454                    : fechaNac.Date.ToUniversalTime();
 455            }
 456
 2457            if (!string.IsNullOrWhiteSpace(request.Email))
 1458                persona.Email = request.Email.Trim();
 459
 2460            if (request.Telefono != null)
 0461                persona.Telefono = string.IsNullOrWhiteSpace(request.Telefono) ? null : request.Telefono.Trim();
 462
 2463            if (request.Direccion != null)
 0464                persona.Direccion = string.IsNullOrWhiteSpace(request.Direccion) ? null : request.Direccion.Trim();
 465
 2466            if (request.CodigoPostal != null)
 0467                persona.CodigoPostal = string.IsNullOrWhiteSpace(request.CodigoPostal) ? null : request.CodigoPostal.Tri
 468
 2469            if (request.Departamento != null)
 0470                persona.Departamento = string.IsNullOrWhiteSpace(request.Departamento) ? null : request.Departamento.Tri
 471
 2472            if (request.Localidad != null)
 0473                persona.Localidad = string.IsNullOrWhiteSpace(request.Localidad) ? null : request.Localidad.Trim();
 474
 2475            if (persona.FechaNacimiento.HasValue)
 476            {
 1477                var fn = persona.FechaNacimiento.Value;
 1478                persona.FechaNacimiento = fn.Kind == DateTimeKind.Unspecified
 1479                    ? DateTime.SpecifyKind(fn.Date, DateTimeKind.Utc)
 1480                    : fn.ToUniversalTime();
 481            }
 482
 2483            persona.FechaActualizacion = DateTime.UtcNow;
 2484            _context.Personas.Update(persona);
 485
 486            // Buscar relación activa para la persona
 2487            var relacionActiva = await _context.RelacionesLaborales
 2488                .FirstOrDefaultAsync(r => r.PersonaId == persona.Id && r.Estado == EstadoRelacion.Activo);
 489
 490            // Obtener la última relación (puede ser null)
 2491            var ultimaRelacion = await _context.RelacionesLaborales
 2492                .Where(r => r.PersonaId == persona.Id)
 2493                .OrderByDescending(r => r.FechaInicio)
 2494                .FirstOrDefaultAsync();
 495
 496            // Validar transición de estado usando la relación más relevante (activa si existe, sino la última)
 2497            var relacionParaValidacion = relacionActiva ?? ultimaRelacion;
 498
 2499            if (!string.IsNullOrWhiteSpace(request.Estado) && relacionParaValidacion != null)
 500            {
 0501                var errorTransicion = EstadoTransicionValidator.ObtenerErrorSiTransicionInvalida(
 0502                    relacionParaValidacion.Estado.ToString(),
 0503                    request.Estado);
 0504                if (errorTransicion != null)
 0505                    return (false, errorTransicion);
 506            }
 507
 508            // Determinar si se solicitaron cambios laborales
 2509            bool cambioLaboralSolicitado = request.GradoId.HasValue || request.RegimenId.HasValue || request.EscalafonId
 2510                request.SituacionId.HasValue || request.ProgramaId.HasValue || request.UnidadId.HasValue || request.SubU
 511
 2512            if (relacionActiva != null)
 513            {
 514                // Si hay relación activa, verificar si los cambios requieren crear una nueva relación
 2515                bool requiereNueva = false;
 3516                if (request.GradoId.HasValue && request.GradoId.Value != relacionActiva.GradoId) requiereNueva = true;
 2517                if (request.RegimenId.HasValue && request.RegimenId.Value != relacionActiva.RegimenId) requiereNueva = t
 2518                if (request.EscalafonId.HasValue && request.EscalafonId.Value != relacionActiva.EscalafonId) requiereNue
 2519                if (request.SituacionId.HasValue && request.SituacionId.Value != relacionActiva.SituacionId) requiereNue
 2520                if (request.ProgramaId.HasValue && request.ProgramaId.Value != relacionActiva.ProgramaId) requiereNueva 
 2521                if (request.UnidadId.HasValue && request.UnidadId.Value != relacionActiva.UnidadId) requiereNueva = true
 2522                if (request.SubUnidadId.HasValue && request.SubUnidadId.Value != relacionActiva.SubUnidadId) requiereNue
 523
 2524                if (requiereNueva)
 525                {
 526                    // Cerrar relación activa
 1527                    relacionActiva.FechaFin = DateTime.UtcNow;
 1528                    relacionActiva.Estado = EstadoRelacion.Inactivo;
 1529                    relacionActiva.FechaActualizacion = DateTime.UtcNow;
 530
 531                    // Crear nueva relación activa con los valores actualizados
 1532                    var nuevaRelacion = new RelacionLaboral
 1533                    {
 1534                        PersonaId = persona.Id,
 1535                        RegimenId = request.RegimenId ?? relacionActiva.RegimenId,
 1536                        EscalafonId = request.EscalafonId ?? relacionActiva.EscalafonId,
 1537                        GradoId = request.GradoId ?? relacionActiva.GradoId,
 1538                        SituacionId = request.SituacionId ?? relacionActiva.SituacionId,
 1539                        ProgramaId = request.ProgramaId ?? relacionActiva.ProgramaId,
 1540                        UnidadId = request.UnidadId ?? relacionActiva.UnidadId,
 1541                        SubUnidadId = request.SubUnidadId ?? relacionActiva.SubUnidadId,
 1542                        Estado = EstadoRelacion.Activo,
 1543                        FechaInicio = DateTime.UtcNow,
 1544                        Observaciones = request.Observaciones ?? relacionActiva.Observaciones,
 1545                        FechaActualizacion = DateTime.UtcNow,
 1546                        RiesgoVuelo = request.RiesgoVuelo ?? relacionActiva.RiesgoVuelo,
 1547                        PrimaTecnica = string.IsNullOrWhiteSpace(request.PrimaTecnica) ? relacionActiva.PrimaTecnica : r
 1548                        PrimaSolidariaFamiliar = string.IsNullOrWhiteSpace(request.PrimaSolidariaFamiliar) ? relacionAct
 1549                        AniosInactivos = request.AniosInactivos ?? relacionActiva.AniosInactivos
 1550                    };
 551
 1552                    _context.RelacionesLaborales.Add(nuevaRelacion);
 553                }
 554                else
 555                {
 556                    // Actualizar en sitio la relación activa
 1557                    if (!string.IsNullOrWhiteSpace(request.Estado))
 558                    {
 0559                        if (Enum.TryParse<EstadoRelacion>(request.Estado, true, out var est))
 0560                            relacionActiva.Estado = est;
 561                        else
 562                        {
 0563                            var estLower = request.Estado.Trim().ToLower();
 0564                            if (estLower == "activo") relacionActiva.Estado = EstadoRelacion.Activo;
 0565                            else if (estLower == "inactivo") relacionActiva.Estado = EstadoRelacion.Inactivo;
 566                        }
 567                    }
 568
 1569                    if (request.RegimenId.HasValue) relacionActiva.RegimenId = request.RegimenId.Value;
 1570                    if (request.EscalafonId.HasValue) relacionActiva.EscalafonId = request.EscalafonId.Value;
 1571                    if (request.GradoId.HasValue) relacionActiva.GradoId = request.GradoId.Value;
 1572                    if (request.SituacionId.HasValue) relacionActiva.SituacionId = request.SituacionId.Value;
 1573                    if (request.ProgramaId.HasValue) relacionActiva.ProgramaId = request.ProgramaId.Value;
 1574                    if (request.UnidadId.HasValue) relacionActiva.UnidadId = request.UnidadId.Value;
 1575                    if (request.SubUnidadId.HasValue) relacionActiva.SubUnidadId = request.SubUnidadId.Value;
 576
 1577                    if (request.RiesgoVuelo.HasValue) relacionActiva.RiesgoVuelo = request.RiesgoVuelo;
 1578                    if (request.PrimaTecnica != null) relacionActiva.PrimaTecnica = string.IsNullOrWhiteSpace(request.Pr
 1579                    if (request.PrimaSolidariaFamiliar != null) relacionActiva.PrimaSolidariaFamiliar = string.IsNullOrW
 1580                    if (request.AniosInactivos.HasValue) relacionActiva.AniosInactivos = request.AniosInactivos;
 1581                    if (request.Observaciones != null) relacionActiva.Observaciones = request.Observaciones;
 582
 1583                    relacionActiva.FechaActualizacion = DateTime.UtcNow;
 584                }
 585            }
 586            else
 587            {
 588                // No hay relación activa
 0589                if (cambioLaboralSolicitado)
 590                {
 591                    // Crear nueva relación activa basándose en la última relación si existe
 0592                    var plantilla = ultimaRelacion;
 593
 0594                    var nuevaRelacion = new RelacionLaboral
 0595                    {
 0596                        PersonaId = persona.Id,
 0597                        RegimenId = request.RegimenId ?? plantilla?.RegimenId ?? 0,
 0598                        EscalafonId = request.EscalafonId ?? plantilla?.EscalafonId ?? 0,
 0599                        GradoId = request.GradoId ?? plantilla?.GradoId ?? 0,
 0600                        SituacionId = request.SituacionId ?? plantilla?.SituacionId ?? 0,
 0601                        ProgramaId = request.ProgramaId ?? plantilla?.ProgramaId ?? 0,
 0602                        UnidadId = request.UnidadId ?? plantilla?.UnidadId ?? 0,
 0603                        SubUnidadId = request.SubUnidadId ?? plantilla?.SubUnidadId,
 0604                        Estado = EstadoRelacion.Activo,
 0605                        FechaInicio = DateTime.UtcNow,
 0606                        Observaciones = request.Observaciones ?? plantilla?.Observaciones,
 0607                        FechaActualizacion = DateTime.UtcNow,
 0608                        RiesgoVuelo = request.RiesgoVuelo ?? plantilla?.RiesgoVuelo,
 0609                        PrimaTecnica = string.IsNullOrWhiteSpace(request.PrimaTecnica) ? plantilla?.PrimaTecnica : reque
 0610                        PrimaSolidariaFamiliar = string.IsNullOrWhiteSpace(request.PrimaSolidariaFamiliar) ? plantilla?.
 0611                        AniosInactivos = request.AniosInactivos ?? plantilla?.AniosInactivos
 0612                    };
 613
 0614                    _context.RelacionesLaborales.Add(nuevaRelacion);
 615                }
 616                else
 617                {
 618                    // Solo actualizar estado u observaciones de la última relación si existe
 0619                    if (ultimaRelacion != null)
 620                    {
 0621                        if (!string.IsNullOrWhiteSpace(request.Estado))
 622                        {
 0623                            if (Enum.TryParse<EstadoRelacion>(request.Estado, true, out var est))
 0624                                ultimaRelacion.Estado = est;
 625                            else
 626                            {
 0627                                var estLower = request.Estado.Trim().ToLower();
 0628                                if (estLower == "activo") ultimaRelacion.Estado = EstadoRelacion.Activo;
 0629                                else if (estLower == "inactivo") ultimaRelacion.Estado = EstadoRelacion.Inactivo;
 630                            }
 631                        }
 632
 0633                        if (request.Observaciones != null) ultimaRelacion.Observaciones = request.Observaciones;
 0634                        ultimaRelacion.FechaActualizacion = DateTime.UtcNow;
 635                    }
 636                }
 637            }
 638
 2639            await _context.SaveChangesAsync();
 640
 2641            return (true, null);
 642        }
 0643        catch (Exception ex)
 644        {
 0645            _logger.LogError(ex, "Error al actualizar personal");
 0646            return (false, $"Error al actualizar personal: {ex.Message}");
 647        }
 3648    }
 649
 650    public async Task<(bool success, string? error)> RegistrarBajaAsync(long id, RegistrarBajaRequest request)
 651    {
 652        try
 653        {
 654            // id is PersonaId: buscar la relación laboral activa de la persona
 4655            var relacionLaboral = await _context.RelacionesLaborales
 4656                .FirstOrDefaultAsync(r => r.PersonaId == id && r.Estado == EstadoRelacion.Activo);
 657
 4658            if (relacionLaboral == null)
 659            {
 1660                return (false, "Relación laboral no encontrada");
 661            }
 662
 663            // Validar y obtener el MotivoBaja (catálogo UCU)
 3664            if (request.MotivoBajaId <= 0)
 1665                return (false, "El motivo de baja (catálogo) es requerido");
 666
 2667            var motivoBaja = await _motivoBajaRepository.GetByIdAsync(request.MotivoBajaId);
 2668            if (motivoBaja == null)
 0669                return (false, "Motivo de baja (catálogo) no encontrado o no vigente");
 670
 671            // Determinar TipoMovimiento automáticamente según el motivo:
 672            //   RETIRO_OBL / RETIRO_VOL â†’ "Retiro" | resto â†’ "Baja"
 2673            var nombreTipo = motivoBaja.Codigo is "RETIRO_OBL" or "RETIRO_VOL" ? "Retiro" : "Baja";
 2674            var tipoMovimiento = await _context.TiposMovimiento
 2675                .FirstOrDefaultAsync(t => t.Nombre == nombreTipo && !t.EsAlta);
 676
 2677            if (tipoMovimiento == null)
 1678                return (false, $"Tipo de movimiento '{nombreTipo}' no encontrado en el catálogo");
 679
 680            // Validar que la fecha de baja no sea futura
 1681            var errorFechaBaja = FechaIngresoValidator.ObtenerErrorSiEsFutura(request.FechaBaja);
 1682            if (errorFechaBaja != null)
 683            {
 0684                return (false, errorFechaBaja);
 685            }
 686
 687            // Validar fecha de baja vs último movimiento y fecha de inicio
 1688            var validadorMovimiento = new MovimientoLaboralValidator(_context);
 1689            var errorFechaMovimiento = await validadorMovimiento.ValidarFechaMovimiento(relacionLaboral.Id, request.Fech
 1690            if (errorFechaMovimiento != null)
 691            {
 0692                return (false, errorFechaMovimiento);
 693            }
 694
 1695            var errorFechaBajaVsMovimiento = await validadorMovimiento.ValidarFechaBajaVsUltimoMovimiento(relacionLabora
 1696            if (errorFechaBajaVsMovimiento != null)
 697            {
 0698                return (false, errorFechaBajaVsMovimiento);
 699            }
 700
 701            // Validar transición de estado: debe ir de activo a inactivo
 1702            var errorTransicion = EstadoTransicionValidator.ObtenerErrorSiTransicionInvalida(
 1703                relacionLaboral.Estado.ToString(),
 1704                "Inactivo");
 705
 1706            if (errorTransicion != null)
 707            {
 0708                return (false, errorTransicion);
 709            }
 710
 711            // Cambiar estado a inactivo
 1712            relacionLaboral.Estado = EstadoRelacion.Inactivo;
 1713            var fechaBajaUtc = request.FechaBaja.Kind == DateTimeKind.Unspecified
 1714                ? DateTime.SpecifyKind(request.FechaBaja.Date, DateTimeKind.Utc)
 1715                : request.FechaBaja.Date.ToUniversalTime();
 1716            relacionLaboral.FechaFin = fechaBajaUtc;
 1717            relacionLaboral.MotivoBajaId = request.MotivoBajaId;
 1718            relacionLaboral.FechaActualizacion = DateTime.UtcNow;
 719
 720            // Registrar movimiento de baja
 1721            var movimiento = new MovimientoLaboral
 1722            {
 1723                RelacionLaboralId = relacionLaboral.Id,
 1724                TipoMovimientoId = tipoMovimiento.Id,
 1725                FechaMovimiento = fechaBajaUtc,
 1726                UsuarioId = _currentUser.UserId ?? 0,
 1727                Observaciones = request.Observaciones
 1728            };
 729
 1730            _context.MovimientosLaborales.Add(movimiento);
 1731            await _context.SaveChangesAsync();
 732
 1733            await _auditoriaService.LogAuditoriaAsync(
 1734                usuarioId: _currentUser.UserId ?? 0,
 1735                AccionEnum.BajaPersonal,
 1736                ContextoEnum.Personal,
 1737                host: _currentUser.Host,
 1738                entidad: nameof(RelacionLaboral),
 1739                entidadId: relacionLaboral.Id,
 1740                detalle: new { personaId = id, relacionLaboralId = relacionLaboral.Id, request });
 741
 1742            return (true, null);
 743        }
 0744        catch (Exception ex)
 745        {
 0746            _logger.LogError(ex, "Error al registrar baja");
 0747            return (false, $"Error al registrar baja: {ex.Message}");
 748        }
 4749    }
 750
 751    public async Task<(bool success, string? error)> RegistrarAscensoAsync(long id, AscensoRequest request)
 752    {
 753        try
 754        {
 5755            var relacionLaboral = await _context.RelacionesLaborales
 5756                .FirstOrDefaultAsync(r => r.PersonaId == id && r.Estado == EstadoRelacion.Activo);
 757
 5758            if (relacionLaboral == null)
 1759                return (false, "Relación laboral no encontrada");
 760
 761            // Validar grado objetivo
 4762            var grado = await _context.Grados.FirstOrDefaultAsync(g => g.Id == request.NuevoGradoId && g.Vigente);
 4763            if (grado == null)
 1764                return (false, "Grado no encontrado");
 765
 766            // Calcular régimen automáticamente según fecha de ingreso y escalafón (igual que en el Alta)
 3767            var regimenAscensoId = await DeterminarRegimenIdAsync(relacionLaboral.FechaInicio, relacionLaboral.Escalafon
 3768            var regimenAscenso = await _context.Regimenes.FindAsync(regimenAscensoId);
 3769            if (regimenAscenso == null)
 0770                return (false, "No se pudo determinar el régimen para el ascenso");
 771
 772            // Validar que el grado objetivo
 3773            if (relacionLaboral.GradoId != 0)
 774            {
 3775                var gradoActual = await _context.Grados.FirstOrDefaultAsync(g => g.Id == relacionLaboral.GradoId);
 3776                if (gradoActual != null)
 777                {
 3778                    if (grado.Orden <= gradoActual.Orden)
 779                    {
 1780                        return (false, "El grado objetivo debe ser superior al grado actual");
 781                    }
 782                }
 783            }
 784
 2785            var fechaAscensoUtc = request.FechaAscenso.Kind == DateTimeKind.Unspecified
 2786                ? DateTime.SpecifyKind(request.FechaAscenso.Date, DateTimeKind.Utc)
 2787                : request.FechaAscenso.Date.ToUniversalTime();
 788
 789            // Validar unidad objetivo si se proporciona
 790            Unidad? unidadObj = null;
 2791            if (request.UnidadId.HasValue)
 792            {
 0793                unidadObj = await _context.Unidades.FirstOrDefaultAsync(u => u.Id == request.UnidadId.Value && u.Vigente
 0794                if (unidadObj == null)
 795                {
 0796                    return (false, "Unidad no válida o no vigente");
 797                }
 798            }
 799
 800            // Cerrar la relación laboral actual
 801            // Validar transición: debe estar activo para poder ascender
 2802            var errorTransicion = EstadoTransicionValidator.ObtenerErrorSiTransicionInvalida(
 2803                relacionLaboral.Estado.ToString(),
 2804                "Inactivo");
 805
 2806            if (errorTransicion != null)
 807            {
 0808                return (false, errorTransicion);
 809            }
 810
 2811            relacionLaboral.FechaFin = fechaAscensoUtc;
 2812            relacionLaboral.FechaActualizacion = DateTime.UtcNow;
 2813            relacionLaboral.Estado = EstadoRelacion.Inactivo;
 2814            relacionLaboral.Observaciones = request.Observacion;
 815
 816            // Validar programa objetivo
 2817            var programaAscenso = await _context.Programas.FirstOrDefaultAsync(p => p.Id == request.ProgramaId && p.Vige
 2818            if (programaAscenso == null)
 1819                return (false, "Programa no válido o no vigente");
 820
 821            // Determinar prima técnica: al ascender a Sgto. 1° se desactiva la prima técnica variable
 1822            var primaTecnicaNueva = relacionLaboral.PrimaTecnica;
 1823            if (grado.Codigo == "SGTO_1RO")
 0824                primaTecnicaNueva = "VACIO";
 825
 826            // Crear nueva relación laboral con el nuevo grado
 1827            var nuevaRelacion = new RelacionLaboral
 1828            {
 1829                PersonaId = relacionLaboral.PersonaId,
 1830                RegimenId = regimenAscenso.Id,
 1831                EscalafonId = relacionLaboral.EscalafonId,
 1832                GradoId = request.NuevoGradoId,
 1833                SituacionId = relacionLaboral.SituacionId,
 1834                ProgramaId = request.ProgramaId,
 1835                UnidadId = request.UnidadId ?? relacionLaboral.UnidadId,
 1836                SubUnidadId = request.SubUnidadId ?? relacionLaboral.SubUnidadId,
 1837                Estado = EstadoRelacion.Activo,
 1838                FechaInicio = fechaAscensoUtc,
 1839                PrimaTecnica = primaTecnicaNueva,
 1840                PrimaSolidariaFamiliar = relacionLaboral.PrimaSolidariaFamiliar,
 1841                RiesgoVuelo = relacionLaboral.RiesgoVuelo,
 1842                Observaciones = relacionLaboral.Observaciones,
 1843                FechaActualizacion = DateTime.UtcNow
 1844            };
 845
 1846            _context.RelacionesLaborales.Add(nuevaRelacion);
 847
 848            // Registrar movimientos
 849            // Prefer specific tipo_movimiento ids: 6 = Cambio de Situacion (cierre), 7 = Ascenso (alta)
 1850            var tipoCierre = await _context.TiposMovimiento.FirstOrDefaultAsync(t => t.Nombre.ToLower().Contains("cambio
 1851            var tipoAlta = await _context.TiposMovimiento.FirstOrDefaultAsync(t => t.Nombre.ToLower().Contains("ascenso"
 852
 1853            if (tipoCierre == null)
 854            {
 1855                tipoCierre = new Entidades.TipoMovimiento { Nombre = "Cambio de Situacion", EsAlta = false };
 1856                _context.TiposMovimiento.Add(tipoCierre);
 1857                await _context.SaveChangesAsync();
 858            }
 859
 1860            if (tipoAlta == null)
 861            {
 1862                tipoAlta = new Entidades.TipoMovimiento { Nombre = "Ascenso", EsAlta = true };
 1863                _context.TiposMovimiento.Add(tipoAlta);
 1864                await _context.SaveChangesAsync();
 865            }
 866
 867            // Movimiento de cierre
 1868            var movimientoCierre = new Entidades.MovimientoLaboral
 1869            {
 1870                RelacionLaboralId = relacionLaboral.Id,
 1871                TipoMovimientoId = tipoCierre.Id,
 1872                FechaMovimiento = fechaAscensoUtc,
 1873                UsuarioId = _currentUser.UserId ?? 0,
 1874                Observaciones = request.Observacion,
 1875                Puesto = request.Puesto,
 1876                Plaza = request.Plaza
 1877            };
 878
 1879            _context.MovimientosLaborales.Add(movimientoCierre);
 880
 1881            await _context.SaveChangesAsync();
 882
 1883            var movimientoAlta = new Entidades.MovimientoLaboral
 1884            {
 1885                RelacionLaboralId = nuevaRelacion.Id,
 1886                TipoMovimientoId = tipoAlta.Id,
 1887                FechaMovimiento = fechaAscensoUtc,
 1888                UsuarioId = _currentUser.UserId ?? 0,
 1889                Observaciones = request.Observacion,
 1890                Puesto = request.Puesto,
 1891                Plaza = request.Plaza
 1892            };
 893
 1894            _context.MovimientosLaborales.Add(movimientoAlta);
 895
 1896            await _context.SaveChangesAsync();
 897
 1898            await _auditoriaService.LogAuditoriaAsync(
 1899                usuarioId: _currentUser.UserId ?? 0,
 1900                AccionEnum.AscensoPersonal,
 1901                ContextoEnum.Personal,
 1902                host: _currentUser.Host,
 1903                entidad: nameof(RelacionLaboral),
 1904                entidadId: nuevaRelacion.Id,
 1905                detalle: new { personaId = id, relacionAnteriorId = relacionLaboral.Id, relacionNuevaId = nuevaRelacion.
 906
 1907            return (true, null);
 908        }
 0909        catch (Exception ex)
 910        {
 0911            _logger.LogError(ex, "Error al registrar ascenso");
 0912            return (false, $"Error al registrar ascenso: {ex.Message}");
 913        }
 5914    }
 915
 916    private IQueryable<RelacionLaboral> ApplyOrdering(
 917        IQueryable<RelacionLaboral> query,
 918        string? orderBy,
 919        string sort)
 920    {
 1921        var ascending = sort.ToLower() == "asc";
 922
 1923        return orderBy?.ToLower() switch
 1924        {
 0925            "ci" => ascending
 0926                ? query.OrderBy(r => r.Persona.Cedula)
 0927                : query.OrderByDescending(r => r.Persona.Cedula),
 0928            "nombre" => ascending
 0929                ? query.OrderBy(r => r.Persona.PrimerNombre)
 0930                : query.OrderByDescending(r => r.Persona.PrimerNombre),
 0931            "apellido" => ascending
 0932                ? query.OrderBy(r => r.Persona.PrimerApellido)
 0933                : query.OrderByDescending(r => r.Persona.PrimerApellido),
 0934            "grado" => ascending
 0935                ? query.OrderBy(r => r.Grado.Orden)
 0936                : query.OrderByDescending(r => r.Grado.Orden),
 0937            "fechaingreso" => ascending
 0938                ? query.OrderBy(r => r.FechaInicio)
 0939                : query.OrderByDescending(r => r.FechaInicio),
 1940            _ => query.OrderByDescending(r => r.FechaInicio)
 1941        };
 942    }
 943
 944    #region Métodos Privados
 945
 946    /// <summary>
 947    /// Determina el régimen (Ley Vieja 14.157 / Ley Nueva 19.695) según la lógica del legacy MONTEPIO.PRG.
 948    /// Fecha límite de la reforma: 28/02/2019.
 949    /// - Escalafón AV (Oficiales): >= 15 años al 28/02/2019 â†’ Ley Vieja
 950    /// - Otros escalafones (Subalternos): >= 3 años al 28/02/2019 â†’ Ley Vieja
 951    /// - Ingreso posterior a la reforma â†’ siempre Ley Nueva
 952    /// </summary>
 953    private async Task<long> DeterminarRegimenIdAsync(DateTime fechaIngreso, long? escalafonId)
 954    {
 4955        var fechaReforma = new DateTime(2019, 2, 28);
 956        string numeroLey;
 957
 4958        if (fechaIngreso.Date >= fechaReforma)
 959        {
 4960            numeroLey = "19.695";
 961        }
 962        else
 963        {
 0964            int aniosAlLimite = fechaReforma.Year - fechaIngreso.Year;
 0965            if (fechaIngreso.Date > fechaReforma.AddYears(-aniosAlLimite)) aniosAlLimite--;
 966
 0967            var escalafon = escalafonId.HasValue
 0968                ? await _context.Escalafones.FindAsync(escalafonId.Value)
 0969                : null;
 0970            bool esOficial = escalafon?.Codigo == "AV";
 0971            int umbral = esOficial ? 15 : 3;
 972
 0973            numeroLey = aniosAlLimite >= umbral ? "14.157" : "19.695";
 974        }
 975
 4976        var regimen = await _context.Regimenes
 4977            .FirstOrDefaultAsync(r => r.NumeroLey == numeroLey && r.Vigente);
 978
 979        // Fallback a Ley Nueva si no se encuentra (no debería ocurrir)
 4980        if (regimen == null)
 981        {
 0982            regimen = await _context.Regimenes
 0983                .FirstOrDefaultAsync(r => r.NumeroLey == "19.695" && r.Vigente);
 984        }
 985
 4986        return regimen!.Id;
 4987    }
 988
 989    #endregion
 990
 991    public async Task<List<MovimientoReporteDto>> ObtenerReporteMovimientosAsync(int mes, int anio, string? tipo)
 992    {
 0993        var tipoNorm = tipo?.ToLower();
 994
 995        // Traer movimientos del mes/año con sus relaciones
 0996        var movimientos = await _context.MovimientosLaborales
 0997            .Include(m => m.TipoMovimiento)
 0998            .Include(m => m.RelacionLaboral)
 0999                .ThenInclude(r => r.Persona)
 01000            .Include(m => m.RelacionLaboral)
 01001                .ThenInclude(r => r.Grado)
 01002            .Include(m => m.RelacionLaboral)
 01003                .ThenInclude(r => r.Escalafon)
 01004            .Include(m => m.RelacionLaboral)
 01005                .ThenInclude(r => r.Unidad)
 01006            .Include(m => m.RelacionLaboral)
 01007                .ThenInclude(r => r.Programa)
 01008            .Include(m => m.RelacionLaboral)
 01009                .ThenInclude(r => r.Regimen)
 01010            .Where(m => m.FechaMovimiento.Month == mes && m.FechaMovimiento.Year == anio)
 01011            .OrderBy(m => m.FechaMovimiento)
 01012            .ToListAsync();
 1013
 1014        // Para ascensos: agrupar los movimientos de cierre + alta para emitir un solo renglón
 01015        var resultado = new List<MovimientoReporteDto>();
 01016        var procesados = new HashSet<long>();
 1017
 01018        foreach (var mov in movimientos)
 1019        {
 01020            if (procesados.Contains(mov.Id)) continue;
 1021
 01022            var nombreTipo = mov.TipoMovimiento.Nombre.ToLower();
 1023            string categoriaReporte;
 1024
 01025            if (nombreTipo.Contains("alta por ascenso") || nombreTipo.Contains("ascenso"))
 1026            {
 01027                categoriaReporte = "Ascenso";
 01028                if (tipoNorm != null && tipoNorm != "ascenso" && tipoNorm != "todos") continue;
 1029
 1030                // Buscar el movimiento de cierre del mismo día para obtener grado anterior
 01031                var cierreRelacionado = movimientos.FirstOrDefault(m =>
 01032                    !procesados.Contains(m.Id) &&
 01033                    m.Id != mov.Id &&
 01034                    m.FechaMovimiento.Date == mov.FechaMovimiento.Date &&
 01035                    m.RelacionLaboral.PersonaId == mov.RelacionLaboral.PersonaId &&
 01036                    (m.TipoMovimiento.Nombre.ToLower().Contains("baja por ascenso") ||
 01037                     m.TipoMovimiento.Nombre.ToLower().Contains("cambio de situaci")));
 1038
 01039                var gradoAnterior = cierreRelacionado?.RelacionLaboral.Grado.Denominacion;
 01040                if (cierreRelacionado != null) procesados.Add(cierreRelacionado.Id);
 1041
 01042                resultado.Add(new MovimientoReporteDto
 01043                {
 01044                    TipoMovimiento = categoriaReporte,
 01045                    Grado = mov.RelacionLaboral.Grado.Denominacion,
 01046                    GradoAnterior = gradoAnterior,
 01047                    Escalafon = mov.RelacionLaboral.Escalafon.Denominacion,
 01048                    Cedula = mov.RelacionLaboral.Persona.Cedula,
 01049                    Nombre = mov.RelacionLaboral.Persona.PrimerNombre,
 01050                    Apellido = mov.RelacionLaboral.Persona.PrimerApellido,
 01051                    Unidad = mov.RelacionLaboral.Unidad.Denominacion,
 01052                    Programa = mov.RelacionLaboral.Programa.Denominacion,
 01053                    Ley = mov.RelacionLaboral.Regimen.NumeroLey,
 01054                    FechaMovimiento = mov.FechaMovimiento
 01055                });
 1056            }
 01057            else if (mov.TipoMovimiento.EsAlta &&
 01058                     !nombreTipo.Contains("baja por ascenso") &&
 01059                     !nombreTipo.Contains("cambio de situaci"))
 1060            {
 01061                categoriaReporte = "Alta";
 01062                if (tipoNorm != null && tipoNorm != "alta" && tipoNorm != "todos") continue;
 1063
 01064                resultado.Add(new MovimientoReporteDto
 01065                {
 01066                    TipoMovimiento = categoriaReporte,
 01067                    Grado = mov.RelacionLaboral.Grado.Denominacion,
 01068                    Escalafon = mov.RelacionLaboral.Escalafon.Denominacion,
 01069                    Cedula = mov.RelacionLaboral.Persona.Cedula,
 01070                    Nombre = mov.RelacionLaboral.Persona.PrimerNombre,
 01071                    Apellido = mov.RelacionLaboral.Persona.PrimerApellido,
 01072                    Unidad = mov.RelacionLaboral.Unidad.Denominacion,
 01073                    Programa = mov.RelacionLaboral.Programa.Denominacion,
 01074                    Ley = mov.RelacionLaboral.Regimen.NumeroLey,
 01075                    FechaMovimiento = mov.FechaMovimiento
 01076                });
 1077            }
 01078            else if (!mov.TipoMovimiento.EsAlta &&
 01079                     !nombreTipo.Contains("baja por ascenso") &&
 01080                     !nombreTipo.Contains("cambio de situaci") &&
 01081                     !nombreTipo.Contains("ascenso"))
 1082            {
 01083                categoriaReporte = "Baja";
 01084                if (tipoNorm != null && tipoNorm != "baja" && tipoNorm != "todos") continue;
 1085
 01086                resultado.Add(new MovimientoReporteDto
 01087                {
 01088                    TipoMovimiento = categoriaReporte,
 01089                    Grado = mov.RelacionLaboral.Grado.Denominacion,
 01090                    Escalafon = mov.RelacionLaboral.Escalafon.Denominacion,
 01091                    Cedula = mov.RelacionLaboral.Persona.Cedula,
 01092                    Nombre = mov.RelacionLaboral.Persona.PrimerNombre,
 01093                    Apellido = mov.RelacionLaboral.Persona.PrimerApellido,
 01094                    Unidad = mov.RelacionLaboral.Unidad.Denominacion,
 01095                    Programa = mov.RelacionLaboral.Programa.Denominacion,
 01096                    Ley = mov.RelacionLaboral.Regimen.NumeroLey,
 01097                    FechaMovimiento = mov.FechaMovimiento
 01098                });
 1099            }
 1100
 01101            procesados.Add(mov.Id);
 1102        }
 1103
 01104        return resultado;
 01105    }
 1106}