diff --git a/PLANTILLAAUT.xlsx b/PLANTILLAAUT.xlsx new file mode 100644 index 0000000..00356f4 Binary files /dev/null and b/PLANTILLAAUT.xlsx differ diff --git a/backend/src/server.js b/backend/src/server.js index 92a3588..86a87ab 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -1358,6 +1358,63 @@ app.post( } ); +app.post( + '/api/cargar-autorizados-masivos', + verificarToken, + esAdministrador, + upload.single('archivo'), + async (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ error: 'No se recibio archivo Excel' }); + } + + const jobId = createJobId(); + const job = { + id: jobId, + type: 'autorizados-masivos', + status: 'queued', + createdAt: new Date().toISOString(), + ownerId: req.usuario.id_usuario, + }; + + jobs.set(jobId, job); + + const jobTmpDir = path.join(os.tmpdir(), 'saludut_jobs', jobId); + await fsPromises.mkdir(jobTmpDir, { recursive: true }); + const inputPath = path.join(jobTmpDir, `autorizados_${jobId}.xlsx`); + await fsPromises.writeFile(inputPath, req.file.buffer); + + enqueueJob(job, async () => { + try { + return await procesarExcelAutorizacionesMasivas(inputPath, req.usuario, { + estadoAutorizacion: 'autorizado', + allowMissingAmbito: true, + defaultAmbito: 'extramural', + allowNumeroOrdenForAnyAmbito: true, + fechaAutorizacionKeys: [ + 'FECHADEREGISTRO', + 'FECHAREGISTRO', + 'FECHADEAUTORIZACION', + 'FECHA', + ], + }); + } finally { + await safeUnlink(inputPath); + await safeRemoveDir(jobTmpDir); + } + }); + + return res.status(202).json(sanitizeJob(job)); + } catch (error) { + console.error('Error creando job de autorizados masivos:', error); + return res.status(500).json({ + error: 'Error creando el job para procesar autorizados masivos', + }); + } + } +); + app.post( '/api/autorizaciones/autorrellenar', verificarToken, @@ -2444,7 +2501,7 @@ async function procesarExcelReps(inputFilePath) { }; } -async function procesarExcelAutorizacionesMasivas(inputFilePath, usuario) { +async function procesarExcelAutorizacionesMasivas(inputFilePath, usuario, options = {}) { const workbook = new ExcelJS.Workbook(); await workbook.xlsx.readFile(inputFilePath); const sheet = workbook.worksheets[0]; @@ -2453,6 +2510,14 @@ async function procesarExcelAutorizacionesMasivas(inputFilePath, usuario) { throw new Error('No se encontro una hoja en el Excel de autorizaciones'); } + const { + estadoAutorizacion = 'pendiente', + allowMissingAmbito = false, + defaultAmbito = '', + allowNumeroOrdenForAnyAmbito = false, + fechaAutorizacionKeys = [], + } = options; + const headerRow = sheet.getRow(1); const headers = {}; headerRow.eachCell((cell, colNumber) => { @@ -2544,6 +2609,7 @@ async function procesarExcelAutorizacionesMasivas(inputFilePath, usuario) { 'NUMERODEDOCUMENTOPPL', 'NUMERODOCUMENTOPPL', 'DOCUMENTOPPL', + 'NUMERODEDOCUMENTO', 'NUMERODOCUMENTO', ]); const cupsRaw = getValueMulti(row, [ @@ -2653,15 +2719,19 @@ async function procesarExcelAutorizacionesMasivas(inputFilePath, usuario) { ambitoAtencion = 'extramural'; } if (!ambitoAtencion) { - resumen.omitidas += 1; - resumen.sin_ambito += 1; - if (resumen.errores.length < 50) { - resumen.errores.push({ - fila: i, - error: 'Ambito intramural/extramural invalido', - }); + if (allowMissingAmbito) { + ambitoAtencion = defaultAmbito ? defaultAmbito : null; + } else { + resumen.omitidas += 1; + resumen.sin_ambito += 1; + if (resumen.errores.length < 50) { + resumen.errores.push({ + fila: i, + error: 'Ambito intramural/extramural invalido', + }); + } + continue; } - continue; } const numeroOrdenRaw = getValueMulti(row, [ @@ -2670,10 +2740,15 @@ async function procesarExcelAutorizacionesMasivas(inputFilePath, usuario) { 'NUMEROORDEN', 'NROORDEN', 'NUMERODELAORDEN', + 'CODIGPODEREGISTRO', + 'CODIGODEREGISTRO', + 'CODIGOREGISTRO', ]); const numeroOrden = String(numeroOrdenRaw || '').trim(); const numeroOrdenFinal = - ambitoAtencion === 'intramural' && numeroOrden ? numeroOrden : null; + (ambitoAtencion === 'intramural' || allowNumeroOrdenForAnyAmbito) && numeroOrden + ? numeroOrden + : null; const estadoEntregaRaw = getValueMulti(row, [ 'ESTADODEENTREGA', @@ -2684,6 +2759,13 @@ async function procesarExcelAutorizacionesMasivas(inputFilePath, usuario) { const estadoEntrega = parseEstadoEntrega(estadoEntregaRaw) || 'pendiente_entrega'; + const fechaAutorizacionRaw = fechaAutorizacionKeys.length + ? getValueMulti(row, fechaAutorizacionKeys) + : ''; + const fechaAutorizacion = fechaAutorizacionRaw + ? String(fechaAutorizacionRaw).trim() + : null; + const cupCodigo = extractCupCodigo(cupsRaw || procedimiento); if (!cupCodigo) { resumen.omitidas += 1; @@ -2720,6 +2802,8 @@ async function procesarExcelAutorizacionesMasivas(inputFilePath, usuario) { 'NOMBREIPSREMITIDO', 'NOMBREDEIPSREMITIDO', 'IPSREMITIDO', + 'IPSSUJERIDA', + 'IPSSUGERIDA', ]); const departamentoExcel = getValue(row, 'DEPARTAMENTO') || @@ -2962,7 +3046,7 @@ async function procesarExcelAutorizacionesMasivas(inputFilePath, usuario) { ` INSERT INTO autorizacion (interno, id_ips, numero_documento_autorizante, id_usuario_solicitante, fecha_autorizacion, observacion, cup_codigo, cie10_codigo, cie10_descripcion, tipo_autorizacion, tipo_servicio, ambito_atencion, numero_orden, estado_entrega, estado_autorizacion) - VALUES ($1, $2, $3, $4, COALESCE($5::timestamp AT TIME ZONE '${BOGOTA_TIMEZONE}', now()), $6, $7, $8, $9, $10, $11, $12, $13, $14, 'pendiente') + VALUES ($1, $2, $3, $4, COALESCE($5::timestamp AT TIME ZONE '${BOGOTA_TIMEZONE}', now()), $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING numero_autorizacion, fecha_autorizacion, version; `, [ @@ -2970,7 +3054,7 @@ async function procesarExcelAutorizacionesMasivas(inputFilePath, usuario) { ipsInfo.id_ips, autorizanteDefault, solicitanteId, - null, + fechaAutorizacion, observacionFinal, cupCodigo, cie10Codigo, @@ -2980,6 +3064,7 @@ async function procesarExcelAutorizacionesMasivas(inputFilePath, usuario) { ambitoAtencion, numeroOrdenFinal, estadoEntrega, + estadoAutorizacion, ] ); diff --git a/saludut-inpec/public/assets/PLANTILLAAUT.xlsx b/saludut-inpec/public/assets/PLANTILLAAUT.xlsx new file mode 100644 index 0000000..00356f4 Binary files /dev/null and b/saludut-inpec/public/assets/PLANTILLAAUT.xlsx differ diff --git a/saludut-inpec/src/app/app.routes.ts b/saludut-inpec/src/app/app.routes.ts index bcd61f0..b1d5bd2 100644 --- a/saludut-inpec/src/app/app.routes.ts +++ b/saludut-inpec/src/app/app.routes.ts @@ -9,6 +9,7 @@ import { UsuariosComponent } from './components/usuarios/usuarios'; import { CargarCupsComponent } from './components/cargar-cups/cargar-cups'; import { EstadisticasAutorizacionesComponent } from './components/estadisticas-autorizaciones/estadisticas-autorizaciones'; import { CargarAutorizacionesMasivasComponent } from './components/cargar-autorizaciones-masivas/cargar-autorizaciones-masivas'; +import { CargarAutorizadosMasivosComponent } from './components/cargar-autorizados-masivos/cargar-autorizados-masivos'; import { CargarIpsRepsComponent } from './components/cargar-ips-reps/cargar-ips-reps'; import { CargarPacientesComponent } from './components/cargar-pacientes/cargar-pacientes'; @@ -67,6 +68,12 @@ export const routes: Routes = [ canActivate: [AuthGuard], }, + { + path: 'cargar-autorizados-masivos', + component: CargarAutorizadosMasivosComponent, + canActivate: [AuthGuard, AdminGuard], + }, + { path: 'cargar-ips-reps', component: CargarIpsRepsComponent, diff --git a/saludut-inpec/src/app/components/cargar-autorizados-masivos/cargar-autorizados-masivos.css b/saludut-inpec/src/app/components/cargar-autorizados-masivos/cargar-autorizados-masivos.css new file mode 100644 index 0000000..54da4b3 --- /dev/null +++ b/saludut-inpec/src/app/components/cargar-autorizados-masivos/cargar-autorizados-masivos.css @@ -0,0 +1,117 @@ +/* ============================ + Carga masiva autorizados + ============================ */ +.masivas-container { + display: flex; + flex-direction: column; + gap: 20px; +} + +.masivas-card { + padding: 20px; +} + +.masivas-card h2 { + margin: 0 0 6px; + font-size: 1.5rem; +} + +.subtitle { + margin: 0 0 16px; + color: var(--color-text-muted); + font-size: 0.95rem; +} + +.form-row { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 14px; +} + +.form-row label { + font-weight: 600; + font-size: 0.9rem; +} + +.form-row input[type="file"] { + font-size: 0.9rem; +} + +.file-name { + font-size: 0.85rem; + color: var(--color-text-main); +} + +.form-row.acciones { + margin-top: 8px; +} + +.template-row { + align-items: flex-start; +} + +.template-note { + font-size: 0.85rem; + color: var(--color-text-muted); +} + +.resumen-card h2 { + margin: 0 0 12px; + font-size: 1.4rem; +} + +.summary-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 12px; +} + +.summary-item { + background: var(--color-cup-bg); + border: 1px solid var(--color-border); + border-radius: 10px; + padding: 12px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.summary-label { + font-size: 0.8rem; + color: var(--color-text-muted); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.summary-value { + font-size: 1.2rem; + font-weight: 700; + color: var(--color-text-main); +} + +.summary-note { + margin-top: 12px; + padding: 10px 12px; + border-radius: 8px; + background: var(--color-primary-soft); + border: 1px solid var(--color-border); + font-size: 0.9rem; +} + +.error-list { + margin-top: 16px; +} + +.error-list h3 { + margin: 0 0 8px; + font-size: 1rem; +} + +.error-table { + border: 1px solid var(--color-border); + border-radius: 8px; + max-height: 260px; + overflow: auto; +} diff --git a/saludut-inpec/src/app/components/cargar-autorizados-masivos/cargar-autorizados-masivos.html b/saludut-inpec/src/app/components/cargar-autorizados-masivos/cargar-autorizados-masivos.html new file mode 100644 index 0000000..a222083 --- /dev/null +++ b/saludut-inpec/src/app/components/cargar-autorizados-masivos/cargar-autorizados-masivos.html @@ -0,0 +1,132 @@ +
+
+ + +
+

Subir plantilla

+

+ Usa la plantilla oficial de autorizados. La fecha de registro se guarda como fecha de autorizacion. +

+ +
+ + Descargar plantilla de autorizados + + Plantilla oficial para carga masiva. +
+ +
+ + + {{ archivoFile.name }} +
+ +
+ +
+ +
{{ statusMessage }}
+
{{ errorMessage }}
+
+ +
+

Resumen de la carga

+
+
+ Total filas + {{ resumen.total || 0 }} +
+
+ Creadas + {{ resumen.creadas || 0 }} +
+
+ Omitidas + {{ resumen.omitidas || resumen.omitidos || 0 }} +
+
+ Duplicadas + {{ resumen.duplicados || 0 }} +
+
+ Sin paciente + {{ resumen.sin_paciente || 0 }} +
+
+ Sin CUPS + {{ resumen.sin_cups || 0 }} +
+
+ Sin IPS + {{ resumen.sin_ips || 0 }} +
+
+ Sin diagnostico + {{ resumen.sin_diagnostico || 0 }} +
+
+ Sin ambito + {{ resumen.sin_ambito || 0 }} +
+
+ CUPS no cubiertos + {{ resumen.cups_no_cubiertos || 0 }} +
+
+ IPS sin convenio + {{ resumen.ips_sin_convenio || 0 }} +
+
+ +
+ Se detectaron CUPS no cubiertos o IPS sin convenio. Revisa estas autorizaciones. +
+ +
+

Errores (max 50)

+
+ + + + + + + + + + + + + +
FilaError
{{ e.fila || '-' }}{{ e.error || 'Error en fila' }}
+
+
+
+
+
diff --git a/saludut-inpec/src/app/components/cargar-autorizados-masivos/cargar-autorizados-masivos.ts b/saludut-inpec/src/app/components/cargar-autorizados-masivos/cargar-autorizados-masivos.ts new file mode 100644 index 0000000..b6f260a --- /dev/null +++ b/saludut-inpec/src/app/components/cargar-autorizados-masivos/cargar-autorizados-masivos.ts @@ -0,0 +1,124 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Router } from '@angular/router'; +import { AppHeaderComponent } from '../shared/app-header/app-header'; +import { AuthService } from '../../services/auth'; +import { PacienteService } from '../../services/paciente'; +import { JobsService } from '../../services/jobs'; +import { JobResult, JobRowError } from '../../services/job-types'; + +@Component({ + selector: 'app-cargar-autorizados-masivos', + standalone: true, + imports: [CommonModule, AppHeaderComponent], + templateUrl: './cargar-autorizados-masivos.html', + styleUrls: ['./cargar-autorizados-masivos.css'] +}) +export class CargarAutorizadosMasivosComponent { + archivoFile: File | null = null; + isLoading = false; + statusMessage: string | null = null; + errorMessage: string | null = null; + resumen: JobResult | null = null; + errores: JobRowError[] = []; + + constructor( + private authService: AuthService, + private pacienteService: PacienteService, + private jobsService: JobsService, + private router: Router, + private cdr: ChangeDetectorRef + ) {} + + volverDashboard(): void { + this.router.navigate(['/dashboard']); + } + + logout(): void { + this.authService.logout(); + } + + isLoggedIn(): boolean { + return this.authService.isLoggedIn(); + } + + getCurrentUser(): any { + return this.authService.getCurrentUser(); + } + + onArchivoSelected(event: Event): void { + const input = event.target as HTMLInputElement; + this.archivoFile = input.files?.[0] || null; + this.limpiarMensajes(); + this.resumen = null; + this.errores = []; + } + + procesarMasivo(input?: HTMLInputElement): void { + this.limpiarMensajes(); + this.resumen = null; + this.errores = []; + + if (!this.archivoFile) { + this.errorMessage = 'Debes seleccionar el archivo Excel de autorizados.'; + return; + } + + this.isLoading = true; + this.statusMessage = 'Subiendo archivo...'; + + const formData = new FormData(); + formData.append('archivo', this.archivoFile); + + this.pacienteService.cargarAutorizadosMasivos(formData).subscribe({ + next: (job) => { + this.statusMessage = 'Archivo en cola. Procesando...'; + if (input) { + input.value = ''; + } + + this.jobsService.pollJob(job.id).subscribe({ + next: (estado) => { + if (estado.status === 'completed') { + this.resumen = estado.result || null; + this.errores = Array.isArray(estado.result?.errores) + ? estado.result?.errores || [] + : []; + this.statusMessage = + estado.result?.mensaje || 'Carga masiva finalizada.'; + this.isLoading = false; + } + + if (estado.status === 'failed') { + this.errorMessage = + estado.error?.message || + 'Error procesando autorizados masivos.'; + this.isLoading = false; + } + + this.cdr.detectChanges(); + }, + error: (error) => { + console.error(error); + this.errorMessage = + 'Error consultando el estado de la carga masiva.'; + this.isLoading = false; + this.cdr.detectChanges(); + } + }); + }, + error: (err) => { + console.error(err); + this.errorMessage = + err?.error?.error || 'Error subiendo el Excel de autorizados.'; + this.isLoading = false; + this.cdr.detectChanges(); + } + }); + } + + private limpiarMensajes(): void { + this.statusMessage = null; + this.errorMessage = null; + } +} diff --git a/saludut-inpec/src/app/components/dashboard/dashboard.html b/saludut-inpec/src/app/components/dashboard/dashboard.html index b93ec11..9849621 100644 --- a/saludut-inpec/src/app/components/dashboard/dashboard.html +++ b/saludut-inpec/src/app/components/dashboard/dashboard.html @@ -132,6 +132,41 @@

Subir plantilla Excel para crear autorizaciones pendientes

+ +
+ +

Carga masiva de autorizados

+

Subir plantilla Excel de autorizados

+
Solo admin
+
+
+
+ + ✓ + + + × + +
+ Cargar autorizados masivos + Subir plantilla de autorizados +
+
+
{ + const token = this.authService.getToken(); + const headers = new HttpHeaders({ + 'Authorization': `Bearer ${token}` + }); + + return this.http.post( + `${this.API_URL}/cargar-autorizados-masivos`, + formData, + { headers } + ); + } + crearAutorizante(payload: CrearAutorizantePayload): Observable { return this.http.post( `${this.API_URL}/autorizantes`,