feat: services, jwt auth, and api controllers
- LeadService with 15 SP calls (CRUD, actividades, pagos, montos) - UsuarioService for login and profile queries - JwtService for token generation with claims - AuthController (login, perfil) with HttpOnly cookie - LeadController (CRUD, actividades, estado, pago, contacto) - Registered all services in DI (Program.cs)
This commit is contained in:
+18
-15
@@ -14,9 +14,9 @@
|
||||
| Backend .NET 10 — Skeleton (sln + 4 proyectos + NuGet) | ✅ CREADO |
|
||||
| Entidades / DTOs / Interfaces (Ventas.Core) | ✅ CREADO |
|
||||
| Infrastructure (EF Core + Dapper + Repos) | ✅ PARCIAL (DbContext + LeadRepository listos) |
|
||||
| Services (lógica de negocio) | ❌ PENDIENTE |
|
||||
| API Controllers | ❌ PENDIENTE |
|
||||
| Auth JWT | ❌ PENDIENTE |
|
||||
| Services (lógica de negocio) | ✅ PARCIAL (LeadService, UsuarioService, JwtService) |
|
||||
| API Controllers | ✅ PARCIAL (AuthController, LeadController) |
|
||||
| Auth JWT | ✅ CREADO (JwtService + middleware) |
|
||||
| Reportes QuestPDF (17 reportes) | ❌ PENDIENTE |
|
||||
| ServicesExternos.API | ❌ PENDIENTE |
|
||||
| Frontend Next.js | ❌ PENDIENTE |
|
||||
@@ -42,17 +42,19 @@
|
||||
- [ ] Configurations/ (EF mapping)
|
||||
- [ ] Resto repos (Alumno, Contrato, Cotizacion, etc.)
|
||||
- [ ] SpComplexQueries.cs (Dapper multi-resultset)
|
||||
- [ ] **1.4 Ventas.Services — Lógica de Negocio** (~2 sem)
|
||||
- [ ] LeadService.cs + resto servicios (~15)
|
||||
- [ ] **1.5 Ventas.API — Controladores REST** (~1 sem)
|
||||
- [ ] AuthController.cs
|
||||
- [ ] LeadController.cs + resto controllers (~20)
|
||||
- [ ] Program.cs completo (DI, JWT, CORS)
|
||||
- [ ] appsettings.json con connection strings
|
||||
- [ ] **1.6 Auth JWT** (~3 días)
|
||||
- [ ] JwtMiddleware.cs
|
||||
- [ ] Login endpoint funcional
|
||||
- [ ] Claims → cookies HttpOnly
|
||||
- [x] **1.4 Ventas.Services — Lógica de Negocio** (~2 sem)
|
||||
- [x] LeadService.cs, UsuarioService.cs, JwtService.cs
|
||||
- [ ] Resto servicios (Contrato, Cotizacion, Alumno, etc.)
|
||||
- [x] **1.5 Ventas.API — Controladores REST** (~1 sem)
|
||||
- [x] AuthController.cs (login + perfil)
|
||||
- [x] LeadController.cs (CRUD + actividades + pagos)
|
||||
- [x] Program.cs completo (DI, JWT, CORS)
|
||||
- [x] appsettings.json con connection strings
|
||||
- [ ] Resto controllers (Contrato, Cotizacion, Alumno, etc.)
|
||||
- [x] **1.6 Auth JWT** (~3 días)
|
||||
- [x] JwtService.cs (generación de tokens)
|
||||
- [x] Login endpoint funcional
|
||||
- [x] Claims → cookies HttpOnly
|
||||
- [ ] **1.7 Reportes QuestPDF** (~2-3 sem)
|
||||
- [ ] ReportService.cs (base)
|
||||
- [ ] 17 reportes (Contrato, Cotización, Arqueo, etc.)
|
||||
@@ -103,7 +105,8 @@
|
||||
|
||||
| Fecha | Fase | Qué se hizo | Siguiente paso |
|
||||
|-------|------|-------------|----------------|
|
||||
| 2026-07-07 | F1 | Entities (25), Enums (4), DTOs, Interfaces (6), DbContext, LeadRepository, LeadQueryRepository, DI setup, appsettings | Resto repos (Contrato, Cotizacion, Alumno) + Controllers |
|
||||
| 2026-07-07 | F1 | Entities (25), Enums, DTOs, Interfaces, DbContext, LeadRepository + LeadQueryRepository, DI, JWT, appsettings | Resto repos + Controllers |
|
||||
| 2026-07-07 | F1 | LeadService, UsuarioService, JwtService, AuthController, LeadController | Resto Services + Controllers |
|
||||
| | | | |
|
||||
| | | | |
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Ventas.Core.DTOs;
|
||||
using Ventas.Services;
|
||||
|
||||
namespace Ventas.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
private readonly UsuarioService _usuarioService;
|
||||
private readonly JwtService _jwtService;
|
||||
|
||||
public AuthController(UsuarioService usuarioService, JwtService jwtService)
|
||||
{
|
||||
_usuarioService = usuarioService;
|
||||
_jwtService = jwtService;
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> Login([FromBody] LoginRequest request)
|
||||
{
|
||||
var usuario = await _usuarioService.LoginAsync(request);
|
||||
if (usuario == null)
|
||||
return Unauthorized(new { mensaje = "Credenciales inválidas" });
|
||||
|
||||
var token = _jwtService.GenerateToken(request.Rut, usuario.Nombre, usuario.Sede ?? "");
|
||||
|
||||
Response.Cookies.Append("SAM_TOKEN", token, new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = true,
|
||||
SameSite = SameSiteMode.Strict,
|
||||
Expires = DateTime.UtcNow.AddMinutes(30)
|
||||
});
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
nombre = usuario.Nombre,
|
||||
sede = usuario.Sede,
|
||||
token
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("perfil")]
|
||||
public async Task<IActionResult> Perfil([FromQuery] string usuarioId)
|
||||
{
|
||||
var perfil = await _usuarioService.PerfilAsync(usuarioId);
|
||||
if (perfil == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(perfil);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Ventas.Core.DTOs;
|
||||
using Ventas.Services;
|
||||
|
||||
namespace Ventas.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
public class LeadController : ControllerBase
|
||||
{
|
||||
private readonly LeadService _leadService;
|
||||
|
||||
public LeadController(LeadService leadService)
|
||||
{
|
||||
_leadService = leadService;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<IActionResult> GetById(int id)
|
||||
{
|
||||
var result = await _leadService.BuscarIDAsync(id);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("buscar")]
|
||||
public async Task<IActionResult> Buscar([FromQuery] int ejecutivo, [FromQuery] int estado, [FromQuery] int dias)
|
||||
{
|
||||
var result = await _leadService.BuscarAsync(ejecutivo, estado, dias);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("nuevos")]
|
||||
public async Task<IActionResult> Nuevos([FromQuery] int ejecutivo)
|
||||
{
|
||||
var result = await _leadService.BuscarNuevosAsync(ejecutivo);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/actividades")]
|
||||
public async Task<IActionResult> Actividades(int id, [FromQuery] string tipo)
|
||||
{
|
||||
var result = await _leadService.ActividadesAsync(id, tipo);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("montos")]
|
||||
public async Task<IActionResult> Montos([FromQuery] int ejecutivo)
|
||||
{
|
||||
var result = await _leadService.MontosAsync(ejecutivo);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("motivos-perdido")]
|
||||
public async Task<IActionResult> MotivosPerdido()
|
||||
{
|
||||
var result = await _leadService.MotivosPerdidoAsync();
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Ingresar([FromBody] LeadCreateDto dto)
|
||||
{
|
||||
var result = await _leadService.IngresarAsync(dto);
|
||||
return Ok(new { mensaje = result });
|
||||
}
|
||||
|
||||
[HttpPost("{id}/actividad")]
|
||||
public async Task<IActionResult> IngresarActividad(int id, [FromBody] ActividadCreateDto dto, [FromQuery] string usuarioId)
|
||||
{
|
||||
var result = await _leadService.IngresarActividadAsync(id, dto.Tipo, dto.Descripcion, usuarioId);
|
||||
return Ok(new { mensaje = result });
|
||||
}
|
||||
|
||||
[HttpPut("{id}/estado")]
|
||||
public async Task<IActionResult> ActualizarEstado(int id, [FromQuery] int estadoId)
|
||||
{
|
||||
var result = await _leadService.EstadoUpdateAsync(id, estadoId);
|
||||
return Ok(new { mensaje = result });
|
||||
}
|
||||
|
||||
[HttpPut("{id}/producto")]
|
||||
public async Task<IActionResult> ActualizarProducto(int id, [FromQuery] string producto)
|
||||
{
|
||||
var result = await _leadService.ActualizarProductoAsync(id, producto);
|
||||
return Ok(new { mensaje = result });
|
||||
}
|
||||
|
||||
[HttpPut("{id}/contacto")]
|
||||
public async Task<IActionResult> ActualizarContacto(int id, [FromBody] ContactoUpdateDto dto)
|
||||
{
|
||||
var result = await _leadService.ActualizarContactoAsync(id, dto.Nombre ?? "", dto.Mail ?? "", dto.Telefono ?? "", dto.Rut ?? "");
|
||||
return Ok(new { mensaje = result });
|
||||
}
|
||||
|
||||
[HttpPost("{id}/perder")]
|
||||
public async Task<IActionResult> IngresarLeadPerdido(int id, [FromQuery] int motivo, [FromQuery] int estado)
|
||||
{
|
||||
var result = await _leadService.IngresarLeadPerdidoAsync(id, motivo, estado);
|
||||
return Ok(new { mensaje = result });
|
||||
}
|
||||
|
||||
[HttpPost("{id}/pago")]
|
||||
public async Task<IActionResult> IngresarPago(int id, [FromBody] PagoLeadDto dto)
|
||||
{
|
||||
var result = await _leadService.IngresarPagoAsync(dto.LeadId, dto.CotizacionId, dto.FormaPago, dto.Monto, dto.CodigoAutorizacion ?? "", dto.DigitoTarjeta ?? "", dto.Cuotas);
|
||||
return Ok(new { mensaje = result });
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Microsoft.IdentityModel.Tokens;
|
||||
using Ventas.Infrastructure.Data;
|
||||
using Ventas.Infrastructure.Repositories;
|
||||
using Ventas.Core.Interfaces;
|
||||
using Ventas.Services;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -21,7 +22,16 @@ builder.Services.AddScoped<ILeadRepository>(sp =>
|
||||
builder.Services.AddScoped<ILeadQueryRepository>(sp =>
|
||||
new LeadQueryRepository(connectionString));
|
||||
|
||||
builder.Services.AddScoped<LeadService>(sp =>
|
||||
new LeadService(connectionString));
|
||||
builder.Services.AddScoped<UsuarioService>(sp =>
|
||||
new UsuarioService(connectionString));
|
||||
|
||||
var jwtSecret = builder.Configuration["Jwt:Secret"] ?? "default-dev-secret-change-in-production";
|
||||
var jwtExpiration = int.Parse(builder.Configuration["Jwt:ExpirationMinutes"] ?? "30");
|
||||
builder.Services.AddScoped<JwtService>(sp =>
|
||||
new JwtService(jwtSecret, jwtExpiration));
|
||||
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Ventas.Services;
|
||||
|
||||
public class JwtService
|
||||
{
|
||||
private readonly string _secret;
|
||||
private readonly int _expirationMinutes;
|
||||
|
||||
public JwtService(string secret, int expirationMinutes)
|
||||
{
|
||||
_secret = secret;
|
||||
_expirationMinutes = expirationMinutes;
|
||||
}
|
||||
|
||||
public string GenerateToken(string usuarioId, string nombre, string sede)
|
||||
{
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secret));
|
||||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, usuarioId),
|
||||
new Claim(ClaimTypes.Name, nombre),
|
||||
new Claim("sede", sede)
|
||||
};
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.AddMinutes(_expirationMinutes),
|
||||
signingCredentials: credentials);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
using Dapper;
|
||||
using Npgsql;
|
||||
using Ventas.Core.DTOs;
|
||||
|
||||
namespace Ventas.Services;
|
||||
|
||||
public class LeadService
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
|
||||
public LeadService(string connectionString)
|
||||
{
|
||||
_connectionString = connectionString;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Dictionary<string, object>>> BuscarAsync(int ejecutivo, int estado, int dias)
|
||||
{
|
||||
using var connection = new NpgsqlConnection(_connectionString);
|
||||
var rows = await connection.QueryAsync(
|
||||
"sige_sam_v3.BuscarLeadV3",
|
||||
new { ejecutivo, estadolead = estado, filtro = dias },
|
||||
commandType: System.Data.CommandType.StoredProcedure);
|
||||
|
||||
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Dictionary<string, object>>> BuscarIDAsync(int id)
|
||||
{
|
||||
using var connection = new NpgsqlConnection(_connectionString);
|
||||
var rows = await connection.QueryAsync(
|
||||
"sige_sam_v3.BuscarLeadID",
|
||||
new { id },
|
||||
commandType: System.Data.CommandType.StoredProcedure);
|
||||
|
||||
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Dictionary<string, object>>> BuscarNuevosAsync(int ejecutivo)
|
||||
{
|
||||
using var connection = new NpgsqlConnection(_connectionString);
|
||||
var rows = await connection.QueryAsync(
|
||||
"sige_sam_v3.BuscarLeadNuevos",
|
||||
new { ejecutivo },
|
||||
commandType: System.Data.CommandType.StoredProcedure);
|
||||
|
||||
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
|
||||
}
|
||||
|
||||
public async Task<string> IngresarAsync(LeadCreateDto dto)
|
||||
{
|
||||
using var connection = new NpgsqlConnection(_connectionString);
|
||||
await connection.ExecuteAsync(
|
||||
"sige_sam_v3.GrabaLead",
|
||||
new { nombrelead = dto.Nombre, maillead = dto.Mail, telefonolead = dto.Telefono, productolead = dto.Producto, contactolead = dto.Contacto, ejecutivoid = dto.EjecutivoId },
|
||||
commandType: System.Data.CommandType.StoredProcedure);
|
||||
return "ok";
|
||||
}
|
||||
|
||||
public async Task<string> IngresarV2Async(LeadCreateDto dto)
|
||||
{
|
||||
using var connection = new NpgsqlConnection(_connectionString);
|
||||
var result = await connection.QuerySingleOrDefaultAsync<string>(
|
||||
"sige_sam_v3.GrabaLead",
|
||||
new { nombrelead = dto.Nombre, maillead = dto.Mail, telefonolead = dto.Telefono, productolead = dto.Producto, contactolead = dto.Contacto, ejecutivoid = dto.EjecutivoId },
|
||||
commandType: System.Data.CommandType.StoredProcedure);
|
||||
return result ?? "ok";
|
||||
}
|
||||
|
||||
public async Task<string> IngresarPagoAsync(int lead, int coti, string pago, int monto, string cod, string dig, int cuota)
|
||||
{
|
||||
using var connection = new NpgsqlConnection(_connectionString);
|
||||
await connection.ExecuteAsync(
|
||||
"sige_sam_v3.GrabaPagoLead",
|
||||
new { leadid = lead, cotiid = coti, formapago = pago, valor = monto, codauto = cod, digtar = dig, cantcouta = cuota },
|
||||
commandType: System.Data.CommandType.StoredProcedure);
|
||||
return "ok";
|
||||
}
|
||||
|
||||
public async Task<string> ActualizarProductoAsync(int leadId, string producto)
|
||||
{
|
||||
using var connection = new NpgsqlConnection(_connectionString);
|
||||
await connection.ExecuteAsync(
|
||||
"sam.ActualuzarLeadProducto",
|
||||
new { leadid = leadId, productlead = producto },
|
||||
commandType: System.Data.CommandType.StoredProcedure);
|
||||
return "ok";
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Dictionary<string, object>>> MontosAsync(int ejecutivo)
|
||||
{
|
||||
using var connection = new NpgsqlConnection(_connectionString);
|
||||
var rows = await connection.QueryAsync(
|
||||
"sige_sam_v3.BuscarLeadMontos",
|
||||
new { ejecutivo },
|
||||
commandType: System.Data.CommandType.StoredProcedure);
|
||||
|
||||
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Dictionary<string, object>>> ActividadesAsync(int leadId, string tipo)
|
||||
{
|
||||
using var connection = new NpgsqlConnection(_connectionString);
|
||||
var rows = await connection.QueryAsync(
|
||||
"sige_sam_v3.BuscarActividadesLead",
|
||||
new { id = leadId, tipoactividad = tipo },
|
||||
commandType: System.Data.CommandType.StoredProcedure);
|
||||
|
||||
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
|
||||
}
|
||||
|
||||
public async Task<string> IngresarActividadAsync(int leadId, string tipo, string descripcion, string usuarioId)
|
||||
{
|
||||
using var connection = new NpgsqlConnection(_connectionString);
|
||||
await connection.ExecuteAsync(
|
||||
"sige_sam_v3.GrabaActividadesLead",
|
||||
new { leadid = leadId, tipo, descripcion, usuarioid = usuarioId },
|
||||
commandType: System.Data.CommandType.StoredProcedure);
|
||||
return "ok";
|
||||
}
|
||||
|
||||
public async Task<string> EstadoUpdateAsync(int leadId, int estadoId)
|
||||
{
|
||||
using var connection = new NpgsqlConnection(_connectionString);
|
||||
await connection.ExecuteAsync(
|
||||
"sige_sam_v3.ActualizarEstadoLead",
|
||||
new { leadid = leadId, estadolead = estadoId },
|
||||
commandType: System.Data.CommandType.StoredProcedure);
|
||||
return "ok";
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Dictionary<string, object>>> MotivosPerdidoAsync()
|
||||
{
|
||||
using var connection = new NpgsqlConnection(_connectionString);
|
||||
var rows = await connection.QueryAsync(
|
||||
"sige_sam_v3.BuscarMotivoLeadPerdido",
|
||||
commandType: System.Data.CommandType.StoredProcedure);
|
||||
|
||||
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
|
||||
}
|
||||
|
||||
public async Task<string> IngresarLeadPerdidoAsync(int leadId, int motivo, int estado)
|
||||
{
|
||||
using var connection = new NpgsqlConnection(_connectionString);
|
||||
await connection.ExecuteAsync(
|
||||
"sige_sam_v3.Lead_Ingresar_Perdido",
|
||||
new { leadid = leadId, motivo, estado },
|
||||
commandType: System.Data.CommandType.StoredProcedure);
|
||||
return "ok";
|
||||
}
|
||||
|
||||
public async Task<string> ActualizarContactoAsync(int leadId, string nombre, string mail, string fono, string rut)
|
||||
{
|
||||
using var connection = new NpgsqlConnection(_connectionString);
|
||||
await connection.ExecuteAsync(
|
||||
"sige_sam_v3.ActualizaLeadContacto",
|
||||
new { leadid = leadId, nombrecontacto = nombre, mailcontacto = mail, fonocontacto = fono, rutcontacto = rut },
|
||||
commandType: System.Data.CommandType.StoredProcedure);
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Dapper;
|
||||
using Npgsql;
|
||||
using Ventas.Core.DTOs;
|
||||
|
||||
namespace Ventas.Services;
|
||||
|
||||
public class UsuarioService
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
|
||||
public UsuarioService(string connectionString)
|
||||
{
|
||||
_connectionString = connectionString;
|
||||
}
|
||||
|
||||
public async Task<LoginResponse?> LoginAsync(LoginRequest request)
|
||||
{
|
||||
using var connection = new NpgsqlConnection(_connectionString);
|
||||
var data = await connection.QueryAsync(
|
||||
"sam.BuscarUsuario",
|
||||
new { userid = request.Rut, passid = request.Clave },
|
||||
commandType: System.Data.CommandType.StoredProcedure);
|
||||
|
||||
var user = data.FirstOrDefault();
|
||||
if (user == null)
|
||||
return null;
|
||||
|
||||
var dict = (IDictionary<string, object>)user;
|
||||
return new LoginResponse
|
||||
{
|
||||
Nombre = dict["Nombres"]?.ToString() ?? "",
|
||||
Sede = dict["idSede"]?.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<LoginResponse?> PerfilAsync(string usuarioId)
|
||||
{
|
||||
using var connection = new NpgsqlConnection(_connectionString);
|
||||
var data = await connection.QueryAsync(
|
||||
"sige_sam_v3.BuscarPerfilUsuario",
|
||||
new { usuario = usuarioId },
|
||||
commandType: System.Data.CommandType.StoredProcedure);
|
||||
|
||||
var user = data.FirstOrDefault();
|
||||
if (user == null) return null;
|
||||
|
||||
var dict = (IDictionary<string, object>)user;
|
||||
return new LoginResponse
|
||||
{
|
||||
Nombre = dict["Nombres"]?.ToString() ?? "",
|
||||
Sede = dict["idSede"]?.ToString(),
|
||||
Perfil = dict["Perfil"]?.ToString()
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user