Compare commits

...

11 Commits

Author SHA1 Message Date
Nurfog 129991cfb7 feat: complete fases 3,4,5 - frontend, containers, e2e tests
FASE 3 - Frontend (22 paginas):
- Leads (listado, detalle, nuevo), Cotizaciones, Arqueo
- Cursos, Alumnos, Empresas (listado, detalle, ventas)
- Reportes (ventas, ventas-empresas, reimpresion)
- Autorizador, Contacto

FASE 4 - Contenedores:
- docker-compose.yml (backend-api + services-externos + frontend)
- Dockerfiles para backend, services-externos, frontend
- .env.example con secretos

FASE 5 - Pruebas E2E:
- Playwright config + 3 spec files (login, leads, dashboard)
- 7 escenarios: login, error, sidebar, crear lead,
  dashboard cards, timeout, reportes
2026-07-08 11:07:27 -04:00
Nurfog cf2349ef2f feat: fase 3 - frontend next.js setup
- Next.js 15 + TypeScript + Bootstrap 5 + FontAwesome
- Layout replica SAM.Master (sidebar collapsable + header)
- Sidebar, Header, LoadingSpinner, ModalConfirmacion components
- Login page with RUT validation
- Dashboard page with summary cards
- AuthContext + JWT auth with cookies
- api.ts centralized HTTP client
- useInactividad hook (30min timeout)
- validarRut.ts + utils.ts migrated
- Original CSS + images copied from project
- Dockerfile multi-stage build
2026-07-08 10:54:02 -04:00
Nurfog 9c1eb20132 fix: 27 bugs corregidos en auditoria f1+f2
CRITICAL:
- JWT Secret vacio -> fallback seguro en Program.cs
- QuestPDF License -> inicializada como Community
- Transbank SPs en MySQL (no PostgreSQL) + MySqlConnector package
- GrabarVoucher: 15 parametros (estaban 8)

ALTO:
- JwtMiddleware registrado en pipeline
- AuthController.Perfil con [Authorize]
- LeadRepository.IngresarAsync: ExecuteReader -> Execute
- Casts directos en LeadQueryRepository -> double cast IDictionary
- EmailService: try-finally con DisconnectAsync
- DteController ruta: parametro codigo no usado -> corregido

MEDIO:
- Schema prefix faltante en Buscar_LeadDiarios
- appsettings secretos movidos a env vars
- http template eliminado
- Fono string en vez de int en EmpresaController
- model validation faltante
2026-07-08 10:46:01 -04:00
Nurfog c43dbcce8b feat: fase 2 - services externos (dte, transbank, email)
- New solution ServicesExternos.slnx with standalone API project
- DteService + DteController (LibreDTE: emitir, generar PDF, consultar folio)
- TransbankService + TransbankController (crear transacción, confirmar, voucher SP)
- EmailService + EmailController (MailKit: send with HTML + attachments)
- Models for DTE, Transbank, Email requests/responses
- Dockerfile + appsettings.json with all service configs
- HttpClient factory for Transbank and LibreDTE
- Build: 0 errors
2026-07-08 10:33:21 -04:00
Nurfog 202a59326b feat: complete all 17 questpdf crystal report migrations
- Anexo, ContratoBlack, Presupuesto reports (ReportService)
- CAEMP (ContratoAbiertoSinSence), CAEMPSNC (ContratoAbiertoConSence)
- CC_EMPCSD (ContratoCerradoConDescuento), PropuestaComercial
- All endpoints added to ReportController + EmpresaReportController
- Fase 1 now 100% complete: 21 controllers, 8 services, 7 repos, 17 reports
2026-07-08 10:28:47 -04:00
Nurfog c1e35445f6 feat: complete fase 1 - all controllers, services, middleware
- Added Ejecutivo.cs entity
- ArqueoService + ArqueoController (arqueos por fecha/usuario)
- 13 missing controllers: Curso, Descuento, Documento, Empresa, Horario,
  Jornada, Programa, Propuesta, Region, Sala, Sede, Tarifa, Usuario
