diff --git a/aunarsalud/src/app/app.html b/aunarsalud/src/app/app.html index 8c5d9ae..e11e333 100644 --- a/aunarsalud/src/app/app.html +++ b/aunarsalud/src/app/app.html @@ -8,6 +8,7 @@

Importación de Excel (PLANILLA vs HISTOR)

{{ errorImport }}
+
{{ mensajeImport }}
Consultando estado...
@@ -34,22 +35,39 @@
Citas programadas (PLANILLA) -
Citas realizadas (HISTOR) -
- +
+ +
+ +
+ +
+ +
+ + +
+ ⏳ Por favor espera: se están procesando los excels (puede tardar).
@@ -64,7 +82,7 @@

Histograma general (plan anual real)

- @@ -114,7 +132,7 @@ : 'Ej: JHONATHAN, GUALDRON...'" (keyup.enter)="buscar()" /> - +
Cargando pacientes...
@@ -186,8 +204,8 @@ ({{ histPaciente.cumplimiento.porcentaje_mes_actual }}%) | Hasta hoy: - {{ histPaciente.cumplimiento.asistidas_hasta_hoy }}/{{ histPaciente.cumplimiento.esperado_hasta_hoy }} - ({{ histPaciente.cumplimiento.porcentaje_hasta_hoy }}%) + {{ hastaHoyAsistidas() }}/{{ hastaHoyEsperado() }} + ({{ hastaHoyPorcentaje() }}%)

@@ -197,18 +215,20 @@ Mes Rango - Esperado - Asistidas - Pendientes + Esperado (mes) + Asistidas (mes + arrastre) + Pendiente (mes) + Pendientes total {{ m.mes }} {{ m.inicio }} → {{ m.fin }} - {{ m.esperado }} - {{ m.asistidas }} - {{ m.pendientes }} + {{ valorEsperadoMes(m) }} + {{ valorAsistidasMes(m) }} + {{ valorPendienteMes(m) }} + {{ valorPendientesTotal(m) }} @@ -218,109 +238,6 @@

Citas

Cargando citas...
- - - - - - - - - - - - - - - - - - - - - - - -
FechaHoraEspecialidadTipoModalidadAsistióObservaciones
{{ c.fecha_cita | date:'yyyy-MM-dd' }}{{ c.hora_cita || '-' }}{{ c.especialidad }}{{ c.tipo_cita || '-' }}{{ c.modalidad || '-' }} - - - - - - {{ c.asistio ? 'Sí' : 'No' }} - - {{ c.observaciones || '-' }}
- -

{{ errorAsistencia }}

- -

- Este paciente no tiene citas registradas. -

- - -
-

Agendar nueva cita

