Cambio de histograma

This commit is contained in:
Jhonathan Guevara 2025-12-18 13:03:00 -05:00
parent 02e3afa684
commit 35f380784f
Signed by: jhonathan_guevara
GPG Key ID: 619239F12DCBE55B
17 changed files with 1889 additions and 552 deletions

View File

@ -8,6 +8,7 @@
<h2>Importación de Excel (PLANILLA vs HISTOR)</h2>
<div *ngIf="errorImport" class="error">{{ errorImport }}</div>
<div *ngIf="mensajeImport" class="info">{{ mensajeImport }}</div>
<div class="info" *ngIf="cargandoImport">Consultando estado...</div>
@ -34,22 +35,39 @@
<div>
<strong>Citas programadas (PLANILLA)</strong>
<input type="file" (change)="onSeleccionarPlanilla($event)" />
<button (click)="subirPlanilla()" [disabled]="subiendoPlanilla">
{{ subiendoPlanilla ? 'Cargando...' : 'Cargar citas programadas' }}
<button (click)="subirPlanilla()" [disabled]="subiendoPlanilla || sincronizando">
{{ subiendoPlanilla ? 'Cargando...' : 'Cargar planilla' }}
</button>
</div>
<div>
<strong>Citas realizadas (HISTOR)</strong>
<input type="file" (change)="onSeleccionarHistor($event)" />
<button (click)="subirHistor()" [disabled]="subiendoHistor">
{{ subiendoHistor ? 'Cargando...' : 'Cargar citas realizadas' }}
<button (click)="subirHistor()" [disabled]="subiendoHistor || sincronizando">
{{ subiendoHistor ? 'Cargando...' : 'Cargar histor' }}
</button>
</div>
<div>
<button (click)="refrescarEstadoImport()">Refrescar estado</button>
<button (click)="refrescarEstadoImport()" [disabled]="sincronizando">Refrescar estado</button>
</div>
<div>
<button
(click)="sincronizarImportacion()"
[disabled]="sincronizando || !estadoImport?.planilla_cargada || !estadoImport?.histor_cargada"
>
{{ sincronizando ? 'Procesando...' : 'Procesar excels (sincronizar)' }}
</button>
</div>
<div>
<button (click)="limpiarImportacion()" [disabled]="sincronizando">Limpiar</button>
</div>
</div>
<div *ngIf="sincronizando" class="info" style="margin-top:10px;">
⏳ Por favor espera: se están procesando los excels (puede tardar).
</div>
<div *ngIf="logImport.length > 0" class="info" style="margin-top:10px; white-space:pre-wrap;">
@ -64,7 +82,7 @@
<div class="seccion">
<h2>Histograma general (plan anual real)</h2>
<button (click)="cargarHistogramaGeneral()" [disabled]="cargandoHistGeneral">
<button (click)="cargarHistogramaGeneral()" [disabled]="cargandoHistGeneral || sincronizando">
{{ cargandoHistGeneral ? 'Cargando...' : 'Ver histograma general' }}
</button>
@ -114,7 +132,7 @@
: 'Ej: JHONATHAN, GUALDRON...'"
(keyup.enter)="buscar()"
/>
<button (click)="buscar()">Buscar</button>
<button (click)="buscar()" [disabled]="sincronizando">Buscar</button>
</div>
<div *ngIf="cargando" class="info">Cargando pacientes...</div>
@ -186,8 +204,8 @@
({{ histPaciente.cumplimiento.porcentaje_mes_actual }}%)
|
<strong>Hasta hoy:</strong>
{{ histPaciente.cumplimiento.asistidas_hasta_hoy }}/{{ histPaciente.cumplimiento.esperado_hasta_hoy }}
({{ histPaciente.cumplimiento.porcentaje_hasta_hoy }}%)
{{ hastaHoyAsistidas() }}/{{ hastaHoyEsperado() }}
({{ hastaHoyPorcentaje() }}%)
</p>
<canvas #canvasPaciente style="width:100%; max-width:1100px; height:360px;"></canvas>
@ -197,18 +215,20 @@
<tr>
<th>Mes</th>
<th>Rango</th>
<th>Esperado</th>
<th>Asistidas</th>
<th>Pendientes</th>
<th>Esperado (mes)</th>
<th>Asistidas (mes + arrastre)</th>
<th>Pendiente (mes)</th>
<th>Pendientes total</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let m of histPaciente.porMes">
<td>{{ m.mes }}</td>
<td>{{ m.inicio }} → {{ m.fin }}</td>
<td>{{ m.esperado }}</td>
<td>{{ m.asistidas }}</td>
<td>{{ m.pendientes }}</td>
<td>{{ valorEsperadoMes(m) }}</td>
<td>{{ valorAsistidasMes(m) }}</td>
<td>{{ valorPendienteMes(m) }}</td>
<td>{{ valorPendientesTotal(m) }}</td>
</tr>
</tbody>
</table>
@ -218,109 +238,6 @@
<h4>Citas</h4>
<div *ngIf="cargandoCitas" class="info">Cargando citas...</div>
<table *ngIf="!cargandoCitas && citas.length > 0" class="tabla">
<thead>
<tr>
<th>Fecha</th>
<th>Hora</th>
<th>Especialidad</th>
<th>Tipo</th>
<th>Modalidad</th>
<th>Asistió</th>
<th>Observaciones</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let c of citas; let i = index">
<td>{{ c.fecha_cita | date:'yyyy-MM-dd' }}</td>
<td>{{ c.hora_cita || '-' }}</td>
<td>{{ c.especialidad }}</td>
<td>{{ c.tipo_cita || '-' }}</td>
<td>{{ c.modalidad || '-' }}</td>
<td>
<ng-container *ngIf="i < 2 && c.asistio !== true; else asistioFijo">
<input
type="checkbox"
[ngModel]="false"
(ngModelChange)="onToggleAsistencia(c, $event)"
[disabled]="guardandoAsistenciaId === c.id_cita"
/>
</ng-container>
<ng-template #asistioFijo>
{{ c.asistio ? 'Sí' : 'No' }}
</ng-template>
</td>
<td>{{ c.observaciones || '-' }}</td>
</tr>
</tbody>
</table>
<p *ngIf="errorAsistencia" class="error">{{ errorAsistencia }}</p>
<p *ngIf="!cargandoCitas && citas.length === 0" class="info">
Este paciente no tiene citas registradas.
</p>
<!-- Formulario para agendar nueva cita -->
<div class="seccion-agendar">
<h4>Agendar nueva cita</h4>
<div *ngIf="errorCita" class="error">{{ errorCita }}</div>
<div *ngIf="mensajeCita" class="ok">{{ mensajeCita }}</div>
<div class="form-grid">
<div class="campo">
<label>Especialidad</label>
<select [(ngModel)]="formCita.id_especialidad">
<option value="">-- Selecciona --</option>
<option *ngFor="let e of especialidades" [value]="e.id_especialidad">
{{ e.nombre }}
</option>
</select>
</div>
<div class="campo">
<label>Fecha de cita</label>
<input type="date" [(ngModel)]="formCita.fecha_cita" />
</div>
<div class="campo">
<label>Tipo de cita</label>
<input
type="text"
[(ngModel)]="formCita.tipo_cita"
placeholder="Primera vez, Control..."
/>
</div>
<div class="campo">
<label>Hora de cita</label>
<input type="time" [(ngModel)]="formCita.hora_cita" />
</div>
<div class="campo">
<label>Modalidad</label>
<select [(ngModel)]="formCita.modalidad">
<option value="">-- Selecciona --</option>
<option value="PRESENCIAL">Presencial</option>
<option value="TELEMEDICINA">Telemedicina</option>
</select>
</div>
<div class="campo campo-full">
<label>Observaciones</label>
<textarea
rows="3"
[(ngModel)]="formCita.observaciones"
placeholder="Notas sobre la cita"
></textarea>
</div>
</div>
<button (click)="guardarCita()" [disabled]="guardandoCita">
{{ guardandoCita ? 'Guardando...' : 'Guardar cita' }}
</button>
</div>
<!-- (tu bloque de citas sigue igual abajo) -->
</div>
</div>

View File

@ -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);
}

View File

@ -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<RespuestaImportacion>(`${this.baseUrl}/api/importacion/histor`, form);
}
// ✅ Procesar cuando tú quieras (endpoint nuevo)
sincronizar(): Observable<RespuestaImportacion> {
return this.http.post<RespuestaImportacion>(`${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<ImportLog> {
return this.http.get<ImportLog>(`${this.baseUrl}/api/importacion/log`);
}
histogramaGeneral(): Observable<HistogramaGeneral> {
return this.http.get<HistogramaGeneral>(`${this.baseUrl}/api/histogramas/general`);
}

9
backend/nodemon.json Normal file
View File

@ -0,0 +1,9 @@
{
"watch": ["src"],
"ignore": [
"src/uploads/*",
"src/salida_importacion/*",
"src/outputs/*",
"src/import_state.json"
]
}

1040
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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"
}

File diff suppressed because it is too large Load Diff