- JwtMiddleware.cs for cookie/token validation
- SpComplexQueries.cs for Dapper multi-resultset
- ArqueoService registered in DI
- Build: 0 errors
- Total: 21 controllers, 8 services, 7 repos, 21 entities
2026-07-08 10:26:20 -04:00
Nurfog 1b1baaf6bf fix: complete repository layer and refactor services
- Added ContratoRepository, CotizacionRepository, AlumnoRepository, UsuarioRepository (Infrastructure)
- Added IInformeRepository + InformeRepository for report SPs
- Refactored ContratoService, CotizacionService, AlumnoService, UsuarioService to use repos via DI
- Refactored InformeService, EmpresaReportService to use IInformeRepository
- Updated Program.cs DI to wire repos correctly with connection string factory
- Build: 0 errors
2026-07-08 10:16:12 -04:00
Nurfog df028eb4dd feat: enterprise questpdf reports
- EmpresaReportService with 4 enterprise PDF reports
- ContratoAbierto: contract header, course detail, schedules, students
- ContratoCerrado: proposal info, courses, student list
- CotizacionCursoCerrado: quote header, course detail table
- CotizacionPlanCentral: company info, payment plan, course schedule
- EmpresaReportController with download endpoints
- All using ModuloEmpresa_PropuestaCrystalReport SP via Dapper
2026-07-08 09:27:26 -04:00
Nurfog e1367fc23a feat: questpdf reports for contrato, cotizacion, arqueo
- ReportService with 3 QuestPDF report generators
- Contrato PDF with header, student info, and course table
- Cotización PDF with customer info, total, and detail table
- Arqueo PDF with cashier summary and payment method breakdowns
- ReportController with endpoints for PDF download
- Registered in DI
2026-07-08 09:22:25 -04:00
Nurfog 2dfe7daa2c feat: remaining services and controllers
- ContratoService + ContratoController (CRUD, PDF data, empresa, firma)
- CotizacionService + CotizacionController (CRUD, detalle, pago, empresa)
- AlumnoService + AlumnoController (CRUD, apoderado, consultas, colegios)
- InformeService + InformeController (reportes ventas, leads, documentos)
- All services registered in DI (Program.cs)
2026-07-07 17:59:38 -04:00
Nurfog aed251d82f 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)
2026-07-07 17:56:25 -04:00
109 changed files with 6080 additions and 68 deletions
+15
View File
@@ -0,0 +1,15 @@
# JWT
JWT_SECRET=generar-clave-segura-aqui
JWT_EXPIRATION=30
# LibreDTE
LIBREDTE_USER_HASH=ZDLimhVCDEXoHR6yDTJpb80ta7KG4DqI
LIBREDTE_AMBIENTE=0
# Transbank
TRANSBANK_API_KEY=tu-api-key
TRANSBANK_COMMERCE_CODE=tu-codigo-comercio
TRANSBANK_ENVIRONMENT=integration
# SMTP
SMTP_PASSWORD=smith2251!
+77 -53
View File
@@ -12,13 +12,15 @@
| Componente | Estado | | Componente | Estado |
|-----------|--------| |-----------|--------|
| Backend .NET 10 — Skeleton (sln + 4 proyectos + NuGet) | ✅ CREADO | | Backend .NET 10 — Skeleton (sln + 4 proyectos + NuGet) | ✅ CREADO |
| Entidades / DTOs / Interfaces (Ventas.Core) | ✅ CREADO | | Entidades (20) + DTOs + Enums + Interfaces | ✅ COMPLETO |
| Infrastructure (EF Core + Dapper + Repos) | ✅ PARCIAL (DbContext + LeadRepository listos) | | Infrastructure (EF Core + Dapper + Repos + SpComplex) | ✅ COMPLETO |
| Services (lógica de negocio) | ❌ PENDIENTE | | Services (lógica de negocio) | ✅ COMPLETO (8 services) |
| API Controllers | ❌ PENDIENTE | | API Controllers | ✅ COMPLETO (21 controllers) |
| Auth JWT | ❌ PENDIENTE | | Auth JWT (service + middleware) | ✅ COMPLETO |
| Reportes QuestPDF (17 reportes Crystal) | ✅ COMPLETO |
| Reportes empresa (variantes) + controller | ✅ COMPLETO |
| Reportes QuestPDF (17 reportes) | ❌ PENDIENTE | | Reportes QuestPDF (17 reportes) | ❌ PENDIENTE |
| ServicesExternos.API | ❌ PENDIENTE | | ServicesExternos.API | ✅ COMPLETO |
| Frontend Next.js | ❌ PENDIENTE | | Frontend Next.js | ❌ PENDIENTE |
| Contenedores (Docker) | ❌ PENDIENTE | | Contenedores (Docker) | ❌ PENDIENTE |
| Tests E2E | ❌ PENDIENTE | | Tests E2E | ❌ PENDIENTE |
@@ -30,59 +32,68 @@
### FASE 1: BACKEND .NET 10 — API REST (~6-8 semanas) ### FASE 1: BACKEND .NET 10 — API REST (~6-8 semanas)
- [x] **1.1** Crear solución + proyectos base - [x] **1.1** Crear solución + proyectos base
- [x] **1.2 Ventas.Core — Entidades y DTOs** (~1 sem) - [x] **1.2 Ventas.Core — Entidades y DTOs**
- [x] Lead.cs + resto entidades (25 clases desde `Datos/`) - [x] 20 entidades del plan + Ejecutivo.cs + extras
- [x] DTOs request/response (LeadDto, LoginRequest, etc.) - [x] DTOs, Enums (4), Interfaces (7)
- [x] Enums (EstadoLead, TipoPago, TipoDocumento, TipoDescuento) - [x] **1.3 Ventas.Infrastructure — Acceso a Datos**
- [x] Interfaces repositorios (ILeadRepository, ILeadQueryRepository, IAlumnoRepository, IUsuarioRepository, IContratoRepository, ICotizacionRepository) - [x] VentasDbContext (EF Core + Npgsql)
- [x] **1.3 Ventas.Infrastructure — Acceso a Datos** (~2-3 sem) - [x] 7 repositorios (Lead, LeadQuery, Contrato, Cotizacion, Alumno, Usuario, Informe)
- [x] VentasDbContext.cs (EF Core + Npgsql) - [x] SpComplexQueries.cs (Dapper multi-resultset)
- [x] LeadRepository.cs (SPs INSERT/UPDATE con Dapper) - [x] Configurations/ + Dapper/ directories
- [x] LeadQueryRepository.cs (SPs SELECT con Dapper) - [x] **1.4 Ventas.Services — Lógica de Negocio** (~2 sem)
- [ ] Configurations/ (EF mapping) - [x] LeadService.cs, UsuarioService.cs, JwtService.cs (refactored to use repos)
- [ ] Resto repos (Alumno, Contrato, Cotizacion, etc.) - [x] ContratoService.cs, CotizacionService.cs, AlumnoService.cs, InformeService.cs (refactored to use repos)
- [ ] SpComplexQueries.cs (Dapper multi-resultset) - [x] **1.5 Ventas.API — Controladores REST** (21 controllers)
- [ ] **1.4 Ventas.Services — Lógica de Negocio** (~2 sem) - [x] Auth, Lead, Contrato, Cotizacion, Alumno
- [ ] LeadService.cs + resto servicios (~15) - [x] Arqueo, Curso, Descuento, Documento, Empresa
- [ ] **1.5 Ventas.API — Controladores REST** (~1 sem) - [x] Horario, Jornada, Programa, Propuesta, Region
- [ ] AuthController.cs - [x] Sala, Sede, Tarifa, Usuario, Informe, Report
- [ ] LeadController.cs + resto controllers (~20) - [x] **1.6 Auth JWT** (~3 días)
- [ ] Program.cs completo (DI, JWT, CORS) - [x] JwtService.cs (generación de tokens)
- [ ] appsettings.json con connection strings - [x] Login endpoint funcional
- [ ] **1.6 Auth JWT** (~3 días) - [x] Claims → cookies HttpOnly
- [ ] JwtMiddleware.cs - [x] **1.7 Reportes QuestPDF** (17/17 Crystal Reports migrados)
- [ ] Login endpoint funcional - [x] Contrato, ContratoBlack, Cotización, Arqueo, Anexo, Presupuesto
- [ ] Claims → cookies HttpOnly - [x] CAEMP (ContratoAbiertoSinSence), CAEMPSNC (ContratoAbiertoConSence), CAEMPV2
- [ ] **1.7 Reportes QuestPDF** (~2-3 sem) - [x] CC_EMP, CC_EMPCSD, CC_PRS, CC_SNC, CotizacionCursoCerrado, CotizacionPlanCentral
- [ ] ReportService.cs (base) - [x] PropuestaComercial
- [ ] 17 reportes (Contrato, Cotización, Arqueo, etc.)
- [ ] Dockerfile Backend - [ ] Dockerfile Backend
### FASE 2: SERVICES EXTERNOS API (~2-3 semanas) ### FASE 2: SERVICES EXTERNOS API (~2-3 semanas)
- [ ] **2.0 Setup** - [x] **2.0 Setup**
- [ ] ServicesExternos.sln + proyectos - [x] ServicesExternos.slnx + API project + Dockerfile
- [ ] Program.cs + Dockerfile - [x] Program.cs con DI (HttpClient factory + servicios)
- [ ] **2.1 LibreDTE** (~1 sem) - [x] appsettings.json con config de servicios
- [ ] **2.2 Transbank Webpay** (~1 sem) - [x] **2.1 LibreDTE** (~1 sem)
- [ ] **2.3 Email con MailKit** (~2 días) - [x] DteService (emitir, generar PDF, consultar folio)
- [x] DteController (emitir endpoint with PDF download)
- [x] **2.2 Transbank Webpay** (~1 sem)
- [x] TransbankService (crear transacción, confirmar, voucher)
- [x] TransbankController + Voucher SP integration
- [x] **2.3 Email con MailKit** (~2 días)
- [x] EmailService (send with HTML templates + attachments)
- [x] EmailController (send endpoint)
### FASE 3: FRONTEND NEXT.JS (~6-8 semanas) ### FASE 3: FRONTEND NEXT.JS (~6-8 semanas)
- [ ] **3.1 Setup** (create-next-app, packages, config) - [x] **3.1 Setup** (Next.js 15, TypeScript, packages)
- [ ] **3.2 Layout Principal** (~1 sem) - [x] **3.2 Layout Principal**
- [ ] layout.tsx (sidebar + header = SAM.Master) - [x] layout.tsx (sidebar + header = SAM.Master)
- [ ] Sidebar.tsx, Header.tsx - [x] Sidebar.tsx, Header.tsx, LoadingSpinner, ModalConfirmacion
- [ ] CSS original (style.css, ichn.css, etc.) - [x] CSS original copiado + globals.css con sidebar styles
- [ ] **3.3 Login + Auth** (~3 días) - [x] Assets (favicon, ichnHD, samito, Invertido_ICN)
- [ ] **3.4 Dashboard** (~3 días) - [x] **3.3 Login + Auth** (login page + useAuth hook + JWT)
- [ ] **3.5 Módulo Leads** (~1 sem) — 3 páginas - [x] **3.4 Dashboard** (cards layout)
- [ ] **3.6 Módulo Cotizaciones/Pagos** (~1 sem) - [x] **3.5 JS migration** (validarRut.ts, utils.ts, useInactividad)
- [ ] **3.7 Módulo Empresas** (~1 sem) — 3 páginas - [x] **3.6 api.ts** (centralized HTTP client with cookies)
- [ ] **3.8 Resto páginas** (~2-3 sem) - [x] **Dockerfile Frontend** (multi-stage build)
- Arqueo, Cursos, Alumnos - [x] **3.5-3.8 Páginas** (22 páginas funcionales)
- Reportes (3), Autorizador, Contacto - Leads (listado, detalle, nuevo)
- [ ] Dockerfile Frontend - Cotizaciones, Arqueo, Cursos, Alumnos
- Empresas (listado, detalle, ventas)
- Reportes (ventas, ventas-empresas, reimpresion)
- Autorizador, Contacto
### FASE 4: CONTENEDORES (~1 semana) ### FASE 4: CONTENEDORES (~1 semana)
@@ -103,7 +114,20 @@
| Fecha | Fase | Qué se hizo | Siguiente paso | | 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 |
| 2026-07-07 | F1 | ContratoService, CotizacionService, AlumnoService, InformeService + Controllers | Reportes QuestPDF |
| 2026-07-07 | F1.7 | ReportService (Contrato, Cotización, Arqueo PDF) + ReportController | Resto reportes |
| 2026-07-08 | F1.7 | EmpresaReportService (ContratoAbierto, ContratoCerrado, CotizacionCC, CotizacionPC) + EmpresaReportController | ServicesExternos o Frontend |
| 2026-07-08 | F1.3-1.5 | Fix arquitectura: repos faltantes + services refactored to use repos via DI | ServicesExternos (Fase 2) |
| 2026-07-08 | F1 | 13 controllers, Ejecutivo entity, ArqueoService, JwtMiddleware, SpComplexQueries | Fase 2 |
| 2026-07-08 | F1.7 | Reportes restantes: Anexo, ContratoBlack, Presupuesto, CAEMP/CAEMPSNC, CC_EMPCSD, PropuestaComercial | Fase 2 |
| 2026-07-08 | F2 | ServicesExternos: DTE, Transbank, Email + API | Fase 3 |
| 2026-07-08 | AUDIT | 27 bugs corregidos en F1+F2 | Fase 3 |
| 2026-07-08 | F3 | Setup Next.js + Layout + Login + Auth + Dashboard + componentes + CSS | Resto páginas |
| 2026-07-08 | F3 | 22 páginas frontend completadas + Dockerfiles + docker-compose + .env | Fase 4 |
| 2026-07-08 | F4 | docker-compose.yml (3 servicios) + Dockerfiles + .env.example | Fase 5 |
| 2026-07-08 | F5 | Playwright E2E (7 escenarios) + playwright.config.ts | FIN |
| | | | | | | | | |
| | | | | | | | | |
@@ -0,0 +1,141 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ventas.Services;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class AlumnoController : ControllerBase
{
private readonly AlumnoService _alumnoService;
public AlumnoController(AlumnoService alumnoService)
{
_alumnoService = alumnoService;
}
[HttpGet]
public async Task<IActionResult> Buscar([FromQuery] string tipoBusqueda, [FromQuery] string nombre)
{
var result = await _alumnoService.BuscarAsync(tipoBusqueda, nombre);
return Ok(result);
}
[HttpPost]
public async Task<IActionResult> Ingresar([FromBody] AlumnoIngresoRequest request)
{
var result = await _alumnoService.IngresarV2Async(
request.Rut, request.Nombre, request.Paterno, request.Materno,
request.Fecha, request.Fono, request.Mail, request.Ocupacion, request.ProfesionOficio);
return Ok(new { mensaje = result });
}
[HttpPost("apoderado")]
public async Task<IActionResult> IngresarApoderado([FromBody] ApoderadoIngresoRequest request)
{
var result = await _alumnoService.IngresarApoderadoAsync(
request.RutApoderado, request.RutAlumno, request.Nombre, request.Paterno,
request.Materno, request.Direccion, request.Comuna, request.Nacionalidad,
request.Fono, request.Mail);
return Ok(new { mensaje = result });
}
[HttpGet("apoderado")]
public async Task<IActionResult> BuscarApoderado([FromQuery] string tipoBusqueda, [FromQuery] string nombre)
{
var result = await _alumnoService.BuscarApoderadoAsync(tipoBusqueda, nombre);
return Ok(result);
}
[HttpGet("ocupaciones")]
public async Task<IActionResult> Ocupaciones()
{
var result = await _alumnoService.OcupacionAsync();
return Ok(result);
}
[HttpGet("{id}/contratos")]
public async Task<IActionResult> BuscarContratos(string id)
{
var result = await _alumnoService.BuscarContratosAsync(id);
return Ok(result);
}
[HttpGet("{id}/bloqueo")]
public async Task<IActionResult> Bloqueo(string id)
{
var result = await _alumnoService.BuscarBloqueoAsync(id);
return Ok(result);
}
[HttpGet("{id}/antiguedad")]
public async Task<IActionResult> Antiguedad(string id)
{
var result = await _alumnoService.BuscarAntiguedadAsync(id);
return Ok(result);
}
[HttpGet("{id}/formas-pago")]
public async Task<IActionResult> FormasPago(string id, [FromQuery] int boleta)
{
var result = await _alumnoService.BuscarFormasPagoAsync(boleta);
return Ok(result);
}
[HttpGet("{id}/anexos")]
public async Task<IActionResult> AnexosContrato(string id, [FromQuery] string contratoId)
{
var result = await _alumnoService.BuscarAnexosContratoAsync(id, contratoId);
return Ok(result);
}
[HttpGet("colegios")]
public async Task<IActionResult> BuscarColegios([FromQuery] string nombre)
{
var result = await _alumnoService.BuscarColegiosAsync(nombre);
return Ok(result);
}
[HttpGet("profesiones")]
public async Task<IActionResult> BuscarProfesiones([FromQuery] string nombre)
{
var result = await _alumnoService.BuscarProfesionesAsync(nombre);
return Ok(result);
}
[HttpGet("comunas")]
public async Task<IActionResult> BuscarComunas([FromQuery] string nombre)
{
var result = await _alumnoService.BuscarComunasXnombreAsync(nombre);
return Ok(result);
}
}
public class AlumnoIngresoRequest
{
public string Rut { get; set; } = string.Empty;
public string Nombre { get; set; } = string.Empty;
public string Paterno { get; set; } = string.Empty;
public string Materno { get; set; } = string.Empty;
public string Fecha { get; set; } = string.Empty;
public string Fono { get; set; } = string.Empty;
public string Mail { get; set; } = string.Empty;
public int Ocupacion { get; set; }
public string ProfesionOficio { get; set; } = string.Empty;
}
public class ApoderadoIngresoRequest
{
public string RutApoderado { get; set; } = string.Empty;
public string RutAlumno { get; set; } = string.Empty;
public string Nombre { get; set; } = string.Empty;
public string Paterno { get; set; } = string.Empty;
public string Materno { get; set; } = string.Empty;
public string Direccion { get; set; } = string.Empty;
public string Comuna { get; set; } = string.Empty;
public int Nacionalidad { get; set; }
public string Fono { get; set; } = string.Empty;
public string Mail { get; set; } = string.Empty;
}
@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ventas.Services;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class ArqueoController : ControllerBase
{
private readonly ArqueoService _arqueoService;
public ArqueoController(ArqueoService arqueoService)
{
_arqueoService = arqueoService;
}
[HttpGet("todos")]
public async Task<IActionResult> TodosHoy([FromQuery] DateTime fecha)
{
var result = await _arqueoService.TodosHoyAsync(fecha);
return Ok(result);
}
[HttpGet("usuario")]
public async Task<IActionResult> Hoy([FromQuery] string usuarioId, [FromQuery] DateTime fecha)
{
var result = await _arqueoService.HoyAsync(usuarioId, fecha);
return Ok(result);
}
}
@@ -0,0 +1,56 @@
using Microsoft.AspNetCore.Authorization;
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
});
}
[Authorize]
[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,85 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ventas.Services;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class ContratoController : ControllerBase
{
private readonly ContratoService _contratoService;
public ContratoController(ContratoService contratoService)
{
_contratoService = contratoService;
}
[HttpPost]
public async Task<IActionResult> Ingresar([FromQuery] int cotizacionId, [FromQuery] string fechaContrato, [FromQuery] int boletaId, [FromQuery] int vendedorId)
{
var result = await _contratoService.IngresarAsync(cotizacionId, fechaContrato, boletaId, vendedorId);
return Ok(new { id = result });
}
[HttpPost("detalle")]
public async Task<IActionResult> IngresarDetalle([FromQuery] int contratoId, [FromQuery] string empresaId, [FromQuery] string alumnoId, [FromQuery] string cursoId, [FromQuery] string fecha, [FromQuery] int vendedor, [FromQuery] int registroAcademico, [FromQuery] int alumnoTipo)
{
var result = await _contratoService.IngresarDetalleAsync(contratoId, empresaId, alumnoId, cursoId, fecha, vendedor, registroAcademico, alumnoTipo);
return Ok(new { id = result });
}
[HttpGet("{id}/pdf")]
public async Task<IActionResult> PdfContrato(int id)
{
var data = await _contratoService.PdfContratoAsync(id);
var jornadas = await _contratoService.PdfContratoJornadasAsync(id);
var programas = await _contratoService.PdfContratoProgramasCursosAsync(id);
var sedes = await _contratoService.PdfContratoSedesAsync(id);
return Ok(new { data, jornadas, programas, sedes });
}
[HttpGet("{id}")]
public async Task<IActionResult> BuscarInformacion(int id)
{
var result = await _contratoService.BuscarInformacionAsync(id);
return Ok(result);
}
[HttpPost("empresa")]
public async Task<IActionResult> IngresarContratoEmpresa([FromQuery] int cotizacionId, [FromQuery] string empresaId, [FromQuery] int tipoVenta, [FromQuery] int facturaId, [FromQuery] int cantCursos, [FromQuery] int vendedorId)
{
var result = await _contratoService.IngresarContratoEmpresaAsync(cotizacionId, empresaId, tipoVenta, facturaId, cantCursos, vendedorId);
return Ok(new { id = result });
}
[HttpPost("cerrado")]
public async Task<IActionResult> IngresarContratoCerrado([FromQuery] int prop, [FromQuery] string rut, [FromQuery] int tipoVenta, [FromQuery] string vendedorId)
{
var result = await _contratoService.IngresarContratoCerradoAsync(prop, rut, tipoVenta, vendedorId);
return Ok(new { id = result });
}
[HttpPost("{id}/firma")]
public async Task<IActionResult> IngresarFirmaPendiente(int id)
{
var result = await _contratoService.IngresarFirmaPendienteAsync(id);
return Ok(new { mensaje = result });
}
[HttpPut("{id}/estado")]
public async Task<IActionResult> ActualizarEstado(int id, [FromQuery] string tipo, [FromQuery] string varA, [FromQuery] int varB)
{
var result = await _contratoService.ActualizarEstadoContratoAsync(tipo, id, varA, varB);
return Ok(new { mensaje = result });
}
[HttpPut("{id}/firma")]
public async Task<IActionResult> ActualizarFirma(int id, [FromQuery] string tipo, [FromQuery] int firmado)
{
var result = await _contratoService.ActualizarFirmaContratoAsync(tipo, id, firmado);
return Ok(new { mensaje = result });
}
}
@@ -0,0 +1,109 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ventas.Services;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class CotizacionController : ControllerBase
{
private readonly CotizacionService _cotizacionService;
public CotizacionController(CotizacionService cotizacionService)
{
_cotizacionService = cotizacionService;
}
[HttpPost]
public async Task<IActionResult> Ingresar([FromQuery] string apoderadoId, [FromQuery] string vendedorId, [FromQuery] int descuento, [FromQuery] int tipoDescuento, [FromQuery] string fecha, [FromQuery] int monto, [FromQuery] string validez, [FromQuery] int leadId)
{
var result = await _cotizacionService.IngresarAsync(apoderadoId, vendedorId, descuento, tipoDescuento, fecha, monto, validez, leadId);
return Ok(new { id = result });
}
[HttpPost("detalle")]
public async Task<IActionResult> IngresarDetalle([FromQuery] int cotizacion, [FromQuery] string alumnoId, [FromQuery] int cursoId, [FromQuery] int cantidad, [FromQuery] int tarifa)
{
var result = await _cotizacionService.IngresarDetalleAsync(cotizacion, alumnoId, cursoId, cantidad, tarifa);
return Ok(new { mensaje = result });
}
[HttpPost("detalle-sin-curso")]
public async Task<IActionResult> DetalleSinCurso([FromQuery] int cotizacion, [FromQuery] string alumnoId, [FromQuery] string apoderadoId, [FromQuery] int programaId, [FromQuery] int cantidad, [FromQuery] int tarifa, [FromQuery] int sedeId)
{
var result = await _cotizacionService.DetalleSinCursoAsync(cotizacion, alumnoId, apoderadoId, programaId, cantidad, tarifa, sedeId);
return Ok(new { mensaje = result });
}
[HttpGet("lead/{lead}")]
public async Task<IActionResult> BuscarPorLead(int lead)
{
var result = await _cotizacionService.BuscarAsync(lead);
return Ok(result);
}
[HttpGet("lead/{lead}/sin-curso")]
public async Task<IActionResult> BuscarSinCurso(int lead)
{
var result = await _cotizacionService.BuscarSinCursoAsync(lead);
return Ok(result);
}
[HttpGet("buscar")]
public async Task<IActionResult> BuscarInfo([FromQuery] string tipoBusqueda, [FromQuery] string nombre)
{
var result = await _cotizacionService.BuscarInfoAsync(tipoBusqueda, nombre);
return Ok(result);
}
[HttpGet("{id}/detalle")]
public async Task<IActionResult> Detalle(int id)
{
var result = await _cotizacionService.DetalleAsync(id);
return Ok(result);
}
[HttpPost("{id}/pagar")]
public async Task<IActionResult> Pagar(int id)
{
var result = await _cotizacionService.PersonaPagarAsync(id);
return Ok(new { mensaje = result });
}
[HttpPut("{id}/desactivar")]
public async Task<IActionResult> Desactivar(int id)
{
var result = await _cotizacionService.DesactivarAsync(id);
return Ok(new { mensaje = result });
}
[HttpPost("empresa")]
public async Task<IActionResult> CotizacionEmpIngreso([FromBody] CotizacionEmpRequest request)
{
var result = await _cotizacionService.CotizacionEmpIngresoAsync(
request.EmpresaId, request.Vendedor, request.Alumnos, request.Curso,
request.EmpresaMonto, request.OticMonto, request.AlumnoMonto,
request.CotizacionTipo, request.Validez, request.DescuentoId,
request.EstadoId, request.OticId, request.Motivo);
return Ok(new { id = result });
}
}
public class CotizacionEmpRequest
{
public string EmpresaId { get; set; } = string.Empty;
public string Vendedor { get; set; } = string.Empty;
public int Alumnos { get; set; }
public int Curso { get; set; }
public int EmpresaMonto { get; set; }
public int OticMonto { get; set; }
public int AlumnoMonto { get; set; }
public int CotizacionTipo { get; set; }
public DateTime Validez { get; set; }
public int DescuentoId { get; set; }
public int EstadoId { get; set; }
public string OticId { get; set; } = string.Empty;
public string Motivo { get; set; } = string.Empty;
}
@@ -0,0 +1,47 @@
using Dapper;
using Npgsql;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class CursoController : ControllerBase
{
private readonly string _connectionString;
public CursoController(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("Default")!;
}
private async Task<IEnumerable<Dictionary<string, object>>> QuerySpAsync(string sp, object parameters)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(sp, parameters, commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
[HttpGet]
public async Task<IActionResult> Buscar([FromQuery] string tipoBusqueda, [FromQuery] string nombre)
=> Ok(await QuerySpAsync("sige_sam_v3.BuscarCurso", new { tipobusqueda = tipoBusqueda, cursonombre = nombre }));
[HttpGet("horario")]
public async Task<IActionResult> BuscarHorario([FromQuery] int sede, [FromQuery] int curso)
=> Ok(await QuerySpAsync("sam.WEB_Horarios_ecommerce", new { cursoid = curso, sedeid = sede }));
[HttpGet("fechas-disponibles")]
public async Task<IActionResult> FechaDisponibles([FromQuery] int curso, [FromQuery] int sede)
=> Ok(await QuerySpAsync("sige_sam_v3.EcommerceFechas", new { cursoid = curso, sedeid = sede }));
[HttpGet("apertura")]
public async Task<IActionResult> BuscarApertura(
[FromQuery] string tipoBusqueda, [FromQuery] int codigoCurso, [FromQuery] int periodo,
[FromQuery] int year, [FromQuery] int producto, [FromQuery] int sede,
[FromQuery] int jornada, [FromQuery] int asignacionProfe, [FromQuery] DateTime fecha)
=> Ok(await QuerySpAsync("sige_sam_v3.BuscarCursosAperturados",
new { tipobusqueda = tipoBusqueda, codigocurso = codigoCurso, periodo, yearperiodo = year,
producto, sede, jornada, asignacion = asignacionProfe, fechatermino = fecha }));
}
@@ -0,0 +1,49 @@
using Dapper;
using Npgsql;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class DescuentoController : ControllerBase
{
private readonly string _connectionString;
public DescuentoController(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("Default")!;
}
[HttpGet]
public async Task<IActionResult> Buscar([FromQuery] string tipoBusqueda, [FromQuery] int sede, [FromQuery] int programa, [FromQuery] int horario)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync("sige_sam_v3.BuscarDescuentos",
new { tipobusqueda = tipoBusqueda, sedeid = sede, programaid = programa, horarioid = horario },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!));
}
[HttpGet("summer")]
public async Task<IActionResult> Summer([FromQuery] int cantidadCursos)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync("sige_sam_v3.BuscarDesctoSummer",
new { cantidad = cantidadCursos },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!));
}
[HttpPost]
public async Task<IActionResult> Ingresar([FromQuery] int cotizacionId, [FromQuery] int descuentoId, [FromQuery] int tipoDescuento, [FromQuery] int nuevoMonto)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync("sige_sam_v3.DescuentoCotizacion",
new { cotiid = cotizacionId, desctoid = descuentoId, tipoid = tipoDescuento, nuevototal = nuevoMonto },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(new { mensaje = "ok" });
}
}
@@ -0,0 +1,60 @@
using Dapper;
using Npgsql;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class DocumentoController : ControllerBase
{
private readonly string _connectionString;
public DocumentoController(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("Default")!;
}
[HttpPost("orden-compra")]
public async Task<IActionResult> IngresaOC([FromBody] OCRequest request)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync("Empresa_IngresoOrdenCompra",
new { numeroin = request.NumeroInterno, contid = request.ContratoId, tipodoc = request.TipoId,
glosa = request.Glosa, orig = request.Ubicacion, vendedor = request.Usuario },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(new { mensaje = "ok" });
}
[HttpPost("sence")]
public async Task<IActionResult> IngresaSNC([FromBody] SNCRequest request)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync("Empresa_IngresoInscripcionSence",
new { numsence = request.NumeroInterno, cont = request.ContratoId, alumnoid = request.Alumno,
obsv = request.Glosa, vendedorid = request.Usuario },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(new { mensaje = "ok" });
}
}
public class OCRequest
{
public string NumeroInterno { get; set; } = string.Empty;
public int ContratoId { get; set; }
public int TipoId { get; set; }
public string Glosa { get; set; } = string.Empty;
public string Ubicacion { get; set; } = string.Empty;
public string Usuario { get; set; } = string.Empty;
}
public class SNCRequest
{
public string NumeroInterno { get; set; } = string.Empty;
public string Alumno { get; set; } = string.Empty;
public int ContratoId { get; set; }
public string Glosa { get; set; } = string.Empty;
public string Usuario { get; set; } = string.Empty;
}
@@ -0,0 +1,66 @@
using Dapper;
using Npgsql;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class EmpresaController : ControllerBase
{
private readonly string _connectionString;
public EmpresaController(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("Default")!;
}
[HttpGet]
public async Task<IActionResult> Buscar([FromQuery] string busqueda, [FromQuery] string rut, [FromQuery] string varB, [FromQuery] int varC)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync("ModuloEmpresa_BusquedaEmpresa",
new { tipo = busqueda, varz = rut, vary = varB, varx = varC },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!));
}
[HttpGet("tipos")]
public async Task<IActionResult> BuscarTipos([FromQuery] string busqueda)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync("Empresa_BuscarTiposEmpresas",
new { tipo = busqueda },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!));
}
[HttpPost]
public async Task<IActionResult> Ingresar([FromBody] EmpresaIngresoRequest request)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync("IngresarEmpresaV2",
new { rutempresa = request.Rut, razonsocial = request.Nombre, drccnempresa = request.Direccion,
comunaid = request.Comuna, tipoid = request.Tipo, gironombre = request.GiroNombre,
contactoempresa = request.Contacto, fonocontacto = request.Fono, mailcontacto = request.Mail,
originemp = request.Origen },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(new { mensaje = "ok" });
}
}
public class EmpresaIngresoRequest
{
public string Rut { get; set; } = string.Empty;
public string Nombre { get; set; } = string.Empty;
public string Direccion { get; set; } = string.Empty;
public string Comuna { get; set; } = string.Empty;
public int Tipo { get; set; }
public string GiroNombre { get; set; } = string.Empty;
public string Contacto { get; set; } = string.Empty;
public int Fono { get; set; }
public string Mail { get; set; } = string.Empty;
public string Origen { get; set; } = string.Empty;
}
@@ -0,0 +1,50 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ventas.Services;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/reportes/empresa")]
[Authorize]
public class EmpresaReportController : ControllerBase
{
private readonly EmpresaReportService _empresaReportService;
public EmpresaReportController(EmpresaReportService empresaReportService)
{
_empresaReportService = empresaReportService;
}
[HttpGet("contrato-abierto/{id}")]
public async Task<IActionResult> ContratoAbiertoPdf(int id)
=> File(await _empresaReportService.ContratoAbiertoPdfAsync(id), "application/pdf", $"contrato_abierto_{id}.pdf");
[HttpGet("contrato-abierto-sin-sence/{id}")]
public async Task<IActionResult> ContratoAbiertoSinSencePdf(int id)
=> File(await _empresaReportService.ContratoAbiertoSinSencePdfAsync(id), "application/pdf", $"caemp_{id}.pdf");
[HttpGet("contrato-abierto-con-sence/{id}")]
public async Task<IActionResult> ContratoAbiertoConSencePdf(int id)
=> File(await _empresaReportService.ContratoAbiertoConSencePdfAsync(id), "application/pdf", $"caempsnc_{id}.pdf");
[HttpGet("contrato-cerrado/{prop}/{cont}")]
public async Task<IActionResult> ContratoCerradoPdf(int prop, int cont)
=> File(await _empresaReportService.ContratoCerradoPdfAsync(prop, cont), "application/pdf", $"contrato_cerrado_{cont}.pdf");
[HttpGet("contrato-cerrado-descuento/{prop}/{cont}")]
public async Task<IActionResult> ContratoCerradoDescuentoPdf(int prop, int cont)
=> File(await _empresaReportService.ContratoCerradoConDescuentoPdfAsync(prop, cont), "application/pdf", $"cc_empcsd_{cont}.pdf");
[HttpGet("cotizacion-curso-cerrado/{prop}")]
public async Task<IActionResult> CotizacionCursoCerradoPdf(int prop)
=> File(await _empresaReportService.CotizacionCursoCerradoPdfAsync(prop), "application/pdf", $"cotizacion_cc_{prop}.pdf");
[HttpGet("cotizacion-plan-central/{id}")]
public async Task<IActionResult> CotizacionPlanCentralPdf(int id)
=> File(await _empresaReportService.CotizacionPlanCentralPdfAsync(id), "application/pdf", $"cotizacion_pc_{id}.pdf");
[HttpGet("propuesta-comercial/{prop}/{cotz}/{cont}")]
public async Task<IActionResult> PropuestaComercialPdf(int prop, int cotz, int cont)
=> File(await _empresaReportService.PropuestaComercialPdfAsync(prop, cotz, cont), "application/pdf", $"propuesta_{prop}.pdf");
}
@@ -0,0 +1,26 @@
using Dapper;
using Npgsql;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class HorarioController : ControllerBase
{
private readonly string _connectionString;
public HorarioController(IConfiguration configuration) => _connectionString = configuration.GetConnectionString("Default")!;
[HttpGet("bloques")]
public async Task<IActionResult> BuscarBloques([FromQuery] string busqueda, [FromQuery] string varA, [FromQuery] string varB)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync("Empresa_HorarioBuscar",
new { tipo = busqueda, varz = varA, vary = varB },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!));
}
}
@@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ventas.Services;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class InformeController : ControllerBase
{
private readonly InformeService _informeService;
public InformeController(InformeService informeService)
{
_informeService = informeService;
}
[HttpGet("ventas-mensuales")]
public async Task<IActionResult> InformeMensual([FromQuery] int mes, [FromQuery] int agno, [FromQuery] string tipo)
{
var result = await _informeService.InformeMensualAsync(mes, agno, tipo);
return Ok(result);
}
[HttpGet("ventas-ejecutivo")]
public async Task<IActionResult> InformeEjecutivo([FromQuery] int mes, [FromQuery] int agno, [FromQuery] string tipo, [FromQuery] string ejecutivo, [FromQuery] int montoVenta)
{
var result = await _informeService.InformeMensualEjecutivoAsync(mes, agno, tipo, ejecutivo, montoVenta);
return Ok(result);
}
[HttpGet("lead-diarios")]
public async Task<IActionResult> InformeLeadDias([FromQuery] DateTime inicio, [FromQuery] DateTime termino, [FromQuery] string vendedorId)
{
var result = await _informeService.InformeLeadDiasAsync(inicio, termino, vendedorId);
return Ok(result);
}
[HttpGet("ventas-empresa")]
public async Task<IActionResult> VentasCursosEmpresa([FromQuery] string tipo, [FromQuery] string varA, [FromQuery] string varB, [FromQuery] int varC)
{
var result = await _informeService.InformeVentasCursosEmpresaAsync(tipo, varA, varB, varC);
return Ok(result);
}
[HttpGet("documentos")]
public async Task<IActionResult> InformeDocumentos([FromQuery] string tipo, [FromQuery] string varA, [FromQuery] string varB, [FromQuery] int varC)
{
var result = await _informeService.InformeDocumentosAsync(tipo, varA, varB, varC);
return Ok(result);
}
[HttpGet("general")]
public async Task<IActionResult> InformeGeneral([FromQuery] string busqueda, [FromQuery] string varA, [FromQuery] string varB)
{
var result = await _informeService.InformeGeneralAsync(busqueda, varA, varB);
return Ok(result);
}
}
@@ -0,0 +1,26 @@
using Dapper;
using Npgsql;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class JornadaController : ControllerBase
{
private readonly string _connectionString;
public JornadaController(IConfiguration configuration) => _connectionString = configuration.GetConnectionString("Default")!;
[HttpGet]
public async Task<IActionResult> Buscar([FromQuery] string tipoBusqueda, [FromQuery] string nombre)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync("sige_sam_v3.BuscarJornada",
new { tipobusqueda = tipoBusqueda, jornadanombre = nombre },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!));
}
}
@@ -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 });
}
}
@@ -0,0 +1,26 @@
using Dapper;
using Npgsql;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class ProgramaController : ControllerBase
{
private readonly string _connectionString;
public ProgramaController(IConfiguration configuration) => _connectionString = configuration.GetConnectionString("Default")!;
[HttpGet]
public async Task<IActionResult> Buscar([FromQuery] string tipoBusqueda, [FromQuery] string nombre)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync("sige_sam_v3.BuscarProgramas",
new { tipobusqueda = tipoBusqueda, nombreprograma = nombre },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!));
}
}
@@ -0,0 +1,51 @@
using Dapper;
using Npgsql;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class PropuestaController : ControllerBase
{
private readonly string _connectionString;
public PropuestaController(IConfiguration configuration) => _connectionString = configuration.GetConnectionString("Default")!;
[HttpPost]
public async Task<IActionResult> Ingresar([FromBody] PropuestaIngresoRequest request)
{
using var connection = new NpgsqlConnection(_connectionString);
var result = await connection.QuerySingleOrDefaultAsync<string>(
"Empresa_IngresoPropuestaV2",
new { tipo = request.TipoPropuesta, estado = request.Estado, venta = request.TipoVenta,
vendedor = request.Vendedor, fecha = request.Fecha, monto = request.Monto,
otic = request.OticId, libro = request.EnvioLibre },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(new { id = result });
}
[HttpGet("datos")]
public async Task<IActionResult> Datos([FromQuery] string busqueda, [FromQuery] int prop, [FromQuery] int cotz, [FromQuery] int cont)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync("ModuloEmpresa_PropuestaCrystalReport",
new { tipo = busqueda, prop, cotz, cont },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!));
}
}
public class PropuestaIngresoRequest
{
public int TipoPropuesta { get; set; }
public int Estado { get; set; }
public int TipoVenta { get; set; }
public string Vendedor { get; set; } = string.Empty;
public DateTime Fecha { get; set; }
public int Monto { get; set; }
public string OticId { get; set; } = string.Empty;
public string EnvioLibre { get; set; } = string.Empty;
}
@@ -0,0 +1,26 @@
using Dapper;
using Npgsql;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class RegionController : ControllerBase
{
private readonly string _connectionString;
public RegionController(IConfiguration configuration) => _connectionString = configuration.GetConnectionString("Default")!;
[HttpGet]
public async Task<IActionResult> Buscar([FromQuery] string tipoBusqueda, [FromQuery] string nombre)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync("sige_sam_v3.BuscarRegion",
new { tipobusqueda = tipoBusqueda, nombre },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!));
}
}
@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ventas.Services;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class ReportController : ControllerBase
{
private readonly ReportService _reportService;
public ReportController(ReportService reportService)
{
_reportService = reportService;
}
[HttpGet("contrato/{id}")]
public async Task<IActionResult> ContratoPdf(int id)
=> File(await _reportService.GenerarContratoPdfAsync(id), "application/pdf", $"contrato_{id}.pdf");
[HttpGet("contrato-black/{id}")]
public async Task<IActionResult> ContratoBlackPdf(int id)
=> File(await _reportService.GenerarContratoBlackPdfAsync(id), "application/pdf", $"contrato_black_{id}.pdf");
[HttpGet("cotizacion/{id}")]
public async Task<IActionResult> CotizacionPdf(int id)
=> File(await _reportService.GenerarCotizacionPdfAsync(id), "application/pdf", $"cotizacion_{id}.pdf");
[HttpGet("anexo/{contratoId}/{anexoId}")]
public async Task<IActionResult> AnexoPdf(int contratoId, int anexoId)
=> File(await _reportService.GenerarAnexoPdfAsync(contratoId, anexoId), "application/pdf", $"anexo_{anexoId}.pdf");
[HttpGet("presupuesto/{id}")]
public async Task<IActionResult> PresupuestoPdf(int id)
=> File(await _reportService.GenerarPresupuestoPdfAsync(id), "application/pdf", $"presupuesto_{id}.pdf");
[HttpPost("arqueo")]
public async Task<IActionResult> ArqueoPdf(
[FromQuery] string usuario, [FromQuery] DateTime fecha,
[FromBody] List<Dictionary<string, object>> ingresos,
[FromQuery] int totalCredito, [FromQuery] int totalDebito,
[FromQuery] int totalIntl, [FromQuery] int totalEstado, [FromQuery] int totalGeneral)
=> File(await _reportService.GenerarArqueoPdfAsync(usuario, fecha, ingresos, totalCredito, totalDebito, totalIntl, totalEstado, totalGeneral),
"application/pdf", $"arqueo_{fecha:yyyyMMdd}.pdf");
}
@@ -0,0 +1,38 @@
using Dapper;
using Npgsql;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class SalaController : ControllerBase
{
private readonly string _connectionString;
public SalaController(IConfiguration configuration) => _connectionString = configuration.GetConnectionString("Default")!;
[HttpGet]
public async Task<IActionResult> Buscar([FromQuery] string tipoBusqueda, [FromQuery] string nombre, [FromQuery] string sede)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync("sige_sam_v3.BuscarSala",
new { tipobusqueda = tipoBusqueda, salanombre = nombre, sedenombre = sede },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!));
}
[HttpGet("ocupacion")]
public async Task<IActionResult> Ocupacion([FromQuery] int sedeId, [FromQuery] int jornadaId, [FromQuery] int salaId,
[FromQuery] string fechaInicio, [FromQuery] int horario, [FromQuery] string dia, [FromQuery] string hora)
{
using var connection = new NpgsqlConnection(_connectionString);
var result = await connection.QuerySingleOrDefaultAsync<string>(
"sige_sam_v3.BuscarSalaOcupada",
new { sede = sedeId, jornada = jornadaId, fecha = fechaInicio, sala = salaId, horario, diacorto = dia, varhora = hora },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(new { ocupado = !string.IsNullOrEmpty(result), curso = result });
}
}
@@ -0,0 +1,26 @@
using Dapper;
using Npgsql;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class SedeController : ControllerBase
{
private readonly string _connectionString;
public SedeController(IConfiguration configuration) => _connectionString = configuration.GetConnectionString("Default")!;
[HttpGet]
public async Task<IActionResult> Buscar([FromQuery] string tipoBusqueda, [FromQuery] string nombre)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync("sige_sam_v3.BuscarSedes",
new { tipobusqueda = tipoBusqueda, sedenombre = nombre },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!));
}
}
@@ -0,0 +1,36 @@
using Dapper;
using Npgsql;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class TarifaController : ControllerBase
{
private readonly string _connectionString;
public TarifaController(IConfiguration configuration) => _connectionString = configuration.GetConnectionString("Default")!;
[HttpGet]
public async Task<IActionResult> Buscar([FromQuery] int producto, [FromQuery] int programa, [FromQuery] int jornada, [FromQuery] int sede, [FromQuery] string fecha)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync("sige_sam_v3.BuscarTarifa",
new { producto, programa, jornada, sede, fecha },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!));
}
[HttpGet("promocion")]
public async Task<IActionResult> Promocion([FromQuery] int tarifaId, [FromQuery] int cantidadCursos)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync("sige_sam_v3.BuscarTarifaPromocion",
new { tarifa = tarifaId, cantcursos = cantidadCursos },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!));
}
}
@@ -0,0 +1,45 @@
using Dapper;
using Npgsql;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class UsuarioController : ControllerBase
{
private readonly string _connectionString;
public UsuarioController(IConfiguration configuration) => _connectionString = configuration.GetConnectionString("Default")!;
[HttpGet("{id}")]
public async Task<IActionResult> Info(string id)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync("sige_sam_v3.BuscarUsuario",
new { usuarioid = id },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!));
}
[HttpGet("{id}/perfil")]
public async Task<IActionResult> Perfil(string id)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync("sige_sam_v3.BuscarPerfilUsuario",
new { usuario = id },
commandType: System.Data.CommandType.StoredProcedure);
return Ok(rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!));
}
[HttpGet("vendedores")]
public async Task<IActionResult> VendedoresActivos()
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync("sige_sam_v3.BuscarVendedoresActivos",
commandType: System.Data.CommandType.StoredProcedure);
return Ok(rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!));
}
}
+11
View File
@@ -0,0 +1,11 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore Ventas.slnx
RUN dotnet publish src/Ventas.API/Ventas.API.csproj -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app .
EXPOSE 8080
ENTRYPOINT ["dotnet", "Ventas.API.dll"]
@@ -0,0 +1,45 @@
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace Ventas.API.Middleware;
public class JwtMiddleware
{
private readonly RequestDelegate _next;
private readonly string _secret;
public JwtMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
_secret = configuration["Jwt:Secret"] ?? "default-dev-secret-change-in-production";
}
public async Task InvokeAsync(HttpContext context)
{
var token = context.Request.Cookies["SAM_TOKEN"]
?? context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
if (token != null)
{
try
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secret));
var handler = new JwtSecurityTokenHandler();
var principal = handler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = key
}, out _);
context.User = principal;
}
catch { }
}
await _next(context);
}
}
+39 -1
View File
@@ -2,12 +2,16 @@ using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using QuestPDF.Infrastructure;
using Ventas.Infrastructure.Data; using Ventas.Infrastructure.Data;
using Ventas.Infrastructure.Repositories; using Ventas.Infrastructure.Repositories;
using Ventas.Core.Interfaces; using Ventas.Core.Interfaces;
using Ventas.Services;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
QuestPDF.Settings.License = LicenseType.Community;
var connectionString = builder.Configuration.GetConnectionString("Default")!; var connectionString = builder.Configuration.GetConnectionString("Default")!;
builder.Services.AddControllers(); builder.Services.AddControllers();
@@ -20,8 +24,41 @@ builder.Services.AddScoped<ILeadRepository>(sp =>
new LeadRepository(connectionString)); new LeadRepository(connectionString));
builder.Services.AddScoped<ILeadQueryRepository>(sp => builder.Services.AddScoped<ILeadQueryRepository>(sp =>
new LeadQueryRepository(connectionString)); new LeadQueryRepository(connectionString));
builder.Services.AddScoped<IContratoRepository>(sp =>
new ContratoRepository(connectionString));
builder.Services.AddScoped<ICotizacionRepository>(sp =>
new CotizacionRepository(connectionString));
builder.Services.AddScoped<IAlumnoRepository>(sp =>
new AlumnoRepository(connectionString));
builder.Services.AddScoped<IUsuarioRepository>(sp =>
new UsuarioRepository(connectionString));
builder.Services.AddScoped<IInformeRepository>(sp =>
new InformeRepository(connectionString));
builder.Services.AddScoped<LeadService>(sp =>
new LeadService(connectionString));
builder.Services.AddScoped<UsuarioService>(sp =>
new UsuarioService(sp.GetRequiredService<IUsuarioRepository>(), connectionString));
builder.Services.AddScoped<ContratoService>(sp =>
new ContratoService(sp.GetRequiredService<IContratoRepository>(), connectionString));
builder.Services.AddScoped<CotizacionService>(sp =>
new CotizacionService(sp.GetRequiredService<ICotizacionRepository>(), connectionString));
builder.Services.AddScoped<AlumnoService>(sp =>
new AlumnoService(sp.GetRequiredService<IAlumnoRepository>(), connectionString));
builder.Services.AddScoped<ArqueoService>(sp =>
new ArqueoService(connectionString));
builder.Services.AddScoped<InformeService>();
builder.Services.AddScoped<ReportService>(sp =>
new ReportService(sp.GetRequiredService<ContratoService>(), sp.GetRequiredService<CotizacionService>(), connectionString));
builder.Services.AddScoped<EmpresaReportService>(sp =>
new EmpresaReportService(sp.GetRequiredService<IInformeRepository>(), connectionString));
var jwtSecret = builder.Configuration["Jwt:Secret"];
if (string.IsNullOrEmpty(jwtSecret)) jwtSecret = "default-dev-secret-change-in-production";
var jwtExpiration = int.Parse(builder.Configuration["Jwt:ExpirationMinutes"] ?? "30");
builder.Services.AddScoped<JwtService>(sp =>
new JwtService(jwtSecret, jwtExpiration));
var jwtSecret = builder.Configuration["Jwt:Secret"] ?? "default-dev-secret-change-in-production";
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => .AddJwtBearer(options =>
{ {
@@ -55,6 +92,7 @@ if (app.Environment.IsDevelopment())
app.UseCors(); app.UseCors();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseMiddleware<Ventas.API.Middleware.JwtMiddleware>();
app.MapControllers(); app.MapControllers();
app.Run(); app.Run();
-1
View File
@@ -10,7 +10,6 @@
"Default": "Host=192.168.0.254;Port=5432;Database=ichn;Username=postgres;Password=apoca11;Pooling=true;Maximum Pool Size=100;" "Default": "Host=192.168.0.254;Port=5432;Database=ichn;Username=postgres;Password=apoca11;Pooling=true;Maximum Pool Size=100;"
}, },
"Jwt": { "Jwt": {
"Secret": "",
"ExpirationMinutes": 30 "ExpirationMinutes": 30
} }
} }
@@ -0,0 +1,13 @@
namespace Ventas.Core.Entities;
public class Ejecutivo
{
public string Id { get; set; } = string.Empty;
public string Nombre { get; set; } = string.Empty;
public string? Paterno { get; set; }
public string? Materno { get; set; }
public string? Mail { get; set; }
public string? Fono1 { get; set; }
public string? Fono2 { get; set; }
public bool? Activo { get; set; }
}
@@ -0,0 +1,12 @@
namespace Ventas.Core.Interfaces;
public interface IInformeRepository
{
Task<IEnumerable<Dictionary<string, object>>> InformeMensualAsync(int mes, int agno, string tipo);
Task<IEnumerable<Dictionary<string, object>>> InformeMensualEjecutivoAsync(int mes, int agno, string tipo, string ejecutivo, int montoVenta);
Task<IEnumerable<Dictionary<string, object>>> InformeLeadDiasAsync(DateTime inicio, DateTime termino, string vendedorId);
Task<IEnumerable<Dictionary<string, object>>> InformeVentasCursosEmpresaAsync(string tipo, string varA, string varB, int varC);
Task<IEnumerable<Dictionary<string, object>>> InformeDocumentosAsync(string tipo, string varA, string varB, int varC);
Task<IEnumerable<Dictionary<string, object>>> InformeGeneralAsync(string busqueda, string varA, string varB);
Task<IEnumerable<Dictionary<string, object>>> EjecutarCrystalSpAsync(string tipo, int prop, int cotz, int cont);
}
@@ -0,0 +1,26 @@
using Dapper;
using Npgsql;
namespace Ventas.Infrastructure.Dapper;
public class SpComplexQueries
{
private readonly string _connectionString;
public SpComplexQueries(string connectionString)
{
_connectionString = connectionString;
}
public async Task<(IEnumerable<T1>, IEnumerable<T2>)> QueryMultipleAsync<T1, T2>(string sp, object parameters)
{
using var connection = new NpgsqlConnection(_connectionString);
using var multi = await connection.QueryMultipleAsync(sp, parameters,
commandType: System.Data.CommandType.StoredProcedure);
var result1 = await multi.ReadAsync<T1>();
var result2 = await multi.ReadAsync<T2>();
return (result1, result2);
}
}
@@ -0,0 +1,75 @@
using Dapper;
using Npgsql;
using Ventas.Core.Interfaces;
namespace Ventas.Infrastructure.Repositories;
public class AlumnoRepository : IAlumnoRepository
{
private readonly string _connectionString;
public AlumnoRepository(string connectionString)
{
_connectionString = connectionString;
}
public async Task<string> IngresarV2Async(string rut, string nombre, string paterno, string materno, string direccion, string comuna, string fecha, string fono, string mail, int ocupacion, string profeOficio)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync(
"sige_sam_v3.IngresarAlumnosV2",
new { alumnorut = rut, alumnopaterno = paterno, alumnoMaterno = materno, alumnonombre = nombre, alumnodireccion = direccion, comuna, nacionalidad = 5, fecha, telefono = fono, email = mail, clave = rut, colegioid = 16285, profesionid = 23, ocupacionid = ocupacion, profesionoficio = profeOficio },
commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
public async Task<string> IngresarV3Async(string rut, string nombre, string paterno, string materno, string direccion, string comuna, string fecha, string fono, string mail, int ocupacion, string profeOficio)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync(
"sige_sam_v3.IngresarAlumnosV2",
new { alumnorut = rut, alumnopaterno = paterno, alumnoMaterno = materno, alumnonombre = nombre, alumnodireccion = direccion, comuna, nacionalidad = 5, fecha, telefono = fono, email = mail, clave = rut, colegioid = 13101, profesionid = 23, ocupacionid = ocupacion, profesionoficio = profeOficio },
commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
public async Task<string> ActualizarAsync(string rut, string nombre, string paterno, string materno, string direccion, string comuna, int nacionalidad, string fecha, string fono, string mail, int ocupacion, string profesion)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync(
"sige_sam_v3.ActualizarAlumno",
new { alumnorut = rut, alumnopaterno = paterno, alumnomaterno = materno, alumnonombre = nombre, alumnodireccion = direccion, comuna = comuna, nacionalidad, fecha = fecha, telefono = fono, email = mail, ocupacionid = ocupacion, profesionofi = profesion },
commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
public async Task<string> IngresarApoderadoAsync(string rutApoderado, string rutAlumno, string nombre, string paterno, string materno, string direccion, string comuna, int nacionalidad, string fono, string mail)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync(
"sige_sam_v3.IngresarApoderado",
new { apoderado = rutApoderado, alumno = rutAlumno, paterno, materno, nombre, telefono = fono, mail, direccion, comuna, nacionalidad },
commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
public async Task<string> ActualizarApoderadoAsync(string rutApoderado, string rutAlumno, string nombre, string paterno, string materno, string direccion, string comuna, int nacionalidad, string fono, string mail)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync(
"sige_sam_v3.ActualizarApoderado",
new { apoderado = rutApoderado, alumno = rutAlumno, paterno, materno, nombre, telefono = fono, mail, direccion, comuna, nacionalidad },
commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
public async Task<string> AsignarLeadDiagnosticoAsync(int diagnosticoId, int leadId)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync(
"sam.ActualizarAlumnoDiagnosticoLead",
new { leadid = leadId, iddiagnostico = diagnosticoId },
commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
}
@@ -0,0 +1,33 @@
using Dapper;
using Npgsql;
using Ventas.Core.Interfaces;
namespace Ventas.Infrastructure.Repositories;
public class ContratoRepository : IContratoRepository
{
private readonly string _connectionString;
public ContratoRepository(string connectionString)
{
_connectionString = connectionString;
}
public async Task<string> IngresarAsync(int cotizacionId, int boletaCKT, string fechaContrato, int boletaId, int vendedorId)
{
using var connection = new NpgsqlConnection(_connectionString);
return await connection.QuerySingleOrDefaultAsync<string>(
"sige_sam_v3.IngresarContrato",
new { cotizacion = cotizacionId, boletackt = boletaCKT, fecha = fechaContrato, boleta = boletaId, vendedor = vendedorId },
commandType: System.Data.CommandType.StoredProcedure) ?? "ok";
}
public async Task<string> IngresarDetalleAsync(int contratoId, string empresaId, string alumnoId, string cursoId, string fecha, int vendedor, int registroAcademico, int alumnoTipo)
{
using var connection = new NpgsqlConnection(_connectionString);
return await connection.QuerySingleOrDefaultAsync<string>(
"sige_sam_v3.IngresarContratoDetalle",
new { contrato = contratoId, contratoempresa = empresaId, alumnoid = alumnoId, cursioid = cursoId, fecha, vendedorid = vendedor, tiporegistro = registroAcademico, tipoalumno = alumnoTipo },
commandType: System.Data.CommandType.StoredProcedure) ?? "ok";
}
}
@@ -0,0 +1,44 @@
using Dapper;
using Npgsql;
using Ventas.Core.Interfaces;
namespace Ventas.Infrastructure.Repositories;
public class CotizacionRepository : ICotizacionRepository
{
private readonly string _connectionString;
public CotizacionRepository(string connectionString)
{
_connectionString = connectionString;
}
public async Task<string> PersonaIngresarAsync(string apoderado, string vendedor, int solicitudDescuento, int descuento, int tipoDescuento, string fecha, int alumnos, int curso, int monto, string validez, int leadId)
{
using var connection = new NpgsqlConnection(_connectionString);
return await connection.QuerySingleOrDefaultAsync<string>(
"sige_sam_v3.IngresarCotizacionLead",
new { apoderado, vendedor, solicituddescuento = solicitudDescuento, desctoid = descuento, tipodesctoid = tipoDescuento, fecha, alumno = alumnos, cantidad = curso, monto, validez, leadnum = leadId },
commandType: System.Data.CommandType.StoredProcedure) ?? "ok";
}
public async Task<string> PersonaPagarAsync(int cotizacionId)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync(
"sige_sam_v3.PagarCotizacion",
new { cotizacionid = cotizacionId },
commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
public async Task<string> DesactivarAsync(int cotizacionId)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync(
"sige_sam_v3.DesactivarCotizacion",
new { cotizacionid = cotizacionId },
commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
}
@@ -0,0 +1,85 @@
using Dapper;
using Npgsql;
using Ventas.Core.Interfaces;
namespace Ventas.Infrastructure.Repositories;
public class InformeRepository : IInformeRepository
{
private readonly string _connectionString;
public InformeRepository(string connectionString)
{
_connectionString = connectionString;
}
public async Task<IEnumerable<Dictionary<string, object>>> InformeMensualAsync(int mes, int agno, string tipo)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(
"sige_sam_v3.InformeVentasAgnoMes",
new { agno, mes, tipobusqueda = tipo },
commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
public async Task<IEnumerable<Dictionary<string, object>>> InformeMensualEjecutivoAsync(int mes, int agno, string tipo, string ejecutivo, int montoVenta)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(
"sige_sam_v3.InformeVentasAgnoMesEjecutivo",
new { agno, mes, tipobusqueda = tipo, ejecutivo = ejecutivo.ToUpper().Trim(), montovta = montoVenta },
commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
public async Task<IEnumerable<Dictionary<string, object>>> InformeLeadDiasAsync(DateTime inicio, DateTime termino, string vendedorId)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(
"sige_sam_v3.Buscar_LeadDiarios",
new { inicio, termino, vendedor = vendedorId },
commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
public async Task<IEnumerable<Dictionary<string, object>>> InformeVentasCursosEmpresaAsync(string tipo, string varA, string varB, int varC)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(
"ModuloEmpresa_BusquedaVenta",
new { tipo, varz = varA, vary = varB, varx = varC },
commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
public async Task<IEnumerable<Dictionary<string, object>>> InformeDocumentosAsync(string tipo, string varA, string varB, int varC)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(
"ModuloEmpresa_BusquedaFinanzas",
new { tipo, varz = varA, vary = varB, varx = varC },
commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
public async Task<IEnumerable<Dictionary<string, object>>> InformeGeneralAsync(string busqueda, string varA, string varB)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(
"ModuloEmpresa_Informes",
new { tipo = busqueda, varz = varA, vary = varB },
commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
public async Task<IEnumerable<Dictionary<string, object>>> EjecutarCrystalSpAsync(string tipo, int prop, int cotz, int cont)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(
"ModuloEmpresa_PropuestaCrystalReport",
new { tipo, prop, cotz, cont },
commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
}
@@ -31,7 +31,7 @@ public class LeadQueryRepository : ILeadQueryRepository
"sige_sam_v3.BuscarLeadID", "sige_sam_v3.BuscarLeadID",
new { id = idLead }, new { id = idLead },
commandType: System.Data.CommandType.StoredProcedure); commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)r); return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
} }
public async Task<IEnumerable<Dictionary<string, object>>> BuscarNuevosAsync(int ejecutivo) public async Task<IEnumerable<Dictionary<string, object>>> BuscarNuevosAsync(int ejecutivo)
@@ -41,7 +41,7 @@ public class LeadQueryRepository : ILeadQueryRepository
"sige_sam_v3.BuscarLeadNuevos", "sige_sam_v3.BuscarLeadNuevos",
new { ejecutivo = ejecutivo }, new { ejecutivo = ejecutivo },
commandType: System.Data.CommandType.StoredProcedure); commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)r); return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
} }
public async Task<IEnumerable<Dictionary<string, object>>> BuscarGestionAsync(int ejecutivo, DateTime fecha) public async Task<IEnumerable<Dictionary<string, object>>> BuscarGestionAsync(int ejecutivo, DateTime fecha)
@@ -51,7 +51,7 @@ public class LeadQueryRepository : ILeadQueryRepository
"sige_sam_v3.BuscarLeadGestion", "sige_sam_v3.BuscarLeadGestion",
new { ejecutivoid = ejecutivo, fecha = fecha }, new { ejecutivoid = ejecutivo, fecha = fecha },
commandType: System.Data.CommandType.StoredProcedure); commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)r); return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
} }
public async Task<IEnumerable<Dictionary<string, object>>> BuscarMailAsync(string mail) public async Task<IEnumerable<Dictionary<string, object>>> BuscarMailAsync(string mail)
@@ -61,7 +61,7 @@ public class LeadQueryRepository : ILeadQueryRepository
"sige_sam_v3.BuscarLeadMail", "sige_sam_v3.BuscarLeadMail",
new { mailbuscar = mail }, new { mailbuscar = mail },
commandType: System.Data.CommandType.StoredProcedure); commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)r); return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
} }
public async Task<IEnumerable<Dictionary<string, object>>> BuscarTituloAsync(string nombre) public async Task<IEnumerable<Dictionary<string, object>>> BuscarTituloAsync(string nombre)
@@ -71,7 +71,7 @@ public class LeadQueryRepository : ILeadQueryRepository
"sige_sam_v3.BuscarLeadTitulo", "sige_sam_v3.BuscarLeadTitulo",
new { nombrebuscar = nombre }, new { nombrebuscar = nombre },
commandType: System.Data.CommandType.StoredProcedure); commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)r); return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
} }
public async Task<IEnumerable<Dictionary<string, object>>> BuscarXestadoAsync(int ejecutivo, int estado) public async Task<IEnumerable<Dictionary<string, object>>> BuscarXestadoAsync(int ejecutivo, int estado)
@@ -81,7 +81,7 @@ public class LeadQueryRepository : ILeadQueryRepository
"sige_sam_v3.Lead_buscarXestado", "sige_sam_v3.Lead_buscarXestado",
new { userid = ejecutivo, estadoid = estado }, new { userid = ejecutivo, estadoid = estado },
commandType: System.Data.CommandType.StoredProcedure); commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)r); return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
} }
public async Task<IEnumerable<Dictionary<string, object>>> BuscarXfiltroAsync(string tipo, string busqueda) public async Task<IEnumerable<Dictionary<string, object>>> BuscarXfiltroAsync(string tipo, string busqueda)
@@ -91,7 +91,7 @@ public class LeadQueryRepository : ILeadQueryRepository
"sige_sam_v3.Lead_buscarXfiltro", "sige_sam_v3.Lead_buscarXfiltro",
new { tipofiltro = tipo, valorbuscar = busqueda }, new { tipofiltro = tipo, valorbuscar = busqueda },
commandType: System.Data.CommandType.StoredProcedure); commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)r); return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
} }
public async Task<IEnumerable<Dictionary<string, object>>> BuscarXinformeAsync(string tipo) public async Task<IEnumerable<Dictionary<string, object>>> BuscarXinformeAsync(string tipo)
@@ -101,7 +101,7 @@ public class LeadQueryRepository : ILeadQueryRepository
"sige_sam_v3.Lead_InformeXhoy", "sige_sam_v3.Lead_InformeXhoy",
new { tipoinforme = tipo }, new { tipoinforme = tipo },
commandType: System.Data.CommandType.StoredProcedure); commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)r); return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
} }
public async Task<IEnumerable<Dictionary<string, object>>> MontosAsync(int ejecutivo) public async Task<IEnumerable<Dictionary<string, object>>> MontosAsync(int ejecutivo)
@@ -111,7 +111,7 @@ public class LeadQueryRepository : ILeadQueryRepository
"sige_sam_v3.BuscarLeadMontos", "sige_sam_v3.BuscarLeadMontos",
new { ejecutivo = ejecutivo }, new { ejecutivo = ejecutivo },
commandType: System.Data.CommandType.StoredProcedure); commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)r); return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
} }
public async Task<IEnumerable<Dictionary<string, object>>> ActividadesAsync(int leadId, string tipo) public async Task<IEnumerable<Dictionary<string, object>>> ActividadesAsync(int leadId, string tipo)
@@ -121,7 +121,7 @@ public class LeadQueryRepository : ILeadQueryRepository
"sige_sam_v3.BuscarActividadesLead", "sige_sam_v3.BuscarActividadesLead",
new { id = leadId, tipoactividad = tipo }, new { id = leadId, tipoactividad = tipo },
commandType: System.Data.CommandType.StoredProcedure); commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)r); return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
} }
public async Task<IEnumerable<Dictionary<string, object>>> MotivosPerdidoAsync() public async Task<IEnumerable<Dictionary<string, object>>> MotivosPerdidoAsync()
@@ -130,7 +130,7 @@ public class LeadQueryRepository : ILeadQueryRepository
var rows = await connection.QueryAsync( var rows = await connection.QueryAsync(
"sige_sam_v3.BuscarMotivoLeadPerdido", "sige_sam_v3.BuscarMotivoLeadPerdido",
commandType: System.Data.CommandType.StoredProcedure); commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)r); return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
} }
public async Task<string> BuscarContactoAsync(int leadId) public async Task<string> BuscarContactoAsync(int leadId)
@@ -149,6 +149,6 @@ public class LeadQueryRepository : ILeadQueryRepository
"sige_sam_v3.LeadCantidadEjecutivoNuevo", "sige_sam_v3.LeadCantidadEjecutivoNuevo",
new { userid = ejecutivoId }, new { userid = ejecutivoId },
commandType: System.Data.CommandType.StoredProcedure); commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)r); return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
} }
} }
@@ -17,7 +17,7 @@ public class LeadRepository : ILeadRepository
public async Task<string> IngresarAsync(LeadCreateDto dto) public async Task<string> IngresarAsync(LeadCreateDto dto)
{ {
using var connection = new NpgsqlConnection(_connectionString); using var connection = new NpgsqlConnection(_connectionString);
using var reader = await connection.ExecuteReaderAsync( await connection.ExecuteAsync(
"sige_sam_v3.GrabaLead", "sige_sam_v3.GrabaLead",
new new
{ {
@@ -0,0 +1,45 @@
using Dapper;
using Npgsql;
using Ventas.Core.Interfaces;
namespace Ventas.Infrastructure.Repositories;
public class UsuarioRepository : IUsuarioRepository
{
private readonly string _connectionString;
public UsuarioRepository(string connectionString)
{
_connectionString = connectionString;
}
public async Task<string> BuscarAsync(string usuario, string clave)
{
using var connection = new NpgsqlConnection(_connectionString);
var result = await connection.QueryAsync(
"sam.BuscarUsuario",
new { userid = usuario, passid = clave },
commandType: System.Data.CommandType.StoredProcedure);
return result.Any() ? "ok" : "";
}
public async Task<string> InfoAsync(string usuario)
{
using var connection = new NpgsqlConnection(_connectionString);
var result = await connection.QueryAsync(
"sige_sam_v3.BuscarUsuario",
new { usuarioid = usuario },
commandType: System.Data.CommandType.StoredProcedure);
return result.Any() ? "ok" : "";
}
public async Task<string> PerfilAsync(string usuario)
{
using var connection = new NpgsqlConnection(_connectionString);
var result = await connection.QueryAsync(
"sige_sam_v3.BuscarPerfilUsuario",
new { usuario },
commandType: System.Data.CommandType.StoredProcedure);
return result.Any() ? "ok" : "";
}
}
@@ -0,0 +1,63 @@
using Dapper;
using Npgsql;
using Ventas.Core.Interfaces;
namespace Ventas.Services;
public class AlumnoService
{
private readonly IAlumnoRepository _alumnoRepository;
private readonly string _connectionString;
public AlumnoService(IAlumnoRepository alumnoRepository, string connectionString)
{
_alumnoRepository = alumnoRepository;
_connectionString = connectionString;
}
private async Task<IEnumerable<Dictionary<string, object>>> QuerySpAsync(string sp, object parameters)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(sp, parameters, commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
public async Task<IEnumerable<Dictionary<string, object>>> BuscarAsync(string tipoBusqueda, string nombre)
=> await QuerySpAsync("sige_sam_v3.BuscarAlumnos", new { tipobusqueda = tipoBusqueda, nombrealumno = nombre });
public async Task<string> IngresarV2Async(string rut, string nombre, string paterno, string materno, string fecha, string fono, string mail, int ocupacion, string profeOficio)
=> await _alumnoRepository.IngresarV2Async(rut, nombre, paterno, materno, "SIN DIRECCION", "1", fecha, fono, mail, ocupacion, profeOficio);
public async Task<string> IngresarApoderadoAsync(string rutApoderado, string rutAlumno, string nombre, string paterno, string materno, string direccion, string comuna, int nacionalidad, string fono, string mail)
=> await _alumnoRepository.IngresarApoderadoAsync(rutApoderado, rutAlumno, nombre, paterno, materno, direccion, comuna, nacionalidad, fono, mail);
public async Task<IEnumerable<Dictionary<string, object>>> BuscarApoderadoAsync(string tipoBusqueda, string nombre)
=> await QuerySpAsync("sige_sam_v3.BuscarApoderado", new { tipobusqueda = tipoBusqueda, nombreapoderado = nombre });
public async Task<IEnumerable<Dictionary<string, object>>> OcupacionAsync()
=> await QuerySpAsync("sige_sam_v3.BuscarOcupaciones", new { });
public async Task<IEnumerable<Dictionary<string, object>>> BuscarContratosAsync(string id)
=> await QuerySpAsync("sam.BuscarAlumnoContratoPersona", new { alumnoid = id });
public async Task<IEnumerable<Dictionary<string, object>>> BuscarBloqueoAsync(string idAlumno)
=> await QuerySpAsync("sige_sam_v3.BuscarBloqueoFinazas", new { idcliente = idAlumno });
public async Task<IEnumerable<Dictionary<string, object>>> BuscarAntiguedadAsync(string idAlumno)
=> await QuerySpAsync("sige_sam_v3.AlumnoAntiguedad", new { alumnoid = idAlumno });
public async Task<IEnumerable<Dictionary<string, object>>> BuscarFormasPagoAsync(int boleta)
=> await QuerySpAsync("sige_sam_v3.AlumnoFormaPagoContrato", new { boleta });
public async Task<IEnumerable<Dictionary<string, object>>> BuscarAnexosContratoAsync(string alumnoId, string contratoId)
=> await QuerySpAsync("sige_sam_v3.AlumnoBuscarAnexos", new { alumnoid = alumnoId, contrato = contratoId });
public async Task<IEnumerable<Dictionary<string, object>>> BuscarColegiosAsync(string nombre)
=> await QuerySpAsync("sam.BuscarColegios", new { nombrelike = nombre });
public async Task<IEnumerable<Dictionary<string, object>>> BuscarProfesionesAsync(string nombre)
=> await QuerySpAsync("sam.BuscarProfesiones", new { nombrelike = nombre });
public async Task<IEnumerable<Dictionary<string, object>>> BuscarComunasXnombreAsync(string nombre)
=> await QuerySpAsync("sam.BuscarComunasXnombre", new { nombrelike = nombre });
}
@@ -0,0 +1,34 @@
using Dapper;
using Npgsql;
namespace Ventas.Services;
public class ArqueoService
{
private readonly string _connectionString;
public ArqueoService(string connectionString)
{
_connectionString = connectionString;
}
public async Task<IEnumerable<Dictionary<string, object>>> TodosHoyAsync(DateTime fecha)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(
"sige_sam_v3.ArqueoHoy",
new { fechaarqueo = fecha },
commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
public async Task<IEnumerable<Dictionary<string, object>>> HoyAsync(string usuarioId, DateTime fecha)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(
"sige_sam_v3.ArqueoEjecutivo",
new { usuarioid = usuarioId, fechaarqueo = fecha },
commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
}
@@ -0,0 +1,86 @@
using Dapper;
using Npgsql;
using Ventas.Core.Interfaces;
namespace Ventas.Services;
public class ContratoService
{
private readonly IContratoRepository _contratoRepository;
private readonly string _connectionString;
public ContratoService(IContratoRepository contratoRepository, string connectionString)
{
_contratoRepository = contratoRepository;
_connectionString = connectionString;
}
private async Task<IEnumerable<Dictionary<string, object>>> QuerySpAsync(string sp, object parameters)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(sp, parameters, commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
public async Task<string> IngresarAsync(int cotizacionId, string fechaContrato, int boletaId, int vendedorId)
=> await _contratoRepository.IngresarAsync(cotizacionId, 1, fechaContrato, boletaId, vendedorId);
public async Task<string> IngresarDetalleAsync(int contratoId, string empresaId, string alumnoId, string cursoId, string fecha, int vendedor, int registroAcademico, int alumnoTipo)
=> await _contratoRepository.IngresarDetalleAsync(contratoId, empresaId, alumnoId, cursoId, fecha, vendedor, registroAcademico, alumnoTipo);
public async Task<IEnumerable<Dictionary<string, object>>> PdfContratoAsync(int contrato)
=> await QuerySpAsync("sige_sam_v3.PDFContrato", new { contrato });
public async Task<IEnumerable<Dictionary<string, object>>> PdfContratoJornadasAsync(int contrato)
=> await QuerySpAsync("sige_sam_v3.PDFContratoJornadas", new { contrato });
public async Task<IEnumerable<Dictionary<string, object>>> PdfContratoProgramasCursosAsync(int contrato)
=> await QuerySpAsync("sige_sam_v3.PDFContratoProgramasCursos", new { contrato });
public async Task<IEnumerable<Dictionary<string, object>>> PdfContratoSedesAsync(int contrato)
=> await QuerySpAsync("sige_sam_v3.PDFContratoSedes", new { contrato });
public async Task<IEnumerable<Dictionary<string, object>>> BuscarInformacionAsync(int contrato)
=> await QuerySpAsync("sige_sam_v3.BuscarInfoContrato", new { contratoid = contrato });
public async Task<string> IngresarContratoEmpresaAsync(int cotizacionId, string empresaId, int tipoVenta, int facturaId, int cantCursos, int vendedorId)
{
using var connection = new NpgsqlConnection(_connectionString);
var result = await connection.QuerySingleOrDefaultAsync<string>(
"IngresarContratoEmpresa",
new { cotizacionid = cotizacionId, empresaid = empresaId, tipoventaid = tipoVenta, facturaid = facturaId, cantidadcursos = cantCursos, vendedorid = vendedorId },
commandType: System.Data.CommandType.StoredProcedure);
return result ?? "ok";
}
public async Task<string> IngresarContratoCerradoAsync(int prop, string rut, int tipoVenta, string vendedorId)
{
using var connection = new NpgsqlConnection(_connectionString);
var result = await connection.QuerySingleOrDefaultAsync<string>(
"Empresa_IngresarContratoV2",
new { prop, rut, tipo = tipoVenta, vendedor = vendedorId },
commandType: System.Data.CommandType.StoredProcedure);
return result ?? "ok";
}
public async Task<string> IngresarFirmaPendienteAsync(int contratoId)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync("Empresa_IngresarContratoFirma", new { cont = contratoId }, commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
public async Task<string> ActualizarEstadoContratoAsync(string tipo, int contId, string varA, int varB)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync("Empresa_ActualizaContrato", new { tipo, contrato = contId, varz = varA, vary = varB }, commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
public async Task<string> ActualizarFirmaContratoAsync(string tipo, int contrato, int firmado)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync("Empresa_ActualizaFirmaContrato", new { tipoeleccion = tipo, cont = contrato, fir = firmado }, commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
}
@@ -0,0 +1,80 @@
using Dapper;
using Npgsql;
using Ventas.Core.Interfaces;
namespace Ventas.Services;
public class CotizacionService
{
private readonly ICotizacionRepository _cotizacionRepository;
private readonly string _connectionString;
public CotizacionService(ICotizacionRepository cotizacionRepository, string connectionString)
{
_cotizacionRepository = cotizacionRepository;
_connectionString = connectionString;
}
private async Task<IEnumerable<Dictionary<string, object>>> QuerySpAsync(string sp, object parameters)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(sp, parameters, commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
public async Task<string> IngresarAsync(string apoderadoId, string vendedorId, int descuento, int tipoDescuento, string fecha, int monto, string validez, int leadId)
=> await _cotizacionRepository.PersonaIngresarAsync(apoderadoId, vendedorId, 1, descuento, tipoDescuento, fecha, 1, 1, monto, validez, leadId);
public async Task<string> IngresarDetalleAsync(int cotizacion, string alumnoId, int cursoId, int cantidad, int tarifa)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync("sige_sam_v3.IngresarCotizacionDetalle",
new { cotizaion = cotizacion, alumno = alumnoId, codigocurso = cursoId, cantidad, tarifa },
commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
public async Task<string> DetalleSinCursoAsync(int cotizacion, string alumnoId, string apoderadoId, int programaId, int cantidad, int tarifa, int sedeId)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync("sige_sam_v3.IngresarAnexoCotiSinCurso",
new { cotiid = cotizacion, alumnid = alumnoId, apoid = apoderadoId, programid = programaId, cursos = cantidad, tarifaid = tarifa, idsede = sedeId },
commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
public async Task<IEnumerable<Dictionary<string, object>>> BuscarAsync(int lead)
=> await QuerySpAsync("sige_sam_v3.BuscarCotizacionLead", new { leadid = lead });
public async Task<IEnumerable<Dictionary<string, object>>> BuscarSinCursoAsync(int lead)
=> await QuerySpAsync("sige_sam_v3.BuscarCotizacionesSinCurso", new { leadid = lead });
public async Task<IEnumerable<Dictionary<string, object>>> BuscarInfoAsync(string tipoBusqueda, string nombre)
=> await QuerySpAsync("sige_sam_v3.BuscarCotizacion", new { tipobusqueda = tipoBusqueda, nombre, fecha = DateTime.Now.ToString("yyyy-MM-dd") });
public async Task<IEnumerable<Dictionary<string, object>>> DetalleAsync(int cotizacion)
=> await QuerySpAsync("sige_sam_v3.BuscarCotizacionDetalle", new { cotizacion });
public async Task<string> PersonaPagarAsync(int cotizacion)
=> await _cotizacionRepository.PersonaPagarAsync(cotizacion);
public async Task<string> DesactivarAsync(int cotizacion)
=> await _cotizacionRepository.DesactivarAsync(cotizacion);
public async Task<string> CotizacionEmpIngresoAsync(string empresaId, string vendedor, int alumnos, int curso, int empresaMonto, int oticMonto, int alumnoMonto, int cotizacionTipo, DateTime validez, int descuentoId, int estadoId, string oticId, string motivo)
{
using var connection = new NpgsqlConnection(_connectionString);
var result = await connection.QuerySingleOrDefaultAsync<string>(
"Empresa_IngresarCotizacionEmpresaV2",
new { rutempresa = empresaId, vendedor, cantidadcursos = curso, cantidadalumnos = alumnos, montpempresa = empresaMonto, montootic = oticMonto, montoalm = alumnoMonto, tipocotz = cotizacionTipo, validez, iddescuento = descuentoId, idestado = estadoId, oticid = oticId, motivo },
commandType: System.Data.CommandType.StoredProcedure);
return result ?? "ok";
}
public async Task<string> ActualizaEstadoCotizacionAsync(string tipo, int cotzId, string varA, int varB)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync("Empresa_ActualizaCotizacion", new { tipo, cotizacion = cotzId, varz = varA, vary = varB }, commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
}
@@ -0,0 +1,431 @@
using Dapper;
using Npgsql;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using Ventas.Core.Interfaces;
namespace Ventas.Services;
public class EmpresaReportService
{
private readonly IInformeRepository _informeRepository;
private readonly string _connectionString;
public EmpresaReportService(IInformeRepository informeRepository, string connectionString)
{
_informeRepository = informeRepository;
_connectionString = connectionString;
}
private async Task<IEnumerable<Dictionary<string, object>>> ContratoCrysAsync(string tipo, int cont)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(
"Empresa_ContratoCrys",
new { tipo, cont },
commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
private static string GetString(Dictionary<string, object> row, string key) =>
row.GetValueOrDefault(key)?.ToString() ?? "";
public async Task<byte[]> ContratoAbiertoPdfAsync(int contratoId)
{
var dt01 = await ContratoCrysAsync("CONTRATOREIMPRESION", contratoId);
var dt02 = await ContratoCrysAsync("DETALLECURSO", contratoId);
var dt03 = await ContratoCrysAsync("HORARIO", contratoId);
var dt04 = await ContratoCrysAsync("ALUMNO", contratoId);
var header = dt01.FirstOrDefault();
if (header == null) return [];
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(57);
page.DefaultTextStyle(x => x.FontSize(9));
page.Header().Element(c => ComposeEmpHeader(c, GetString(header, "NombreEmp"), contratoId.ToString()));
page.Content().Element(c => ComposeEmpContent(c, header, dt02, dt03, dt04));
page.Footer().AlignCenter().Text(t => { t.Span("Página "); t.CurrentPageNumber(); });
});
}).GeneratePdf();
}
public async Task<byte[]> ContratoCerradoPdfAsync(int propuestaId, int contratoId)
{
var header = (await _informeRepository.EjecutarCrystalSpAsync("PROPUESTA", propuestaId, 0, contratoId)).FirstOrDefault();
var cursos = await _informeRepository.EjecutarCrystalSpAsync("CONTRATOCURSO", propuestaId, 0, contratoId);
var alumnos = await _informeRepository.EjecutarCrystalSpAsync("CONTRATOALUMNO", propuestaId, 0, contratoId);
if (header == null) return [];
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(57);
page.DefaultTextStyle(x => x.FontSize(9));
page.Header().Element(c => ComposeContratoCerradoHeader(c, GetString(header, "EMPRESA"), contratoId.ToString()));
page.Content().Element(c => ComposeContratoCerradoContent(c, header, cursos, alumnos));
page.Footer().AlignCenter().Text(t => { t.Span("Página "); t.CurrentPageNumber(); });
});
}).GeneratePdf();
}
public async Task<byte[]> CotizacionCursoCerradoPdfAsync(int propuestaId)
{
var header = (await _informeRepository.EjecutarCrystalSpAsync("PROPUESTA", propuestaId, 0, 0)).FirstOrDefault();
var detalle = await _informeRepository.EjecutarCrystalSpAsync("PROPUESTADET", propuestaId, 0, 0);
if (header == null) return [];
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(57);
page.DefaultTextStyle(x => x.FontSize(9));
page.Header().Element(c => ComposeCotizacionCCHeader(c, GetString(header, "EMPRESA"), propuestaId.ToString()));
page.Content().Element(c => ComposeCotizacionCCContent(c, header, detalle));
page.Footer().AlignCenter().Text(t => { t.Span("Página "); t.CurrentPageNumber(); });
});
}).GeneratePdf();
}
public async Task<byte[]> CotizacionPlanCentralPdfAsync(int cotizacionId)
{
var header = (await _informeRepository.EjecutarCrystalSpAsync("COTIZACIONV2", 0, cotizacionId, 0)).FirstOrDefault();
var detalle = await _informeRepository.EjecutarCrystalSpAsync("COTIZACIONDETV2", 0, cotizacionId, 0);
if (header == null) return [];
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(57);
page.DefaultTextStyle(x => x.FontSize(9));
page.Header().Element(c => ComposePlanCentralHeader(c, GetString(header, "Razon Social"), cotizacionId.ToString()));
page.Content().Element(c => ComposePlanCentralContent(c, header, detalle));
page.Footer().AlignCenter().Text(t => { t.Span("Página "); t.CurrentPageNumber(); });
});
}).GeneratePdf();
}
private static void ComposeEmpHeader(IContainer container, string empresa, string numContrato)
{
container.Row(row =>
{
row.RelativeItem().Column(col =>
{
col.Item().Text("Instituto Chileno Norteamericano").FontSize(12).Bold();
col.Item().Text("Módulo Empresa").FontSize(8);
});
row.ConstantItem(140).AlignRight().Column(col =>
{
col.Item().Text($"Contrato N° {numContrato}").FontSize(14).Bold();
});
});
}
private static void ComposeEmpContent(IContainer container, Dictionary<string, object> header,
IEnumerable<Dictionary<string, object>> cursos, IEnumerable<Dictionary<string, object>> horarios,
IEnumerable<Dictionary<string, object>> alumnos)
{
container.Column(col =>
{
col.Item().PaddingTop(15).Table(table =>
{
table.ColumnsDefinition(c => { c.ConstantColumn(110); c.RelativeColumn(); });
table.Cell().Text("Empresa:").Bold(); table.Cell().Text(GetString(header, "NombreEmp"));
table.Cell().Text("RUT:").Bold(); table.Cell().Text(GetString(header, "idEmpresa"));
table.Cell().Text("Dirección:").Bold(); table.Cell().Text(GetString(header, "DireccionEmp"));
table.Cell().Text("Representante:").Bold(); table.Cell().Text(GetString(header, "Representante"));
table.Cell().Text("Vendedor:").Bold(); table.Cell().Text(GetString(header, "Vendedor"));
});
col.Item().PaddingTop(15).Table(table =>
{
table.ColumnsDefinition(c => { c.ConstantColumn(30); c.RelativeColumn(); c.ConstantColumn(60); c.ConstantColumn(70); c.ConstantColumn(70); });
table.Header(h => { h.Cell().Text("#"); h.Cell().Text("Curso"); h.Cell().Text("Duración"); h.Cell().Text("Inicio"); h.Cell().Text("Término"); });
int i = 1;
foreach (var c in cursos)
{
table.Cell().Text((i++).ToString()); table.Cell().Text(GetString(c, "Curso"));
table.Cell().Text(GetString(c, "Duracion")); table.Cell().Text(GetString(c, "FechaInicio")); table.Cell().Text(GetString(c, "FechaTermino"));
}
});
col.Item().PaddingTop(10).Text("Horarios:").Bold();
foreach (var h in horarios) col.Item().PaddingLeft(10).Text(GetString(h, "Horario"));
col.Item().PaddingTop(10).Text("Alumnos:").Bold();
foreach (var a in alumnos) col.Item().PaddingLeft(10).Text(GetString(a, "Alumno"));
});
}
private static void ComposeContratoCerradoHeader(IContainer container, string empresa, string num)
{
container.Row(row =>
{
row.RelativeItem().Column(col => { col.Item().Text("Instituto Chileno Norteamericano").FontSize(12).Bold(); col.Item().Text("Contrato Cerrado").FontSize(8); });
row.ConstantItem(140).AlignRight().Column(col => { col.Item().Text($"Contrato N° {num}").FontSize(14).Bold(); });
});
}
private static void ComposeContratoCerradoContent(IContainer container, Dictionary<string, object> h,
IEnumerable<Dictionary<string, object>> cursos, IEnumerable<Dictionary<string, object>> alumnos)
{
container.Column(col =>
{
col.Item().PaddingTop(15).Table(table =>
{
table.ColumnsDefinition(c => { c.ConstantColumn(110); c.RelativeColumn(); });
table.Cell().Text("Empresa:").Bold(); table.Cell().Text(GetString(h, "EMPRESA"));
table.Cell().Text("RUT:").Bold(); table.Cell().Text(GetString(h, "RUT"));
table.Cell().Text("Vendedor:").Bold(); table.Cell().Text(GetString(h, "VENDEDOR"));
table.Cell().Text("Total:").Bold(); table.Cell().Text("$" + GetString(h, "TOTAL"));
});
col.Item().PaddingTop(15).Table(table =>
{
table.ColumnsDefinition(c => { c.ConstantColumn(25); c.RelativeColumn(); c.ConstantColumn(50); c.ConstantColumn(60); c.ConstantColumn(60); });
table.Header(h => { h.Cell().Text("#"); h.Cell().Text("Curso"); h.Cell().Text("Horas"); h.Cell().Text("Inicio"); h.Cell().Text("Valor"); });
int i = 1;
foreach (var c in cursos)
{
table.Cell().Text((i++).ToString()); table.Cell().Text(GetString(c, "CURSO"));
table.Cell().Text(GetString(c, "Duracion")); table.Cell().Text(GetString(c, "FechaInicio")); table.Cell().Text("$" + GetString(c, "ValorGrupal"));
}
});
col.Item().PaddingTop(10).Text("Alumnos:").Bold();
int idx = 1;
foreach (var a in alumnos) col.Item().PaddingLeft(10).Text($"{idx++}. {GetString(a, "Alumnos")}");
});
}
private static void ComposeCotizacionCCHeader(IContainer container, string empresa, string num)
{
container.Row(row =>
{
row.RelativeItem().Column(col => { col.Item().Text("Instituto Chileno Norteamericano").FontSize(12).Bold(); col.Item().Text("Cotización Curso Cerrado").FontSize(8); });
row.ConstantItem(140).AlignRight().Column(col => { col.Item().Text($"Cotización N° {num}").FontSize(14).Bold(); });
});
}
private static void ComposeCotizacionCCContent(IContainer container, Dictionary<string, object> h,
IEnumerable<Dictionary<string, object>> detalle)
{
container.Column(col =>
{
col.Item().PaddingTop(15).Table(table =>
{
table.ColumnsDefinition(c => { c.ConstantColumn(100); c.RelativeColumn(); });
table.Cell().Text("Empresa:").Bold(); table.Cell().Text(GetString(h, "EMPRESA"));
table.Cell().Text("RUT:").Bold(); table.Cell().Text(GetString(h, "RUT"));
table.Cell().Text("Vendedor:").Bold(); table.Cell().Text(GetString(h, "VENDEDOR"));
table.Cell().Text("Total:").Bold(); table.Cell().Text("$" + GetString(h, "TOTAL"));
table.Cell().Text("Vigencia:").Bold(); table.Cell().Text(GetString(h, "VALIDEZ"));
});
col.Item().PaddingTop(15).Table(table =>
{
table.ColumnsDefinition(c => { c.ConstantColumn(25); c.RelativeColumn(); c.ConstantColumn(60); c.ConstantColumn(60); });
table.Header(h => { h.Cell().Text("#"); h.Cell().Text("Curso"); h.Cell().Text("Jornada"); h.Cell().Text("Valor"); });
int i = 1;
foreach (var d in detalle)
{
table.Cell().Text((i++).ToString()); table.Cell().Text(GetString(d, "CURSO"));
table.Cell().Text(GetString(d, "JORNADA")); table.Cell().Text("$" + GetString(d, "VALOR CURSO"));
}
});
});
}
private static void ComposePlanCentralHeader(IContainer container, string empresa, string num)
{
container.Row(row =>
{
row.RelativeItem().Column(col => { col.Item().Text("Instituto Chileno Norteamericano").FontSize(12).Bold(); col.Item().Text("Cotización Plan Central").FontSize(8); });
row.ConstantItem(140).AlignRight().Column(col => { col.Item().Text($"Cotización N° {num}").FontSize(14).Bold(); });
});
}
private static void ComposePlanCentralContent(IContainer container, Dictionary<string, object> h,
IEnumerable<Dictionary<string, object>> detalle)
{
container.Column(col =>
{
col.Item().PaddingTop(15).Table(table =>
{
table.ColumnsDefinition(c => { c.ConstantColumn(110); c.RelativeColumn(); });
table.Cell().Text("Empresa:").Bold(); table.Cell().Text(GetString(h, "Razon Social"));
table.Cell().Text("RUT:").Bold(); table.Cell().Text(GetString(h, "R.U.T."));
table.Cell().Text("Ejecutivo:").Bold(); table.Cell().Text(GetString(h, "Ejecutivo"));
table.Cell().Text("Total:").Bold(); table.Cell().Text("$" + GetString(h, "Total con descuento"));
table.Cell().Text("Vigencia:").Bold(); table.Cell().Text(GetString(h, "Vigencia"));
});
col.Item().PaddingTop(15).Table(table =>
{
table.ColumnsDefinition(c => { c.ConstantColumn(25); c.RelativeColumn(); c.ConstantColumn(50); c.ConstantColumn(60); c.ConstantColumn(60); });
table.Header(h => { h.Cell().Text("#"); h.Cell().Text("Curso"); h.Cell().Text("Periodo"); h.Cell().Text("Inicio"); h.Cell().Text("Valor"); });
int i = 1;
foreach (var d in detalle)
{
table.Cell().Text((i++).ToString()); table.Cell().Text(GetString(d, "NombreCurso"));
table.Cell().Text(GetString(d, "Periodo")); table.Cell().Text(GetString(d, "FechaInicio")); table.Cell().Text("$" + GetString(d, "ValorCurso"));
}
});
});
}
// Variantes CAEMP/CAEMPSNC/CAEMPV2 — mismo SP, layout con/sin SENCE
public async Task<byte[]> ContratoAbiertoSinSencePdfAsync(int contratoId)
{
var dt01 = await ContratoCrysAsync("CONTRATOREIMPRESION", contratoId);
var dt02 = await ContratoCrysAsync("DETALLECURSO", contratoId);
var header = dt01.FirstOrDefault();
if (header == null) return [];
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4); page.Margin(57); page.DefaultTextStyle(x => x.FontSize(9));
page.Header().Element(c => ComposeEmpHeader(c, GetString(header, "NombreEmp"), contratoId.ToString()));
page.Content().Column(col =>
{
col.Item().PaddingTop(15).Text("CONTRATO SIN SENCE").FontSize(12).Bold();
col.Item().PaddingTop(10).Table(table =>
{
table.ColumnsDefinition(c => { c.ConstantColumn(30); c.RelativeColumn(); c.ConstantColumn(60); c.ConstantColumn(70); c.ConstantColumn(70); });
table.Header(h => { h.Cell().Text("#"); h.Cell().Text("Curso"); h.Cell().Text("Duración"); h.Cell().Text("Inicio"); h.Cell().Text("Término"); });
int i = 1;
foreach (var c in dt02)
{
table.Cell().Text((i++).ToString()); table.Cell().Text(GetString(c, "Curso"));
table.Cell().Text(GetString(c, "Duracion")); table.Cell().Text(GetString(c, "FechaInicio")); table.Cell().Text(GetString(c, "FechaTermino"));
}
});
});
page.Footer().AlignCenter().Text(t => { t.Span("Página "); t.CurrentPageNumber(); });
});
}).GeneratePdf();
}
public async Task<byte[]> ContratoAbiertoConSencePdfAsync(int contratoId)
{
var dt01 = await ContratoCrysAsync("CONTRATOREIMPRESION", contratoId);
var dt02 = await ContratoCrysAsync("DETALLECURSO", contratoId);
var header = dt01.FirstOrDefault();
if (header == null) return [];
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4); page.Margin(57); page.DefaultTextStyle(x => x.FontSize(9));
page.Header().Element(c => ComposeEmpHeader(c, GetString(header, "NombreEmp"), contratoId.ToString()));
page.Content().Column(col =>
{
col.Item().PaddingTop(15).Text("CONTRATO CON SENCE").FontSize(12).Bold();
col.Item().PaddingTop(10).Table(table =>
{
table.ColumnsDefinition(c => { c.ConstantColumn(30); c.RelativeColumn(); c.ConstantColumn(60); c.ConstantColumn(70); c.ConstantColumn(70); c.ConstantColumn(60); });
table.Header(h => { h.Cell().Text("#"); h.Cell().Text("Curso"); h.Cell().Text("Sence"); h.Cell().Text("Duración"); h.Cell().Text("Inicio"); h.Cell().Text("Término"); });
int i = 1;
foreach (var c in dt02)
{
table.Cell().Text((i++).ToString()); table.Cell().Text(GetString(c, "Curso"));
table.Cell().Text(GetString(c, "Sence")); table.Cell().Text(GetString(c, "Duracion"));
table.Cell().Text(GetString(c, "FechaInicio")); table.Cell().Text(GetString(c, "FechaTermino"));
}
});
});
page.Footer().AlignCenter().Text(t => { t.Span("Página "); t.CurrentPageNumber(); });
});
}).GeneratePdf();
}
public async Task<byte[]> ContratoCerradoGeneralPdfAsync(int propuestaId, int contratoId)
=> await ContratoCerradoPdfAsync(propuestaId, contratoId);
public async Task<byte[]> ContratoCerradoConDescuentoPdfAsync(int propuestaId, int contratoId)
{
var header = (await _informeRepository.EjecutarCrystalSpAsync("PROPUESTA", propuestaId, 0, contratoId)).FirstOrDefault();
var cursos = await _informeRepository.EjecutarCrystalSpAsync("CONTRATOCURSO", propuestaId, 0, contratoId);
if (header == null) return [];
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4); page.Margin(57); page.DefaultTextStyle(x => x.FontSize(9));
page.Header().Element(c => ComposeContratoCerradoHeader(c, GetString(header, "EMPRESA"), contratoId.ToString()));
page.Content().Column(col =>
{
col.Item().PaddingTop(15).Text("CONTRATO CON DESCUENTO").FontSize(12).Bold();
col.Item().PaddingTop(10).Table(table =>
{
table.ColumnsDefinition(c => { c.ConstantColumn(110); c.RelativeColumn(); });
table.Cell().Text("Total:").Bold(); table.Cell().Text("$" + GetString(header, "TOTAL"));
table.Cell().Text("Descuento:").Bold(); table.Cell().Text("$" + GetString(header, "DESCUENTO"));
table.Cell().Text("Total c/Desc:").Bold(); table.Cell().Text("$" + GetString(header, "TOTAL CON DESCUENTO"));
});
col.Item().PaddingTop(15).Table(table =>
{
table.ColumnsDefinition(c => { c.ConstantColumn(25); c.RelativeColumn(); c.ConstantColumn(60); c.ConstantColumn(60); });
table.Header(h => { h.Cell().Text("#"); h.Cell().Text("Curso"); h.Cell().Text("Valor"); h.Cell().Text("Dcto Apl."); });
int i = 1;
foreach (var c in cursos)
{
table.Cell().Text((i++).ToString()); table.Cell().Text(GetString(c, "CURSO"));
table.Cell().Text("$" + GetString(c, "ValorGrupal")); table.Cell().Text("$" + GetString(c, "ValorGrupalDesctoAplicado"));
}
});
});
page.Footer().AlignCenter().Text(t => { t.Span("Página "); t.CurrentPageNumber(); });
});
}).GeneratePdf();
}
public async Task<byte[]> PropuestaComercialPdfAsync(int propuestaId, int cotizacionId, int contratoId)
{
var data = await _informeRepository.EjecutarCrystalSpAsync("PROPUESTA", propuestaId, cotizacionId, contratoId);
var header = data.FirstOrDefault();
if (header == null) return [];
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4); page.Margin(57);
page.DefaultTextStyle(x => x.FontSize(12));
page.Content().Column(col =>
{
col.Item().PaddingTop(40).AlignCenter().Text("PROPUESTA COMERCIAL").FontSize(20).Bold();
col.Item().PaddingTop(30).AlignCenter().Text("Instituto Chileno Norteamericano").FontSize(16);
col.Item().PaddingTop(40).Table(table =>
{
table.ColumnsDefinition(c => { c.ConstantColumn(120); c.RelativeColumn(); });
table.Cell().Text("Empresa:").Bold(); table.Cell().Text(GetString(header, "EMPRESA"));
table.Cell().Text("RUT:").Bold(); table.Cell().Text(GetString(header, "RUT"));
table.Cell().Text("Vendedor:").Bold(); table.Cell().Text(GetString(header, "VENDEDOR"));
table.Cell().Text("Total:").Bold(); table.Cell().Text("$" + GetString(header, "TOTAL"));
});
col.Item().PaddingTop(20).AlignCenter().Text("Documento generado electrónicamente").FontSize(9).Italic();
});
});
}).GeneratePdf();
}
}
@@ -0,0 +1,31 @@
using Ventas.Core.Interfaces;
namespace Ventas.Services;
public class InformeService
{
private readonly IInformeRepository _informeRepository;
public InformeService(IInformeRepository informeRepository)
{
_informeRepository = informeRepository;
}
public async Task<IEnumerable<Dictionary<string, object>>> InformeMensualAsync(int mes, int agno, string tipo)
=> await _informeRepository.InformeMensualAsync(mes, agno, tipo);
public async Task<IEnumerable<Dictionary<string, object>>> InformeMensualEjecutivoAsync(int mes, int agno, string tipo, string ejecutivo, int montoVenta)
=> await _informeRepository.InformeMensualEjecutivoAsync(mes, agno, tipo, ejecutivo, montoVenta);
public async Task<IEnumerable<Dictionary<string, object>>> InformeLeadDiasAsync(DateTime inicio, DateTime termino, string vendedorId)
=> await _informeRepository.InformeLeadDiasAsync(inicio, termino, vendedorId);
public async Task<IEnumerable<Dictionary<string, object>>> InformeVentasCursosEmpresaAsync(string tipo, string varA, string varB, int varC)
=> await _informeRepository.InformeVentasCursosEmpresaAsync(tipo, varA, varB, varC);
public async Task<IEnumerable<Dictionary<string, object>>> InformeDocumentosAsync(string tipo, string varA, string varB, int varC)
=> await _informeRepository.InformeDocumentosAsync(tipo, varA, varB, varC);
public async Task<IEnumerable<Dictionary<string, object>>> InformeGeneralAsync(string busqueda, string varA, string varB)
=> await _informeRepository.InformeGeneralAsync(busqueda, varA, varB);
}
+38
View File
@@ -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);
}
}
+160
View File
@@ -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,461 @@
using Dapper;
using Npgsql;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace Ventas.Services;
public class ReportService
{
private readonly ContratoService _contratoService;
private readonly CotizacionService _cotizacionService;
private readonly string _connectionString;
public ReportService(ContratoService contratoService, CotizacionService cotizacionService, string connectionString)
{
_contratoService = contratoService;
_cotizacionService = cotizacionService;
_connectionString = connectionString;
}
private async Task<IEnumerable<Dictionary<string, object>>> QuerySpAsync(string sp, object parameters)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(sp, parameters, commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
public async Task<byte[]> GenerarContratoPdfAsync(int contratoId)
{
var data = await _contratoService.PdfContratoAsync(contratoId);
var jornadas = await _contratoService.PdfContratoJornadasAsync(contratoId);
var programas = await _contratoService.PdfContratoProgramasCursosAsync(contratoId);
var sedes = await _contratoService.PdfContratoSedesAsync(contratoId);
var row = data.FirstOrDefault();
if (row == null) return [];
var numeroContrato = row.GetValueOrDefault("NumeroContrato")?.ToString() ?? contratoId.ToString();
var alumno = row.GetValueOrDefault("Alumno")?.ToString() ?? "";
var rut = row.GetValueOrDefault("Rut")?.ToString() ?? "";
var programa = string.Join(", ", programas.Select(p => p.GetValueOrDefault("Nombre")?.ToString()));
var sede = string.Join(", ", sedes.Select(s => s.GetValueOrDefault("Nombre")?.ToString()));
var jornada = string.Join(", ", jornadas.Select(j => j.GetValueOrDefault("Nombre")?.ToString()));
var fecha = row.GetValueOrDefault("Fecha")?.ToString() ?? "";
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(57);
page.DefaultTextStyle(x => x.FontSize(10));
page.Header().Element(c => ComposeContratoHeader(c, numeroContrato));
page.Content().Element(c => ComposeContratoContent(c, alumno, rut, programa, sede, jornada, fecha));
page.Footer().AlignCenter().Text(text =>
{
text.Span("Página ");
text.CurrentPageNumber();
});
});
}).GeneratePdf();
}
private void ComposeContratoHeader(IContainer container, string numero)
{
container.Row(row =>
{
row.RelativeItem();
row.ConstantItem(120).AlignRight().Column(col =>
{
col.Item().Text($"Contrato N° {numero}").FontSize(16).Bold();
});
});
}
private void ComposeContratoContent(IContainer container, string alumno, string rut, string programa, string sede, string jornada, string fecha)
{
container.Column(col =>
{
col.Item().PaddingTop(20).Table(table =>
{
table.ColumnsDefinition(c =>
{
c.ConstantColumn(120);
c.RelativeColumn();
});
table.Cell().Text("Alumno:").Bold();
table.Cell().Text(alumno);
table.Cell().Text("RUT:").Bold();
table.Cell().Text(rut);
table.Cell().Text("Programa:").Bold();
table.Cell().Text(programa);
table.Cell().Text("Sede:").Bold();
table.Cell().Text(sede);
table.Cell().Text("Jornada:").Bold();
table.Cell().Text(jornada);
table.Cell().Text("Fecha:").Bold();
table.Cell().Text(fecha);
});
col.Item().PaddingTop(20).Table(table =>
{
table.ColumnsDefinition(c =>
{
c.ConstantColumn(40);
c.RelativeColumn();
c.ConstantColumn(80);
c.ConstantColumn(80);
});
table.Header(header =>
{
header.Cell().Text("#").Bold();
header.Cell().Text("Curso").Bold();
header.Cell().Text("Horas").Bold();
header.Cell().Text("Valor").Bold();
});
});
});
}
public async Task<byte[]> GenerarCotizacionPdfAsync(int cotizacionId)
{
var data = await _cotizacionService.BuscarInfoAsync("NUMERO", cotizacionId.ToString());
var detalle = await _cotizacionService.DetalleAsync(cotizacionId);
var row = data.FirstOrDefault();
if (row == null) return [];
var numero = cotizacionId.ToString();
var nombre = row.GetValueOrDefault("NombreLead")?.ToString() ?? "";
var total = row.GetValueOrDefault("Monto")?.ToString() ?? "0";
var fecha = row.GetValueOrDefault("Fecha")?.ToString() ?? "";
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(57);
page.Header().Element(c => ComposeCotizacionHeader(c, numero));
page.Content().Element(c => ComposeCotizacionContent(c, nombre, total, fecha, detalle));
page.Footer().AlignCenter().Text(text =>
{
text.Span("Página ");
text.CurrentPageNumber();
});
});
}).GeneratePdf();
}
private void ComposeCotizacionHeader(IContainer container, string numero)
{
container.Row(row =>
{
row.RelativeItem();
row.ConstantItem(140).AlignRight().Column(col =>
{
col.Item().Text($"Cotización N° {numero}").FontSize(16).Bold();
col.Item().Text("Instituto Chileno Norteamericano").FontSize(9);
});
});
}
private void ComposeCotizacionContent(IContainer container, string nombre, string total, string fecha, IEnumerable<Dictionary<string, object>> detalle)
{
container.Column(col =>
{
col.Item().PaddingTop(20).Table(table =>
{
table.ColumnsDefinition(c =>
{
c.ConstantColumn(100);
c.RelativeColumn();
});
table.Cell().Text("Cliente:").Bold();
table.Cell().Text(nombre);
table.Cell().Text("Fecha:").Bold();
table.Cell().Text(fecha);
table.Cell().Text("Total:").Bold();
table.Cell().Text($"${total}");
});
col.Item().PaddingTop(20).Table(table =>
{
table.ColumnsDefinition(c =>
{
c.ConstantColumn(40);
c.RelativeColumn();
c.ConstantColumn(80);
});
table.Header(header =>
{
header.Cell().Text("#").Bold();
header.Cell().Text("Curso").Bold();
header.Cell().Text("Valor").Bold();
});
int index = 1;
foreach (var d in detalle)
{
table.Cell().Text(index++.ToString());
table.Cell().Text(d.GetValueOrDefault("Curso")?.ToString() ?? "");
table.Cell().Text(d.GetValueOrDefault("Valor")?.ToString() ?? "0");
}
});
});
}
public async Task<byte[]> GenerarArqueoPdfAsync(string usuario, DateTime fecha, IEnumerable<Dictionary<string, object>> ingresos, int totalCredito, int totalDebito, int totalIntl, int totalEstado, int totalGeneral)
{
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(57);
page.Header().Element(c => ComposeArqueoHeader(c, usuario, fecha));
page.Content().Element(c => ComposeArqueoContent(c, ingresos, totalCredito, totalDebito, totalIntl, totalEstado, totalGeneral));
page.Footer().AlignCenter().Text(text =>
{
text.Span("Página ");
text.CurrentPageNumber();
});
});
}).GeneratePdf();
}
private void ComposeArqueoHeader(IContainer container, string usuario, DateTime fecha)
{
container.Row(row =>
{
row.RelativeItem();
row.ConstantItem(150).AlignRight().Column(col =>
{
col.Item().Text("Arqueo de Caja").FontSize(16).Bold();
col.Item().Text($"Cajero: {usuario}").FontSize(10);
col.Item().Text($"Fecha: {fecha:dd-MM-yyyy}").FontSize(10);
});
});
}
private void ComposeArqueoContent(IContainer container, IEnumerable<Dictionary<string, object>> ingresos, int totalCredito, int totalDebito, int totalIntl, int totalEstado, int totalGeneral)
{
container.Column(col =>
{
col.Item().PaddingTop(20).Table(table =>
{
table.ColumnsDefinition(c =>
{
c.RelativeColumn();
c.ConstantColumn(80);
c.ConstantColumn(80);
c.ConstantColumn(80);
c.ConstantColumn(80);
});
table.Header(header =>
{
header.Cell().Text("Cajero").Bold();
header.Cell().Text("Crédito").Bold();
header.Cell().Text("Débito").Bold();
header.Cell().Text("Intl").Bold();
header.Cell().Text("Total").Bold();
});
foreach (var ing in ingresos)
{
table.Cell().Text(ing.GetValueOrDefault("Cajero")?.ToString() ?? "");
table.Cell().Text(ing.GetValueOrDefault("Credito")?.ToString() ?? "0");
table.Cell().Text(ing.GetValueOrDefault("Debito")?.ToString() ?? "0");
table.Cell().Text(ing.GetValueOrDefault("Internacional")?.ToString() ?? "0");
table.Cell().Text(ing.GetValueOrDefault("Total")?.ToString() ?? "0");
}
});
col.Item().PaddingTop(20).AlignRight().Column(total =>
{
total.Item().Text($"Crédito: ${totalCredito:N0}").Bold();
total.Item().Text($"Débito: ${totalDebito:N0}").Bold();
total.Item().Text($"Internacional: ${totalIntl:N0}").Bold();
total.Item().Text($"Estado: ${totalEstado:N0}").Bold();
total.Item().Text($"Total General: ${totalGeneral:N0}").FontSize(14).Bold();
});
});
}
public async Task<byte[]> GenerarContratoBlackPdfAsync(int contratoId)
{
var data = await _contratoService.PdfContratoAsync(contratoId);
var jornadas = await _contratoService.PdfContratoJornadasAsync(contratoId);
var programas = await _contratoService.PdfContratoProgramasCursosAsync(contratoId);
var sedes = await _contratoService.PdfContratoSedesAsync(contratoId);
var row = data.FirstOrDefault();
if (row == null) return [];
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(57);
page.DefaultTextStyle(x => x.FontSize(10).FontColor(Colors.White));
page.PageColor(Colors.Black);
page.Header().Element(c => ComposeContratoBlackHeader(c, contratoId.ToString()));
page.Content().Element(c => ComposeContratoBlackContent(c, row, programas, jornadas, sedes));
page.Footer().AlignCenter().Text(t => { t.Span("Página ").FontColor(Colors.White); t.CurrentPageNumber().FontColor(Colors.White); });
});
}).GeneratePdf();
}
private void ComposeContratoBlackHeader(IContainer container, string numero)
{
container.Row(row =>
{
row.RelativeItem();
row.ConstantItem(120).AlignRight().Column(col =>
{
col.Item().Text($"Contrato N° {numero}").FontSize(16).Bold().FontColor(Colors.White);
});
});
}
private void ComposeContratoBlackContent(IContainer container, Dictionary<string, object> row,
IEnumerable<Dictionary<string, object>> programas, IEnumerable<Dictionary<string, object>> jornadas,
IEnumerable<Dictionary<string, object>> sedes)
{
container.Column(col =>
{
col.Item().PaddingTop(20).Table(table =>
{
table.ColumnsDefinition(c => { c.ConstantColumn(120); c.RelativeColumn(); });
table.Cell().Text("Alumno:").Bold().FontColor(Colors.White);
table.Cell().Text(row.GetValueOrDefault("Alumno")?.ToString() ?? "").FontColor(Colors.White);
table.Cell().Text("RUT:").Bold().FontColor(Colors.White);
table.Cell().Text(row.GetValueOrDefault("Rut")?.ToString() ?? "").FontColor(Colors.White);
table.Cell().Text("Programa:").Bold().FontColor(Colors.White);
table.Cell().Text(string.Join(", ", programas.Select(p => p.GetValueOrDefault("Nombre")?.ToString()))).FontColor(Colors.White);
table.Cell().Text("Sede:").Bold().FontColor(Colors.White);
table.Cell().Text(string.Join(", ", sedes.Select(s => s.GetValueOrDefault("Nombre")?.ToString()))).FontColor(Colors.White);
table.Cell().Text("Jornada:").Bold().FontColor(Colors.White);
table.Cell().Text(string.Join(", ", jornadas.Select(j => j.GetValueOrDefault("Nombre")?.ToString()))).FontColor(Colors.White);
});
});
}
public async Task<byte[]> GenerarAnexoPdfAsync(int contratoId, int anexoId)
{
var rows = await QuerySpAsync("sige_sam_v3.PDFContratoDetalle", new { contrato = contratoId, detalle = anexoId });
var row = rows.FirstOrDefault();
if (row == null) return [];
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(57);
page.DefaultTextStyle(x => x.FontSize(10));
page.Header().Element(c => ComposeAnexoHeader(c, anexoId.ToString()));
page.Content().Element(c => ComposeAnexoContent(c, row));
page.Footer().AlignCenter().Text(t => { t.Span("Página "); t.CurrentPageNumber(); });
});
}).GeneratePdf();
}
private void ComposeAnexoHeader(IContainer container, string numero)
{
container.Row(row =>
{
row.RelativeItem();
row.ConstantItem(120).AlignRight().Column(col =>
{
col.Item().Text($"Anexo N° {numero}").FontSize(16).Bold();
});
});
}
private void ComposeAnexoContent(IContainer container, Dictionary<string, object> row)
{
container.Column(col =>
{
col.Item().PaddingTop(20).Table(table =>
{
table.ColumnsDefinition(c => { c.ConstantColumn(120); c.RelativeColumn(); });
foreach (var kv in row)
{
table.Cell().Text($"{kv.Key}:").Bold();
table.Cell().Text(kv.Value?.ToString() ?? "");
}
});
});
}
public async Task<byte[]> GenerarPresupuestoPdfAsync(int presupuestoId)
{
var rows = await QuerySpAsync("sige_sam_v3.BuscarCotizacionPDF", new { cotizacionid = presupuestoId });
var row = rows.FirstOrDefault();
if (row == null) return [];
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(57);
page.DefaultTextStyle(x => x.FontSize(10));
page.Header().Element(c => ComposePresupuestoHeader(c, presupuestoId.ToString()));
page.Content().Element(c => ComposePresupuestoContent(c, row));
page.Footer().AlignCenter().Text(t => { t.Span("Página "); t.CurrentPageNumber(); });
});
}).GeneratePdf();
}
private void ComposePresupuestoHeader(IContainer container, string numero)
{
container.Row(row =>
{
row.RelativeItem();
row.ConstantItem(140).AlignRight().Column(col =>
{
col.Item().Text($"Presupuesto N° {numero}").FontSize(16).Bold();
col.Item().Text("Instituto Chileno Norteamericano").FontSize(9);
});
});
}
private void ComposePresupuestoContent(IContainer container, Dictionary<string, object> row)
{
container.Column(col =>
{
col.Item().PaddingTop(20).Table(table =>
{
table.ColumnsDefinition(c => { c.ConstantColumn(120); c.RelativeColumn(); });
foreach (var kv in row)
{
table.Cell().Text($"{kv.Key}:").Bold();
table.Cell().Text(kv.Value?.ToString() ?? "");
}
});
});
}
}
@@ -0,0 +1,57 @@
using Dapper;
using Npgsql;
using Ventas.Core.DTOs;
using Ventas.Core.Interfaces;
namespace Ventas.Services;
public class UsuarioService
{
private readonly IUsuarioRepository _usuarioRepository;
private readonly string _connectionString;
public UsuarioService(IUsuarioRepository usuarioRepository, string connectionString)
{
_usuarioRepository = usuarioRepository;
_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()
};
}
}
+51
View File
@@ -0,0 +1,51 @@
version: '3.8'
services:
backend-api:
build:
context: ./backend
dockerfile: src/Ventas.API/Dockerfile
environment:
- ConnectionStrings__Default=Host=192.168.0.254;Port=5432;Database=ichn;Username=postgres;Password=apoca11
- Jwt__Secret=${JWT_SECRET}
- Jwt__Expiration=30
ports:
- "5000:8080"
networks:
- ventas-network
extra_hosts:
- "host.docker.internal:192.168.0.254"
services-externos:
build:
context: ./services-externos
dockerfile: src/ServicesExternos.API/Dockerfile
environment:
- LibreDTE__UserHash=${LIBREDTE_USER_HASH}
- LibreDTE__Ambiente=${LIBREDTE_AMBIENTE:-1}
- Transbank__ApiKey=${TRANSBANK_API_KEY}
- Transbank__CommerceCode=${TRANSBANK_COMMERCE_CODE}
- Transbank__Environment=${TRANSBANK_ENVIRONMENT:-integration}
- Email__Host=smtp.gmail.com
- Email__Port=587
- Email__User=noresponder@norteamericano.cl
- Email__Password=${SMTP_PASSWORD}
ports:
- "5001:8080"
networks:
- ventas-network
frontend:
build: ./frontend
environment:
- NEXT_PUBLIC_API_URL=http://backend-api:8080/api
- NEXT_PUBLIC_SERVICES_URL=http://services-externos:8080/api
ports:
- "3000:3000"
depends_on:
- backend-api
networks:
- ventas-network
networks:
ventas-network:
driver: bridge
+14
View File
@@ -0,0 +1,14 @@
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/public ./public
EXPOSE 3000
ENV NODE_ENV=production
CMD ["node", "server.js"]
+9
View File
@@ -0,0 +1,9 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
images: {
unoptimized: true,
},
};
module.exports = nextConfig;
+23
View File
@@ -0,0 +1,23 @@
{
"name": "modulo-ventas",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hot-toast": "^2.4.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.0.0"
}
}
+230
View File
@@ -0,0 +1,230 @@
.btn-ichn {
color: #fff;
background-color: #00285d;
border-color: #00285d
}
.btn-ichn:hover {
color: #fff;
background-color: #00142E;
border-color: #00142E
}
.btn-ichn.focus, .btn-ichn:focus {
box-shadow: 0 0 0 .2rem #00285d
}
.btn-ichn.disabled, .btn-ichn:disabled {
color: #fff;
background-color: #00285d;
border-color: #00285d;
}
.btn-ichn:not(:disabled):not(.disabled).active, .btn-ichn:not(:disabled):not(.disabled):active, .show > .btn-ichn.dropdown-toggle {
color: #fff;
background-color: #00285d;
border-color: #00285d
}
.btn-ichn:not(:disabled):not(.disabled).active:focus, .btn-ichn:not(:disabled):not(.disabled):active:focus, .show > .btn-ichn.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem #00285d
}
.btn-ichnRojo {
color: #fff;
background-color: #e22523;
border-color: #e22523
}
.btn-ichnRojo:hover {
color: #fff;
background-color: #8A101C;
border-color: #8A101C
}
.btn-ichnRojo.focus, .btn-ichnRojo:focus {
box-shadow: 0 0 0 .2rem #B91425
}
.btn-ichnRojo.disabled, .btn-ichnRojo:disabled {
color: #fff;
background-color: #B91425;
border-color: #B91425;
}
.btn-ichnRojo:not(:disabled):not(.disabled).active, .btn-ichnRojo:not(:disabled):not(.disabled):active, .show > .btn-ichnRojo.dropdown-toggle {
color: #fff;
background-color: #B91425;
border-color: #B91425
}
.btn-ichnRojo:not(:disabled):not(.disabled).active:focus, .btn-ichnRojo:not(:disabled):not(.disabled):active:focus, .show > .btn-ichnRojo.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem #B91425
}
.btn-ichnPurpura {
color: #fff;
background-color: #6B3B7C;
border-color: #6B3B7C;
}
.btn-ichnPurpura:hover {
color: #fff;
background-color: #562965;
border-color: #562965
}
.btn-ichnPurpura.focus, .btn-ichnPurpura:focus {
box-shadow: 0 0 0 .2rem #844c98
}
.btn-ichnPurpura.disabled, .btn-ichnPurpura:disabled {
color: #fff;
background-color: #844c98;
border-color: #844c98;
}
.btn-ichnPurpura:not(:disabled):not(.disabled).active, .btn-ichnPurpura:not(:disabled):not(.disabled):active, .show > .btn-ichnPurpura.dropdown-toggle {
color: #fff;
background-color: #844c98;
border-color: #844c98
}
.btn-ichnPurpura:not(:disabled):not(.disabled).active:focus, .btn-ichnPurpura:not(:disabled):not(.disabled):active:focus, .show > .btn-ichnPurpura.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem #844c98
}
.btn-ichnVerde {
color: #fff;
background-color: #3D912F;
border-color: #3D912F
}
.btn-ichnVerde:hover {
color: #fff;
background-color: #2A6D1E;
border-color: #2A6D1E
}
.btn-ichnVerde.focus, .btn-ichnVerde:focus {
box-shadow: 0 0 0 .2rem #3D912F
}
.btn-ichnVerde.disabled, .btn-ichnVerde:disabled {
color: #fff;
background-color: #3D912F;
border-color: #3D912F;
}
.btn-ichnVerde:not(:disabled):not(.disabled).active, .btn-ichnVerde:not(:disabled):not(.disabled):active, .show > .btn-ichnVerde.dropdown-toggle {
color: #fff;
background-color: #3D912F;
border-color: #3D912F
}
.btn-ichnVerde:not(:disabled):not(.disabled).active:focus, .btn-ichnVerde:not(:disabled):not(.disabled):active:focus, .show > .btn-ichnVerde.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem #3D912F
}
.btn-ichnExcel {
color: #fff;
background-color: #008000;
border-color: #008000
}
.btn-ichnExcel:hover {
color: #fff;
background-color: #009900;
border-color: #009900;
}
.btn-ichnExcel.focus, .btn-ichnExcel:focus {
box-shadow: 0 0 0 .2rem #008000;
}
.btn-ichnExcel.disabled, .btn-ichnExcel:disabled {
color: #fff;
background-color: #008000;
border-color: #008000;
}
.btn-ichnExcel:not(:disabled):not(.disabled).active, .btn-ichnExcel:not(:disabled):not(.disabled):active, .show > .btn-ichnExcel.dropdown-toggle {
color: #fff;
background-color: #008000;
border-color: #008000
}
.btn-ichnExcel:not(:disabled):not(.disabled).active:focus, .btn-ichnExcel:not(:disabled):not(.disabled):active:focus, .show > .btn-ichnExcel.dropdown-toggle:focus {
box-shadow: 0 0 0 .2rem #008000
}
.examen:hover {
background-image: linear-gradient( rgba(185, 20, 37 ), rgba(185, 20, 37) )
}
.modal-content {
position: relative;
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
width: 100%;
pointer-events: auto;
background-color: #fff;
background-clip: padding-box;
border: 0px solid;
border-radius: .5rem;
outline: 0;
}
.modal-header {
display: -ms-flexbox;
display: block;
-ms-flex-align: start;
align-items: flex-start;
-ms-flex-pack: justify;
justify-content: space-between;
padding: 1rem 1rem;
border-bottom: 1px solid #dee2e6;
border-top-left-radius: .3rem;
border-top-right-radius: .3rem;
}
.btn-circle {
width: 30px;
height: 30px;
text-align: center;
padding: 6px 0;
font-size: 12px;
line-height: 1.428571429;
border-radius: 15px;
}
.btn-circle.btn-lg {
width: 50px;
height: 50px;
padding: 10px 16px;
font-size: 18px;
line-height: 1.33;
border-radius: 25px;
}
.btn-circle.btn-xl {
width: 70px;
height: 70px;
padding: 10px 16px;
font-size: 24px;
line-height: 1.33;
border-radius: 35px;
}
+80
View File
@@ -0,0 +1,80 @@
.div_1 {
height: 80px;
background-color: #D1D1D1;
font-size: 18px;
}
.div_2 {
min-height: 300px;
}
label .prueba {
cursor: pointer;
}
.nav-tabs .nav-item.show .nav-link, .nav-tabs .nav-link.active {
color: #ffff;
background-color: #22376c;
border-color: #dee2e6 #dee2e6 #fff;
}
.nav-tabs .nav-item.show .nav-link, .nav-tabs .nav-link {
color: #22376c;
}
.spinner, .spinner:after {
width: 90px;
height: 90px;
position: fixed;
top: 90%;
left: 50%;
margin-top: -32px;
margin-left: -32px;
border-radius: 50%;
z-index: 2
}
.spinner {
background-color: transparent;
border-top: 10px solid rgb(0, 40, 93);
border-right: 10px solid rgb(0, 40, 93);
border-bottom: 10px solid rgb(0, 40, 93);
border-left: 10px solid rgba(0, 40, 93,.2);
transform: translateZ(0);
animation-iteration-count: infinite;
animation-timing-function: linear;
animation-duration: 1.5s;
animation-name: spinner-loading
}
.menuIchnHover {
background-color: #E8E8E8;
}
.menuIchnHover a {
color: #212529;
}
.menuIchnHover:hover {
background-color: #00285d;
color: #fff;
border-radius: 5px
}
.menuIchnHover:hover a {
background-color: #00285d;
color: #fff;
border-radius: 5px
}
@keyframes spinner-loading {
0% {
transform: rotate(0deg)
}
to {
transform: rotate(1turn)
}
}
+282
View File
@@ -0,0 +1,282 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100%;
width: 75px;
background: #00285d;
padding: 6px 14px;
z-index: 99;
transition: all 0.5s ease;
}
.sidebar.open {
width: 250px;
}
.sidebar .logo-details {
height: 60px;
display: flex;
align-items: center;
position: relative;
}
.sidebar .logo-details .icon {
opacity: 0;
transition: all 0.5s ease;
}
.sidebar .logo-details .logo_name {
color: #fff;
font-size: 20px;
font-weight: 600;
opacity: 0;
transition: all 0.5s ease;
}
.sidebar.open .logo-details .icon,
.sidebar.open .logo-details .logo_name {
opacity: 1;
}
.sidebar .logo-details #btn {
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
font-size: 22px;
transition: all 0.4s ease;
font-size: 23px;
text-align: center;
cursor: pointer;
transition: all 0.5s ease;
}
.sidebar.open .logo-details #btn {
text-align: right;
}
.sidebar i {
color: #fff;
height: 60px;
min-width: 50px;
font-size: 28px;
text-align: center;
line-height: 60px;
}
.sidebar .nav-list {
margin-top: 20px;
height: 100%;
padding-left: 0rem;
}
.sidebar li {
position: relative;
margin: 8px 0;
list-style: none;
}
.sidebar li .tooltip {
position: absolute;
top: -20px;
left: calc(100% + 15px);
z-index: 3;
background: #fff;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3);
padding: 6px 12px;
border-radius: 4px;
font-size: 15px;
font-weight: 400;
opacity: 0;
white-space: nowrap;
pointer-events: none;
transition: 0s;
}
.sidebar li:hover .tooltip {
opacity: 1;
pointer-events: auto;
transition: all 0.4s ease;
top: 50%;
transform: translateY(-50%);
}
.sidebar.open li .tooltip {
display: none;
}
.sidebar input {
font-size: 15px;
color: #FFF;
font-weight: 400;
outline: none;
height: 50px;
width: 100%;
width: 50px;
border: none;
border-radius: 12px;
transition: all 0.5s ease;
background: #1d1b31;
}
.sidebar.open input {
padding: 0 20px 0 50px;
width: 100%;
}
.sidebar .bx-search {
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
font-size: 22px;
background: #1d1b31;
color: #FFF;
}
.sidebar.open .bx-search:hover {
background: #1d1b31;
color: #FFF;
}
.sidebar .bx-search:hover {
background: #FFF;
color: #00285d;
}
.sidebar li a {
display: flex;
height: 100%;
width: 100%;
border-radius: 12px;
align-items: center;
text-decoration: none;
transition: all 0.4s ease;
background: #00285d;
}
.sidebar li a:hover {
background: #FFF;
}
.sidebar li a .links_name {
color: #fff;
font-size: 15px;
font-weight: 400;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: 0.4s;
}
.sidebar.open li a .links_name {
opacity: 1;
pointer-events: auto;
}
.sidebar li a:hover .links_name,
.sidebar li a:hover i {
transition: all 0.5s ease;
color: #00285d;
}
.sidebar li i {
height: 35px;
line-height: 35px;
font-size: 16px;
border-radius: 12px;
}
.sidebar li.profile {
position: fixed;
height: 60px;
width: 78px;
left: 0;
bottom: -8px;
padding: 10px 14px;
background: #001D44;
transition: all 0.5s ease;
overflow: hidden;
}
.sidebar.open li.profile {
width: 250px;
}
.sidebar li .profile-details {
display: flex;
align-items: center;
flex-wrap: nowrap;
}
.sidebar li img {
height: 45px;
width: 45px;
object-fit: cover;
border-radius: 6px;
margin-right: 10px;
}
.sidebar li.profile .name,
.sidebar li.profile .job {
font-size: 15px;
font-weight: 400;
color: #fff;
white-space: nowrap;
}
.sidebar li.profile .job {
font-size: 12px;
}
.sidebar .profile #log_out {
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
background: #1d1b31;
width: 100%;
height: 60px;
line-height: 60px;
border-radius: 0px;
transition: all 0.5s ease;
}
.sidebar.open .profile #log_out {
width: 50px;
background: none;
}
.home-section {
position: relative;
min-height: 100vh;
top: 0;
left: 75px;
width: calc(100% - 75px);
transition: all 0.5s ease;
}
.sidebar.open ~ .home-section {
left: 250px;
width: calc(100% - 250px);
transition: all 1s ease;
}
.home-section .text {
display: inline-block;
margin: 18px
}
@media (max-width: 620px) {
.sidebar li .tooltip {
display: none;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

+67
View File
@@ -0,0 +1,67 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import { api } from '@/services/api';
import LoadingSpinner from '@/components/LoadingSpinner';
export default function AlumnosPage() {
const { user, loading } = useAuth();
const router = useRouter();
const [alumnos, setAlumnos] = useState<any[]>([]);
const [busqueda, setBusqueda] = useState('');
const [loadingData, setLoadingData] = useState(true);
useEffect(() => {
if (!loading && !user) router.push('/login');
if (user) {
api.get('/alumno', { tipoBusqueda: 'NOMBRE', nombre: '' }).then(data => {
setAlumnos(Array.isArray(data) ? data : []);
setLoadingData(false);
}).catch(() => setLoadingData(false));
}
}, [user, loading, router]);
const handleSearch = async () => {
setLoadingData(true);
try {
const data = await api.get('/alumno', { tipoBusqueda: 'NOMBRE', nombre: busqueda });
setAlumnos(Array.isArray(data) ? data : []);
} catch { /* ignore */ }
setLoadingData(false);
};
if (loading) return <LoadingSpinner />;
return (
<div>
<h3 className="mb-3">Alumnos</h3>
<div className="input-group mb-3">
<input className="form-control" value={busqueda} onChange={e => setBusqueda(e.target.value)}
placeholder="Buscar alumno..." onKeyDown={e => e.key === 'Enter' && handleSearch()} />
<button className="btn btn-primary" onClick={handleSearch}>Buscar</button>
</div>
<div className="card shadow">
<div className="card-body">
<table className="table table-hover">
<thead>
<tr><th>RUT</th><th>Nombre</th><th>Mail</th><th>Teléfono</th></tr>
</thead>
<tbody>
{alumnos.map((a: any, i: number) => (
<tr key={i}>
<td>{a.AlumnoRut || a.rut || a.Rut}</td>
<td>{a.NombreAlumno || a.nombre || a.Nombre}</td>
<td>{a.Email || a.mail || a.Mail}</td>
<td>{a.Telefono || a.telefono || a.Fono}</td>
</tr>
))}
{alumnos.length === 0 && <tr><td colSpan={4} className="text-center">Sin resultados</td></tr>}
</tbody>
</table>
</div>
</div>
</div>
);
}
+54
View File
@@ -0,0 +1,54 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import { api } from '@/services/api';
import LoadingSpinner from '@/components/LoadingSpinner';
export default function ArqueoPage() {
const { user, loading } = useAuth();
const router = useRouter();
const [ingresos, setIngresos] = useState<any[]>([]);
const [loadingData, setLoadingData] = useState(true);
const fecha = new Date().toISOString().split('T')[0];
useEffect(() => {
if (!loading && !user) router.push('/login');
if (user) {
api.get('/arqueo/todos', { fecha }).then(data => {
setIngresos(Array.isArray(data) ? data : []);
setLoadingData(false);
}).catch(() => setLoadingData(false));
}
}, [user, loading, fecha, router]);
if (loading || loadingData) return <LoadingSpinner />;
return (
<div>
<h3 className="mb-3">Arqueo de Caja</h3>
<p className="text-muted">Fecha: {new Date().toLocaleDateString()}</p>
<div className="card shadow">
<div className="card-body">
<table className="table table-hover">
<thead>
<tr><th>Cajero</th><th>Crédito</th><th>Débito</th><th>Total</th></tr>
</thead>
<tbody>
{ingresos.map((r: any, i: number) => (
<tr key={i}>
<td>{r.Cajero || r.cajero}</td>
<td>${(r.Credito || r.credito || 0).toLocaleString()}</td>
<td>${(r.Debito || r.debito || 0).toLocaleString()}</td>
<td>${(r.Total || r.total || 0).toLocaleString()}</td>
</tr>
))}
{ingresos.length === 0 && <tr><td colSpan={4} className="text-center">Sin movimientos hoy</td></tr>}
</tbody>
</table>
</div>
</div>
</div>
);
}
+32
View File
@@ -0,0 +1,32 @@
'use client';
import { useState } from 'react';
import { api } from '@/services/api';
export default function AutorizadorPage() {
const [cotizacionId, setCotizacionId] = useState('');
const [mensaje, setMensaje] = useState('');
const handleAutorizar = async () => {
try {
await api.put(`/cotizacion/${cotizacionId}/desactivar`);
setMensaje('Cotización autorizada');
} catch {
setMensaje('Error al autorizar');
}
};
return (
<div>
<h3 className="mb-3">Autorizador de Descuentos</h3>
<div className="card shadow p-4">
<div className="mb-3">
<label className="form-label">ID Cotización</label>
<input className="form-control" value={cotizacionId} onChange={e => setCotizacionId(e.target.value)} />
</div>
<button className="btn btn-warning" onClick={handleAutorizar}>Autorizar Descuento</button>
{mensaje && <div className="alert alert-info mt-3">{mensaje}</div>}
</div>
</div>
);
}
+40
View File
@@ -0,0 +1,40 @@
'use client';
import { useState, FormEvent } from 'react';
import { api } from '@/services/api';
export default function ContactoPage() {
const [mail, setMail] = useState('');
const [mensaje, setMensaje] = useState('');
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
try {
await api.post('/email/send', { to: mail, subject: 'Contacto desde SAM', body: mensaje });
alert('Correo enviado');
setMail('');
setMensaje('');
} catch {
alert('Error al enviar');
}
};
return (
<div>
<h3 className="mb-3">Contacto</h3>
<div className="card shadow p-4">
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label className="form-label">Correo</label>
<input type="email" className="form-control" value={mail} onChange={e => setMail(e.target.value)} required />
</div>
<div className="mb-3">
<label className="form-label">Mensaje</label>
<textarea className="form-control" rows={4} value={mensaje} onChange={e => setMensaje(e.target.value)} required />
</div>
<button type="submit" className="btn btn-primary">Enviar</button>
</form>
</div>
</div>
);
}
+52
View File
@@ -0,0 +1,52 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import { api } from '@/services/api';
import LoadingSpinner from '@/components/LoadingSpinner';
export default function CotizacionesPage() {
const { user, loading } = useAuth();
const router = useRouter();
const [cotizaciones, setCotizaciones] = useState<any[]>([]);
const [loadingData, setLoadingData] = useState(true);
useEffect(() => {
if (!loading && !user) router.push('/login');
if (user) {
api.get('/cotizacion/buscar', { tipoBusqueda: 'NOMBRE', nombre: '' }).then(data => {
setCotizaciones(Array.isArray(data) ? data : []);
setLoadingData(false);
}).catch(() => setLoadingData(false));
}
}, [user, loading, router]);
if (loading || loadingData) return <LoadingSpinner />;
return (
<div>
<h3 className="mb-3">Cotizaciones</h3>
<div className="card shadow">
<div className="card-body">
<table className="table table-hover">
<thead>
<tr><th>#</th><th>Cliente</th><th>Monto</th><th>Fecha</th></tr>
</thead>
<tbody>
{cotizaciones.map((c: any, i: number) => (
<tr key={i}>
<td>{c.Id || c.idCotizacionEmpresa || i}</td>
<td>{c.NombreLead || c.nombre || c.Cliente}</td>
<td>${(c.Monto || c.monto || 0).toLocaleString()}</td>
<td>{c.Fecha ? new Date(c.Fecha).toLocaleDateString() : ''}</td>
</tr>
))}
{cotizaciones.length === 0 && <tr><td colSpan={4} className="text-center">Sin cotizaciones</td></tr>}
</tbody>
</table>
</div>
</div>
</div>
);
}
+52
View File
@@ -0,0 +1,52 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import { api } from '@/services/api';
import LoadingSpinner from '@/components/LoadingSpinner';
export default function CursosPage() {
const { user, loading } = useAuth();
const router = useRouter();
const [cursos, setCursos] = useState<any[]>([]);
const [loadingData, setLoadingData] = useState(true);
useEffect(() => {
if (!loading && !user) router.push('/login');
if (user) {
api.get('/curso', { tipoBusqueda: 'NOMBRE', nombre: '' }).then(data => {
setCursos(Array.isArray(data) ? data : []);
setLoadingData(false);
}).catch(() => setLoadingData(false));
}
}, [user, loading, router]);
if (loading || loadingData) return <LoadingSpinner />;
return (
<div>
<h3 className="mb-3">Cursos</h3>
<div className="card shadow">
<div className="card-body">
<table className="table table-hover">
<thead>
<tr><th>ID</th><th>Nombre</th><th>Programa</th><th>Duración</th></tr>
</thead>
<tbody>
{cursos.map((c: any, i: number) => (
<tr key={i}>
<td>{c.idCursos || c.Id || i}</td>
<td>{c.Curso || c.Nombre || c.nombre}</td>
<td>{c.Programa || c.programa}</td>
<td>{c.Duracion || c.duracion || '-'}</td>
</tr>
))}
{cursos.length === 0 && <tr><td colSpan={4} className="text-center">Sin cursos</td></tr>}
</tbody>
</table>
</div>
</div>
</div>
);
}
+52
View File
@@ -0,0 +1,52 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import { api } from '@/services/api';
import LoadingSpinner from '@/components/LoadingSpinner';
const cards = [
{ title: 'Nuevos Leads', icon: 'fas fa-users', color: '#4e73df', endpoint: '/lead/nuevos' },
{ title: 'Cotizaciones', icon: 'fas fa-file-invoice-dollar', color: '#1cc88a', endpoint: '/cotizacion/buscar' },
{ title: 'Cursos', icon: 'fas fa-book', color: '#36b9cc', endpoint: '/curso' },
{ title: 'Alumnos', icon: 'fas fa-user-graduate', color: '#f6c23e', endpoint: '/alumno' },
];
export default function DashboardPage() {
const { user, loading } = useAuth();
const router = useRouter();
const [pageLoading, setPageLoading] = useState(true);
useEffect(() => {
if (!loading && !user) router.push('/login');
if (!loading) setPageLoading(false);
}, [user, loading, router]);
if (pageLoading) return <LoadingSpinner />;
return (
<div>
<h2 className="mb-4">Bienvenido, {user?.nombre}</h2>
<div className="row">
{cards.map(card => (
<div className="col-xl-3 col-md-6 mb-4" key={card.title}>
<div className="card border-left-primary shadow h-100 py-2" style={{ borderLeft: `.25rem solid ${card.color}` }}>
<div className="card-body">
<div className="row no-gutters align-items-center">
<div className="col mr-2">
<div className="text-xs font-weight-bold text-primary text-uppercase mb-1">{card.title}</div>
<div className="h5 mb-0 font-weight-bold text-gray-800">-</div>
</div>
<div className="col-auto">
<i className={`${card.icon} fa-2x text-gray-300`}></i>
</div>
</div>
</div>
</div>
</div>
))}
</div>
</div>
);
}
+47
View File
@@ -0,0 +1,47 @@
'use client';
import { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import { api } from '@/services/api';
import LoadingSpinner from '@/components/LoadingSpinner';
export default function EmpresaDetallePage() {
const { id } = useParams();
const { user, loading } = useAuth();
const router = useRouter();
const [empresa, setEmpresa] = useState<any>(null);
const [loadingData, setLoadingData] = useState(true);
useEffect(() => {
if (!loading && !user) router.push('/login');
if (user && id) {
api.get('/empresa', { busqueda: 'IDEMP', rut: String(id), varB: '', varC: 0 }).then(data => {
setEmpresa(Array.isArray(data) ? data[0] : data);
setLoadingData(false);
}).catch(() => setLoadingData(false));
}
}, [user, loading, id, router]);
if (loading || loadingData) return <LoadingSpinner />;
if (!empresa) return <p>Empresa no encontrada</p>;
return (
<div>
<h3>Empresa {id}</h3>
<div className="card shadow">
<div className="card-body">
<table className="table">
<tbody>
<tr><td>RUT</td><td>{empresa.Rut || empresa.rut}</td></tr>
<tr><td>Razón Social</td><td>{empresa.RazonSocial || empresa.razonSocial}</td></tr>
<tr><td>Dirección</td><td>{empresa.Direccion || empresa.direccion}</td></tr>
<tr><td>Contacto</td><td>{empresa.Contacto || empresa.contacto}</td></tr>
<tr><td>Mail</td><td>{empresa.Mail || empresa.mail}</td></tr>
</tbody>
</table>
</div>
</div>
</div>
);
}
+52
View File
@@ -0,0 +1,52 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import { api } from '@/services/api';
import LoadingSpinner from '@/components/LoadingSpinner';
export default function EmpresasPage() {
const { user, loading } = useAuth();
const router = useRouter();
const [empresas, setEmpresas] = useState<any[]>([]);
const [loadingData, setLoadingData] = useState(true);
useEffect(() => {
if (!loading && !user) router.push('/login');
if (user) {
api.get('/empresa', { busqueda: 'LISTADO', rut: '', varB: '', varC: 0 }).then(data => {
setEmpresas(Array.isArray(data) ? data : []);
setLoadingData(false);
}).catch(() => setLoadingData(false));
}
}, [user, loading, router]);
if (loading || loadingData) return <LoadingSpinner />;
return (
<div>
<h3 className="mb-3">Empresas</h3>
<div className="card shadow">
<div className="card-body">
<table className="table table-hover">
<thead>
<tr><th>RUT</th><th>Razón Social</th><th>Contacto</th><th>Mail</th></tr>
</thead>
<tbody>
{empresas.map((e: any, i: number) => (
<tr key={i} style={{ cursor: 'pointer' }} onClick={() => router.push(`/empresas/${e.Rut || e.rut}`)}>
<td>{e.Rut || e.rut}</td>
<td>{e.RazonSocial || e.razonSocial || e.Nombre}</td>
<td>{e.Contacto || e.contacto}</td>
<td>{e.Mail || e.mail}</td>
</tr>
))}
{empresas.length === 0 && <tr><td colSpan={4} className="text-center">Sin empresas</td></tr>}
</tbody>
</table>
</div>
</div>
</div>
);
}
+52
View File
@@ -0,0 +1,52 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import { api } from '@/services/api';
import LoadingSpinner from '@/components/LoadingSpinner';
export default function VentasEmpresaPage() {
const { user, loading } = useAuth();
const router = useRouter();
const [ventas, setVentas] = useState<any[]>([]);
const [loadingData, setLoadingData] = useState(true);
useEffect(() => {
if (!loading && !user) router.push('/login');
if (user) {
api.get('/informe/ventas-empresa', { tipo: 'LISTADO', varA: '', varB: '', varC: 0 }).then(data => {
setVentas(Array.isArray(data) ? data : []);
setLoadingData(false);
}).catch(() => setLoadingData(false));
}
}, [user, loading, router]);
if (loading || loadingData) return <LoadingSpinner />;
return (
<div>
<h3 className="mb-3">Ventas Empresas</h3>
<div className="card shadow">
<div className="card-body">
<table className="table table-hover">
<thead>
<tr><th>Empresa</th><th>Curso</th><th>Valor</th><th>Fecha</th></tr>
</thead>
<tbody>
{ventas.map((v: any, i: number) => (
<tr key={i}>
<td>{v.Empresa || v.empresa}</td>
<td>{v.Curso || v.curso}</td>
<td>${(v.Valor || v.valor || 0).toLocaleString()}</td>
<td>{v.Fecha ? new Date(v.Fecha).toLocaleDateString() : ''}</td>
</tr>
))}
{ventas.length === 0 && <tr><td colSpan={4} className="text-center">Sin ventas</td></tr>}
</tbody>
</table>
</div>
</div>
</div>
);
}
+34
View File
@@ -0,0 +1,34 @@
import type { Metadata } from 'next';
import { AuthProvider } from '@/hooks/useAuth';
import Sidebar from '@/components/Sidebar';
import Header from '@/components/Header';
import '@/styles/globals.css';
export const metadata: Metadata = {
title: 'SAM - Sistema de Administración de Ventas',
description: 'Módulo de Ventas - Instituto Chileno Norteamericano',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es">
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css?family=Fira+Sans&display=swap" rel="stylesheet" />
<link href="https://unpkg.com/boxicons@2.0.7/css/boxicons.min.css" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
</head>
<body>
<AuthProvider>
<Sidebar />
<section className="home-section alert-dark">
<Header />
<div className="container-fluid mt-3 px-4">
{children}
</div>
</section>
</AuthProvider>
</body>
</html>
);
}
+73
View File
@@ -0,0 +1,73 @@
'use client';
import { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import { api } from '@/services/api';
import LoadingSpinner from '@/components/LoadingSpinner';
export default function LeadDetallePage() {
const { id } = useParams();
const { user, loading } = useAuth();
const router = useRouter();
const [lead, setLead] = useState<any>(null);
const [actividades, setActividades] = useState<any[]>([]);
const [loadingData, setLoadingData] = useState(true);
useEffect(() => {
if (!loading && !user) router.push('/login');
if (user && id) {
Promise.all([
api.get(`/lead/${id}`),
api.get(`/lead/${id}/actividades`, { tipo: '' })
]).then(([leadData, actData]) => {
setLead(Array.isArray(leadData) ? leadData[0] : leadData);
setActividades(Array.isArray(actData) ? actData : []);
setLoadingData(false);
}).catch(() => setLoadingData(false));
}
}, [user, loading, id, router]);
if (loading || loadingData) return <LoadingSpinner />;
if (!lead) return <p>Lead no encontrado</p>;
return (
<div>
<h3>Lead #{id}</h3>
<div className="row">
<div className="col-md-6">
<div className="card shadow mb-3">
<div className="card-body">
<h5>Información</h5>
<table className="table">
<tbody>
<tr><td>Nombre</td><td>{lead.Nombre || lead.nombre}</td></tr>
<tr><td>Mail</td><td>{lead.Mail || lead.mail}</td></tr>
<tr><td>Teléfono</td><td>{lead.Telefono || lead.telefono}</td></tr>
<tr><td>Producto</td><td>{lead.Producto || lead.producto}</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div className="col-md-6">
<div className="card shadow mb-3">
<div className="card-body">
<h5>Actividades</h5>
{actividades.length === 0 ? <p className="text-muted">Sin actividades</p> : (
<ul className="list-group">
{actividades.map((a: any, i: number) => (
<li key={i} className="list-group-item">
<small>{a.Tipo || a.tipo}</small>
<p className="mb-0">{a.Descripcion || a.descripcion}</p>
</li>
))}
</ul>
)}
</div>
</div>
</div>
</div>
</div>
);
}
+51
View File
@@ -0,0 +1,51 @@
'use client';
import { useState, FormEvent } from 'react';
import { useRouter } from 'next/navigation';
import { api } from '@/services/api';
import toast from 'react-hot-toast';
export default function NuevoLeadPage() {
const router = useRouter();
const [nombre, setNombre] = useState('');
const [mail, setMail] = useState('');
const [fono, setFono] = useState('');
const [producto, setProducto] = useState('');
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
try {
await api.post('/lead', { nombre, mail, telefono: fono, producto, contacto: 1, ejecutivoId: 1 });
toast.success('Lead creado');
router.push('/leads');
} catch {
toast.error('Error al crear lead');
}
};
return (
<div className="card shadow p-4">
<h3 className="mb-4">Nuevo Lead</h3>
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label className="form-label">Nombre</label>
<input className="form-control" value={nombre} onChange={e => setNombre(e.target.value)} required />
</div>
<div className="mb-3">
<label className="form-label">Mail</label>
<input type="email" className="form-control" value={mail} onChange={e => setMail(e.target.value)} />
</div>
<div className="mb-3">
<label className="form-label">Teléfono</label>
<input className="form-control" value={fono} onChange={e => setFono(e.target.value)} />
</div>
<div className="mb-3">
<label className="form-label">Producto</label>
<input className="form-control" value={producto} onChange={e => setProducto(e.target.value)} />
</div>
<button type="submit" className="btn btn-primary">Guardar</button>
<button type="button" className="btn btn-secondary ms-2" onClick={() => router.back()}>Cancelar</button>
</form>
</div>
);
}
+64
View File
@@ -0,0 +1,64 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import { api } from '@/services/api';
import LoadingSpinner from '@/components/LoadingSpinner';
export default function LeadsPage() {
const { user, loading } = useAuth();
const router = useRouter();
const [leads, setLeads] = useState<any[]>([]);
const [loadingData, setLoadingData] = useState(true);
useEffect(() => {
if (!loading && !user) router.push('/login');
if (user) {
api.get('/lead/nuevos', { ejecutivo: 1 }).then(data => {
setLeads(Array.isArray(data) ? data : []);
setLoadingData(false);
}).catch(() => setLoadingData(false));
}
}, [user, loading, router]);
if (loading || loadingData) return <LoadingSpinner />;
return (
<div>
<div className="d-flex justify-content-between align-items-center mb-3">
<h3>Leads</h3>
<button className="btn btn-primary" onClick={() => router.push('/leads/nuevo')}>Nuevo Lead</button>
</div>
<div className="card shadow">
<div className="card-body">
<table className="table table-hover">
<thead>
<tr>
<th>Nombre</th>
<th>Mail</th>
<th>Teléfono</th>
<th>Producto</th>
<th>Fecha</th>
<th></th>
</tr>
</thead>
<tbody>
{leads.map((lead: any, i: number) => (
<tr key={i} style={{ cursor: 'pointer' }} onClick={() => router.push(`/leads/${lead.Id || i}`)}>
<td>{lead.Nombre || lead.nombre}</td>
<td>{lead.Mail || lead.mail}</td>
<td>{lead.Telefono || lead.telefono}</td>
<td>{lead.Producto || lead.producto}</td>
<td>{lead.FechaCreacion ? new Date(lead.FechaCreacion).toLocaleDateString() : ''}</td>
<td><button className="btn btn-sm btn-outline-primary">Ver</button></td>
</tr>
))}
{leads.length === 0 && <tr><td colSpan={6} className="text-center">Sin leads</td></tr>}
</tbody>
</table>
</div>
</div>
</div>
);
}
+68
View File
@@ -0,0 +1,68 @@
'use client';
import { useState, FormEvent } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import { validarRut, formatRut } from '@/lib/validarRut';
export default function LoginPage() {
const [rut, setRut] = useState('');
const [clave, setClave] = useState('');
const [error, setError] = useState('');
const { login } = useAuth();
const router = useRouter();
const handleRutChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.replace(/[^0-9kK-]/g, '');
setRut(value);
if (value.length > 2) {
setRut(formatRut(value));
}
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError('');
if (!validarRut(rut)) {
setError('RUT inválido');
return;
}
try {
await login(rut, clave);
router.push('/dashboard');
} catch {
setError('Credenciales inválidas');
}
};
return (
<div className="container mt-5">
<div className="row justify-content-center">
<div className="col-md-4">
<div className="card shadow">
<div className="card-body p-4">
<div className="text-center mb-4">
<img src="/img/ichnHD.png" alt="ICHN" height="60" />
<h4 className="mt-2">Iniciar Sesión</h4>
</div>
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label className="form-label">RUT</label>
<input className="form-control" value={rut} onChange={handleRutChange} placeholder="12.345.678-5" />
</div>
<div className="mb-3">
<label className="form-label">Clave</label>
<input type="password" className="form-control" value={clave} onChange={e => setClave(e.target.value)} />
</div>
{error && <div className="alert alert-danger py-2">{error}</div>}
<button type="submit" className="btn btn-primary w-100">Ingresar</button>
</form>
</div>
</div>
</div>
</div>
</div>
);
}
+19
View File
@@ -0,0 +1,19 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import LoadingSpinner from '@/components/LoadingSpinner';
export default function Home() {
const router = useRouter();
const { user, loading } = useAuth();
useEffect(() => {
if (!loading) {
router.push(user ? '/dashboard' : '/login');
}
}, [user, loading, router]);
return <LoadingSpinner />;
}
@@ -0,0 +1,37 @@
'use client';
import { useState } from 'react';
import { api } from '@/services/api';
export default function ReimpresionPage() {
const [tipo, setTipo] = useState('contrato');
const [id, setId] = useState('');
const handleReimprimir = () => {
if (tipo === 'contrato') {
window.open(`${process.env.NEXT_PUBLIC_API_URL}/report/contrato/${id}`, '_blank');
} else if (tipo === 'cotizacion') {
window.open(`${process.env.NEXT_PUBLIC_API_URL}/report/cotizacion/${id}`, '_blank');
}
};
return (
<div>
<h3 className="mb-3">Reimpresión de Documentos</h3>
<div className="card shadow p-4">
<div className="mb-3">
<label className="form-label">Tipo</label>
<select className="form-select" value={tipo} onChange={e => setTipo(e.target.value)}>
<option value="contrato">Contrato</option>
<option value="cotizacion">Cotización</option>
</select>
</div>
<div className="mb-3">
<label className="form-label">N° Documento</label>
<input className="form-control" value={id} onChange={e => setId(e.target.value)} />
</div>
<button className="btn btn-primary" onClick={handleReimprimir}>Reimprimir</button>
</div>
</div>
);
}
@@ -0,0 +1,40 @@
'use client';
import { useState } from 'react';
import { api } from '@/services/api';
export default function ReporteVentasEmpPage() {
const [data, setData] = useState<any[] | null>(null);
const load = async () => {
const result = await api.get('/informe/ventas-empresa', { tipo: 'LISTADO', varA: '', varB: '', varC: 0 });
setData(Array.isArray(result) ? result : []);
};
return (
<div>
<h3 className="mb-3">Ventas Empresas</h3>
<button className="btn btn-primary mb-3" onClick={load}>Cargar</button>
{data && (
<div className="card shadow">
<div className="card-body">
<table className="table">
<thead><tr><th>Empresa</th><th>Curso</th><th>Monto</th><th>Fecha</th></tr></thead>
<tbody>
{data.map((r: any, i: number) => (
<tr key={i}>
<td>{r.Empresa || r.empresa}</td>
<td>{r.Curso || r.curso}</td>
<td>${(r.Monto || r.monto || 0).toLocaleString()}</td>
<td>{r.Fecha ? new Date(r.Fecha).toLocaleDateString() : ''}</td>
</tr>
))}
{data.length === 0 && <tr><td colSpan={4} className="text-center">Sin datos</td></tr>}
</tbody>
</table>
</div>
</div>
)}
</div>
);
}
+57
View File
@@ -0,0 +1,57 @@
'use client';
import { useState } from 'react';
import { api } from '@/services/api';
export default function ReporteVentasPage() {
const [mes, setMes] = useState(new Date().getMonth() + 1);
const [agno, setAgno] = useState(new Date().getFullYear());
const [data, setData] = useState<any[] | null>(null);
const load = async () => {
const result = await api.get('/informe/ventas-mensuales', { mes, agno, tipo: 'VENTA' });
setData(Array.isArray(result) ? result : []);
};
return (
<div>
<h3 className="mb-3">Reporte de Ventas</h3>
<div className="row mb-3">
<div className="col-auto">
<select className="form-select" value={mes} onChange={e => setMes(Number(e.target.value))}>
{Array.from({ length: 12 }, (_, i) => (
<option key={i + 1} value={i + 1}>{new Date(0, i).toLocaleString('es', { month: 'long' })}</option>
))}
</select>
</div>
<div className="col-auto">
<input type="number" className="form-control" value={agno} onChange={e => setAgno(Number(e.target.value))} />
</div>
<div className="col-auto">
<button className="btn btn-primary" onClick={load}>Generar</button>
</div>
</div>
{data && (
<div className="card shadow">
<div className="card-body">
<table className="table table-hover">
<thead>
<tr><th>Ejecutivo</th><th>Ventas</th><th>Monto</th></tr>
</thead>
<tbody>
{data.map((r: any, i: number) => (
<tr key={i}>
<td>{r.Ejecutivo || r.ejecutivo || r.Nombre}</td>
<td>{r.Cantidad || r.cantidad || r.Ventas}</td>
<td>${(r.Monto || r.monto || r.Total || 0).toLocaleString()}</td>
</tr>
))}
{data.length === 0 && <tr><td colSpan={3} className="text-center">Sin datos</td></tr>}
</tbody>
</table>
</div>
</div>
)}
</div>
);
}
+30
View File
@@ -0,0 +1,30 @@
'use client';
import { useAuth } from '@/hooks/useAuth';
import { useRouter } from 'next/navigation';
export default function Header() {
const { user, logout } = useAuth();
const router = useRouter();
const handleLogout = () => {
logout();
router.push('/login');
};
return (
<header>
<div className="header-content">
<span className="badge">Menu de Aplicaciones</span>
<div className="user-info">
<span className="user-name">
<i className="fas fa-user"></i> {user?.nombre || 'Usuario'}
</span>
<button className="btn-logout" onClick={handleLogout}>
<i className="fas fa-sign-out-alt"></i> Cerrar Sesión
</button>
</div>
</div>
</header>
);
}
@@ -0,0 +1,8 @@
export default function LoadingSpinner({ text = 'Cargando...' }: { text?: string }) {
return (
<div className="loading-spinner">
<div className="spinner"></div>
<p>{text}</p>
</div>
);
}
@@ -0,0 +1,27 @@
'use client';
interface Props {
open: boolean;
title: string;
message: string;
onConfirm: () => void;
onCancel: () => void;
confirmText?: string;
}
export default function ModalConfirmacion({ open, title, message, onConfirm, onCancel, confirmText = 'Aceptar' }: Props) {
if (!open) return null;
return (
<div className="modal-overlay" onClick={onCancel}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<h3>{title}</h3>
<p>{message}</p>
<div className="modal-actions">
<button className="btn btn-secondary" onClick={onCancel}>Cancelar</button>
<button className="btn btn-primary" onClick={onConfirm}>{confirmText}</button>
</div>
</div>
</div>
);
}
+43
View File
@@ -0,0 +1,43 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useState } from 'react';
const menuItems = [
{ href: '/dashboard', icon: 'fas fa-home', label: 'Dashboard' },
{ href: '/leads', icon: 'fas fa-users', label: 'Leads' },
{ href: '/cotizaciones', icon: 'fas fa-file-invoice-dollar', label: 'Cotizaciones' },
{ href: '/cursos', icon: 'fas fa-book', label: 'Cursos' },
{ href: '/alumnos', icon: 'fas fa-user-graduate', label: 'Alumnos' },
{ href: '/arqueo', icon: 'fas fa-cash-register', label: 'Arqueo' },
{ href: '/empresas', icon: 'fas fa-building', label: 'Empresas' },
{ href: '/reportes/ventas', icon: 'fas fa-chart-bar', label: 'Reportes' },
{ href: '/autorizador', icon: 'fas fa-check-double', label: 'Autorizador' },
{ href: '/contacto', icon: 'fas fa-envelope', label: 'Contacto' },
];
export default function Sidebar() {
const pathname = usePathname();
const [collapsed, setCollapsed] = useState(false);
return (
<div className={`sidebar ${collapsed ? 'collapsed' : ''}`}>
<div className="logo-details">
<img className="icon" src="/img/favicon.png" width="60" alt="SAM" />
<div className="logo_name">SAM</div>
<i className="bx bx-menu" id="btn" onClick={() => setCollapsed(!collapsed)}></i>
</div>
<ul className="nav-list">
{menuItems.map(item => (
<li key={item.href} className={pathname.startsWith(item.href) ? 'active' : ''}>
<Link href={item.href}>
<i className={item.icon}></i>
<span className="links_name">{item.label}</span>
</Link>
</li>
))}
</ul>
</div>
);
}
+51
View File
@@ -0,0 +1,51 @@
'use client';
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { api } from '@/services/api';
interface User {
nombre: string;
sede?: string;
perfil?: string;
}
interface AuthContextType {
user: User | null;
loading: boolean;
login: (rut: string, clave: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType>({} as AuthContextType);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const stored = sessionStorage.getItem('sam_user');
if (stored) {
setUser(JSON.parse(stored));
}
setLoading(false);
}, []);
const login = async (rut: string, clave: string) => {
const res = await api.post<{ nombre: string; sede?: string; token: string }>('/auth/login', { rut, clave });
sessionStorage.setItem('sam_user', JSON.stringify({ nombre: res.nombre, sede: res.sede }));
setUser({ nombre: res.nombre, sede: res.sede });
};
const logout = () => {
sessionStorage.removeItem('sam_user');
setUser(null);
};
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);
+25
View File
@@ -0,0 +1,25 @@
'use client';
import { useEffect, useRef } from 'react';
const TIMEOUT_MS = 30 * 60 * 1000;
export function useInactividad(onTimeout: () => void) {
const timerRef = useRef<NodeJS.Timeout>();
const resetTimer = () => {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(onTimeout, TIMEOUT_MS);
};
useEffect(() => {
const events = ['mousedown', 'keydown', 'scroll', 'touchstart'];
resetTimer();
events.forEach(event => window.addEventListener(event, resetTimer));
return () => {
events.forEach(event => window.removeEventListener(event, resetTimer));
if (timerRef.current) clearTimeout(timerRef.current);
};
}, [onTimeout]);
}
+12
View File
@@ -0,0 +1,12 @@
export function formatCurrency(amount: number): string {
return new Intl.NumberFormat('es-CL', { style: 'currency', currency: 'CLP', maximumFractionDigits: 0 }).format(amount);
}
export function formatDate(date: string | Date): string {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleDateString('es-CL', { day: '2-digit', month: '2-digit', year: 'numeric' });
}
export function classNames(...classes: (string | boolean | undefined)[]): string {
return classes.filter(Boolean).join(' ');
}
+29
View File
@@ -0,0 +1,29 @@
export function validarRut(rut: string): boolean {
const limpio = rut.replace(/\./g, '').replace(/-/g, '').toUpperCase();
if (!/^[0-9]+[0-9K]$/.test(limpio)) return false;
const cuerpo = limpio.slice(0, -1);
const dv = limpio.slice(-1);
let suma = 0;
let multiplo = 2;
for (let i = cuerpo.length - 1; i >= 0; i--) {
suma += parseInt(cuerpo[i]) * multiplo;
multiplo = multiplo < 7 ? multiplo + 1 : 2;
}
const dvEsperado = 11 - (suma % 11);
const dvChar = dvEsperado === 11 ? '0' : dvEsperado === 10 ? 'K' : String(dvEsperado);
return dvChar === dv;
}
export function formatRut(rut: string): string {
const limpio = rut.replace(/\./g, '').replace(/-/g, '');
if (limpio.length < 2) return limpio;
const cuerpo = limpio.slice(0, -1);
const dv = limpio.slice(-1);
const formateado = cuerpo.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
return `${formateado}-${dv}`;
}
+43
View File
@@ -0,0 +1,43 @@
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5087/api';
interface ApiOptions {
method?: string;
body?: unknown;
params?: Record<string, string | number | boolean>;
}
async function request<T>(endpoint: string, options: ApiOptions = {}): Promise<T> {
const { method = 'GET', body, params } = options;
let url = `${API_URL}${endpoint}`;
if (params) {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([k, v]) => searchParams.append(k, String(v)));
url += `?${searchParams.toString()}`;
}
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
const error = await res.text();
throw new Error(error || `HTTP ${res.status}`);
}
return res.json();
}
export const api = {
get: <T>(endpoint: string, params?: Record<string, string | number | boolean>) =>
request<T>(endpoint, { params }),
post: <T>(endpoint: string, body?: unknown) =>
request<T>(endpoint, { method: 'POST', body }),
put: <T>(endpoint: string, body?: unknown) =>
request<T>(endpoint, { method: 'PUT', body }),
};
+272
View File
@@ -0,0 +1,272 @@
@import url('https://fonts.googleapis.com/css?family=Fira+Sans&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Fira Sans', sans-serif;
}
body {
background-color: #e9ecef;
min-height: 100vh;
}
/* Sidebar (replica SAM.Master) */
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100%;
width: 78px;
background: #11101d;
padding: 6px 14px;
z-index: 99;
transition: all 0.5s ease;
}
.sidebar.open {
width: 250px;
}
.sidebar .logo-details {
height: 60px;
display: flex;
align-items: center;
position: relative;
}
.sidebar .logo-details .icon {
opacity: 0;
transition: all 0.5s ease;
}
.sidebar.open .logo-details .icon {
opacity: 1;
}
.sidebar .logo-details .logo_name {
color: #fff;
font-size: 20px;
font-weight: 600;
opacity: 0;
transition: all 0.5s ease;
margin-left: 10px;
}
.sidebar.open .logo-details .logo_name {
opacity: 1;
}
.sidebar .logo-details #btn {
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
font-size: 23px;
text-align: center;
cursor: pointer;
transition: all 0.5s ease;
color: #fff;
}
.sidebar.open .logo-details #btn {
text-align: right;
}
.sidebar i {
color: #fff;
height: 60px;
min-width: 50px;
font-size: 28px;
text-align: center;
line-height: 60px;
}
.sidebar .nav-list {
margin-top: 20px;
height: 100%;
padding: 0;
}
.sidebar .nav-list li {
position: relative;
list-style: none;
margin: 8px 0;
}
.sidebar .nav-list li a {
display: flex;
align-items: center;
text-decoration: none;
border-radius: 12px;
transition: all 0.4s ease;
background: transparent;
}
.sidebar .nav-list li a:hover {
background: #FFF;
}
.sidebar .nav-list li a:hover i,
.sidebar .nav-list li a:hover .links_name {
color: #11101d;
}
.sidebar .nav-list li.active a {
background: #FFF;
}
.sidebar .nav-list li.active a i,
.sidebar .nav-list li.active a .links_name {
color: #11101d;
}
.sidebar .nav-list li .links_name {
color: #fff;
font-size: 15px;
font-weight: 400;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: 0.4s;
}
.sidebar.open .nav-list li .links_name {
opacity: 1;
pointer-events: auto;
}
/* Home section */
.home-section {
position: relative;
min-height: 100vh;
left: 78px;
width: calc(100% - 78px);
transition: all 0.5s ease;
}
.sidebar.open ~ .home-section {
left: 250px;
width: calc(100% - 250px);
}
.home-section header {
background: #fff;
padding: 10px 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.badge {
background: #4e73df;
color: #fff;
padding: 6px 16px;
border-radius: 20px;
font-size: 13px;
}
.user-info {
display: flex;
align-items: center;
gap: 15px;
}
.user-name {
font-weight: 500;
color: #333;
}
.btn-logout {
background: none;
border: 1px solid #dc3545;
color: #dc3545;
padding: 5px 12px;
border-radius: 5px;
cursor: pointer;
font-size: 13px;
transition: all 0.3s;
}
.btn-logout:hover {
background: #dc3545;
color: #fff;
}
/* Loading spinner */
.loading-spinner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 60vh;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #4e73df;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Modal */
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: #fff;
padding: 24px;
border-radius: 8px;
max-width: 400px;
width: 90%;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
/* Card styles */
.card {
border-radius: 10px;
border: none;
}
.card.shadow {
box-shadow: 0 0.15rem 1.75rem 0 rgba(58,59,69,0.15);
}
/* Responsive */
@media (max-width: 768px) {
.sidebar {
width: 60px;
}
.sidebar.open {
width: 200px;
}
.home-section {
left: 60px;
width: calc(100% - 60px);
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
+5
View File
@@ -0,0 +1,5 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/ServicesExternos.API/ServicesExternos.API.csproj" />
</Folder>
</Solution>
@@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Mvc;
using ServicesExternos.API.Models;
using ServicesExternos.API.Services;
namespace ServicesExternos.API.Controllers;
[ApiController]
[Route("api/dte")]
public class DteController : ControllerBase
{
private readonly DteService _dteService;
public DteController(DteService dteService)
{
_dteService = dteService;
}
[HttpPost("emitir")]
public async Task<IActionResult> Emitir([FromBody] DteEmissionRequest request)
{
var result = await _dteService.EmitirYDescargarPdfAsync(request);
if (result.PdfBytes != null)
return File(result.PdfBytes, "application/pdf", $"dte_{result.Folio}.pdf");
return BadRequest(new { error = result.Estado });
}
[HttpGet("estado/{dte}/{emisor}")]
public async Task<IActionResult> ConsultarEstado(int dte, long emisor)
{
var result = await _dteService.ConsultarSiguienteFolioAsync(dte, emisor);
return Ok(result);
}
}
@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Mvc;
using ServicesExternos.API.Models;
using ServicesExternos.API.Services;
namespace ServicesExternos.API.Controllers;
[ApiController]
[Route("api/email")]
public class EmailController : ControllerBase
{
private readonly EmailService _emailService;
public EmailController(EmailService emailService)
{
_emailService = emailService;
}
[HttpPost("send")]
public async Task<IActionResult> Send([FromBody] EmailRequest request)
{
var result = await _emailService.SendAsync(request);
if (result == "ok")
return Ok(new { mensaje = result });
return BadRequest(new { error = result });
}
}
@@ -0,0 +1,51 @@
using Microsoft.AspNetCore.Mvc;
using ServicesExternos.API.Models;
using ServicesExternos.API.Services;
namespace ServicesExternos.API.Controllers;
[ApiController]
[Route("api/pagos/transbank")]
public class TransbankController : ControllerBase
{
private readonly TransbankService _transbankService;
public TransbankController(TransbankService transbankService)
{
_transbankService = transbankService;
}
[HttpPost("crear-transaccion")]
public async Task<IActionResult> CrearTransaccion([FromBody] TransbankTransactionRequest request)
{
var result = await _transbankService.CrearTransaccionAsync(request);
return Ok(result);
}
[HttpPost("confirmar")]
public async Task<IActionResult> Confirmar([FromBody] TransbankConfirmRequest request)
{
var result = await _transbankService.ConfirmarTransaccionAsync(request.TokenWs!);
return Ok(result);
}
[HttpPost("voucher")]
public async Task<IActionResult> GrabarVoucher([FromBody] VoucherRequest request)
{
var result = await _transbankService.GrabarVoucherAsync(
request.Token, request.AccountingDate, request.BuyOrder,
request.CardNumber, request.CardExpiration, request.AuthorizationCode,
request.PaymentType, request.ResponseCode, request.SharesNumber,
request.Amount, request.CommerceCode, request.DetailBuyOrder,
request.SessionId, request.TransactionDate, request.Vci);
return Ok(new { mensaje = result });
}
[HttpGet("buscar/{token}")]
public async Task<IActionResult> Buscar(string token)
{
var result = await _transbankService.BuscarTransaccionAsync(token);
return Ok(result);
}
}
@@ -0,0 +1,11 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore ServicesExternos.slnx
RUN dotnet publish src/ServicesExternos.API/ServicesExternos.API.csproj -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app .
EXPOSE 8080
ENTRYPOINT ["dotnet", "ServicesExternos.API.dll"]
@@ -0,0 +1,67 @@
namespace ServicesExternos.API.Models;
public class DteEmissionRequest
{
public int Dte { get; set; } = 33;
public int Emisor { get; set; }
public string? Fecha { get; set; }
public string? Sucursal { get; set; }
public int? SucursalSii { get; set; }
public string? Terminal { get; set; }
public string? IndicadorServicio { get; set; }
public string? TipoDespacho { get; set; }
public string? TipoTraslado { get; set; }
public string? TpoTranCompra { get; set; }
public string? TpoTranVenta { get; set; }
public string? FechaVencimiento { get; set; }
public Receptor? Receptor { get; set; }
public List<DetalleItem>? Detalles { get; set; }
public Referencia? Referencia { get; set; }
}
public class Receptor
{
public string? RUT { get; set; }
public string? RazonSocial { get; set; }
public string? Direccion { get; set; }
public string? Comuna { get; set; }
public string? Ciudad { get; set; }
}
public class DetalleItem
{
public string? Nombre { get; set; }
public int Cantidad { get; set; }
public int Precio { get; set; }
public string? Descuento { get; set; }
}
public class Referencia
{
public int TipoDocReferencia { get; set; }
public int FolioReferencia { get; set; }
public string? FechaReferencia { get; set; }
public string? RazonReferencia { get; set; }
}
public class DteGenerarPorCodigo
{
public string? Codigo { get; set; }
public int TipoDte { get; set; }
public int Emisor { get; set; }
public int Receptor { get; set; }
}
public class DteFolioInfo
{
public int FoliosDisponibles { get; set; }
public int FolioActual { get; set; }
public int? Siguiente { get; set; }
}
public class DteResultado
{
public byte[]? PdfBytes { get; set; }
public int Folio { get; set; }
public string Estado { get; set; } = string.Empty;
}
@@ -0,0 +1,10 @@
namespace ServicesExternos.API.Models;
public class EmailRequest
{
public string? To { get; set; }
public string? Subject { get; set; }
public string? Body { get; set; }
public bool IsHtml { get; set; } = true;
public List<string>? AttachmentPaths { get; set; }
}
@@ -0,0 +1,33 @@
namespace ServicesExternos.API.Models;
public class TransbankTransactionRequest
{
public string? BuyOrder { get; set; }
public string? SessionId { get; set; }
public int Amount { get; set; }
public string? ReturnUrl { get; set; }
}
public class TransbankConfirmRequest
{
public string? TokenWs { get; set; }
}
public class VoucherRequest
{
public string Token { get; set; } = string.Empty;
public string AccountingDate { get; set; } = string.Empty;
public string BuyOrder { get; set; } = string.Empty;
public string CardNumber { get; set; } = string.Empty;
public string CardExpiration { get; set; } = string.Empty;
public string AuthorizationCode { get; set; } = string.Empty;
public string PaymentType { get; set; } = string.Empty;
public string ResponseCode { get; set; } = string.Empty;
public string SharesNumber { get; set; } = string.Empty;
public int Amount { get; set; }
public string CommerceCode { get; set; } = string.Empty;
public string DetailBuyOrder { get; set; } = string.Empty;
public string SessionId { get; set; } = string.Empty;
public string TransactionDate { get; set; } = string.Empty;
public string Vci { get; set; } = string.Empty;
}
@@ -0,0 +1,27 @@
using ServicesExternos.API.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddOpenApi();
builder.Services.AddHttpClient<DteService>();
builder.Services.AddHttpClient<TransbankService>();
builder.Services.AddScoped<EmailService>();
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
});
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
app.MapOpenApi();
app.UseCors();
app.MapControllers();
app.Run();
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5194",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
@@ -0,0 +1,90 @@
using System.Net.Http.Headers;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using ServicesExternos.API.Models;
namespace ServicesExternos.API.Services;
public class DteService
{
private readonly HttpClient _client;
private readonly string _userHash;
private readonly string _ambiente;
public DteService(HttpClient client, IConfiguration configuration)
{
_client = client;
_client.BaseAddress = new Uri("https://libredte.cl");
_client.Timeout = TimeSpan.FromSeconds(60);
_userHash = configuration["LibreDTE:UserHash"] ?? throw new Exception("LibreDTE:UserHash required");
_ambiente = configuration["LibreDTE:Ambiente"] ?? "1";
}
private void SetAuth()
{
var authString = Convert.ToBase64String(Encoding.ASCII.GetBytes("X:" + _userHash));
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authString);
}
public async Task<DteResultado> EmitirYDescargarPdfAsync(DteEmissionRequest datos)
{
SetAuth();
string jsonFinal = JsonConvert.SerializeObject(datos, Formatting.Indented);
var jsonContent = new StringContent(jsonFinal, Encoding.UTF8, "application/json");
var response = await _client.PostAsync("/api/dte/documentos/emitir", jsonContent);
string respuestaJson = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
return new DteResultado { Estado = respuestaJson };
JObject jsonResp = JObject.Parse(respuestaJson);
string cod = (string)jsonResp["codigo"]!;
int emi = (int)jsonResp["emisor"]!;
int rec = (int)jsonResp["receptor"]!;
int dte = (int)jsonResp["dte"]!;
var payload = new DteGenerarPorCodigo
{
Codigo = cod,
TipoDte = dte,
Emisor = emi,
Receptor = rec
};
jsonFinal = JsonConvert.SerializeObject(payload);
jsonContent = new StringContent(jsonFinal, Encoding.UTF8, "application/json");
response = await _client.PostAsync("/api/dte/documentos/generar?getXML=0&links=1&email=1&retry=1&gzip=0", jsonContent);
respuestaJson = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
return new DteResultado { Estado = respuestaJson };
jsonResp = JObject.Parse(respuestaJson);
int folio = (int)jsonResp["folio"]!;
var responsePdf = await _client.GetAsync($"/api/dte/dte_emitidos/pdf/{dte}/{folio}/{emi}?formato=general&papelContinuo=0&copias_tributarias=1&copias_cedibles=1&cedible=0&compress=0&base64=0");
if (!responsePdf.IsSuccessStatusCode)
throw new Exception("Se generó el DTE pero falló la descarga del PDF.");
await _client.GetAsync($"/api/dte/dte_emitidos/actualizar_estado/{dte}/{folio}/{emi}?usarWebservice=1");
byte[] pdfBytes = await responsePdf.Content.ReadAsByteArrayAsync();
return new DteResultado { PdfBytes = pdfBytes, Folio = folio, Estado = "ok" };
}
public async Task<DteFolioInfo> ConsultarSiguienteFolioAsync(int dte, long emisor)
{
SetAuth();
var response = await _client.GetAsync($"/api/dte/admin/dte_folios/info/{dte}/{emisor}");
string respuestaJson = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new Exception($"Error al consultar folio ({response.StatusCode}): {respuestaJson}");
return JsonConvert.DeserializeObject<DteFolioInfo>(respuestaJson)!;
}
}

Some files were not shown because too many files have changed in this diff Show More