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)
-
- {{ subiendoHistor ? 'Cargando...' : 'Cargar citas realizadas' }}
+
+ {{ subiendoHistor ? 'Cargando...' : 'Cargar histor' }}
- Refrescar estado
+ Refrescar estado
+
+
+
+ {{ sincronizando ? 'Procesando...' : 'Procesar excels (sincronizar)' }}
+
+
+
+
+ Limpiar
+
+
+
+
+ ⏳ Por favor espera: se están procesando los excels (puede tardar).
0" class="info" style="margin-top:10px; white-space:pre-wrap;">
@@ -64,7 +82,7 @@
Histograma general (plan anual real)
-
+
{{ cargandoHistGeneral ? 'Cargando...' : 'Ver histograma general' }}
@@ -114,7 +132,7 @@
: 'Ej: JHONATHAN, GUALDRON...'"
(keyup.enter)="buscar()"
/>
- Buscar
+ 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...
-
-
-
{{ errorAsistencia }}
-
-
- Este paciente no tiene citas registradas.
-
-
-
-
-
Agendar nueva cita
-
-
{{ errorCita }}
-
{{ mensajeCita }}
-
-
-
-
- {{ guardandoCita ? 'Guardando...' : 'Guardar cita' }}
-
-
+
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