< 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
89%
Covered lines: 521
Uncovered lines: 63
Coverable lines: 584
Total lines: 935
Line coverage: 89.2%
Branch coverage
71%
Covered branches: 172
Total branches: 240
Branch coverage: 71.6%
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()71.42%141492.68%
GetDetalleAsync()75%44100%
CreatePersonalAsync()66.66%121285.71%
UpdatePersonalAsync()50%3435252.45%
RegistrarBajaAsync()87.5%8890%
RegistrarAscensoAsync()95.45%222295.71%
ApplyOrdering(...)77.27%2222100%
GetAllAsync()100%11100%
GetByIdAsync()50%22100%
GetByCedulaAsync()50%22100%
GetActivosAsync()100%11100%
CreateAsync()67.85%282895.83%
UpdateAsync()88.46%545290.74%
DarBajaAsync()100%2266.66%
MapToDto(...)50%44100%
ValidarCatalogosAsync()56.25%301662.5%

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 ApplicationDbContext _context;
 18    private readonly ILogger<PersonalService> _logger;
 19
 4220    public PersonalService(
 4221        IPersonalRepository personalRepository,
 4222        ApplicationDbContext context,
 4223        ILogger<PersonalService> logger)
 24    {
 4225        _personalRepository = personalRepository;
 4226        _context = context;
 4227        _logger = logger;
 4228    }
 29
 30    public async Task<object> GetPersonalListAsync(
 31        string? buscar,
 32        long? cargoId,
 33        string? estado,
 34        string? ley,
 35        int page,
 36        int pageSize,
 37        string? orderBy,
 38        string sort)
 39    {
 1140        var query = _context.RelacionesLaborales
 1141            .Include(r => r.Persona)
 1142            .Include(r => r.Regimen)
 1143            .Include(r => r.Escalafon)
 1144            .Include(r => r.Grado)
 1145            .Include(r => r.Situacion)
 1146            .AsQueryable();
 47
 48        // Filtro por búsqueda (CI, nombre o apellido)
 1149        if (!string.IsNullOrWhiteSpace(buscar))
 50        {
 151            var buscarLower = buscar.ToLower();
 152            query = query.Where(r =>
 153                r.Persona.Cedula.Contains(buscarLower) ||
 154                r.Persona.PrimerNombre.ToLower().Contains(buscarLower) ||
 155                r.Persona.PrimerApellido.ToLower().Contains(buscarLower) ||
 156                (r.Persona.SegundoNombre != null && r.Persona.SegundoNombre.ToLower().Contains(buscarLower)) ||
 157                (r.Persona.SegundoApellido != null && r.Persona.SegundoApellido.ToLower().Contains(buscarLower)));
 58        }
 59
 60        // Filtro por cargo (grado)
 1161        if (cargoId.HasValue)
 62        {
 163            query = query.Where(r => r.GradoId == cargoId.Value);
 64        }
 65
 66        // Filtro por estado (parse string to enum)
 1167        if (!string.IsNullOrWhiteSpace(estado))
 68        {
 269            if (Enum.TryParse<EstadoRelacion>(estado, true, out var estadoEnum))
 70            {
 271                query = query.Where(r => r.Estado == estadoEnum);
 72            }
 73            else
 74            {
 75                // Si no se puede parsear, intentar aceptar valores comunes
 076                var estLower = estado.Trim().ToLower();
 077                if (estLower == "activo") query = query.Where(r => r.Estado == EstadoRelacion.Activo);
 078                else if (estLower == "inactivo") query = query.Where(r => r.Estado == EstadoRelacion.Inactivo);
 79            }
 80        }
 81
 82        // Filtro por ley (régimen)
 1183        if (!string.IsNullOrWhiteSpace(ley))
 84        {
 185            var leyLower = ley.ToLower();
 186            query = query.Where(r => r.Regimen.NumeroLey.ToLower().Contains(leyLower));
 87        }
 88
 1189        var total = await query.CountAsync();
 90
 1191        query = ApplyOrdering(query, orderBy, sort);
 92
 93        // Paginación: obtener entidades y luego proyectar a DTO en memoria para poder convertir enums a string
 1194        var relacionesPage = await query
 1195            .Skip((page - 1) * pageSize)
 1196            .Take(pageSize)
 1197            .ToListAsync();
 98
 2199        var items = relacionesPage.Select(r => new PersonalListDto
 21100        {
 21101            Id = r.Id,
 21102            Ci = r.Persona.Cedula,
 21103            PrimerNombre = r.Persona.PrimerNombre,
 21104            SegundoNombre = r.Persona.SegundoNombre,
 21105            PrimerApellido = r.Persona.PrimerApellido,
 21106            SegundoApellido = r.Persona.SegundoApellido,
 21107            Cargo = r.Grado.Denominacion,
 21108            Grado = r.Grado.Denominacion,
 21109            Ley = r.Regimen.NumeroLey,
 21110            Estado = r.Estado.ToString(),
 21111            FechaIngreso = r.FechaInicio
 21112        }).ToList();
 113
 11114        return new
 11115        {
 11116            items,
 11117            total,
 11118            page,
 11119            pageSize,
 11120            totalPages = (int)Math.Ceiling((double)total / pageSize)
 11121        };
 11122    }
 123
 124    public async Task<PersonalDetalleDto?> GetDetalleAsync(long id)
 125    {
 3126        var relacionLaboral = await _context.RelacionesLaborales
 3127            .Include(r => r.Persona)
 3128            .Include(r => r.Regimen)
 3129            .Include(r => r.Escalafon)
 3130            .Include(r => r.Grado)
 3131            .Include(r => r.Situacion)
 3132            .Include(r => r.Programa)
 3133            .Include(r => r.Unidad)
 3134            .Include(r => r.Compania)
 3135            .FirstOrDefaultAsync(r => r.Id == id);
 136
 3137        if (relacionLaboral == null)
 1138            return null;
 139
 140        // Obtener historial de movimientos
 2141        var historial = await _context.MovimientosLaborales
 2142            .Include(m => m.TipoMovimiento)
 2143            .Include(m => m.Usuario)
 2144                .ThenInclude(u => u.Persona)
 2145            .Where(m => m.RelacionLaboralId == id)
 2146            .OrderByDescending(m => m.FechaMovimiento)
 2147            .Select(m => new HistorialMovimientoDto
 2148            {
 2149                Id = m.Id,
 2150                TipoMovimiento = m.TipoMovimiento.Nombre,
 2151                EsAlta = m.TipoMovimiento.EsAlta,
 2152                FechaMovimiento = m.FechaMovimiento,
 2153                Observaciones = null,
 2154                RegistradoPor = m.Usuario.Persona != null ? m.Usuario.Persona.PrimerNombre + " " + m.Usuario.Persona.Pri
 2155                FechaRegistro = m.FechaMovimiento
 2156            })
 2157            .ToListAsync();
 158
 2159        return new PersonalDetalleDto
 2160        {
 2161            Id = relacionLaboral.Id,
 2162            DatosPersonales = new DatosPersonalesDto
 2163            {
 2164                PersonaId = relacionLaboral.PersonaId,
 2165                Ci = relacionLaboral.Persona.Cedula,
 2166                PrimerNombre = relacionLaboral.Persona.PrimerNombre,
 2167                SegundoNombre = relacionLaboral.Persona.SegundoNombre,
 2168                PrimerApellido = relacionLaboral.Persona.PrimerApellido,
 2169                SegundoApellido = relacionLaboral.Persona.SegundoApellido,
 2170                FechaNacimiento = relacionLaboral.Persona.FechaNacimiento,
 2171                Email = relacionLaboral.Persona.Email,
 2172                Telefono = relacionLaboral.Persona.Telefono,
 2173                Direccion = relacionLaboral.Persona.Direccion
 2174            },
 2175            DatosLaborales = new DatosLaboralesDto
 2176            {
 2177                RelacionLaboralId = relacionLaboral.Id,
 2178                Estado = relacionLaboral.Estado.ToString(),
 2179                FechaInicio = relacionLaboral.FechaInicio,
 2180                FechaFin = relacionLaboral.FechaFin,
 2181                RegimenId = relacionLaboral.RegimenId,
 2182                Regimen = relacionLaboral.Regimen.Denominacion,
 2183                Ley = relacionLaboral.Regimen.NumeroLey,
 2184                EscalafonId = relacionLaboral.EscalafonId,
 2185                Escalafon = relacionLaboral.Escalafon.Denominacion,
 2186                GradoId = relacionLaboral.GradoId,
 2187                Grado = relacionLaboral.Grado.Denominacion,
 2188                Cargo = relacionLaboral.Grado.Denominacion,
 2189                SituacionId = relacionLaboral.SituacionId,
 2190                Situacion = relacionLaboral.Situacion.Denominacion,
 2191                ProgramaId = relacionLaboral.ProgramaId,
 2192                Programa = relacionLaboral.Programa.Denominacion,
 2193                UnidadId = relacionLaboral.UnidadId,
 2194                Unidad = relacionLaboral.Unidad.Denominacion,
 2195                CompaniaId = relacionLaboral.CompaniaId,
 2196                Compania = relacionLaboral.Compania?.Denominacion,
 2197                RiesgoVuelo = relacionLaboral.RiesgoVuelo,
 2198                PrimaTecnica = relacionLaboral.PrimaTecnica,
 2199                PrimaSolidariaFamiliar = relacionLaboral.PrimaSolidariaFamiliar,
 2200                AniosInactivos = relacionLaboral.AniosInactivos,
 2201                Observaciones = relacionLaboral.Observaciones
 2202            },
 2203            Historial = historial
 2204        };
 3205    }
 206
 207    public async Task<(bool success, long? personalId, string? error)> CreatePersonalAsync(CreatePersonalRequest request
 208    {
 209        try
 210        {
 211            // Validar y limpiar CI
 4212            var cedulaLimpia = CedulaValidator.Limpiar(request.Ci);
 4213            if (!CedulaValidator.Validar(cedulaLimpia))
 214            {
 1215                return (false, null, "La cédula ingresada no es válida.");
 216            }
 217
 218            // Verificar que no exista
 3219            var existe = await _context.Personas.AnyAsync(p => p.Cedula == cedulaLimpia);
 3220            if (existe)
 221            {
 1222                return (false, null, "Ya existe una persona con esa cédula.");
 223            }
 224
 225            // Crear persona - Convertir fechas a UTC
 2226            var fechaNacimientoUtc = request.FechaNacimiento.Kind == DateTimeKind.Unspecified
 2227                ? DateTime.SpecifyKind(request.FechaNacimiento.Date, DateTimeKind.Utc)
 2228                : request.FechaNacimiento.Date.ToUniversalTime();
 229
 2230            var persona = new Persona
 2231            {
 2232                Cedula = cedulaLimpia,
 2233                PrimerNombre = request.PrimerNombre,
 2234                SegundoNombre = request.SegundoNombre,
 2235                PrimerApellido = request.PrimerApellido,
 2236                SegundoApellido = request.SegundoApellido,
 2237                FechaNacimiento = fechaNacimientoUtc,
 2238                Email = request.Email,
 2239                Telefono = request.Telefono,
 2240                Direccion = request.Direccion
 2241            };
 242
 2243            _context.Personas.Add(persona);
 2244            await _context.SaveChangesAsync();
 245
 246            // Buscar régimen por ley
 2247            var regimen = await _context.Regimenes
 2248                .FirstOrDefaultAsync(r => r.NumeroLey == request.Ley);
 249
 2250            if (regimen == null)
 251            {
 1252                return (false, null, $"No se encontró el régimen con ley {request.Ley}");
 253            }
 254
 255            // Crear relación laboral - Convertir fechas a UTC
 1256            var fechaIngresoUtc = request.FechaIngreso.Kind == DateTimeKind.Unspecified
 1257                ? DateTime.SpecifyKind(request.FechaIngreso.Date, DateTimeKind.Utc)
 1258                : request.FechaIngreso.Date.ToUniversalTime();
 259
 1260            var relacionLaboral = new RelacionLaboral
 1261            {
 1262                PersonaId = persona.Id,
 1263                RegimenId = regimen.Id,
 1264                EscalafonId = request.EscalafonId ?? 1,
 1265                GradoId = request.GradoId,
 1266                SituacionId = request.SituacionId ?? 1,
 1267                ProgramaId = request.ProgramaId ?? 1,
 1268                UnidadId = request.UnidadId ?? 1,
 1269                CompaniaId = request.CompaniaId,
 1270                Estado = EstadoRelacion.Activo,
 1271                FechaInicio = fechaIngresoUtc,
 1272                Observaciones = request.Observaciones
 1273            };
 274
 1275            _context.RelacionesLaborales.Add(relacionLaboral);
 1276            await _context.SaveChangesAsync();
 277
 1278            return (true, relacionLaboral.Id, null);
 279        }
 0280        catch (Exception ex)
 281        {
 0282            _logger.LogError(ex, "Error al crear personal");
 0283            var errorMessage = ex.Message;
 0284            var innerEx = ex.InnerException;
 0285            while (innerEx != null)
 286            {
 0287                errorMessage += $" | {innerEx.Message}";
 0288                innerEx = innerEx.InnerException;
 289            }
 0290            return (false, null, $"Error al crear personal: {errorMessage}");
 291        }
 4292    }
 293
 294    // TODO: Actualizar para que busque la relacion laboral activa del personal. Tambien guardar el cambio de relacion l
 295    public async Task<(bool success, string? error)> UpdatePersonalAsync(long id, UpdatePersonalRequest request)
 296    {
 297        try
 298        {
 2299            var relacionLaboral = await _context.RelacionesLaborales
 2300                .Include(r => r.Persona)
 2301                .FirstOrDefaultAsync(r => r.Id == id);
 302
 2303            if (relacionLaboral == null)
 304            {
 1305                return (false, "Personal no encontrado");
 306            }
 307
 308            // Actualizar datos personales
 1309            if (!string.IsNullOrWhiteSpace(request.PrimerNombre))
 1310                relacionLaboral.Persona.PrimerNombre = request.PrimerNombre;
 311
 1312            if (request.SegundoNombre != null)
 0313                relacionLaboral.Persona.SegundoNombre = request.SegundoNombre;
 314
 1315            if (!string.IsNullOrWhiteSpace(request.PrimerApellido))
 0316                relacionLaboral.Persona.PrimerApellido = request.PrimerApellido;
 317
 1318            if (request.SegundoApellido != null)
 0319                relacionLaboral.Persona.SegundoApellido = request.SegundoApellido;
 320
 1321            if (request.FechaNacimiento.HasValue)
 322            {
 0323                var fechaNac = request.FechaNacimiento.Value;
 0324                relacionLaboral.Persona.FechaNacimiento = fechaNac.Kind == DateTimeKind.Unspecified
 0325                    ? DateTime.SpecifyKind(fechaNac.Date, DateTimeKind.Utc)
 0326                    : fechaNac.Date.ToUniversalTime();
 327            }
 328
 1329            if (!string.IsNullOrWhiteSpace(request.Email))
 1330                relacionLaboral.Persona.Email = request.Email;
 331
 1332            if (request.Telefono != null)
 0333                relacionLaboral.Persona.Telefono = request.Telefono;
 334
 1335            if (request.Direccion != null)
 0336                relacionLaboral.Persona.Direccion = request.Direccion;
 337
 338            // Actualizar estado si se proporciona (parsear a enum)
 1339            if (!string.IsNullOrWhiteSpace(request.Estado))
 340            {
 0341                if (Enum.TryParse<EstadoRelacion>(request.Estado, true, out var est))
 342                {
 0343                    relacionLaboral.Estado = est;
 344                }
 345                else
 346                {
 0347                    var estLower = request.Estado.Trim().ToLower();
 0348                    if (estLower == "activo") relacionLaboral.Estado = EstadoRelacion.Activo;
 0349                    else if (estLower == "inactivo") relacionLaboral.Estado = EstadoRelacion.Inactivo;
 350                }
 351            }
 352
 353            // Actualizar datos laborales
 1354            if (request.RegimenId.HasValue)
 0355                relacionLaboral.RegimenId = request.RegimenId.Value;
 356
 1357            if (request.EscalafonId.HasValue)
 0358                relacionLaboral.EscalafonId = request.EscalafonId.Value;
 359
 1360            if (request.GradoId.HasValue)
 0361                relacionLaboral.GradoId = request.GradoId.Value;
 362
 1363            if (request.SituacionId.HasValue)
 0364                relacionLaboral.SituacionId = request.SituacionId.Value;
 365
 1366            if (request.ProgramaId.HasValue)
 0367                relacionLaboral.ProgramaId = request.ProgramaId.Value;
 368
 1369            if (request.UnidadId.HasValue)
 0370                relacionLaboral.UnidadId = request.UnidadId.Value;
 371
 1372            if (request.CompaniaId.HasValue)
 0373                relacionLaboral.CompaniaId = request.CompaniaId.Value;
 374
 375            // Actualizar compensaciones
 1376            if (request.RiesgoVuelo.HasValue)
 0377                relacionLaboral.RiesgoVuelo = request.RiesgoVuelo;
 378
 1379            if (request.PrimaTecnica != null)
 0380                relacionLaboral.PrimaTecnica = request.PrimaTecnica;
 381
 1382            if (request.PrimaSolidariaFamiliar != null)
 0383                relacionLaboral.PrimaSolidariaFamiliar = request.PrimaSolidariaFamiliar;
 384
 1385            if (request.AniosInactivos.HasValue)
 0386                relacionLaboral.AniosInactivos = request.AniosInactivos;
 387
 1388            if (request.Observaciones != null)
 0389                relacionLaboral.Observaciones = request.Observaciones;
 390
 1391            relacionLaboral.FechaActualizacion = DateTime.UtcNow;
 392
 1393            await _context.SaveChangesAsync();
 394
 1395            return (true, null);
 396        }
 0397        catch (Exception ex)
 398        {
 0399            _logger.LogError(ex, "Error al actualizar personal");
 0400            return (false, $"Error al actualizar personal: {ex.Message}");
 401        }
 2402    }
 403
 404    public async Task<(bool success, string? error)> RegistrarBajaAsync(long id, RegistrarBajaRequest request)
 405    {
 406        try
 407        {
 4408            var relacionLaboral = await _context.RelacionesLaborales
 4409                .FirstOrDefaultAsync(r => r.Id == id);
 410
 4411            if (relacionLaboral == null)
 412            {
 1413                return (false, "Personal no encontrado");
 414            }
 415
 416            // Validar motivo de movimiento: debe existir y NO ser una alta
 3417            var tipoMovimiento = await _context.TiposMovimiento
 3418                .FirstOrDefaultAsync(t => t.Id == request.MotivoId);
 419
 3420            if (tipoMovimiento == null)
 421            {
 1422                return (false, "Motivo de baja no encontrado");
 423            }
 424
 2425            if (tipoMovimiento.EsAlta)
 426            {
 1427                return (false, "El motivo proporcionado corresponde a un alta, no puede usarse para una baja");
 428            }
 429
 430            // Cambiar estado a inactivo
 1431            relacionLaboral.Estado = EstadoRelacion.Inactivo;
 1432            var fechaBajaUtc = request.FechaBaja.Kind == DateTimeKind.Unspecified
 1433                ? DateTime.SpecifyKind(request.FechaBaja.Date, DateTimeKind.Utc)
 1434                : request.FechaBaja.Date.ToUniversalTime();
 1435            relacionLaboral.FechaFin = fechaBajaUtc;
 1436            relacionLaboral.FechaActualizacion = DateTime.UtcNow;
 437
 438            // Registrar movimiento de baja
 1439            var movimiento = new MovimientoLaboral
 1440            {
 1441                RelacionLaboralId = id,
 1442                TipoMovimientoId = request.MotivoId,
 1443                FechaMovimiento = fechaBajaUtc,
 1444                UsuarioId = 1 // TODO: Obtener del contexto de autenticación
 1445            };
 446
 1447            _context.MovimientosLaborales.Add(movimiento);
 1448            await _context.SaveChangesAsync();
 449
 1450            return (true, null);
 451        }
 0452        catch (Exception ex)
 453        {
 0454            _logger.LogError(ex, "Error al registrar baja");
 0455            return (false, $"Error al registrar baja: {ex.Message}");
 456        }
 4457    }
 458
 459    public async Task<(bool success, string? error)> RegistrarAscensoAsync(long id, AscensoRequest request)
 460    {
 461        try
 462        {
 6463            var relacionLaboral = await _context.RelacionesLaborales
 6464                .FirstOrDefaultAsync(r => r.PersonaId == id && r.Estado == EstadoRelacion.Activo);
 465
 6466            if (relacionLaboral == null)
 1467                return (false, "Personal no encontrado");
 468
 469            // Validar grado objetivo
 5470            var grado = await _context.Grados.FirstOrDefaultAsync(g => g.Id == request.NuevoGradoId && g.Vigente);
 5471            if (grado == null)
 1472                return (false, "Grado no encontrado");
 473
 474            // Validar ley objetivo
 4475            var regimenAscenso = await _context.Regimenes.FirstOrDefaultAsync(r => r.Id == request.LeyId && r.Vigente);
 4476            if (regimenAscenso == null)
 1477                return (false, "Ley no encontrada");
 478
 479            // Validar que el grado objetivo
 3480            if (relacionLaboral.GradoId != 0)
 481            {
 2482                var gradoActual = await _context.Grados.FirstOrDefaultAsync(g => g.Id == relacionLaboral.GradoId);
 2483                if (gradoActual != null)
 484                {
 2485                    if (grado.Orden <= gradoActual.Orden)
 486                    {
 1487                        return (false, "El grado objetivo debe ser superior al grado actual");
 488                    }
 489                }
 490            }
 491
 2492            var fechaAscensoUtc = request.FechaAscenso.Kind == DateTimeKind.Unspecified
 2493                ? DateTime.SpecifyKind(request.FechaAscenso.Date, DateTimeKind.Utc)
 2494                : request.FechaAscenso.Date.ToUniversalTime();
 495
 496            // Validar unidad objetivo si se proporciona
 497            Unidad? unidadObj = null;
 2498            if (request.UnidadId.HasValue)
 499            {
 1500                unidadObj = await _context.Unidades.FirstOrDefaultAsync(u => u.Id == request.UnidadId.Value && u.Vigente
 1501                if (unidadObj == null)
 502                {
 1503                    return (false, "Unidad no válida o no vigente");
 504                }
 505            }
 506
 507            // Cerrar la relación laboral actual
 1508            relacionLaboral.FechaFin = fechaAscensoUtc;
 1509            relacionLaboral.FechaActualizacion = DateTime.UtcNow;
 1510            relacionLaboral.Estado = EstadoRelacion.Inactivo;
 1511            relacionLaboral.Observaciones = request.Observacion;
 512
 513            // Crear nueva relación laboral con el nuevo grado
 1514            var nuevaRelacion = new RelacionLaboral
 1515            {
 1516                PersonaId = relacionLaboral.PersonaId,
 1517                RegimenId = regimenAscenso.Id,
 1518                EscalafonId = relacionLaboral.EscalafonId,
 1519                GradoId = request.NuevoGradoId,
 1520                SituacionId = relacionLaboral.SituacionId,
 1521                ProgramaId = relacionLaboral.ProgramaId,
 1522                UnidadId = request.UnidadId ?? relacionLaboral.UnidadId,
 1523                CompaniaId = relacionLaboral.CompaniaId,
 1524                Estado = EstadoRelacion.Activo,
 1525                FechaInicio = fechaAscensoUtc,
 1526                Observaciones = relacionLaboral.Observaciones,
 1527                FechaActualizacion = DateTime.UtcNow
 1528            };
 529
 1530            _context.RelacionesLaborales.Add(nuevaRelacion);
 531
 532            // Registrar movimientos
 1533            var tipoMovimiento = await _context.TiposMovimiento.FirstOrDefaultAsync(t => t.Nombre.ToLower().Contains("ca
 1534            if (tipoMovimiento == null)
 535            {
 1536                tipoMovimiento = new Entidades.TipoMovimiento { Nombre = "Cambio de Situación", EsAlta = false };
 1537                _context.TiposMovimiento.Add(tipoMovimiento);
 1538                await _context.SaveChangesAsync();
 539            }
 540
 541            // Movimiento de cierre
 1542            var movimientoCierre = new Entidades.MovimientoLaboral
 1543            {
 1544                RelacionLaboralId = relacionLaboral.Id,
 1545                TipoMovimientoId = tipoMovimiento.Id,
 1546                FechaMovimiento = fechaAscensoUtc,
 1547                UsuarioId = 1 // TODO: obtener usuario del contexto
 1548            };
 549
 1550            _context.MovimientosLaborales.Add(movimientoCierre);
 551
 1552            await _context.SaveChangesAsync();
 553
 1554            var movimientoAlta = new Entidades.MovimientoLaboral
 1555            {
 1556                RelacionLaboralId = nuevaRelacion.Id,
 1557                TipoMovimientoId = tipoMovimiento.Id,
 1558                FechaMovimiento = fechaAscensoUtc,
 1559                UsuarioId = 1 // TODO: obtener usuario del contexto
 1560            };
 561
 1562            _context.MovimientosLaborales.Add(movimientoAlta);
 563
 1564            await _context.SaveChangesAsync();
 565
 1566            return (true, null);
 567        }
 0568        catch (Exception ex)
 569        {
 0570            _logger.LogError(ex, "Error al registrar ascenso");
 0571            return (false, $"Error al registrar ascenso: {ex.Message}");
 572        }
 6573    }
 574
 575    private IQueryable<RelacionLaboral> ApplyOrdering(
 576        IQueryable<RelacionLaboral> query,
 577        string? orderBy,
 578        string sort)
 579    {
 11580        var ascending = sort.ToLower() == "asc";
 581
 11582        return orderBy?.ToLower() switch
 11583        {
 1584            "ci" => ascending
 1585                ? query.OrderBy(r => r.Persona.Cedula)
 1586                : query.OrderByDescending(r => r.Persona.Cedula),
 1587            "nombre" => ascending
 1588                ? query.OrderBy(r => r.Persona.PrimerNombre)
 1589                : query.OrderByDescending(r => r.Persona.PrimerNombre),
 1590            "apellido" => ascending
 1591                ? query.OrderBy(r => r.Persona.PrimerApellido)
 1592                : query.OrderByDescending(r => r.Persona.PrimerApellido),
 1593            "grado" => ascending
 1594                ? query.OrderBy(r => r.Grado.Orden)
 1595                : query.OrderByDescending(r => r.Grado.Orden),
 1596            "fechaingreso" => ascending
 1597                ? query.OrderBy(r => r.FechaInicio)
 1598                : query.OrderByDescending(r => r.FechaInicio),
 6599            _ => query.OrderByDescending(r => r.FechaInicio)
 11600        };
 601    }
 602
 603    // ==================== MÉTODOS ANTERIORES (DEPRECAR) ====================
 604
 605    public async Task<IEnumerable<PersonalDto>> GetAllAsync()
 606    {
 1607        var relaciones = await _personalRepository.GetAllAsync();
 1608        return relaciones.Select(MapToDto);
 1609    }
 610
 611    public async Task<PersonalDto?> GetByIdAsync(long id)
 612    {
 1613        var relacionLaboral = await _personalRepository.GetByIdAsync(id);
 1614        return relacionLaboral != null ? MapToDto(relacionLaboral) : null;
 1615    }
 616
 617    public async Task<PersonalDto?> GetByCedulaAsync(string cedula)
 618    {
 1619        var cedulaLimpia = CedulaValidator.Limpiar(cedula);
 1620        var relacionLaboral = await _personalRepository.GetByCedulaAsync(cedulaLimpia);
 1621        return relacionLaboral != null ? MapToDto(relacionLaboral) : null;
 1622    }
 623
 624    public async Task<IEnumerable<PersonalDto>> GetActivosAsync()
 625    {
 1626        var relaciones = await _personalRepository.GetActivosAsync();
 1627        return relaciones.Select(MapToDto);
 1628    }
 629
 630    public async Task<(bool success, PersonalDto? personal, string? error)> CreateAsync(CreatePersonalRequest request)
 631    {
 632        try
 633        {
 634            // Validar cédula
 6635            var cedulaLimpia = CedulaValidator.Limpiar(request.Ci);
 6636            if (!CedulaValidator.Validar(cedulaLimpia))
 637            {
 1638                return (false, null, "La cédula ingresada no es válida.");
 639            }
 640
 641            // Verificar si ya existe
 5642            if (await _personalRepository.ExisteByCedulaAsync(cedulaLimpia))
 643            {
 1644                return (false, null, "Ya existe personal registrado con esta cédula.");
 645            }
 646
 647            // Validar edad (debe ser mayor de 18 años)
 4648            var edad = DateTime.Now.Year - request.FechaNacimiento.Year;
 4649            if (request.FechaNacimiento > DateTime.Now.AddYears(-edad)) edad--;
 4650            if (edad < 18)
 651            {
 1652                return (false, null, "La persona debe ser mayor de 18 años.");
 653            }
 654
 655            // Determinar IDs de catálogos
 3656            long regimenId = 1; // Por defecto
 3657            long escalafonId = request.EscalafonId ?? 1;
 3658            long situacionId = request.SituacionId ?? 1;
 3659            long programaId = request.ProgramaId ?? 1;
 3660            long unidadId = request.UnidadId ?? 1;
 661
 662            // Validar que existan los catálogos
 663            bool valido;
 664            string? errorValidacion;
 3665            (valido, errorValidacion) = await ValidarCatalogosAsync(
 3666                regimenId, escalafonId, request.GradoId,
 3667                situacionId, programaId, unidadId, request.CompaniaId);
 668
 3669            if (!valido)
 670            {
 1671                return (false, null, errorValidacion);
 672            }
 673
 674            // Crear persona - Convertir fechas a UTC
 2675            var fechaNacUtc = request.FechaNacimiento.Kind == DateTimeKind.Unspecified
 2676                ? DateTime.SpecifyKind(request.FechaNacimiento.Date, DateTimeKind.Utc)
 2677                : request.FechaNacimiento.Date.ToUniversalTime();
 678
 2679            var persona = new Persona
 2680            {
 2681                Cedula = cedulaLimpia,
 2682                PrimerNombre = request.PrimerNombre.Trim(),
 2683                SegundoNombre = string.IsNullOrWhiteSpace(request.SegundoNombre) ? null : request.SegundoNombre.Trim(),
 2684                PrimerApellido = request.PrimerApellido.Trim(),
 2685                SegundoApellido = string.IsNullOrWhiteSpace(request.SegundoApellido) ? null : request.SegundoApellido.Tr
 2686                FechaNacimiento = fechaNacUtc,
 2687                Email = request.Email.Trim(),
 2688                Telefono = request.Telefono?.Trim(),
 2689                Direccion = request.Direccion?.Trim(),
 2690                FechaActualizacion = DateTime.UtcNow
 2691            };
 692
 2693            _context.Personas.Add(persona);
 2694            await _context.SaveChangesAsync();
 695
 696            // Crear relación laboral (usar variables ya definidas)
 2697            var relacionLaboral = new RelacionLaboral
 2698            {
 2699                PersonaId = persona.Id,
 2700                RegimenId = regimenId,
 2701                UnidadId = unidadId,
 2702                ProgramaId = programaId,
 2703                SituacionId = situacionId,
 2704                EscalafonId = escalafonId,
 2705                GradoId = request.GradoId,
 2706                FechaInicio = request.FechaIngreso.Kind == DateTimeKind.Unspecified
 2707                    ? DateTime.SpecifyKind(request.FechaIngreso.Date, DateTimeKind.Utc)
 2708                    : request.FechaIngreso.Date.ToUniversalTime(),
 2709                CompaniaId = request.CompaniaId,
 2710                Observaciones = request.Observaciones?.Trim(),
 2711                Estado = EstadoRelacion.Activo
 2712            };
 713
 2714            var resultado = await _personalRepository.CreateAsync(relacionLaboral);
 715
 716            // Crear movimiento de alta (si se proporciona)
 2717            if (request.TipoMovimientoId.HasValue)
 718            {
 1719                var movimiento = new MovimientoLaboral
 1720                {
 1721                    RelacionLaboralId = resultado.Id,
 1722                    TipoMovimientoId = request.TipoMovimientoId.Value,
 1723                    FechaMovimiento = request.FechaIngreso.Kind == DateTimeKind.Unspecified
 1724                        ? DateTime.SpecifyKind(request.FechaIngreso.Date, DateTimeKind.Utc)
 1725                        : request.FechaIngreso.Date.ToUniversalTime(),
 1726                    UsuarioId = 1 // TODO: Obtener del contexto
 1727                };
 728
 1729                await _personalRepository.CrearMovimientoAsync(movimiento);
 730            }
 731
 2732            var dto = MapToDto(resultado);
 733
 2734            _logger.LogInformation("Personal creado exitosamente: {Cedula} - {Nombre}", cedulaLimpia, persona.NombreComp
 735
 2736            return (true, dto, null);
 737        }
 0738        catch (Exception ex)
 739        {
 0740            _logger.LogError(ex, "Error al crear personal");
 0741            return (false, null, "Error al crear el personal. Por favor, intente nuevamente.");
 742        }
 6743    }
 744
 745    public async Task<(bool success, PersonalDto? personal, string? error)> UpdateAsync(long id, UpdatePersonalRequest r
 746    {
 747        try
 748        {
 3749            var relacionLaboral = await _personalRepository.GetByIdAsync(id);
 3750            if (relacionLaboral == null)
 751            {
 1752                return (false, null, "Personal no encontrado.");
 753            }
 754
 2755            var persona = relacionLaboral.Persona;
 756
 757            // Actualizar datos personales si se proporcionan
 2758            if (!string.IsNullOrWhiteSpace(request.PrimerNombre))
 1759                persona.PrimerNombre = request.PrimerNombre.Trim();
 760
 2761            if (request.SegundoNombre != null)
 1762                persona.SegundoNombre = string.IsNullOrWhiteSpace(request.SegundoNombre) ? null : request.SegundoNombre.
 763
 2764            if (!string.IsNullOrWhiteSpace(request.PrimerApellido))
 0765                persona.PrimerApellido = request.PrimerApellido.Trim();
 766
 2767            if (request.SegundoApellido != null)
 1768                persona.SegundoApellido = string.IsNullOrWhiteSpace(request.SegundoApellido) ? null : request.SegundoApe
 769
 2770            if (request.FechaNacimiento.HasValue)
 1771                persona.FechaNacimiento = request.FechaNacimiento.Value;
 772
 2773            if (!string.IsNullOrWhiteSpace(request.Email))
 1774                persona.Email = request.Email.Trim();
 775
 2776            if (request.Telefono != null)
 1777                persona.Telefono = string.IsNullOrWhiteSpace(request.Telefono) ? null : request.Telefono.Trim();
 778
 2779            if (request.Direccion != null)
 1780                persona.Direccion = string.IsNullOrWhiteSpace(request.Direccion) ? null : request.Direccion.Trim();
 781
 2782            persona.FechaActualizacion = DateTime.UtcNow;
 2783            _context.Personas.Update(persona);
 784
 785            // Actualizar datos laborales si se proporcionan
 2786            if (request.RegimenId.HasValue)
 1787                relacionLaboral.RegimenId = request.RegimenId.Value;
 788
 2789            if (request.EscalafonId.HasValue)
 1790                relacionLaboral.EscalafonId = request.EscalafonId.Value;
 791
 2792            if (request.GradoId.HasValue)
 1793                relacionLaboral.GradoId = request.GradoId.Value;
 794
 2795            if (request.SituacionId.HasValue)
 1796                relacionLaboral.SituacionId = request.SituacionId.Value;
 797
 2798            if (request.ProgramaId.HasValue)
 1799                relacionLaboral.ProgramaId = request.ProgramaId.Value;
 800
 2801            if (request.UnidadId.HasValue)
 1802                relacionLaboral.UnidadId = request.UnidadId.Value;
 803
 2804            if (request.CompaniaId.HasValue)
 0805                relacionLaboral.CompaniaId = request.CompaniaId.Value;
 806
 2807            if (request.Observaciones != null)
 2808                relacionLaboral.Observaciones = string.IsNullOrWhiteSpace(request.Observaciones) ? null : request.Observ
 809
 2810            if (request.RiesgoVuelo.HasValue)
 1811                relacionLaboral.RiesgoVuelo = request.RiesgoVuelo.Value;
 812
 2813            if (request.PrimaTecnica != null)
 1814                relacionLaboral.PrimaTecnica = request.PrimaTecnica;
 815
 2816            if (request.PrimaSolidariaFamiliar != null)
 1817                relacionLaboral.PrimaSolidariaFamiliar = request.PrimaSolidariaFamiliar;
 818
 2819            if (request.AniosInactivos.HasValue)
 1820                relacionLaboral.AniosInactivos = request.AniosInactivos.Value;
 821
 2822            var resultado = await _personalRepository.UpdateAsync(relacionLaboral);
 2823            var dto = MapToDto(resultado);
 824
 2825            _logger.LogInformation("Personal actualizado exitosamente: {Id}", id);
 826
 2827            return (true, dto, null);
 828        }
 0829        catch (Exception ex)
 830        {
 0831            _logger.LogError(ex, "Error al actualizar personal {Id}", id);
 0832            return (false, null, "Error al actualizar el personal. Por favor, intente nuevamente.");
 833        }
 3834    }
 835
 836    public async Task<(bool success, string? error)> DarBajaAsync(long id)
 837    {
 838        try
 839        {
 2840            var resultado = await _personalRepository.DarBajaAsync(id, DateTime.UtcNow);
 2841            if (!resultado)
 842            {
 1843                return (false, "Personal no encontrado.");
 844            }
 845
 1846            _logger.LogInformation("Personal dado de baja: {Id}", id);
 1847            return (true, null);
 848        }
 0849        catch (Exception ex)
 850        {
 0851            _logger.LogError(ex, "Error al dar de baja personal {Id}", id);
 0852            return (false, "Error al dar de baja el personal. Por favor, intente nuevamente.");
 853        }
 2854    }
 855
 856    #region Métodos Privados
 857
 858    private PersonalDto MapToDto(RelacionLaboral relacionLaboral)
 859    {
 4860        return new PersonalDto
 4861        {
 4862            Id = relacionLaboral.Id,
 4863            PersonaId = relacionLaboral.PersonaId,
 4864            Cedula = relacionLaboral.Persona.Cedula,
 4865            PrimerNombre = relacionLaboral.Persona.PrimerNombre,
 4866            SegundoNombre = relacionLaboral.Persona.SegundoNombre,
 4867            PrimerApellido = relacionLaboral.Persona.PrimerApellido,
 4868            SegundoApellido = relacionLaboral.Persona.SegundoApellido,
 4869            NombreCompleto = relacionLaboral.Persona.NombreCompleto,
 4870            FechaNacimiento = relacionLaboral.Persona.FechaNacimiento,
 4871            Email = relacionLaboral.Persona.Email,
 4872            Telefono = relacionLaboral.Persona.Telefono,
 4873            Direccion = relacionLaboral.Persona.Direccion,
 4874            Estado = relacionLaboral.Estado.ToString(),
 4875            FechaInicio = relacionLaboral.FechaInicio,
 4876            FechaFin = relacionLaboral.FechaFin,
 4877            RegimenId = relacionLaboral.RegimenId,
 4878            RegimenDenominacion = relacionLaboral.Regimen.Denominacion,
 4879            EscalafonId = relacionLaboral.EscalafonId,
 4880            EscalafonCodigo = relacionLaboral.Escalafon.Codigo,
 4881            EscalafonDenominacion = relacionLaboral.Escalafon.Denominacion,
 4882            GradoId = relacionLaboral.GradoId,
 4883            GradoCodigo = relacionLaboral.Grado.Codigo,
 4884            GradoDenominacion = relacionLaboral.Grado.Denominacion,
 4885            SituacionId = relacionLaboral.SituacionId,
 4886            SituacionCodigo = relacionLaboral.Situacion.Codigo,
 4887            SituacionDenominacion = relacionLaboral.Situacion.Denominacion,
 4888            ProgramaId = relacionLaboral.ProgramaId,
 4889            ProgramaCodigo = relacionLaboral.Programa.Codigo,
 4890            ProgramaDenominacion = relacionLaboral.Programa.Denominacion,
 4891            UnidadId = relacionLaboral.UnidadId,
 4892            UnidadCodigo = relacionLaboral.Unidad.Codigo,
 4893            UnidadDenominacion = relacionLaboral.Unidad.Denominacion,
 4894            CompaniaId = relacionLaboral.CompaniaId,
 4895            CompaniaCodigo = relacionLaboral.Compania?.Codigo,
 4896            CompaniaDenominacion = relacionLaboral.Compania?.Denominacion,
 4897            RiesgoVuelo = relacionLaboral.RiesgoVuelo,
 4898            PrimaTecnica = relacionLaboral.PrimaTecnica,
 4899            PrimaSolidariaFamiliar = relacionLaboral.PrimaSolidariaFamiliar,
 4900            AniosInactivos = relacionLaboral.AniosInactivos,
 4901            Observaciones = relacionLaboral.Observaciones,
 4902            FechaActualizacion = relacionLaboral.FechaActualizacion
 4903        };
 904    }
 905
 906    private async Task<(bool valido, string? error)> ValidarCatalogosAsync(
 907        long regimenId, long escalafonId, long gradoId,
 908        long situacionId, long programaId, long unidadId, long? companiaId)
 909    {
 3910        if (!await _context.Regimenes.AnyAsync(r => r.Id == regimenId && r.Vigente))
 1911            return (false, "El régimen seleccionado no es válido.");
 912
 2913        if (!await _context.Escalafones.AnyAsync(e => e.Id == escalafonId && e.Vigente))
 0914            return (false, "El escalafón seleccionado no es válido.");
 915
 2916        if (!await _context.Grados.AnyAsync(g => g.Id == gradoId && g.Vigente))
 0917            return (false, "El grado seleccionado no es válido.");
 918
 2919        if (!await _context.Situaciones.AnyAsync(s => s.Id == situacionId && s.Vigente))
 0920            return (false, "La situación seleccionada no es válida.");
 921
 2922        if (!await _context.Programas.AnyAsync(p => p.Id == programaId && p.Vigente))
 0923            return (false, "El programa seleccionado no es válido.");
 924
 2925        if (!await _context.Unidades.AnyAsync(u => u.Id == unidadId && u.Vigente))
 0926            return (false, "La unidad seleccionada no es válida.");
 927
 2928        if (companiaId.HasValue && !await _context.Companias.AnyAsync(c => c.Id == companiaId && c.Vigente))
 0929            return (false, "La compañía seleccionada no es válida.");
 930
 2931        return (true, null);
 3932    }
 933
 934    #endregion
 935}