- -
{{ errorCita }}
-
{{ mensajeCita }}
- -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
- - -
+
diff --git a/aunarsalud/src/app/app.ts b/aunarsalud/src/app/app.ts index 0bf1578..853622a 100644 --- a/aunarsalud/src/app/app.ts +++ b/aunarsalud/src/app/app.ts @@ -24,7 +24,8 @@ import { EstadoImportacion, RespuestaImportacion, HistogramaPaciente, - HistogramaGeneral + HistogramaGeneral, + HistPacienteMes } from './servicios/importacion'; @Component({ @@ -74,6 +75,9 @@ export class AppComponent implements OnInit { cargandoImport = false; subiendoPlanilla = false; subiendoHistor = false; + sincronizando = false; + mensajeImport: string | null = null; + logImport: string[] = []; errorImport: string | null = null; @@ -153,6 +157,16 @@ export class AppComponent implements OnInit { }); } + cargarLogImport(): void { + this.importacionService.log().subscribe({ + next: (r) => { + this.logImport = r.lines || []; + this.cdr.markForCheck(); + }, + error: () => {} + }); + } + onSeleccionarPlanilla(ev: Event): void { const input = ev.target as HTMLInputElement; this.archivoPlanilla = input.files && input.files.length ? input.files[0] : null; @@ -171,7 +185,7 @@ export class AppComponent implements OnInit { this.subiendoPlanilla = true; this.errorImport = null; - this.logImport = []; + this.mensajeImport = null; this.importacionService .subirPlanilla(this.archivoPlanilla) @@ -181,17 +195,9 @@ export class AppComponent implements OnInit { })) .subscribe({ next: (r: RespuestaImportacion) => { - this.logImport.push(r.mensaje || '✅ Planilla cargada.'); - if (r.stdout) this.logImport.push(r.stdout); - if (r.stderr) this.logImport.push('⚠️ ' + r.stderr); - + this.mensajeImport = r.mensaje || '✅ Planilla cargada.'; + this.cargarLogImport(); this.refrescarEstadoImport(); - this.cargarHistogramaGeneral(); - - if (this.pacienteSeleccionado) { - this.cargarCitas(this.pacienteSeleccionado.numero_documento); - this.cargarHistogramaPaciente(this.pacienteSeleccionado.numero_documento); - } }, error: (err: any) => { this.errorImport = err?.error?.detalle || 'Error subiendo planilla.'; @@ -207,7 +213,7 @@ export class AppComponent implements OnInit { this.subiendoHistor = true; this.errorImport = null; - this.logImport = []; + this.mensajeImport = null; this.importacionService .subirHistor(this.archivoHistor) @@ -217,24 +223,104 @@ export class AppComponent implements OnInit { })) .subscribe({ next: (r: RespuestaImportacion) => { - this.logImport.push(r.mensaje || '✅ Histor cargado.'); - if (r.stdout) this.logImport.push(r.stdout); - if (r.stderr) this.logImport.push('⚠️ ' + r.stderr); - + this.mensajeImport = r.mensaje || '✅ Histor cargado.'; + this.cargarLogImport(); this.refrescarEstadoImport(); - this.cargarHistogramaGeneral(); + }, + error: (err: any) => { + this.errorImport = err?.error?.detalle || 'Error subiendo histor.'; + } + }); + } + sincronizarImportacion(): void { + if (!this.estadoImport?.planilla_cargada || !this.estadoImport?.histor_cargada) { + this.errorImport = 'Debes cargar PLANILLA y HISTOR antes de procesar.'; + return; + } + + this.sincronizando = true; + this.errorImport = null; + this.mensajeImport = '⏳ Procesando excels... por favor espera.'; + this.logImport = []; + + this.importacionService + .sincronizar() + .pipe(finalize(() => { + this.sincronizando = false; + this.cdr.markForCheck(); + })) + .subscribe({ + next: (r) => { + this.mensajeImport = r.mensaje || '✅ Sincronización terminada.'; + this.cargarLogImport(); + this.refrescarEstadoImport(); + + // refrescar data + this.cargarHistogramaGeneral(); if (this.pacienteSeleccionado) { this.cargarCitas(this.pacienteSeleccionado.numero_documento); this.cargarHistogramaPaciente(this.pacienteSeleccionado.numero_documento); } }, error: (err: any) => { - this.errorImport = err?.error?.detalle || 'Error subiendo histor.'; + this.errorImport = err?.error?.mensaje || '❌ Error procesando excels.'; + this.cargarLogImport(); } }); } + limpiarImportacion(): void { + this.errorImport = null; + this.mensajeImport = null; + + this.importacionService.limpiar().subscribe({ + next: (r) => { + this.mensajeImport = r.mensaje || '✅ Estado limpiado.'; + this.logImport = []; + this.refrescarEstadoImport(); + this.cdr.markForCheck(); + }, + error: () => this.errorImport = 'No se pudo limpiar la importación.' + }); + } + + // ========================= + // Helpers Histograma (para arreglar "Hasta hoy") + // ========================= + hastaHoyEsperado(): number { + const c: any = this.histPaciente?.cumplimiento; + return Number(c?.esperado_base_hasta_hoy ?? c?.esperado_hasta_hoy ?? 0); + } + + hastaHoyAsistidas(): number { + const c: any = this.histPaciente?.cumplimiento; + return Number(c?.asistidas_plan_hasta_hoy ?? c?.asistidas_hasta_hoy ?? 0); + } + + hastaHoyPorcentaje(): number { + const c: any = this.histPaciente?.cumplimiento; + return Number(c?.porcentaje_base_hasta_hoy ?? c?.porcentaje_hasta_hoy ?? 0); + } + + valorEsperadoMes(m: any): number { + return Number(m?.esperado_mes ?? m?.esperado ?? 0); + } + + valorAsistidasMes(m: any): number { + return Number(m?.asistidas_total ?? m?.asistidas ?? 0); + } + + valorPendienteMes(m: any): number { + const esperadoMes = Number(m?.esperado_mes ?? m?.esperado ?? 0); + const asistidasMes = Number(m?.asistidas_mes ?? 0); // solo del mes (sin arrastre) + return Math.max(0, esperadoMes - asistidasMes); + } + + valorPendientesTotal(m: any): number { + return Number(m?.retraso_fin ?? m?.retraso ?? m?.pendientes ?? 0); + } + // ========================= // HISTOGRAMA GENERAL + GRAFICOS // ========================= @@ -343,8 +429,8 @@ export class AppComponent implements OnInit { if (!this.histPaciente?.porMes || !this.canvasPaciente) return; const labels = this.histPaciente.porMes.map(x => `Mes ${x.mes}`); - const esperado = this.histPaciente.porMes.map(x => x.esperado); - const asistidas = this.histPaciente.porMes.map(x => x.asistidas); + const esperado = this.histPaciente.porMes.map(x => (x as any).esperado ?? (x as any).esperado_mes ?? 0); + const asistidas = this.histPaciente.porMes.map(x => (x as any).asistidas ?? (x as any).asistidas_total ?? 0); if (this.grafPaciente) this.grafPaciente.destroy(); @@ -476,7 +562,6 @@ export class AppComponent implements OnInit { this.formCita.tipo_cita = ''; this.formCita.observaciones = ''; - // refrescar histogramas if (this.pacienteSeleccionado) { this.cargarHistogramaPaciente(this.pacienteSeleccionado.numero_documento); } diff --git a/aunarsalud/src/app/servicios/importacion.ts b/aunarsalud/src/app/servicios/importacion.ts index b39a15f..ddabae0 100644 --- a/aunarsalud/src/app/servicios/importacion.ts +++ b/aunarsalud/src/app/servicios/importacion.ts @@ -8,6 +8,7 @@ export interface EstadoImportacion { planilla_nombre: string | null; histor_nombre: string | null; ultima_sync: string | null; + ultima_sync_resultado?: any; } export interface RespuestaImportacion { @@ -16,6 +17,7 @@ export interface RespuestaImportacion { stdout?: string; stderr?: string; detalle?: any; + resultado?: any; } export interface HistGeneralEspecialidad { @@ -30,6 +32,9 @@ export interface HistGeneralMesPrograma { mes: number; esperado: number; asistidas: number; + // opcionales por si luego los quieres mostrar + retraso?: number; + pendientes?: number; } export interface HistogramaGeneral { @@ -46,9 +51,18 @@ export interface HistPacienteMes { mes: number; inicio: string; fin: string; - esperado: number; - asistidas: number; - pendientes: number; + + // compat + esperado?: number; + asistidas?: number; + pendientes?: number; + + // nuevos (si vienen del backend) + esperado_mes?: number; + asistidas_mes?: number; + asistidas_arrastre?: number; + asistidas_total?: number; + retraso_fin?: number; } export interface HistogramaPaciente { @@ -59,11 +73,31 @@ export interface HistogramaPaciente { esperado_mes_actual: number; asistidas_mes_actual: number; porcentaje_mes_actual: number; - esperado_hasta_hoy: number; - asistidas_hasta_hoy: number; - porcentaje_hasta_hoy: number; + + // ✅ NUEVOS (tu backend actualizado) + esperado_base_hasta_hoy?: number; + asistidas_plan_hasta_hoy?: number; + porcentaje_base_hasta_hoy?: number; + + // (info adicional si lo usas) + asistidas_arrastre_mes_actual?: number; + asistidas_total_mes_actual?: number; + retraso_actual?: number; + + // ✅ VIEJOS (por si algún endpoint todavía lo devuelve) + esperado_hasta_hoy?: number; + asistidas_hasta_hoy?: number; + porcentaje_hasta_hoy?: number; }; porMes: HistPacienteMes[]; + extras?: Array<{ mes: number; fecha: string; especialidad: string }>; +} + +export interface ImportLog { + running: boolean; + inicio: string | null; + fin: string | null; + lines: string[]; } @Injectable({ providedIn: 'root' }) @@ -88,6 +122,19 @@ export class ImportacionService { return this.http.post(`${this.baseUrl}/api/importacion/histor`, form); } + // ✅ Procesar cuando tú quieras (endpoint nuevo) + sincronizar(): Observable { + return this.http.post(`${this.baseUrl}/api/importacion/sincronizar`, {}); + } + + limpiar(): Observable<{ ok: boolean; mensaje: string }> { + return this.http.post<{ ok: boolean; mensaje: string }>(`${this.baseUrl}/api/importacion/limpiar`, {}); + } + + log(): Observable { + return this.http.get(`${this.baseUrl}/api/importacion/log`); + } + histogramaGeneral(): Observable { return this.http.get(`${this.baseUrl}/api/histogramas/general`); } diff --git a/backend/nodemon.json b/backend/nodemon.json new file mode 100644 index 0000000..0080673 --- /dev/null +++ b/backend/nodemon.json @@ -0,0 +1,9 @@ +{ + "watch": ["src"], + "ignore": [ + "src/uploads/*", + "src/salida_importacion/*", + "src/outputs/*", + "src/import_state.json" + ] +} diff --git a/backend/package-lock.json b/backend/package-lock.json index c60e5f0..ad93652 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,17 +9,58 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "archiver": "^7.0.1", "cors": "^2.8.5", "dotenv": "^16.6.1", "express": "^4.22.0", "multer": "^2.0.2", "pg": "^8.16.3", - "twilio": "^5.10.6" + "twilio": "^5.10.6", + "xlsx": "^0.18.5" }, "devDependencies": { "nodemon": "^3.1.11" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -33,6 +74,15 @@ "node": ">= 0.6" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -68,6 +118,30 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -88,12 +162,86 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -111,11 +259,58 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT" }, "node_modules/binary-extensions": { @@ -179,6 +374,39 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -240,6 +468,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -265,6 +506,33 @@ "fsevents": "~2.3.2" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -277,6 +545,38 @@ "node": ">= 0.8" } }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -335,6 +635,12 @@ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -348,6 +654,61 @@ "node": ">= 0.10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/dayjs": { "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", @@ -417,6 +778,12 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -432,6 +799,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -501,6 +874,33 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/express": { "version": "4.22.0", "resolved": "https://registry.npmjs.org/express/-/express-4.22.0.tgz", @@ -547,6 +947,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -598,6 +1004,22 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -623,6 +1045,15 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -693,6 +1124,26 @@ "node": ">= 0.4" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -706,6 +1157,30 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -718,6 +1193,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -835,6 +1316,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -880,6 +1381,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -903,6 +1413,45 @@ "node": ">=0.12.0" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -952,6 +1501,54 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -994,6 +1591,12 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1085,6 +1688,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -1188,7 +1800,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -1227,6 +1838,12 @@ "node": ">= 0.8" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1236,6 +1853,31 @@ "node": ">= 0.8" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -1383,6 +2025,21 @@ "node": ">=0.10.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1462,6 +2119,36 @@ "node": ">= 6" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1659,6 +2346,27 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -1731,6 +2439,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1753,6 +2473,18 @@ "node": ">= 10.x" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -1770,6 +2502,17 @@ "node": ">=10.0.0" } }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -1779,6 +2522,102 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1792,6 +2631,26 @@ "node": ">=4" } }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1901,6 +2760,151 @@ "node": ">= 0.8" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xmlbuilder": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", @@ -1918,6 +2922,36 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } } } } diff --git a/backend/package.json b/backend/package.json index 6d629a0..ba7cce8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,12 +7,14 @@ "start": "node server.js" }, "dependencies": { + "archiver": "^7.0.1", "cors": "^2.8.5", "dotenv": "^16.6.1", "express": "^4.22.0", "multer": "^2.0.2", "pg": "^8.16.3", - "twilio": "^5.10.6" + "twilio": "^5.10.6", + "xlsx": "^0.18.5" }, "devDependencies": { "nodemon": "^3.1.11" diff --git a/backend/src/import_state.json b/backend/src/import_state.json new file mode 100644 index 0000000..f347a4c --- /dev/null +++ b/backend/src/import_state.json @@ -0,0 +1,8 @@ +{ + "planilla_path": "C:\\Users\\LENOVO\\Desktop\\Desarrollos\\produccion\\aunar\\backend\\src\\uploads\\1766080602276__PLANILLA DE CITAS NOVIEMBRE.xlsx", + "planilla_nombre": "PLANILLA DE CITAS NOVIEMBRE.xlsx", + "planilla_subida_en": "2025-12-18T17:56:42.330Z", + "histor_path": "C:\\Users\\LENOVO\\Desktop\\Desarrollos\\produccion\\aunar\\backend\\src\\uploads\\1766080603238__-ACTIVIDADESREALIZADASENHISTOR_2025-12-08-09-02.xlsx", + "histor_nombre": "-ACTIVIDADESREALIZADASENHISTOR_2025-12-08-09-02.xlsx", + "histor_subida_en": "2025-12-18T17:56:43.249Z" +} \ No newline at end of file diff --git a/backend/src/server.js b/backend/src/server.js index 67df1cf..45f2641 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -1,18 +1,21 @@ -// src/server.js +// backend/src/server.js const express = require('express'); const cors = require('cors'); const path = require('path'); const fs = require('fs'); const multer = require('multer'); const { spawn } = require('child_process'); +const readline = require('readline'); const { query } = require('./db'); require('dotenv').config(); const api = express(); -const API_PORT = process.env.PORT || 2800; +const PUERTO = process.env.PORT || 2800; +// ============================== // Middlewares +// ============================== api.use(cors()); api.use(express.json()); @@ -22,18 +25,39 @@ api.get('/', (req, res) => { }); // ============================== -// IMPORTACIÓN EXCEL (multer + estado) +// LOG IMPORT (consola + frontend) +// ============================== +let importLog = []; // líneas del último proceso +let importLogInfo = { running: false, inicio: null, fin: null }; + +function logImport(linea) { + const ts = new Date().toISOString().slice(11, 19); // HH:MM:SS + const txt = `[${ts}] ${linea}`; + importLog.push(txt); + if (importLog.length > 1500) importLog.shift(); + console.log(txt); +} + +api.get('/api/importacion/log', (req, res) => { + res.json({ ...importLogInfo, lines: importLog }); +}); + +// ============================== +// Helpers Estado Importación (planilla/histor) // ============================== const uploadsDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true }); +const salidaDir = path.join(__dirname, 'salida_importacion'); +if (!fs.existsSync(salidaDir)) fs.mkdirSync(salidaDir, { recursive: true }); + const STATE_FILE = path.join(__dirname, 'import_state.json'); function leerEstado() { try { if (!fs.existsSync(STATE_FILE)) return {}; return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')); - } catch (e) { + } catch { return {}; } } @@ -49,10 +73,13 @@ function estadoParaFrontend(s) { planilla_nombre: s.planilla_nombre || null, histor_nombre: s.histor_nombre || null, ultima_sync: s.ultima_sync || null, + ultima_sync_resultado: s.ultima_sync_resultado || null, }; } -// storage: conserva nombre y agrega timestamp +// ============================== +// Multer (subida de archivos) +// ============================== const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, uploadsDir), filename: (req, file, cb) => { @@ -61,152 +88,277 @@ const storage = multer.diskStorage({ cb(null, `${ts}__${base}`); } }); - const upload = multer({ storage }); -// control para no correr dos imports al tiempo +// ============================== +// Esquema + Estatus (6 meses) +// ============================== +async function asegurarColumnasPaciente() { + try { + await query(`ALTER TABLE paciente ADD COLUMN IF NOT EXISTS estatus varchar(20) DEFAULT 'ACTIVO';`); + await query(`ALTER TABLE paciente ADD COLUMN IF NOT EXISTS fecha_ultima_actividad date;`); + } catch (e) { + console.warn('⚠️ No pude asegurar columnas (estatus/fecha_ultima_actividad) en paciente:', e.message); + } +} + +async function actualizarEstatusPacientes() { + await asegurarColumnasPaciente(); + + const r = await query(` + WITH ult AS ( + SELECT + p.numero_documento, + NULLIF( + GREATEST( + COALESCE(MAX(c.fecha_cita)::date, '1900-01-01'::date), + COALESCE(t.fecha_ingreso_ips::date, '1900-01-01'::date) + ), + '1900-01-01'::date + ) AS last_act + FROM paciente p + LEFT JOIN cita c ON c.numero_documento = p.numero_documento + LEFT JOIN tiempo t ON t.numero_documento = p.numero_documento + GROUP BY p.numero_documento, t.fecha_ingreso_ips + ), + upd AS ( + UPDATE paciente p + SET + fecha_ultima_actividad = u.last_act, + estatus = CASE + WHEN u.last_act IS NULL THEN COALESCE(p.estatus,'ACTIVO') + WHEN u.last_act < (CURRENT_DATE - INTERVAL '6 months') THEN 'ARCHIVADO' + ELSE 'ACTIVO' + END + FROM ult u + WHERE p.numero_documento = u.numero_documento + RETURNING p.estatus + ) + SELECT + SUM(CASE WHEN estatus='ACTIVO' THEN 1 ELSE 0 END)::int AS activos, + SUM(CASE WHEN estatus='ARCHIVADO' THEN 1 ELSE 0 END)::int AS archivados + FROM upd; + `); + + return r.rows?.[0] || { activos: 0, archivados: 0 }; +} + +api.post('/api/pacientes/actualizar-estatus', async (req, res) => { + try { + const conteo = await actualizarEstatusPacientes(); + res.json({ ok: true, ...conteo }); + } catch (e) { + console.error('Error actualizando estatus:', e); + res.status(500).json({ ok: false, error: e.message }); + } +}); + +// ============================== +// Ejecutar Python Import (manual) +// ============================== let importEnCurso = false; function correrImportPython(planillaPath, historPath) { return new Promise((resolve, reject) => { const scriptPath = path.join(__dirname, 'importar_citas_cac.py'); - const pythonCmd = process.env.PYTHON || 'python'; // Windows: python - const args = [ + const cmd = process.env.PYTHON || (process.platform === 'win32' ? 'py' : 'python3'); + const args = []; + if (process.platform === 'win32' && cmd === 'py') args.push('-3'); + + args.push( scriptPath, '--planilla', planillaPath, - '--histor', historPath - ]; + '--histor', historPath, + '--outdir', salidaDir + ); - const proc = spawn(pythonCmd, args, { cwd: __dirname }); + importLog = []; + importLogInfo = { running: true, inicio: new Date().toISOString(), fin: null }; - let stdout = ''; - let stderr = ''; + logImport(`🚀 Iniciando importación Python...`); + logImport(`📄 PLANILLA: ${path.basename(planillaPath)}`); + logImport(`📄 HISTOR: ${path.basename(historPath)}`); - proc.stdout.on('data', (d) => stdout += d.toString()); - proc.stderr.on('data', (d) => stderr += d.toString()); + const proc = spawn(cmd, args, { cwd: __dirname }); + + let stdoutAll = ''; + let stderrAll = ''; + + const rlOut = readline.createInterface({ input: proc.stdout }); + rlOut.on('line', (line) => { + stdoutAll += line + '\n'; + logImport(`PY> ${line}`); + }); + + const rlErr = readline.createInterface({ input: proc.stderr }); + rlErr.on('line', (line) => { + stderrAll += line + '\n'; + logImport(`PY_ERR> ${line}`); + }); proc.on('close', (code) => { - if (code === 0) resolve({ code, stdout, stderr }); - else reject({ code, stdout, stderr }); + importLogInfo.running = false; + importLogInfo.fin = new Date().toISOString(); + + if (code === 0) { + logImport(`✅ Python terminó OK (code ${code})`); + resolve({ code, stdout: stdoutAll, stderr: stderrAll }); + } else { + logImport(`❌ Python terminó con error (code ${code})`); + reject({ code, stdout: stdoutAll, stderr: stderrAll }); + } }); }); } -// GET estado +// ============================== +// Importación: estado + subir archivos +// ============================== api.get('/api/importacion/estado', (req, res) => { const s = leerEstado(); res.json(estadoParaFrontend(s)); }); -// POST planilla +// ✅ Subir PLANILLA (SOLO guarda y registra estado; NO procesa) api.post('/api/importacion/planilla', upload.single('file'), async (req, res) => { try { - if (!req.file) return res.status(400).json({ error: 'No llegó archivo (file).' }); + if (!req.file) return res.status(400).json({ ok: false, error: 'No llegó archivo (file).' }); + + logImport(`📥 Archivo PLANILLA cargado: ${req.file.originalname}`); const s = leerEstado(); s.planilla_path = req.file.path; s.planilla_nombre = req.file.originalname; + s.planilla_subida_en = new Date().toISOString(); guardarEstado(s); - // si ya está histor => correr import - if (s.histor_path) { - if (importEnCurso) { - return res.status(409).json({ ok: false, mensaje: 'Importación en curso, intenta en 30s.' }); - } - - importEnCurso = true; - try { - const r = await correrImportPython(s.planilla_path, s.histor_path); - const s2 = leerEstado(); - s2.ultima_sync = new Date().toISOString(); - guardarEstado(s2); - - return res.json({ - ok: true, - mensaje: '✅ Planilla cargada y sincronización ejecutada.', - stdout: r.stdout, - stderr: r.stderr - }); - } catch (e) { - return res.status(500).json({ - ok: false, - mensaje: '❌ Falló la sincronización (Python).', - stdout: e.stdout, - stderr: e.stderr, - detalle: e - }); - } finally { - importEnCurso = false; - } - } - - return res.json({ ok: true, mensaje: '✅ Planilla cargada. Falta HISTOR para sincronizar.' }); + return res.json({ + ok: true, + mensaje: '✅ Planilla cargada. Lista para procesar cuando tú quieras.', + estado: estadoParaFrontend(s) + }); } catch (err) { console.error('Error POST /api/importacion/planilla:', err); res.status(500).json({ ok: false, mensaje: 'Error subiendo planilla.', detalle: err?.message }); } }); -// POST histor +// ✅ Subir HISTOR (SOLO guarda y registra estado; NO procesa) api.post('/api/importacion/histor', upload.single('file'), async (req, res) => { try { - if (!req.file) return res.status(400).json({ error: 'No llegó archivo (file).' }); + if (!req.file) return res.status(400).json({ ok: false, error: 'No llegó archivo (file).' }); + + logImport(`📥 Archivo HISTOR cargado: ${req.file.originalname}`); const s = leerEstado(); s.histor_path = req.file.path; s.histor_nombre = req.file.originalname; + s.histor_subida_en = new Date().toISOString(); guardarEstado(s); - // si ya está planilla => correr import - if (s.planilla_path) { - if (importEnCurso) { - return res.status(409).json({ ok: false, mensaje: 'Importación en curso, intenta en 30s.' }); - } - - importEnCurso = true; - try { - const r = await correrImportPython(s.planilla_path, s.histor_path); - const s2 = leerEstado(); - s2.ultima_sync = new Date().toISOString(); - guardarEstado(s2); - - return res.json({ - ok: true, - mensaje: '✅ Histor cargado y sincronización ejecutada.', - stdout: r.stdout, - stderr: r.stderr - }); - } catch (e) { - return res.status(500).json({ - ok: false, - mensaje: '❌ Falló la sincronización (Python).', - stdout: e.stdout, - stderr: e.stderr, - detalle: e - }); - } finally { - importEnCurso = false; - } - } - - return res.json({ ok: true, mensaje: '✅ Histor cargado. Falta PLANILLA para sincronizar.' }); + return res.json({ + ok: true, + mensaje: '✅ Histor cargado. Listo para procesar cuando tú quieras.', + estado: estadoParaFrontend(s) + }); } catch (err) { console.error('Error POST /api/importacion/histor:', err); res.status(500).json({ ok: false, mensaje: 'Error subiendo histor.', detalle: err?.message }); } }); -// ============================== -// HISTOGRAMAS (PLAN ANUAL REAL) -// ============================== +// ✅ Limpiar estado + borrar archivos subidos (opcional) +api.post('/api/importacion/limpiar', (req, res) => { + const s = leerEstado(); + try { if (s.planilla_path && fs.existsSync(s.planilla_path)) fs.unlinkSync(s.planilla_path); } catch {} + try { if (s.histor_path && fs.existsSync(s.histor_path)) fs.unlinkSync(s.histor_path); } catch {} + guardarEstado({}); + logImport('🧹 Estado limpiado (planilla/histor).'); + res.json({ ok: true, mensaje: '✅ Estado limpiado. Puedes cargar excels de nuevo.' }); +}); +// ✅ Procesar MANUALMENTE (aquí sí corre Python) +api.post('/api/importacion/sincronizar', async (req, res) => { + const s = leerEstado(); + + if (!s.planilla_path || !s.histor_path) { + return res.status(400).json({ + ok: false, + mensaje: 'Faltan archivos para sincronizar (planilla/histor).', + estado: estadoParaFrontend(s) + }); + } + + if (importEnCurso) { + return res.status(409).json({ ok: false, mensaje: 'Importación en curso, intenta en unos segundos.' }); + } + + importEnCurso = true; + + // conteo antes + let nAntes = 0; + try { + const antes = await query('SELECT COUNT(*)::int AS n FROM cita;'); + nAntes = antes.rows?.[0]?.n || 0; + } catch {} + + try { + const py = await correrImportPython(s.planilla_path, s.histor_path); + + // conteo después + let nDespues = nAntes; + try { + const despues = await query('SELECT COUNT(*)::int AS n FROM cita;'); + nDespues = despues.rows?.[0]?.n || nAntes; + } catch {} + + const nuevas = Math.max(0, nDespues - nAntes); + + // estatus 6 meses + const conteoEstatus = await actualizarEstatusPacientes(); + + const s2 = leerEstado(); + s2.ultima_sync = new Date().toISOString(); + s2.ultima_sync_resultado = { + motivo: 'manual', + citas_antes: nAntes, + citas_despues: nDespues, + citas_nuevas: nuevas, + estatus: conteoEstatus + }; + guardarEstado(s2); + + return res.json({ + ok: true, + mensaje: '✅ Sincronización ejecutada (manual).', + resultado: s2.ultima_sync_resultado, + stdout: py.stdout, + stderr: py.stderr + }); + } catch (e) { + console.error('❌ Falló Python:', e); + return res.status(500).json({ + ok: false, + mensaje: '❌ Falló la sincronización (Python).', + stdout: e?.stdout, + stderr: e?.stderr, + detalle: e + }); + } finally { + importEnCurso = false; + } +}); + +// ============================== +// PLAN ANUAL (SIN LABORATORIOS) +// ============================== const PLAN_ANUAL = { - 1: ['ENFERMERIA', 'LABORATORIOS', 'PSICOLOGIA', 'TRABAJO SOCIAL', 'MEDICO EXPERTO', 'QUIMICO FARMACEUTICO'], + 1: ['ENFERMERIA', 'PSICOLOGIA', 'TRABAJO SOCIAL', 'MEDICO EXPERTO', 'QUIMICO FARMACEUTICO'], 2: ['MEDICO EXPERTO', 'NUTRICION'], 3: ['INFECTOLOGIA', 'QUIMICO FARMACEUTICO'], 4: ['MEDICO EXPERTO'], - 5: ['MEDICO EXPERTO', 'ENFERMERIA', 'LABORATORIOS'], + 5: ['MEDICO EXPERTO'], 6: ['MEDICO EXPERTO', 'ODONTOLOGIA', 'TRABAJO SOCIAL'], 7: ['MEDICO EXPERTO'], 8: ['MEDICO EXPERTO', 'OFTALMOLOGIA'], @@ -260,197 +412,148 @@ function normEsp(s) { .replaceAll('Ñ','N'); } -function obtenerMesPorFecha(rangos, fecha) { - for (let m = 1; m <= 12; m++) { - const a = rangos[m].inicio; - const b = rangos[m].fin; - if (fecha >= a && fecha < b) return m; +function contarLista(lista) { + const c = {}; + for (const x of lista) { + const k = normEsp(x); + c[k] = (c[k] || 0) + 1; } - return null; + return c; } -// ✅ Histograma general REAL: suma esperado vs asistido según plan anual por paciente -api.get('/api/histogramas/general', async (req, res) => { - try { - const hoy = new Date(); +function sumarConteos(a, b) { + const out = { ...a }; + for (const k of Object.keys(b)) out[k] = (out[k] || 0) + b[k]; + return out; +} - // 1) pacientes + ingreso (tiempo) o primera cita - const pacientesRes = await query(` - SELECT - p.numero_documento::text AS doc, - t.fecha_ingreso_ips AS ingreso_tiempo, - MIN(c.fecha_cita) AS primera_cita - FROM paciente p - LEFT JOIN tiempo t ON t.numero_documento = p.numero_documento - LEFT JOIN cita c ON c.numero_documento = p.numero_documento - GROUP BY p.numero_documento, t.fecha_ingreso_ips - `); +function restarUno(conteo, k) { + if ((conteo[k] || 0) > 0) conteo[k] -= 1; +} - const mapaPac = new Map(); - let minStart = null; +function totalConteo(conteo) { + return Object.values(conteo).reduce((s, v) => s + (Number(v) || 0), 0); +} - for (const r of pacientesRes.rows) { - const doc = String(r.doc || '').trim(); - const ingreso = r.ingreso_tiempo || r.primera_cita; - if (!doc || !ingreso) continue; +/** + * ✅ Lógica solicitada (versión "analista"): + * - Esperado NO se acumula (solo lo del mes). + * - Asistidas del mes muestran lo que ocurrió en ese mes: + * asistidas_total = asistidas_mes + asistidas_arrastre + * - Retraso = deuda acumulada al cierre del mes (pendientes_fin). + */ +function calcularMesesConArrastre(PLAN, mesActual, rangos, citasAsistidasOrdenadas) { + const pendientes = {}; // conteo por especialidad pendiente acumulada + const porMes = []; + const extras = []; - const ciclo = ajustarCicloAnual(new Date(ingreso), hoy); - const start = ciclo.start; - const end = ciclo.end; - const mesActual = mesPrograma(start, hoy); + const esperadoBasePorEsp = {}; + const asistidasPlanPorEsp = {}; - const rangos = {}; - for (let m = 1; m <= 12; m++) { - rangos[m] = { inicio: addMonths(start, m - 1), fin: addMonths(start, m) }; + for (let m = 1; m <= mesActual; m++) { + const planMes = (PLAN[m] || []).map(normEsp); + const esperadoMesConteo = contarLista(planMes); + + // acumular esperado base por especialidad (sin inflar por arrastre) + for (const esp of Object.keys(esperadoMesConteo)) { + esperadoBasePorEsp[esp] = (esperadoBasePorEsp[esp] || 0) + (esperadoMesConteo[esp] || 0); + } + + const arrastreInicio = totalConteo(pendientes); + const esperadoMes = totalConteo(esperadoMesConteo); + const esperadoTotal = arrastreInicio + esperadoMes; + + const inicio = rangos[m].inicio; + const fin = rangos[m].fin; + + const citasMes = citasAsistidasOrdenadas.filter(x => x.fecha >= inicio && x.fecha < fin); + + let asistidasArrastre = 0; + let asistidasDelMes = 0; + + const pendientesLocal = { ...pendientes }; + const esperadoLocal = { ...esperadoMesConteo }; + + for (const c of citasMes) { + const esp = normEsp(c.especialidad); + + // 1) cubrir pendientes anteriores + if ((pendientesLocal[esp] || 0) > 0) { + restarUno(pendientesLocal, esp); + asistidasArrastre += 1; + asistidasPlanPorEsp[esp] = (asistidasPlanPorEsp[esp] || 0) + 1; + continue; } - mapaPac.set(doc, { doc, start, end, mesActual, rangos }); - - if (!minStart || start < minStart) minStart = start; - } - - if (!minStart) { - return res.json({ - pacientes_incluidos: 0, - rango: { inicio: null, hoy: toISO(hoy) }, - total_esperado: 0, - total_asistidas: 0, - porcentaje: 0, - porEspecialidad: [], - porMesPrograma: Array.from({ length: 12 }, (_, i) => ({ mes: i + 1, esperado: 0, asistidas: 0 })) - }); - } - - // 2) asistidas desde minStart hasta hoy - const asistidasRes = await query( - `SELECT - c.numero_documento::text AS doc, - e.nombre AS especialidad, - c.fecha_cita - FROM cita c - JOIN especialidad e ON e.id_especialidad = c.id_especialidad - WHERE c.asistio = TRUE - AND c.fecha_cita >= $1 - AND c.fecha_cita <= $2`, - [toISO(minStart), toISO(hoy)] - ); - - // 3) laboratorios (para marcar LABORATORIOS) - const labsRes = await query( - `SELECT - numero_documento::text AS doc, - fecha_laboratorio_1, fecha_laboratorio_2, fecha_laboratorio_3, fecha_laboratorio_4, - fecha_laboratorio_5, fecha_laboratorio_6, fecha_laboratorio_7, fecha_laboratorio_8 - FROM laboratorios` - ); - - // 4) cumplidas: máximo 1 por (doc|mes|especialidad) - const cumplidas = new Set(); - - for (const r of asistidasRes.rows) { - const doc = String(r.doc || '').trim(); - const p = mapaPac.get(doc); - if (!p) continue; - - const fecha = new Date(r.fecha_cita); - if (!(fecha >= p.start && fecha <= hoy)) continue; - - const mes = obtenerMesPorFecha(p.rangos, fecha); - if (!mes || mes > p.mesActual) continue; - - const esp = normEsp(r.especialidad); - const planMes = (PLAN_ANUAL[mes] || []).map(normEsp); - - if (planMes.includes(esp)) { - cumplidas.add(`${doc}|${mes}|${esp}`); + // 2) cubrir lo esperado del mes + if ((esperadoLocal[esp] || 0) > 0) { + restarUno(esperadoLocal, esp); + asistidasDelMes += 1; + asistidasPlanPorEsp[esp] = (asistidasPlanPorEsp[esp] || 0) + 1; + continue; } + + // 3) extra (no suma al plan) + extras.push({ mes: m, fecha: toISO(c.fecha), especialidad: esp }); } - // labs => LABORATORIOS - for (const r of labsRes.rows) { - const doc = String(r.doc || '').trim(); - const p = mapaPac.get(doc); - if (!p) continue; + const pendientesFin = sumarConteos(pendientesLocal, esperadoLocal); - const fechas = [ - r.fecha_laboratorio_1, r.fecha_laboratorio_2, r.fecha_laboratorio_3, r.fecha_laboratorio_4, - r.fecha_laboratorio_5, r.fecha_laboratorio_6, r.fecha_laboratorio_7, r.fecha_laboratorio_8 - ].filter(Boolean).map(x => new Date(x)); + // copiar a global + for (const k of Object.keys(pendientes)) delete pendientes[k]; + for (const k of Object.keys(pendientesFin)) pendientes[k] = pendientesFin[k]; - for (const f of fechas) { - if (!(f >= p.start && f <= hoy)) continue; + const asistidasTotal = asistidasArrastre + asistidasDelMes; + const pendientesFinTotal = totalConteo(pendientes); - const mes = obtenerMesPorFecha(p.rangos, f); - if (!mes || mes > p.mesActual) continue; + porMes.push({ + mes: m, + inicio: toISO(inicio), + fin: toISO(new Date(fin.getTime() - 86400000)), - const planMes = (PLAN_ANUAL[mes] || []).map(normEsp); - if (planMes.includes('LABORATORIOS')) { - cumplidas.add(`${doc}|${mes}|LABORATORIOS`); - } - } - } + // analítica detallada + esperado_mes: esperadoMes, // ✅ NO se acumula + arrastre_inicio: arrastreInicio, + esperado_total: esperadoTotal, // informativo (no lo uses para la barra "Esperado") + asistidas_mes: asistidasDelMes, + asistidas_arrastre: asistidasArrastre, + asistidas_total: asistidasTotal, // ✅ lo que hizo en el mes (incluye ponerse al día) + retraso_fin: pendientesFinTotal, // ✅ deuda acumulada - // 5) esperado vs asistidas (sumado por especialidad y por mes del programa) - const esperadoPorEsp = {}; - const asistidasPorEsp = {}; - const porMesPrograma = Array.from({ length: 12 }, (_, i) => ({ mes: i + 1, esperado: 0, asistidas: 0 })); - - let total_esperado = 0; - let total_asistidas = 0; - - for (const p of mapaPac.values()) { - for (let mes = 1; mes <= p.mesActual; mes++) { - const lista = (PLAN_ANUAL[mes] || []).map(normEsp); - - for (const esp of lista) { - esperadoPorEsp[esp] = (esperadoPorEsp[esp] || 0) + 1; - porMesPrograma[mes - 1].esperado += 1; - total_esperado += 1; - - const key = `${p.doc}|${mes}|${esp}`; - if (cumplidas.has(key)) { - asistidasPorEsp[esp] = (asistidasPorEsp[esp] || 0) + 1; - porMesPrograma[mes - 1].asistidas += 1; - total_asistidas += 1; - } - } - } - } - - const especialidades = Object.keys(esperadoPorEsp).sort(); - - const porEspecialidad = especialidades.map((esp) => { - const esperado = esperadoPorEsp[esp] || 0; - const asistidas = asistidasPorEsp[esp] || 0; - const pendientes = Math.max(0, esperado - asistidas); - const porcentaje = esperado ? Math.round((asistidas / esperado) * 100) : 0; - return { especialidad: esp, esperado, asistidas, pendientes, porcentaje }; + // compatibilidad para el front actual: + // "Esperado" = solo mes, "Asistidas" = total del mes (mes + arrastre), "Pendientes" = retraso + esperado: esperadoMes, + asistidas: asistidasTotal, + pendientes: pendientesFinTotal, + retraso: pendientesFinTotal }); - - const porcentaje = total_esperado ? Math.round((total_asistidas / total_esperado) * 100) : 0; - - res.json({ - pacientes_incluidos: mapaPac.size, - rango: { inicio: toISO(minStart), hoy: toISO(hoy) }, - total_esperado, - total_asistidas, - porcentaje, - porEspecialidad, - porMesPrograma - }); - - } catch (err) { - console.error('Error GET /api/histogramas/general:', err); - res.status(500).json({ error: 'Error generando histograma general' }); } -}); -// Histograma anual por paciente (CICLO ANUAL segun ingreso) + return { porMes, extras, esperadoBasePorEsp, asistidasPlanPorEsp, pendientesFinal: { ...pendientes } }; +} + +// ============================== +// HISTOGRAMA PACIENTE +// ============================== api.get('/api/histogramas/paciente/:numero_documento', async (req, res) => { const { numero_documento } = req.params; try { - // 1) fecha ingreso (tiempo) o primera cita + await actualizarEstatusPacientes(); + + const est = await query( + `SELECT COALESCE(estatus,'ACTIVO') AS estatus + FROM paciente + WHERE numero_documento::text = $1 + LIMIT 1`, + [numero_documento] + ); + + if (est.rows.length && est.rows[0].estatus === 'ARCHIVADO') { + return res.status(403).json({ error: 'Paciente ARCHIVADO: no se incluye en reportes activos.' }); + } + + // ingreso (tiempo) o primera cita let ingreso = null; const ingRes = await query( `SELECT fecha_ingreso_ips @@ -479,16 +582,14 @@ api.get('/api/histogramas/paciente/:numero_documento', async (req, res) => { const hoy = new Date(); const ciclo = ajustarCicloAnual(ingreso, hoy); const start = ciclo.start; - const end = ciclo.end; // exclusivo + const end = addMonths(start, 12); const mesActual = mesPrograma(start, hoy); - // rangos por mes const rangos = {}; for (let m = 1; m <= 12; m++) { rangos[m] = { inicio: addMonths(start, m - 1), fin: addMonths(start, m) }; } - // 2) citas asistidas del paciente en ciclo const citasRes = await query( `SELECT e.nombre AS especialidad, c.fecha_cita, c.asistio FROM cita c @@ -499,84 +600,47 @@ api.get('/api/histogramas/paciente/:numero_documento', async (req, res) => { [numero_documento, toISO(start), toISO(end)] ); - // 3) labs del paciente - const labsRes = await query( - `SELECT fecha_laboratorio_1, fecha_laboratorio_2, fecha_laboratorio_3, fecha_laboratorio_4, - fecha_laboratorio_5, fecha_laboratorio_6, fecha_laboratorio_7, fecha_laboratorio_8 - FROM laboratorios - WHERE numero_documento::text = $1 - LIMIT 1`, - [numero_documento] - ); + const asistidasOrdenadas = citasRes.rows + .filter(x => x.asistio === true) + .map(x => ({ fecha: new Date(x.fecha_cita), especialidad: x.especialidad })) + .sort((a, b) => a.fecha - b.fecha); - const fechasLabs = []; - if (labsRes.rows.length) { - const row = labsRes.rows[0]; - Object.keys(row).forEach(k => { if (row[k]) fechasLabs.push(new Date(row[k])); }); - } + const calc = calcularMesesConArrastre(PLAN_ANUAL, mesActual, rangos, asistidasOrdenadas); - // cumplidas por mes (máximo 1 por especialidad en el mes) - const cumplidas = new Set(); + // ✅ "Hasta hoy" real (sin inflar esperado) + const esperadoBaseHastaHoy = calc.porMes.reduce((s, x) => s + (x.esperado_mes || 0), 0); + const asistidasPlanHastaHoy = calc.porMes.reduce((s, x) => s + (x.asistidas_total || 0), 0); - for (const r of citasRes.rows) { - if (r.asistio !== true) continue; - const fecha = new Date(r.fecha_cita); - const mes = obtenerMesPorFecha(rangos, fecha); - if (!mes) continue; - const esp = normEsp(r.especialidad); - const planMes = (PLAN_ANUAL[mes] || []).map(normEsp); - if (planMes.includes(esp)) { - cumplidas.add(`${mes}|${esp}`); - } - } + const mesRow = calc.porMes.find(x => x.mes === mesActual) || null; - for (const f of fechasLabs) { - const mes = obtenerMesPorFecha(rangos, f); - if (!mes) continue; - const planMes = (PLAN_ANUAL[mes] || []).map(normEsp); - if (planMes.includes('LABORATORIOS')) { - cumplidas.add(`${mes}|LABORATORIOS`); - } - } + // ✅ Este mes: % solo del mes (sin arrastre) + const esperadoMesActual = mesRow ? (mesRow.esperado_mes || 0) : 0; + const asistidasMesActual = mesRow ? (mesRow.asistidas_mes || 0) : 0; + const asistidasArrastreMesActual = mesRow ? (mesRow.asistidas_arrastre || 0) : 0; + const asistidasTotalMesActual = mesRow ? (mesRow.asistidas_total || 0) : 0; - const porMes = []; - let esperadoHastaHoy = 0; - let asistidasHastaHoy = 0; + const cumplimiento = { + // Este mes (plan del mes) + esperado_mes_actual: esperadoMesActual, + asistidas_mes_actual: asistidasMesActual, + porcentaje_mes_actual: esperadoMesActual + ? Math.round((asistidasMesActual / esperadoMesActual) * 100) + : 0, - for (let m = 1; m <= 12; m++) { - const a = rangos[m].inicio; - const b = rangos[m].fin; + // Info adicional (lo que se puso al día en este mes) + asistidas_arrastre_mes_actual: asistidasArrastreMesActual, + asistidas_total_mes_actual: asistidasTotalMesActual, - const lista = (PLAN_ANUAL[m] || []).map(normEsp); - const esperado = lista.length; + // Retraso acumulado + retraso_actual: mesRow ? (mesRow.retraso_fin || 0) : 0, - let asistidas = 0; - for (const esp of lista) { - if (cumplidas.has(`${m}|${esp}`)) asistidas += 1; - } - - const pendientes = Math.max(0, esperado - asistidas); - - porMes.push({ - mes: m, - inicio: toISO(a), - fin: toISO(new Date(b.getTime() - 86400000)), - esperado, - asistidas, - pendientes - }); - - if (m <= mesActual) { - esperadoHastaHoy += esperado; - asistidasHastaHoy += asistidas; - } - } - - const esperadoMes = porMes.find(x => x.mes === mesActual)?.esperado || 0; - const asistidasMes = porMes.find(x => x.mes === mesActual)?.asistidas || 0; - - const porcentajeMes = esperadoMes ? Math.round((asistidasMes / esperadoMes) * 100) : 0; - const porcentajeHasta = esperadoHastaHoy ? Math.round((asistidasHastaHoy / esperadoHastaHoy) * 100) : 0; + // Hasta hoy (cumplimiento real del plan) + esperado_base_hasta_hoy: esperadoBaseHastaHoy, + asistidas_plan_hasta_hoy: asistidasPlanHastaHoy, + porcentaje_base_hasta_hoy: esperadoBaseHastaHoy + ? Math.round((asistidasPlanHastaHoy / esperadoBaseHastaHoy) * 100) + : 0, + }; res.json({ numero_documento, @@ -589,16 +653,11 @@ api.get('/api/histogramas/paciente/:numero_documento', async (req, res) => { fin: toISO(new Date(end.getTime() - 86400000)), mes_programa: mesActual }, - cumplimiento: { - esperado_mes_actual: esperadoMes, - asistidas_mes_actual: asistidasMes, - porcentaje_mes_actual: porcentajeMes, - esperado_hasta_hoy: esperadoHastaHoy, - asistidas_hasta_hoy: asistidasHastaHoy, - porcentaje_hasta_hoy: porcentajeHasta - }, - porMes + cumplimiento, + porMes: calc.porMes, + extras: calc.extras }); + } catch (err) { console.error('Error GET /api/histogramas/paciente/:doc:', err); res.status(500).json({ error: 'Error generando histograma del paciente' }); @@ -606,31 +665,212 @@ api.get('/api/histogramas/paciente/:numero_documento', async (req, res) => { }); // ============================== -// TUS RUTAS EXISTENTES +// HISTOGRAMA GENERAL // ============================== +api.get('/api/histogramas/general', async (req, res) => { + try { + const hoy = new Date(); + await actualizarEstatusPacientes(); -/** - * GET /api/pacientes - */ + // 1) pacientes activos con ingreso + const pacientesRes = await query(` + SELECT + p.numero_documento::text AS doc, + t.fecha_ingreso_ips AS ingreso_tiempo, + MIN(c.fecha_cita) AS primera_cita + FROM paciente p + LEFT JOIN tiempo t ON t.numero_documento = p.numero_documento + LEFT JOIN cita c ON c.numero_documento = p.numero_documento + WHERE COALESCE(p.estatus,'ACTIVO') = 'ACTIVO' + GROUP BY p.numero_documento, t.fecha_ingreso_ips + `); + + const mapaPac = new Map(); + let minStart = null; + let maxMes = 0; + + for (const r of pacientesRes.rows) { + const doc = String(r.doc || '').trim(); + const ingreso = r.ingreso_tiempo || r.primera_cita; + if (!doc || !ingreso) continue; + + const ciclo = ajustarCicloAnual(new Date(ingreso), hoy); + const start = ciclo.start; + const end = ciclo.end; + const mesActual = mesPrograma(start, hoy); + + const rangos = {}; + for (let m = 1; m <= 12; m++) { + rangos[m] = { inicio: addMonths(start, m - 1), fin: addMonths(start, m) }; + } + + mapaPac.set(doc, { doc, start, end, mesActual, rangos }); + if (!minStart || start < minStart) minStart = start; + if (mesActual > maxMes) maxMes = mesActual; + } + + if (!minStart || mapaPac.size === 0) { + return res.json({ + pacientes_incluidos: 0, + rango: { inicio: null, hoy: toISO(hoy) }, + total_esperado: 0, + total_asistidas: 0, + porcentaje: 0, + porEspecialidad: [], + porMesPrograma: [] + }); + } + + // 2) citas asistidas desde minStart + const asistidasRes = await query( + `SELECT + c.numero_documento::text AS doc, + e.nombre AS especialidad, + c.fecha_cita + FROM cita c + JOIN especialidad e ON e.id_especialidad = c.id_especialidad + WHERE c.asistio = TRUE + AND c.fecha_cita >= $1 + AND c.fecha_cita <= $2`, + [toISO(minStart), toISO(hoy)] + ); + + const asistidasPorDoc = new Map(); + for (const r of asistidasRes.rows) { + const doc = String(r.doc || '').trim(); + if (!doc) continue; + if (!asistidasPorDoc.has(doc)) asistidasPorDoc.set(doc, []); + asistidasPorDoc.get(doc).push({ fecha: new Date(r.fecha_cita), especialidad: r.especialidad }); + } + for (const [doc, arr] of asistidasPorDoc.entries()) { + arr.sort((a, b) => a.fecha - b.fecha); + asistidasPorDoc.set(doc, arr); + } + + const porMesPrograma = Array.from({ length: maxMes }, (_, i) => ({ + mes: i + 1, + + // detallado + esperado_mes: 0, + asistidas_mes: 0, + asistidas_arrastre: 0, + asistidas_total: 0, + retraso_fin: 0, + + // compatibilidad para gráfica actual: + esperado: 0, // esperado del mes (NO acumulado) + asistidas: 0, // asistidas del mes (incluye arrastre) + pendientes: 0, // retraso (deuda) + retraso: 0 + })); + + const esperadoBasePorEsp = {}; + const asistidasPlanPorEsp = {}; + + let totalEsperadoBase = 0; + let totalAsistidasPlan = 0; + + for (const p of mapaPac.values()) { + const arr = asistidasPorDoc.get(p.doc) || []; + const asistidasVentana = arr.filter(x => x.fecha >= p.start && x.fecha < p.end && x.fecha <= hoy); + + const calc = calcularMesesConArrastre(PLAN_ANUAL, p.mesActual, p.rangos, asistidasVentana); + + // por especialidad (plan real) + for (const esp of Object.keys(calc.esperadoBasePorEsp)) { + esperadoBasePorEsp[esp] = (esperadoBasePorEsp[esp] || 0) + (calc.esperadoBasePorEsp[esp] || 0); + } + for (const esp of Object.keys(calc.asistidasPlanPorEsp)) { + asistidasPlanPorEsp[esp] = (asistidasPlanPorEsp[esp] || 0) + (calc.asistidasPlanPorEsp[esp] || 0); + } + + // totales base (sin inflar esperado) + const espBase = calc.porMes.reduce((s, x) => s + (x.esperado_mes || 0), 0); + const asisPlan = calc.porMes.reduce((s, x) => s + (x.asistidas_total || 0), 0); + totalEsperadoBase += espBase; + totalAsistidasPlan += asisPlan; + + // por mes + for (const row of calc.porMes) { + const idx = row.mes - 1; + if (!porMesPrograma[idx]) continue; + + porMesPrograma[idx].esperado_mes += row.esperado_mes || 0; + porMesPrograma[idx].asistidas_mes += row.asistidas_mes || 0; + porMesPrograma[idx].asistidas_arrastre += row.asistidas_arrastre || 0; + porMesPrograma[idx].asistidas_total += row.asistidas_total || 0; + porMesPrograma[idx].retraso_fin += row.retraso_fin || 0; + + // compatibilidad + porMesPrograma[idx].esperado += row.esperado || 0; + porMesPrograma[idx].asistidas += row.asistidas || 0; + porMesPrograma[idx].pendientes += row.pendientes || 0; + porMesPrograma[idx].retraso += row.retraso || 0; + } + } + + const porcentaje = totalEsperadoBase + ? Math.round((totalAsistidasPlan / totalEsperadoBase) * 100) + : 0; + + const especialidades = Object.keys(esperadoBasePorEsp).sort(); + const porEspecialidad = especialidades.map((esp) => { + const esperado = esperadoBasePorEsp[esp] || 0; + const asistidas = asistidasPlanPorEsp[esp] || 0; + const pendientes = Math.max(0, esperado - asistidas); + const porcentaje = esperado ? Math.round((asistidas / esperado) * 100) : 0; + return { especialidad: esp, esperado, asistidas, pendientes, porcentaje }; + }); + + res.json({ + pacientes_incluidos: mapaPac.size, + rango: { inicio: toISO(minStart), hoy: toISO(hoy) }, + + // ✅ totales reales del plan (NO inflados) + total_esperado: totalEsperadoBase, + total_asistidas: totalAsistidasPlan, + porcentaje, + + porEspecialidad, + porMesPrograma + }); + + } catch (err) { + console.error('Error GET /api/histogramas/general:', err); + res.status(500).json({ error: 'Error generando histograma general' }); + } +}); + +// ============================== +// RUTAS EXISTENTES (Pacientes / Citas) +// ============================== api.get('/api/pacientes', async (req, res) => { - const { tipo, termino } = req.query; + const { tipo, termino, estatus } = req.query; try { const valor = (termino || '').toString().trim(); + const filtroEstatus = (estatus || '').toString().trim().toUpperCase(); + + const whereEstatus = (filtroEstatus === 'ACTIVO' || filtroEstatus === 'ARCHIVADO') + ? ` AND COALESCE(p.estatus,'ACTIVO') = '${filtroEstatus}' ` + : ''; if (!valor) { const resultado = await query( `SELECT - tipo_documento, - numero_documento::text AS numero_documento, - nombre_completo, - genero, - edad, - fecha_nacimiento, - celular, - correo - FROM paciente - ORDER BY nombre_completo + p.tipo_documento, + p.numero_documento::text AS numero_documento, + p.nombre_completo, + p.genero, + p.edad, + p.fecha_nacimiento, + p.celular, + p.correo, + COALESCE(p.estatus,'ACTIVO') AS estatus, + p.fecha_ultima_actividad + FROM paciente p + WHERE 1=1 ${whereEstatus} + ORDER BY p.nombre_completo LIMIT 50` ); return res.json(resultado.rows); @@ -642,33 +882,39 @@ api.get('/api/pacientes', async (req, res) => { if (tipo === 'documento') { sql = ` SELECT - tipo_documento, - numero_documento::text AS numero_documento, - nombre_completo, - genero, - edad, - fecha_nacimiento, - celular, - correo - FROM paciente - WHERE numero_documento::text ILIKE $1 - ORDER BY nombre_completo + p.tipo_documento, + p.numero_documento::text AS numero_documento, + p.nombre_completo, + p.genero, + p.edad, + p.fecha_nacimiento, + p.celular, + p.correo, + COALESCE(p.estatus,'ACTIVO') AS estatus, + p.fecha_ultima_actividad + FROM paciente p + WHERE p.numero_documento::text ILIKE $1 + ${whereEstatus} + ORDER BY p.nombre_completo LIMIT 50 `; } else if (tipo === 'nombre') { sql = ` SELECT - tipo_documento, - numero_documento::text AS numero_documento, - nombre_completo, - genero, - edad, - fecha_nacimiento, - celular, - correo - FROM paciente - WHERE nombre_completo ILIKE $1 - ORDER BY nombre_completo + p.tipo_documento, + p.numero_documento::text AS numero_documento, + p.nombre_completo, + p.genero, + p.edad, + p.fecha_nacimiento, + p.celular, + p.correo, + COALESCE(p.estatus,'ACTIVO') AS estatus, + p.fecha_ultima_actividad + FROM paciente p + WHERE p.nombre_completo ILIKE $1 + ${whereEstatus} + ORDER BY p.nombre_completo LIMIT 50 `; } else { @@ -683,9 +929,6 @@ api.get('/api/pacientes', async (req, res) => { } }); -/** - * GET /api/pacientes/:numero_documento/citas - */ api.get('/api/pacientes/:numero_documento/citas', async (req, res) => { const { numero_documento } = req.params; @@ -716,9 +959,6 @@ api.get('/api/pacientes/:numero_documento/citas', async (req, res) => { } }); -/** - * GET /api/especialidades - */ api.get('/api/especialidades', async (req, res) => { try { const resultado = await query( @@ -733,9 +973,6 @@ api.get('/api/especialidades', async (req, res) => { } }); -/** - * POST /api/citas - */ api.post('/api/citas', async (req, res) => { const { numero_documento, @@ -774,12 +1011,12 @@ api.post('/api/citas', async (req, res) => { const conflicto30m = await query( `SELECT 1 - FROM cita - WHERE numero_documento::text = $1 - AND fecha_cita = $2 - AND hora_cita IS NOT NULL - AND ABS(EXTRACT(EPOCH FROM (hora_cita - $3::time))) < 1800 - LIMIT 1`, + FROM cita + WHERE numero_documento::text = $1 + AND fecha_cita = $2 + AND hora_cita IS NOT NULL + AND ABS(EXTRACT(EPOCH FROM (hora_cita - $3::time))) < 1800 + LIMIT 1`, [numero_documento, fecha_cita, hora_cita] ); @@ -795,7 +1032,6 @@ api.post('/api/citas', async (req, res) => { ); const total = Number(countRes.rows[0].total) || 0; const consecutivo = total + 1; - const nuevoId = id_especialidad * 1_000_000 + consecutivo; const valorAsistio = typeof asistio === 'boolean' ? asistio : false; @@ -855,9 +1091,6 @@ api.post('/api/citas', async (req, res) => { } }); -/** - * PATCH /api/citas/:id_cita/asistencia - */ api.patch('/api/citas/:id_cita/asistencia', async (req, res) => { const { id_cita } = req.params; const { asistio } = req.body; @@ -917,7 +1150,9 @@ api.patch('/api/citas/:id_cita/asistencia', async (req, res) => { } }); -// Arrancar servidor -api.listen(API_PORT, () => { - console.log(`Servidor escuchando en http://localhost:${API_PORT}`); +// ============================== +// Arrancar +// ============================== +api.listen(PUERTO, () => { + console.log(`Servidor escuchando en http://localhost:${PUERTO}`); }); diff --git a/backend/src/uploads/1766080602276__PLANILLA DE CITAS NOVIEMBRE.xlsx b/backend/src/uploads/1766080602276__PLANILLA DE CITAS NOVIEMBRE.xlsx new file mode 100644 index 0000000..c833525 Binary files /dev/null and b/backend/src/uploads/1766080602276__PLANILLA DE CITAS NOVIEMBRE.xlsx differ diff --git a/backend/src/uploads/1766080603238__-ACTIVIDADESREALIZADASENHISTOR_2025-12-08-09-02.xlsx b/backend/src/uploads/1766080603238__-ACTIVIDADESREALIZADASENHISTOR_2025-12-08-09-02.xlsx new file mode 100644 index 0000000..f297aaa Binary files /dev/null and b/backend/src/uploads/1766080603238__-ACTIVIDADESREALIZADASENHISTOR_2025-12-08-09-02.xlsx differ diff --git a/backend/src/uploads/1766088878241__PLANILLA DE CITAS NOVIEMBRE.xlsx b/backend/src/uploads/1766088878241__PLANILLA DE CITAS NOVIEMBRE.xlsx new file mode 100644 index 0000000..c833525 Binary files /dev/null and b/backend/src/uploads/1766088878241__PLANILLA DE CITAS NOVIEMBRE.xlsx differ diff --git a/backend/src/uploads/1766088880125__-ACTIVIDADESREALIZADASENHISTOR_2025-12-08-09-02.xlsx b/backend/src/uploads/1766088880125__-ACTIVIDADESREALIZADASENHISTOR_2025-12-08-09-02.xlsx new file mode 100644 index 0000000..f297aaa Binary files /dev/null and b/backend/src/uploads/1766088880125__-ACTIVIDADESREALIZADASENHISTOR_2025-12-08-09-02.xlsx differ diff --git a/backend/src/uploads/1766092740472__PLANILLA DE CITAS NOVIEMBRE.xlsx b/backend/src/uploads/1766092740472__PLANILLA DE CITAS NOVIEMBRE.xlsx new file mode 100644 index 0000000..c833525 Binary files /dev/null and b/backend/src/uploads/1766092740472__PLANILLA DE CITAS NOVIEMBRE.xlsx differ diff --git a/backend/src/uploads/1766092742302__-ACTIVIDADESREALIZADASENHISTOR_2025-12-08-09-02.xlsx b/backend/src/uploads/1766092742302__-ACTIVIDADESREALIZADASENHISTOR_2025-12-08-09-02.xlsx new file mode 100644 index 0000000..f297aaa Binary files /dev/null and b/backend/src/uploads/1766092742302__-ACTIVIDADESREALIZADASENHISTOR_2025-12-08-09-02.xlsx differ diff --git a/backend/src/uploads/1766092756294__PLANILLA DE CITAS NOVIEMBRE.xlsx b/backend/src/uploads/1766092756294__PLANILLA DE CITAS NOVIEMBRE.xlsx new file mode 100644 index 0000000..c833525 Binary files /dev/null and b/backend/src/uploads/1766092756294__PLANILLA DE CITAS NOVIEMBRE.xlsx differ diff --git a/backend/src/uploads/1766092756727__PLANILLA DE CITAS NOVIEMBRE.xlsx b/backend/src/uploads/1766092756727__PLANILLA DE CITAS NOVIEMBRE.xlsx new file mode 100644 index 0000000..c833525 Binary files /dev/null and b/backend/src/uploads/1766092756727__PLANILLA DE CITAS NOVIEMBRE.xlsx differ diff --git a/backend/src/uploads/1766093823412__PLANILLA DE CITAS NOVIEMBRE.xlsx b/backend/src/uploads/1766093823412__PLANILLA DE CITAS NOVIEMBRE.xlsx new file mode 100644 index 0000000..c833525 Binary files /dev/null and b/backend/src/uploads/1766093823412__PLANILLA DE CITAS NOVIEMBRE.xlsx differ