cambios nuevos

This commit is contained in:
Jhonathan Guevara 2026-01-13 09:10:16 -05:00
parent 6ace09ec6c
commit 50e6aef612
Signed by: jhonathan_guevara
GPG Key ID: 619239F12DCBE55B
19 changed files with 1286 additions and 123 deletions

View File

@ -343,5 +343,3 @@ Reinicia el backend:
rc-service saludut-backend restart
```

BIN
backend/src/CIE-10.xlsx Normal file

Binary file not shown.

View File

@ -327,29 +327,66 @@ def extract_cups(lines, norm_lines):
return None, None
def normalize_cie10_code(code):
if not code:
return None
cleaned = re.sub(r"[\s\.]", "", code).upper()
cleaned = re.sub(r"[^A-Z0-9]", "", cleaned)
return cleaned or None
def clean_diagnosis_desc(desc):
if not desc:
return None
desc = re.sub(r"(?i)impresion diagnostica", "", desc)
desc = re.sub(
r"(?i)diagnostico(s)?( principal| de egreso| egreso| principal)?", "", desc
)
desc = re.sub(r"(?i)dx( principal| de egreso| egreso| secundaria| secundario)?", "", desc)
desc = re.sub(r"(?i)cie[-\s]*10", "", desc)
desc = desc.strip(" -:")
return desc or None
def extract_cie10_codes_from_line(line):
if not line:
return []
matches = re.findall(
r"\b([A-Z]\s*\d{2,4}(?:\s*\.\s*\d{1,2})?[A-Z0-9]?)\b",
line,
re.IGNORECASE,
)
codes = []
for match in matches:
code = normalize_cie10_code(match)
if code and code not in codes:
codes.append(code)
return codes
def parse_cie10_from_line(line):
if not line:
return None, None
match = re.search(r"\(([A-Z][0-9]{2,4}[A-Z0-9]?)\)", line, re.IGNORECASE)
match = re.search(
r"\(([A-Z][0-9]{2,4}(?:\.[0-9]{1,2})?[A-Z0-9]?)\)",
line,
re.IGNORECASE,
)
if match:
code = match.group(1).upper()
desc = line[: match.start()].strip(" -:")
desc = re.sub(r"(?i)diagnostico principal", "", desc).strip()
desc = re.sub(r"(?i)impresion diagnostica", "", desc).strip()
code = normalize_cie10_code(match.group(1))
desc = clean_diagnosis_desc(line[: match.start()].strip(" -:"))
return code, desc.title() if desc else None
match = re.search(r"\b([A-Z])(?:\s*\d){2,4}[A-Z0-9]?\b", line, re.IGNORECASE)
match = re.search(
r"\b([A-Z]\s*\d{2,4}(?:\s*\.\s*\d{1,2})?[A-Z0-9]?)\b",
line,
re.IGNORECASE,
)
if match:
code = re.sub(r"\s+", "", match.group(0)).upper()
code = normalize_cie10_code(match.group(1))
desc = line[match.end():].strip(" -:")
if not desc:
desc = line[: match.start()].strip(" -:")
desc = re.sub(r"(?i)diagnostico principal", "", desc).strip()
desc = re.sub(r"(?i)impresion diagnostica", "", desc).strip()
return code, desc.title() if desc else None
match = re.search(r"\b([A-Z][0-9]{2,4}[A-Z0-9]?)\b", line, re.IGNORECASE)
if match:
code = match.group(1).upper()
desc = line[match.end():].strip(" -:")
desc = clean_diagnosis_desc(desc)
return code, desc.title() if desc else None
return None, None
@ -390,20 +427,31 @@ def extract_cups_hint(lines, norm_lines):
def extract_cie10_list(lines, norm_lines):
results = []
seen = set()
diag_tokens = [
"DIAGNOSTICO",
"DIAGNOSTICOS",
"IMPRESION DIAGNOSTICA",
"CIE10",
"CIE-10",
"DX",
"DX PRINCIPAL",
"DX EGRESO",
"DIAGNOSTICO PRINCIPAL",
"DIAGNOSTICO DE EGRESO",
"DIAGNOSTICO EGRESO",
]
for i, nline in enumerate(norm_lines):
if (
"DIAGNOSTICO" in nline
or "IMPRESION DIAGNOSTICA" in nline
or "CIE10" in nline
):
for j in range(i, min(i + 6, len(lines))):
code, desc = parse_cie10_from_line(lines[j])
if not code:
continue
if code in seen:
continue
if not any(token in nline for token in diag_tokens):
continue
for j in range(i, min(i + 12, len(lines))):
code, desc = parse_cie10_from_line(lines[j])
if code and code not in seen:
seen.add(code)
results.append((code, desc))
for extra_code in extract_cie10_codes_from_line(lines[j]):
if extra_code and extra_code not in seen:
seen.add(extra_code)
results.append((extra_code, None))
return results
@ -443,6 +491,37 @@ def extract_fecha_ingreso_urgencias(lines, norm_lines):
return None
def extract_fecha_egreso(lines, norm_lines):
keys = [
"FECHA DE EGRESO",
"FECHA EGRESO",
"EGRESO",
"EGRESA",
"ALTA MEDICA",
"ALTA HOSPITALARIA",
"FECHA DE ALTA",
"FECHA ALTA",
]
for i, nline in enumerate(norm_lines):
if not any(key in nline for key in keys):
continue
match = re.search(
r"(EGRESO|EGRESA|ALTA)[^0-9]{0,20}(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})",
nline,
)
date = match.group(2) if match else None
if not date:
date = extract_date_from_text(lines[i])
if not date and i + 1 < len(lines):
date = extract_date_from_text(lines[i + 1])
if date:
time = extract_time_from_text(lines[i]) or (
extract_time_from_text(lines[i + 1]) if i + 1 < len(lines) else None
)
return f"{date} {time}" if time else date
return None
def clean_ips_name(value):
if not value:
return ""
@ -549,6 +628,8 @@ def merge_response(base, extra):
"fecha_ingreso_urgencias"
):
result["fecha_ingreso_urgencias"] = extra.get("fecha_ingreso_urgencias")
if not result.get("fecha_egreso") and extra.get("fecha_egreso"):
result["fecha_egreso"] = extra.get("fecha_egreso")
if not result.get("cups_busqueda") and extra.get("cups_busqueda"):
result["cups_busqueda"] = extra.get("cups_busqueda")
if result.get("formato") == "DESCONOCIDO" and extra.get("formato"):
@ -594,6 +675,7 @@ def build_response(text, ocr_used, ocr_available, ocr_error):
cie_desc = ", ".join([d for d in cie_descs if d]) if any(cie_descs) else None
ips_nombre, ips_nit = extract_ips(lines, norm_lines)
fecha_ingreso = extract_fecha_ingreso_urgencias(lines, norm_lines)
fecha_egreso = extract_fecha_egreso(lines, norm_lines)
formato = detect_format(norm_text, norm_lines)
warnings = []
@ -627,6 +709,7 @@ def build_response(text, ocr_used, ocr_available, ocr_error):
"ips_nombre": ips_nombre,
"ips_nit": ips_nit,
"fecha_ingreso_urgencias": fecha_ingreso,
"fecha_egreso": fecha_egreso,
"warnings": warnings,
}

View File

@ -1,6 +1,48 @@
const ExcelJS = require('exceljs');
const path = require('path');
const BOGOTA_TIMEZONE = 'America/Bogota';
const getBogotaDateParts = (value) => {
if (!value) return null;
const date = new Date(value);
if (Number.isNaN(date.getTime())) return null;
const formatter = new Intl.DateTimeFormat('en-CA', {
timeZone: BOGOTA_TIMEZONE,
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
const parts = formatter.formatToParts(date);
const data = {};
for (const part of parts) {
if (part.type !== 'literal') {
data[part.type] = part.value;
}
}
return data.year && data.month && data.day
? { year: data.year, month: data.month, day: data.day }
: null;
};
const buildBogotaDate = (value) => {
const parts = getBogotaDateParts(value);
if (!parts) return null;
return new Date(Number(parts.year), Number(parts.month) - 1, Number(parts.day));
};
const formatBogotaTime = (value) => {
if (!value) return '';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '';
return new Intl.DateTimeFormat('es-CO', {
timeZone: BOGOTA_TIMEZONE,
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23',
}).format(date);
};
async function crearLibroAutorizacion(a) {
const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet('GAR-F-01');
@ -2693,9 +2735,8 @@ async function crearLibroAutorizacion(a) {
sheet.getCell(celda).value = a.numero_autorizacion || '';
});
// 2) Hora -> si no tienes hora en BD, usamos la hora actual como texto
const ahora = new Date();
const horaStr = ahora.toTimeString().slice(0, 5); // "HH:MM"
// 2) Hora -> usamos la hora registrada en Bogota (o la actual si no hay fecha)
const horaStr = formatBogotaTime(a.fecha_autorizacion) || formatBogotaTime(new Date());
['K8', 'L8', 'M8'].forEach(celda => {
sheet.getCell(celda).value = horaStr;
});
@ -2707,11 +2748,11 @@ async function crearLibroAutorizacion(a) {
sheet.getCell(celda).value = versionValor;
});
if (a.fecha_autorizacion) {
const fecha = new Date(a.fecha_autorizacion);
const fechaBogota = buildBogotaDate(a.fecha_autorizacion);
if (fechaBogota) {
['P8', 'Q8', 'R8'].forEach(celda => {
const cell = sheet.getCell(celda);
cell.value = fecha;
cell.value = fechaBogota;
cell.numFmt = 'dd/mm/yyyy';
});
}

View File

@ -1,6 +1,48 @@
const ExcelJS = require('exceljs');
const path = require('path');
const BOGOTA_TIMEZONE = 'America/Bogota';
const getBogotaDateParts = (value) => {
if (!value) return null;
const date = new Date(value);
if (Number.isNaN(date.getTime())) return null;
const formatter = new Intl.DateTimeFormat('en-CA', {
timeZone: BOGOTA_TIMEZONE,
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
const parts = formatter.formatToParts(date);
const data = {};
for (const part of parts) {
if (part.type !== 'literal') {
data[part.type] = part.value;
}
}
return data.year && data.month && data.day
? { year: data.year, month: data.month, day: data.day }
: null;
};
const buildBogotaDate = (value) => {
const parts = getBogotaDateParts(value);
if (!parts) return null;
return new Date(Number(parts.year), Number(parts.month) - 1, Number(parts.day));
};
const formatBogotaTime = (value) => {
if (!value) return '';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '';
return new Intl.DateTimeFormat('es-CO', {
timeZone: BOGOTA_TIMEZONE,
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23',
}).format(date);
};
async function crearLibroAutorizacionBrigadasAmbulanciasHospitalarios(a) {
const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet("Hoja1");
@ -2368,8 +2410,7 @@ async function crearLibroAutorizacionBrigadasAmbulanciasHospitalarios(a) {
sheet.getCell(celda).value = a.numero_autorizacion || '';
});
const ahora = new Date();
const horaStr = ahora.toTimeString().slice(0, 5);
const horaStr = formatBogotaTime(a.fecha_autorizacion) || formatBogotaTime(new Date());
['K8', 'L8', 'M8'].forEach((celda) => {
sheet.getCell(celda).value = horaStr;
});
@ -2380,11 +2421,11 @@ async function crearLibroAutorizacionBrigadasAmbulanciasHospitalarios(a) {
sheet.getCell(celda).value = versionValor;
});
if (a.fecha_autorizacion) {
const fecha = new Date(a.fecha_autorizacion);
const fechaBogota = buildBogotaDate(a.fecha_autorizacion);
if (fechaBogota) {
['P8', 'Q8', 'R8'].forEach((celda) => {
const cell = sheet.getCell(celda);
cell.value = fecha;
cell.value = fechaBogota;
cell.numFmt = 'dd/mm/yyyy';
});
}

View File

@ -53,6 +53,13 @@ CREATE TABLE IF NOT EXISTS ips (
tiene_convenio boolean DEFAULT true
);
-- ===== CIE-10 =====
CREATE TABLE IF NOT EXISTS cie10 (
codigo VARCHAR(20) PRIMARY KEY,
descripcion TEXT NOT NULL,
descripcion_busqueda TEXT
);
-- ===== Ingreso =====
CREATE TABLE IF NOT EXISTS ingreso (
interno text PRIMARY KEY REFERENCES paciente(interno),
@ -181,7 +188,7 @@ CREATE TABLE IF NOT EXISTS autorizacion (
id_ips integer NOT NULL REFERENCES ips(id_ips),
numero_documento_autorizante bigint NOT NULL REFERENCES autorizante(numero_documento),
id_usuario_solicitante integer REFERENCES usuario(id_usuario),
fecha_autorizacion date NOT NULL DEFAULT current_date,
fecha_autorizacion TIMESTAMPTZ NOT NULL DEFAULT now(),
observacion text,
cup_codigo varchar(20),
cie10_codigo text,
@ -252,6 +259,11 @@ ALTER TABLE autorizacion
ALTER COLUMN correo_inpec_enviado SET DEFAULT false,
ALTER COLUMN correo_inpec_respuesta SET DEFAULT false;
ALTER TABLE autorizacion
ALTER COLUMN fecha_autorizacion TYPE TIMESTAMPTZ
USING fecha_autorizacion::timestamp AT TIME ZONE 'America/Bogota',
ALTER COLUMN fecha_autorizacion SET DEFAULT now();
ALTER TABLE autorizacion
ALTER COLUMN tipo_autorizacion SET NOT NULL,
ALTER COLUMN estado_entrega SET NOT NULL,
@ -328,7 +340,7 @@ CREATE TABLE IF NOT EXISTS autorizacion_version (
id_ips INTEGER NOT NULL,
numero_documento_autorizante BIGINT NOT NULL,
id_usuario_solicitante INTEGER,
fecha_autorizacion DATE,
fecha_autorizacion TIMESTAMPTZ,
observacion TEXT,
cup_codigo VARCHAR(20),
cie10_codigo TEXT,
@ -341,6 +353,10 @@ CREATE TABLE IF NOT EXISTS autorizacion_version (
fecha_version TIMESTAMP NOT NULL DEFAULT NOW()
);
ALTER TABLE autorizacion_version
ALTER COLUMN fecha_autorizacion TYPE TIMESTAMPTZ
USING fecha_autorizacion::timestamp AT TIME ZONE 'America/Bogota';
-- ===== Profesionales REPS =====
CREATE TABLE IF NOT EXISTS profesional_reps (
id SERIAL PRIMARY KEY,

View File

@ -111,6 +111,129 @@ const normalizeSearch = (value) =>
.toUpperCase()
.trim();
const BOGOTA_TIMEZONE = 'America/Bogota';
const getBogotaNowParts = () => {
const formatter = new Intl.DateTimeFormat('en-CA', {
timeZone: BOGOTA_TIMEZONE,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hourCycle: 'h23',
});
const parts = formatter.formatToParts(new Date());
const data = {};
for (const part of parts) {
if (part.type !== 'literal') {
data[part.type] = part.value;
}
}
return {
date: `${data.year}-${data.month}-${data.day}`,
time: `${data.hour}:${data.minute}:${data.second}`,
};
};
const normalizeDateInput = (value) => {
const raw = String(value || '').trim();
const match = raw.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) return '';
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
const check = new Date(Date.UTC(year, month - 1, day));
if (
check.getUTCFullYear() !== year ||
check.getUTCMonth() + 1 !== month ||
check.getUTCDate() !== day
) {
return '';
}
return `${match[1]}-${match[2]}-${match[3]}`;
};
const normalizeTimeInput = (value) => {
const raw = String(value || '').trim();
if (!raw) return '';
const withoutZone = raw.split(/[Z+-]/)[0];
const cleaned = withoutZone.split('.')[0];
const match = cleaned.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
if (!match) return '';
const hour = Number(match[1]);
const minute = Number(match[2]);
const second = Number(match[3] || '0');
if (
!Number.isFinite(hour) ||
!Number.isFinite(minute) ||
!Number.isFinite(second) ||
hour < 0 ||
hour > 23 ||
minute < 0 ||
minute > 59 ||
second < 0 ||
second > 59
) {
return '';
}
return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:${String(second).padStart(2, '0')}`;
};
const splitDateTimeInput = (raw) => {
const value = String(raw || '').trim();
if (!value) return { datePart: '', timePart: '' };
const [datePart, timePart = ''] = value.split(/[T ]/);
return { datePart, timePart };
};
const buildFechaAutorizacionInput = (fechaValue, horaValue, fallbackNow) => {
const { datePart, timePart } = splitDateTimeInput(fechaValue);
const date = normalizeDateInput(datePart);
const timeFromFecha = normalizeTimeInput(timePart);
const timeFromHora = normalizeTimeInput(horaValue);
if (!date && !fallbackNow) {
return null;
}
const nowParts = getBogotaNowParts();
const finalDate = date || nowParts.date;
const finalTime = timeFromFecha || timeFromHora || nowParts.time;
return `${finalDate} ${finalTime}`;
};
const buildFechaHoraRange = (fechaInicio, fechaFin, horaInicio, horaFin) => {
const inicioDate = normalizeDateInput(fechaInicio);
const finDate = normalizeDateInput(fechaFin);
if (!inicioDate || !finDate) {
return null;
}
const inicioTime = normalizeTimeInput(horaInicio) || '00:00:00';
const finTime = normalizeTimeInput(horaFin) || '23:59:59';
return {
inicio: `${inicioDate} ${inicioTime}`,
fin: `${finDate} ${finTime}`,
};
};
const validateFechaHoraInput = (fechaValue, horaValue) => {
const rawFecha = String(fechaValue || '').trim();
const rawHora = String(horaValue || '').trim();
const { datePart, timePart } = splitDateTimeInput(rawFecha);
if (rawFecha && !normalizeDateInput(datePart)) {
return { error: 'fecha_autorizacion invalida' };
}
if (timePart && !normalizeTimeInput(timePart)) {
return { error: 'hora en fecha_autorizacion invalida' };
}
if (rawHora && !normalizeTimeInput(rawHora)) {
return { error: 'hora_autorizacion invalida' };
}
return { error: null };
};
const isPdfFile = (file) => {
if (!file) return false;
const mime = String(file.mimetype || '').toLowerCase();
@ -350,18 +473,33 @@ const ensureCupsTables = async () => {
`);
};
const ensureCie10Table = async () => {
await pool.query(`
CREATE TABLE IF NOT EXISTS cie10 (
codigo VARCHAR(20) PRIMARY KEY,
descripcion TEXT NOT NULL,
descripcion_busqueda TEXT
);
`);
await pool.query(`
ALTER TABLE cie10
ADD COLUMN IF NOT EXISTS descripcion_busqueda TEXT;
`);
};
const ensureAutorizacionVersionTables = async () => {
await pool.query(`
CREATE TABLE IF NOT EXISTS autorizacion_version (
id SERIAL PRIMARY KEY,
numero_autorizacion VARCHAR(50) NOT NULL,
version INTEGER NOT NULL,
id_ips INTEGER NOT NULL,
numero_documento_autorizante BIGINT NOT NULL,
id_usuario_solicitante INTEGER,
fecha_autorizacion DATE,
observacion TEXT,
cup_codigo VARCHAR(20),
await pool.query(`
CREATE TABLE IF NOT EXISTS autorizacion_version (
id SERIAL PRIMARY KEY,
numero_autorizacion VARCHAR(50) NOT NULL,
version INTEGER NOT NULL,
id_ips INTEGER NOT NULL,
numero_documento_autorizante BIGINT NOT NULL,
id_usuario_solicitante INTEGER,
fecha_autorizacion TIMESTAMPTZ,
observacion TEXT,
cup_codigo VARCHAR(20),
cie10_codigo VARCHAR(20),
cie10_descripcion TEXT,
tipo_autorizacion VARCHAR(50),
@ -373,6 +511,24 @@ const ensureAutorizacionVersionTables = async () => {
);
`);
const versionFechaRes = await pool.query(
`
SELECT data_type
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'autorizacion_version'
AND column_name = 'fecha_autorizacion'
`
);
const versionFechaTipo = versionFechaRes.rows[0]?.data_type;
if (versionFechaTipo && versionFechaTipo !== 'timestamp with time zone') {
await pool.query(`
ALTER TABLE autorizacion_version
ALTER COLUMN fecha_autorizacion TYPE TIMESTAMPTZ
USING fecha_autorizacion::timestamp AT TIME ZONE '${BOGOTA_TIMEZONE}';
`);
}
await pool.query(`
CREATE UNIQUE INDEX IF NOT EXISTS autorizacion_version_unique
ON autorizacion_version (numero_autorizacion, version);
@ -540,6 +696,31 @@ const ensureAutorizacionExtras = async () => {
`);
};
const ensureAutorizacionFechaHora = async () => {
const fechaRes = await pool.query(
`
SELECT data_type
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'autorizacion'
AND column_name = 'fecha_autorizacion'
`
);
const fechaTipo = fechaRes.rows[0]?.data_type;
if (fechaTipo && fechaTipo !== 'timestamp with time zone') {
await pool.query(`
ALTER TABLE autorizacion
ALTER COLUMN fecha_autorizacion TYPE TIMESTAMPTZ
USING fecha_autorizacion::timestamp AT TIME ZONE '${BOGOTA_TIMEZONE}';
`);
}
await pool.query(`
ALTER TABLE autorizacion
ALTER COLUMN fecha_autorizacion SET DEFAULT now();
`);
};
const ensureIpsConvenio = async () => {
await pool.query(`
ALTER TABLE ips
@ -994,6 +1175,52 @@ app.post(
}
);
app.post(
'/api/cargar-cie10',
verificarToken,
esAdministrador,
upload.single('archivo'),
async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No se recibio archivo Excel' });
}
const jobId = createJobId();
const job = {
id: jobId,
type: 'cie10-carga',
status: 'queued',
createdAt: new Date().toISOString(),
ownerId: req.usuario.id_usuario,
};
jobs.set(jobId, job);
const jobTmpDir = path.join(os.tmpdir(), 'saludut_jobs', jobId);
await fsPromises.mkdir(jobTmpDir, { recursive: true });
const inputPath = path.join(jobTmpDir, `cie10_${jobId}.xlsx`);
await fsPromises.writeFile(inputPath, req.file.buffer);
enqueueJob(job, async () => {
try {
return await procesarExcelCie10(inputPath);
} finally {
await safeUnlink(inputPath);
await safeRemoveDir(jobTmpDir);
}
});
return res.status(202).json(sanitizeJob(job));
} catch (error) {
console.error('Error creando job de CIE-10:', error);
return res.status(500).json({
error: 'Error creando el job para procesar el Excel de CIE-10',
});
}
}
);
app.post(
'/api/cargar-ips',
verificarToken,
@ -1507,6 +1734,24 @@ const normalizeDigits = (value) => String(value || '').replace(/\D/g, '');
const normalizeNameKey = (value) =>
normalizeSearch(value).replace(/[^A-Z0-9]/g, '');
const normalizeLocationKey = (value) =>
normalizeSearch(value).replace(/[^A-Z0-9]/g, '');
const pickIpsRowByLocation = (rows, departamentoKey, municipioKey) => {
if (!rows || rows.length === 0) return null;
if (!departamentoKey && !municipioKey) {
return rows[0];
}
const match = rows.find((row) => {
const rowDep = normalizeLocationKey(row.departamento || '');
const rowMun = normalizeLocationKey(row.municipio || '');
if (departamentoKey && rowDep && departamentoKey !== rowDep) return false;
if (municipioKey && rowMun && municipioKey !== rowMun) return false;
return true;
});
return match || null;
};
const IPS_EMPTY_MARKERS = new Set([
'NA',
'SD',
@ -1529,6 +1774,108 @@ const cleanIpsField = (value) => {
return raw;
};
async function procesarExcelCie10(inputFilePath) {
await ensureCie10Table();
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.readFile(inputFilePath);
const sheet = workbook.worksheets[0];
if (!sheet) {
throw new Error('No se encontro una hoja en el Excel de CIE-10');
}
const headerRow = sheet.getRow(1);
const headers = {};
headerRow.eachCell((cell, colNumber) => {
const base = normalizeHeader(getExcelCellText(cell.value));
if (base) headers[base] = colNumber;
});
const getValue = (row, key) => {
const col = headers[key];
if (!col) return '';
return getExcelCellText(row.getCell(col).value);
};
const getValueMulti = (row, keys) => {
for (const key of keys) {
const value = getValue(row, key);
if (value) return value;
}
return '';
};
const resumen = {
total: 0,
omitidas: 0,
};
const client = await pool.connect();
try {
await client.query('BEGIN');
for (let i = 2; i <= sheet.rowCount; i++) {
const row = sheet.getRow(i);
let codigo = getValueMulti(row, [
'CODIGOCIE10',
'CODIGOCIE',
'CODIGODELACIE10CUATROCARACTERES',
'CODIGO',
]);
let descripcion = getValueMulti(row, [
'DESCRIPCIONCIE10',
'DESCRIPCION',
'DESCRIPCIONDECODIGOSACUATROCARACTERES',
]);
if (!codigo && !descripcion) {
continue;
}
codigo = String(codigo || '').trim().toUpperCase();
descripcion = String(descripcion || '').trim();
if (!codigo || !descripcion) {
resumen.omitidas += 1;
continue;
}
const descripcionBusqueda = normalizeSearch(descripcion);
await client.query(
`
INSERT INTO cie10 (codigo, descripcion, descripcion_busqueda)
VALUES ($1, $2, $3)
ON CONFLICT (codigo) DO UPDATE
SET descripcion = EXCLUDED.descripcion,
descripcion_busqueda = EXCLUDED.descripcion_busqueda
`,
[codigo, descripcion, descripcionBusqueda]
);
resumen.total += 1;
}
const totalRes = await client.query('SELECT COUNT(*)::int AS total FROM cie10');
await client.query('COMMIT');
return {
ok: true,
mensaje: 'CIE-10 cargado correctamente',
procesadas: resumen.total,
omitidas: resumen.omitidas,
total: totalRes.rows[0]?.total ?? null,
};
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
const extractCupCodigo = (value) => {
const text = String(value || '').trim();
if (!text) return '';
@ -1730,54 +2077,65 @@ async function procesarExcelIps(inputFilePath) {
const nombreKey = normalizeNameKey(nombre);
if (nombreKey) nombreKeys.add(nombreKey);
const departamentoKey = normalizeLocationKey(departamento);
const municipioKey = normalizeLocationKey(municipio);
let existente = null;
if (lookupKeys.length > 0) {
const res = await client.query(
`
SELECT id_ips
SELECT id_ips, departamento, municipio
FROM ips
WHERE regexp_replace(nit, '\\D', '', 'g') = ANY($1::text[])
OR regexp_replace(codigo_ips, '\\D', '', 'g') = ANY($1::text[])
LIMIT 1
`,
[lookupKeys]
);
existente = res.rows[0] || null;
existente = pickIpsRowByLocation(
res.rows,
departamentoKey,
municipioKey
);
}
if (!existente && nitDigits && nitDigits.length >= 8) {
const res = await client.query(
`
SELECT id_ips
SELECT id_ips, departamento, municipio
FROM ips
WHERE regexp_replace(nit, '\\D', '', 'g') LIKE $1
AND length(regexp_replace(nit, '\\D', '', 'g')) = $2
LIMIT 1
`,
[`${nitDigits}%`, nitDigits.length + 1]
);
existente = res.rows[0] || null;
existente = pickIpsRowByLocation(
res.rows,
departamentoKey,
municipioKey
);
}
if (!existente && codigoDigits && codigoDigits.length >= 8) {
const res = await client.query(
`
SELECT id_ips
SELECT id_ips, departamento, municipio
FROM ips
WHERE regexp_replace(codigo_ips, '\\D', '', 'g') LIKE $1
AND length(regexp_replace(codigo_ips, '\\D', '', 'g')) = $2
LIMIT 1
`,
[`${codigoDigits}%`, codigoDigits.length + 1]
);
existente = res.rows[0] || null;
existente = pickIpsRowByLocation(
res.rows,
departamentoKey,
municipioKey
);
}
if (!existente && nombreKey) {
const res = await client.query(
`
SELECT id_ips
SELECT id_ips, departamento, municipio
FROM ips
WHERE regexp_replace(
translate(UPPER(nombre_ips), 'ÁÉÍÓÚÜÑ', 'AEIOUUN'),
@ -1785,11 +2143,14 @@ async function procesarExcelIps(inputFilePath) {
'',
'g'
) = $1
LIMIT 1
`,
[nombreKey]
);
existente = res.rows[0] || null;
existente = pickIpsRowByLocation(
res.rows,
departamentoKey,
municipioKey
);
}
if (existente) {
@ -2601,7 +2962,7 @@ async function procesarExcelAutorizacionesMasivas(inputFilePath, usuario) {
`
INSERT INTO autorizacion
(interno, id_ips, numero_documento_autorizante, id_usuario_solicitante, fecha_autorizacion, observacion, cup_codigo, cie10_codigo, cie10_descripcion, tipo_autorizacion, tipo_servicio, ambito_atencion, numero_orden, estado_entrega, estado_autorizacion)
VALUES ($1, $2, $3, $4, COALESCE($5::date, current_date), $6, $7, $8, $9, $10, $11, $12, $13, $14, 'pendiente')
VALUES ($1, $2, $3, $4, COALESCE($5::timestamp AT TIME ZONE '${BOGOTA_TIMEZONE}', now()), $6, $7, $8, $9, $10, $11, $12, $13, $14, 'pendiente')
RETURNING numero_autorizacion, fecha_autorizacion, version;
`,
[
@ -2879,7 +3240,9 @@ async function generarZipAutorizacionesPorFecha(
throw err;
}
const whereParts = ['a.fecha_autorizacion BETWEEN $1 AND $2'];
const whereParts = [
`a.fecha_autorizacion BETWEEN ($1::timestamp AT TIME ZONE '${BOGOTA_TIMEZONE}') AND ($2::timestamp AT TIME ZONE '${BOGOTA_TIMEZONE}')`,
];
const params = [fechaInicio, fechaFin];
const joinParts = [];
if (soloAutorizadas) {
@ -2930,9 +3293,15 @@ async function generarZipAutorizacionesPorFecha(
const jobTmpDir = path.join(os.tmpdir(), 'saludut_jobs', jobId);
await fsPromises.mkdir(jobTmpDir, { recursive: true });
const safeInicio = String(fechaInicio)
.replace(/[:]/g, '')
.replace(/\s+/g, '_');
const safeFin = String(fechaFin)
.replace(/[:]/g, '')
.replace(/\s+/g, '_');
const zipPath = path.join(
jobTmpDir,
`autorizaciones_${fechaInicio}_${fechaFin}.zip`
`autorizaciones_${safeInicio}_${safeFin}.zip`
);
const output = fs.createWriteStream(zipPath);
@ -3126,18 +3495,35 @@ const crearJobPdfHandler = async (req, res) => {
return res.status(202).json(sanitizeJob(job));
};
const crearJobZipHandler = async (req, res) => {
const fecha_inicio = req.body?.fecha_inicio || req.query?.fecha_inicio;
const fecha_fin = req.body?.fecha_fin || req.query?.fecha_fin;
const establecimiento = req.body?.establecimiento || req.query?.establecimiento;
const ambito = req.body?.ambito || req.query?.ambito;
const ips = req.body?.ips || req.query?.ips;
const esAdmin = req.usuario?.nombre_rol === 'administrador';
const soloAutorizadas = !esAdmin;
const crearJobZipHandler = async (req, res) => {
const fecha_inicio = req.body?.fecha_inicio || req.query?.fecha_inicio;
const fecha_fin = req.body?.fecha_fin || req.query?.fecha_fin;
const hora_inicio = req.body?.hora_inicio || req.query?.hora_inicio;
const hora_fin = req.body?.hora_fin || req.query?.hora_fin;
const establecimiento = req.body?.establecimiento || req.query?.establecimiento;
const ambito = req.body?.ambito || req.query?.ambito;
const ips = req.body?.ips || req.query?.ips;
const esAdmin = req.usuario?.nombre_rol === 'administrador';
const soloAutorizadas = !esAdmin;
if (!fecha_inicio || !fecha_fin) {
return res.status(400).json({ error: 'fecha_inicio y fecha_fin son requeridos' });
}
if (!fecha_inicio || !fecha_fin) {
return res.status(400).json({ error: 'fecha_inicio y fecha_fin son requeridos' });
}
if (hora_inicio && !normalizeTimeInput(hora_inicio)) {
return res.status(400).json({ error: 'hora_inicio invalida' });
}
if (hora_fin && !normalizeTimeInput(hora_fin)) {
return res.status(400).json({ error: 'hora_fin invalida' });
}
const rango = buildFechaHoraRange(fecha_inicio, fecha_fin, hora_inicio, hora_fin);
if (!rango) {
return res.status(400).json({ error: 'fecha_inicio o fecha_fin invalida' });
}
if (rango.inicio > rango.fin) {
return res.status(400).json({ error: 'fecha_inicio no puede ser mayor que fecha_fin' });
}
const jobId = createJobId();
const job = {
@ -3150,18 +3536,20 @@ const crearJobPdfHandler = async (req, res) => {
jobs.set(jobId, job);
enqueueJob(job, async () => {
const zipPath = await generarZipAutorizacionesPorFecha(fecha_inicio, fecha_fin, jobId, {
establecimiento,
ambito,
ips,
soloAutorizadas,
});
return {
ok: true,
mensaje: 'ZIP generado correctamente',
enqueueJob(job, async () => {
const zipPath = await generarZipAutorizacionesPorFecha(rango.inicio, rango.fin, jobId, {
establecimiento,
ambito,
ips,
soloAutorizadas,
});
const safeInicio = String(rango.inicio).replace(/[:]/g, '').replace(/\s+/g, '_');
const safeFin = String(rango.fin).replace(/[:]/g, '').replace(/\s+/g, '_');
return {
ok: true,
mensaje: 'ZIP generado correctamente',
filePath: zipPath,
fileName: `autorizaciones_${fecha_inicio}_${fecha_fin}.zip`,
fileName: `autorizaciones_${safeInicio}_${safeFin}.zip`,
contentType: 'application/zip',
downloadUrl: `/api/jobs/${jobId}/download`,
};
@ -3242,6 +3630,48 @@ app.get('/api/cups', verificarToken, async (req, res) => {
}
});
// CIE-10
app.get('/api/cie10', verificarToken, async (req, res) => {
const codigo = String(req.query.codigo || '').trim().toUpperCase();
const qRaw = req.query.q || '';
const q = String(qRaw).trim();
const qNormalized = normalizeSearch(q);
const limit = Math.min(Number(req.query.limit) || 20, 200);
try {
if (codigo) {
const { rows } = await pool.query(
'SELECT codigo, descripcion FROM cie10 WHERE codigo = $1 LIMIT 1',
[codigo]
);
if (rows.length === 0) {
return res.status(404).json({ error: 'CIE-10 no encontrado' });
}
return res.json(rows[0]);
}
if (!q) {
return res.status(400).json({ error: 'q o codigo son requeridos' });
}
const params = [`%${q}%`, `%${qNormalized}%`, limit];
const sql = `
SELECT codigo, descripcion
FROM cie10
WHERE codigo ILIKE $1
OR descripcion ILIKE $1
OR descripcion_busqueda LIKE $2
ORDER BY codigo
LIMIT $3;
`;
const { rows } = await pool.query(sql, params);
return res.json(rows);
} catch (error) {
console.error('Error en /api/cie10:', error.message);
return res.status(500).json({ error: 'Error consultando CIE-10' });
}
});
app.get('/api/pacientes', verificarToken, async (req, res) => {
const { numero_documento, interno, nombre } = req.query;
@ -3632,6 +4062,7 @@ app.post('/api/autorizaciones', verificarToken, puedeGenerarAutorizaciones, asyn
numero_documento_autorizante,
observacion,
fecha_autorizacion,
hora_autorizacion,
cup_codigo,
cie10_codigo,
cie10_descripcion,
@ -3653,6 +4084,19 @@ app.post('/api/autorizaciones', verificarToken, puedeGenerarAutorizaciones, asyn
return res.status(400).json({ error: 'cup_codigo es obligatorio' });
}
const fechaHoraValidacion = validateFechaHoraInput(
fecha_autorizacion,
hora_autorizacion
);
if (fechaHoraValidacion.error) {
return res.status(400).json({ error: fechaHoraValidacion.error });
}
const fechaAutorizacionInput = buildFechaAutorizacionInput(
fecha_autorizacion,
hora_autorizacion,
true
);
const cie10Codigo = String(cie10_codigo || '').trim().toUpperCase();
const cie10Descripcion = String(cie10_descripcion || '').trim();
if (!cie10Codigo) {
@ -3745,7 +4189,7 @@ app.post('/api/autorizaciones', verificarToken, puedeGenerarAutorizaciones, asyn
numero_orden,
estado_entrega
)
VALUES ($1, $2, $3, $4, COALESCE($5::date, current_date), $6, $7, $8, $9, $10, $11, $12, $13, $14)
VALUES ($1, $2, $3, $4, COALESCE($5::timestamp AT TIME ZONE '${BOGOTA_TIMEZONE}', now()), $6, $7, $8, $9, $10, $11, $12, $13, $14)
RETURNING numero_autorizacion, fecha_autorizacion, cup_codigo, cie10_codigo, cie10_descripcion, tipo_autorizacion, tipo_servicio, ambito_atencion, numero_orden, estado_entrega, version, estado_autorizacion;
`;
@ -3754,7 +4198,7 @@ app.post('/api/autorizaciones', verificarToken, puedeGenerarAutorizaciones, asyn
id_ips,
numero_documento_autorizante,
solicitanteId,
fecha_autorizacion || null,
fechaAutorizacionInput,
observacion || null,
cupCodigo,
cie10Codigo,
@ -3884,6 +4328,7 @@ app.put('/api/autorizaciones/:numero_autorizacion', verificarToken, puedeGenerar
numero_documento_autorizante,
observacion,
fecha_autorizacion,
hora_autorizacion,
cup_codigo,
cie10_codigo,
cie10_descripcion,
@ -3905,6 +4350,19 @@ app.put('/api/autorizaciones/:numero_autorizacion', verificarToken, puedeGenerar
return res.status(400).json({ error: 'cup_codigo es obligatorio' });
}
const fechaHoraValidacion = validateFechaHoraInput(
fecha_autorizacion,
hora_autorizacion
);
if (fechaHoraValidacion.error) {
return res.status(400).json({ error: fechaHoraValidacion.error });
}
const fechaAutorizacionInput = buildFechaAutorizacionInput(
fecha_autorizacion,
hora_autorizacion,
false
);
const cie10CodigoInput = String(cie10_codigo || '').trim().toUpperCase();
const cie10DescripcionInput = String(cie10_descripcion || '').trim();
const ambitoInput = String(ambito_atencion || '').trim().toLowerCase();
@ -4034,14 +4492,14 @@ app.put('/api/autorizaciones/:numero_autorizacion', verificarToken, puedeGenerar
}
const nuevaVersion = versionActual + 1;
const fechaAut = fecha_autorizacion || actual.fecha_autorizacion || null;
const fechaAut = fechaAutorizacionInput || null;
const updateRes = await client.query(
`
UPDATE autorizacion
SET id_ips = $1,
numero_documento_autorizante = $2,
fecha_autorizacion = COALESCE($3::date, fecha_autorizacion),
fecha_autorizacion = COALESCE($3::timestamp AT TIME ZONE '${BOGOTA_TIMEZONE}', fecha_autorizacion),
observacion = $4,
cup_codigo = $5,
cie10_codigo = $6,
@ -4721,6 +5179,7 @@ app.patch('/api/usuarios/:id/estado', verificarToken, esAdministrador, async (re
app.patch('/api/usuarios/:id', verificarToken, esAdministrador, async (req, res) => {
const { id } = req.params;
const { username, password, email, nombre_completo, id_rol } = req.body || {};
let idRolActualizado = null;
const idUsuario = Number(id);
if (!Number.isFinite(idUsuario)) {
@ -4888,6 +5347,8 @@ app.get('/api/roles', verificarToken, esAdministrador, async (req, res) => {
*/
app.get('/api/autorizaciones-por-fecha', verificarToken, async (req, res) => {
const { fecha_inicio, fecha_fin } = req.query;
const hora_inicio = req.query.hora_inicio;
const hora_fin = req.query.hora_fin;
const establecimiento = String(req.query.establecimiento || '').trim();
const ambito = String(req.query.ambito || '').trim().toLowerCase();
const ips = String(req.query.ips || '').trim();
@ -4905,8 +5366,25 @@ app.get('/api/autorizaciones-por-fecha', verificarToken, async (req, res) => {
return res.status(400).json({ error: 'ambito invalido' });
}
const whereParts = ['a.fecha_autorizacion BETWEEN $1 AND $2'];
const params = [fecha_inicio, fecha_fin];
if (hora_inicio && !normalizeTimeInput(hora_inicio)) {
return res.status(400).json({ error: 'hora_inicio invalida' });
}
if (hora_fin && !normalizeTimeInput(hora_fin)) {
return res.status(400).json({ error: 'hora_fin invalida' });
}
const rango = buildFechaHoraRange(fecha_inicio, fecha_fin, hora_inicio, hora_fin);
if (!rango) {
return res.status(400).json({ error: 'fecha_inicio o fecha_fin invalida' });
}
if (rango.inicio > rango.fin) {
return res.status(400).json({ error: 'fecha_inicio no puede ser mayor que fecha_fin' });
}
const whereParts = [
`a.fecha_autorizacion BETWEEN ($1::timestamp AT TIME ZONE '${BOGOTA_TIMEZONE}') AND ($2::timestamp AT TIME ZONE '${BOGOTA_TIMEZONE}')`,
];
const params = [rango.inicio, rango.fin];
if (!esAdmin) {
whereParts.push("COALESCE(a.estado_autorizacion, 'pendiente') IN ('pendiente', 'autorizado')");
@ -5017,7 +5495,8 @@ app.get('/api/autorizaciones-estadisticas', verificarToken, esAdministrador, asy
COUNT(*) FILTER (WHERE COALESCE(estado_autorizacion, 'pendiente') = 'pendiente') AS pendientes,
COUNT(*) AS total
FROM autorizacion
WHERE fecha_autorizacion BETWEEN $1::date AND $2::date;
WHERE (fecha_autorizacion AT TIME ZONE '${BOGOTA_TIMEZONE}')::date
BETWEEN $1::date AND $2::date;
`;
const diasSql = `
@ -5032,7 +5511,7 @@ app.get('/api/autorizaciones-estadisticas', verificarToken, esAdministrador, asy
COUNT(*) FILTER (WHERE COALESCE(a.estado_autorizacion, 'pendiente') = 'pendiente') AS pendientes
FROM dias d
LEFT JOIN autorizacion a
ON a.fecha_autorizacion::date = d.fecha
ON (a.fecha_autorizacion AT TIME ZONE '${BOGOTA_TIMEZONE}')::date = d.fecha
GROUP BY d.fecha
ORDER BY d.fecha;
`;
@ -5064,6 +5543,8 @@ app.get('/api/autorizaciones-estadisticas', verificarToken, esAdministrador, asy
app.patch('/api/autorizaciones/estado-masivo', verificarToken, esAdministrador, async (req, res) => {
const fecha_inicio = req.body?.fecha_inicio || req.query?.fecha_inicio;
const fecha_fin = req.body?.fecha_fin || req.query?.fecha_fin;
const hora_inicio = req.body?.hora_inicio || req.query?.hora_inicio;
const hora_fin = req.body?.hora_fin || req.query?.hora_fin;
const estado = String(req.body?.estado_autorizacion || '')
.trim()
.toLowerCase();
@ -5078,12 +5559,28 @@ app.patch('/api/autorizaciones/estado-masivo', verificarToken, esAdministrador,
return res.status(400).json({ error: 'estado_autorizacion invalido' });
}
if (hora_inicio && !normalizeTimeInput(hora_inicio)) {
return res.status(400).json({ error: 'hora_inicio invalida' });
}
if (hora_fin && !normalizeTimeInput(hora_fin)) {
return res.status(400).json({ error: 'hora_fin invalida' });
}
const rango = buildFechaHoraRange(fecha_inicio, fecha_fin, hora_inicio, hora_fin);
if (!rango) {
return res.status(400).json({ error: 'fecha_inicio o fecha_fin invalida' });
}
if (rango.inicio > rango.fin) {
return res.status(400).json({ error: 'fecha_inicio no puede ser mayor que fecha_fin' });
}
try {
const params = [estado, fecha_inicio, fecha_fin];
const params = [estado, rango.inicio, rango.fin];
let sql = `
UPDATE autorizacion
SET estado_autorizacion = $1
WHERE fecha_autorizacion BETWEEN $2::date AND $3::date
WHERE fecha_autorizacion BETWEEN ($2::timestamp AT TIME ZONE '${BOGOTA_TIMEZONE}')
AND ($3::timestamp AT TIME ZONE '${BOGOTA_TIMEZONE}')
`;
if (soloPendientes) {
@ -5111,6 +5608,8 @@ app.patch('/api/autorizaciones/estado-masivo', verificarToken, esAdministrador,
app.patch('/api/autorizaciones/autorizante-masivo', verificarToken, esAdministrador, async (req, res) => {
const fecha_inicio = req.body?.fecha_inicio || req.query?.fecha_inicio;
const fecha_fin = req.body?.fecha_fin || req.query?.fecha_fin;
const hora_inicio = req.body?.hora_inicio || req.query?.hora_inicio;
const hora_fin = req.body?.hora_fin || req.query?.hora_fin;
const numeroDocumento =
req.body?.numero_documento_autorizante || req.query?.numero_documento_autorizante;
const establecimiento = String(
@ -5135,6 +5634,21 @@ app.patch('/api/autorizaciones/autorizante-masivo', verificarToken, esAdministra
return res.status(400).json({ error: 'ambito invalido' });
}
if (hora_inicio && !normalizeTimeInput(hora_inicio)) {
return res.status(400).json({ error: 'hora_inicio invalida' });
}
if (hora_fin && !normalizeTimeInput(hora_fin)) {
return res.status(400).json({ error: 'hora_fin invalida' });
}
const rango = buildFechaHoraRange(fecha_inicio, fecha_fin, hora_inicio, hora_fin);
if (!rango) {
return res.status(400).json({ error: 'fecha_inicio o fecha_fin invalida' });
}
if (rango.inicio > rango.fin) {
return res.status(400).json({ error: 'fecha_inicio no puede ser mayor que fecha_fin' });
}
try {
const autorizanteRes = await pool.query(
'SELECT 1 FROM autorizante WHERE numero_documento = $1 LIMIT 1',
@ -5144,8 +5658,10 @@ app.patch('/api/autorizaciones/autorizante-masivo', verificarToken, esAdministra
return res.status(404).json({ error: 'Autorizante no encontrado' });
}
const whereParts = ['a.fecha_autorizacion BETWEEN $2::date AND $3::date'];
const params = [numeroAutorizante, fecha_inicio, fecha_fin];
const whereParts = [
`a.fecha_autorizacion BETWEEN ($2::timestamp AT TIME ZONE '${BOGOTA_TIMEZONE}') AND ($3::timestamp AT TIME ZONE '${BOGOTA_TIMEZONE}')`,
];
const params = [numeroAutorizante, rango.inicio, rango.fin];
if (establecimiento) {
params.push(`%${establecimiento}%`);
@ -5207,6 +5723,10 @@ ensureCupsTables().catch((error) => {
console.error('Error inicializando tablas CUPS:', error.message);
});
ensureCie10Table().catch((error) => {
console.error('Error inicializando tabla CIE-10:', error.message);
});
ensureAutorizacionVersionTables().catch((error) => {
console.error('Error inicializando tablas de versiones:', error.message);
});
@ -5215,6 +5735,10 @@ ensureAutorizacionExtras().catch((error) => {
console.error('Error inicializando columnas extra de autorizaciones:', error.message);
});
ensureAutorizacionFechaHora().catch((error) => {
console.error('Error inicializando fecha/hora de autorizaciones:', error.message);
});
ensureIpsConvenio().catch((error) => {
console.error('Error inicializando convenio de IPS:', error.message);
});

View File

@ -60,6 +60,26 @@
</div>
</div>
<div class="form-group">
<label for="hora_inicio">Hora de inicio:</label>
<input
id="hora_inicio"
type="time"
formControlName="hora_inicio"
(change)="validarFechas()"
/>
</div>
<div class="form-group">
<label for="hora_fin">Hora de fin:</label>
<input
id="hora_fin"
type="time"
formControlName="hora_fin"
(change)="validarFechas()"
/>
</div>
<div class="form-actions">
<button
type="submit"

View File

@ -47,6 +47,8 @@ export class AutorizacionesPorFechaComponent implements OnInit {
// Fechas en formato yyyy-MM-dd para usar en la API
private fechaInicioApi: string | null = null;
private fechaFinApi: string | null = null;
private horaInicioApi: string | null = null;
private horaFinApi: string | null = null;
constructor(
private fb: FormBuilder,
@ -59,6 +61,8 @@ export class AutorizacionesPorFechaComponent implements OnInit {
this.filtroForm = this.fb.group({
fecha_inicio: ['', Validators.required],
fecha_fin: ['', Validators.required],
hora_inicio: [''],
hora_fin: [''],
establecimiento: [''],
ambito: [''],
ips: [''],
@ -81,7 +85,9 @@ export class AutorizacionesPorFechaComponent implements OnInit {
this.filtroForm.patchValue({
fecha_inicio: this.formatDateForInput(hace30Dias),
fecha_fin: this.formatDateForInput(hoy)
fecha_fin: this.formatDateForInput(hoy),
hora_inicio: '00:00',
hora_fin: '23:59'
});
}
@ -94,17 +100,39 @@ export class AutorizacionesPorFechaComponent implements OnInit {
return;
}
const inicio = new Date(this.fecha_inicio.value);
const fin = new Date(this.fecha_fin.value);
const fechaInicio = this.normalizeDateInput(this.fecha_inicio.value);
const fechaFin = this.normalizeDateInput(this.fecha_fin.value);
if (!fechaInicio || !fechaFin) {
this.errorMessage = 'Debes ingresar fechas validas.';
return;
}
if (inicio > fin) {
const horaInicioRaw = this.filtroForm.get('hora_inicio')?.value;
const horaFinRaw = this.filtroForm.get('hora_fin')?.value;
const horaInicio = this.normalizeTimeInput(horaInicioRaw) || '00:00';
const horaFin = this.normalizeTimeInput(horaFinRaw) || '23:59';
if (horaInicioRaw && !this.normalizeTimeInput(horaInicioRaw)) {
this.errorMessage = 'La hora de inicio es invalida.';
return;
}
if (horaFinRaw && !this.normalizeTimeInput(horaFinRaw)) {
this.errorMessage = 'La hora de fin es invalida.';
return;
}
const inicioStr = `${fechaInicio} ${horaInicio}`;
const finStr = `${fechaFin} ${horaFin}`;
if (inicioStr > finStr) {
this.errorMessage = 'La fecha de inicio no puede ser mayor que la fecha de fin.';
return;
}
// Guardamos las fechas ya formateadas para reutilizarlas
this.fechaInicioApi = this.formatDateForInput(inicio);
this.fechaFinApi = this.formatDateForInput(fin);
this.fechaInicioApi = fechaInicio;
this.fechaFinApi = fechaFin;
this.horaInicioApi = horaInicio;
this.horaFinApi = horaFin;
this.establecimientoFiltro = String(this.filtroForm.get('establecimiento')?.value || '').trim();
this.ambitoFiltro = String(this.filtroForm.get('ambito')?.value || '').trim().toLowerCase();
this.ipsFiltro = String(this.filtroForm.get('ips')?.value || '').trim();
@ -118,6 +146,8 @@ export class AutorizacionesPorFechaComponent implements OnInit {
.getAutorizacionesPorFecha(
this.fechaInicioApi!,
this.fechaFinApi!,
this.horaInicioApi || undefined,
this.horaFinApi || undefined,
this.limiteAutorizaciones,
0,
this.establecimientoFiltro || undefined,
@ -286,6 +316,8 @@ export class AutorizacionesPorFechaComponent implements OnInit {
.actualizarEstadoAutorizacionesMasivo(
this.fechaInicioApi,
this.fechaFinApi,
this.horaInicioApi || undefined,
this.horaFinApi || undefined,
this.estadoMasivo
)
.subscribe({
@ -348,6 +380,8 @@ export class AutorizacionesPorFechaComponent implements OnInit {
.actualizarAutorizanteMasivo(
this.fechaInicioApi,
this.fechaFinApi,
this.horaInicioApi || undefined,
this.horaFinApi || undefined,
autorizadoId,
this.establecimientoFiltro || undefined,
this.ambitoFiltro || undefined,
@ -466,6 +500,8 @@ export class AutorizacionesPorFechaComponent implements OnInit {
.crearJobZipAutorizaciones(
this.fechaInicioApi,
this.fechaFinApi,
this.horaInicioApi || undefined,
this.horaFinApi || undefined,
this.establecimientoFiltro || undefined,
this.ambitoFiltro || undefined,
this.ipsFiltro || undefined
@ -475,9 +511,15 @@ export class AutorizacionesPorFechaComponent implements OnInit {
this.jobsService.pollJob(job.id).subscribe({
next: (estado) => {
if (estado.status === 'completed') {
const inicioLabel = this.horaInicioApi
? `${this.fechaInicioApi}_${this.horaInicioApi.replace(':', '')}`
: this.fechaInicioApi;
const finLabel = this.horaFinApi
? `${this.fechaFinApi}_${this.horaFinApi.replace(':', '')}`
: this.fechaFinApi;
const fileName =
estado.result?.fileName ||
`autorizaciones_${this.fechaInicioApi}_${this.fechaFinApi}.zip`;
`autorizaciones_${inicioLabel}_${finLabel}.zip`;
this.jobsService.downloadJobFile(job.id).subscribe({
next: (blob) => {
@ -536,8 +578,59 @@ export class AutorizacionesPorFechaComponent implements OnInit {
return `${year}-${month}-${day}`;
}
private normalizeDateInput(value: string): string {
const raw = String(value || '').trim();
if (!/^\d{4}-\d{2}-\d{2}$/.test(raw)) return '';
return raw;
}
private normalizeTimeInput(value: string): string {
const raw = String(value || '').trim();
if (!raw) return '';
const match = raw.match(/^(\d{2}):(\d{2})$/);
if (!match) return '';
const hour = Number(match[1]);
const minute = Number(match[2]);
if (
!Number.isFinite(hour) ||
!Number.isFinite(minute) ||
hour < 0 ||
hour > 23 ||
minute < 0 ||
minute > 59
) {
return '';
}
return `${match[1]}:${match[2]}`;
}
private parseLocalDateTime(value: string): Date | null {
if (!value) return null;
const raw = String(value).trim();
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
const [year, month, day] = raw.split('-').map(Number);
return new Date(year, month - 1, day);
}
const parsed = new Date(raw);
if (!Number.isNaN(parsed.getTime())) {
return parsed;
}
const match = raw.match(
/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?/
);
if (!match) return null;
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
const hour = Number(match[4]);
const minute = Number(match[5]);
const second = Number(match[6] || '0');
return new Date(year, month - 1, day, hour, minute, second);
}
formatDate(dateString: string): string {
const date = new Date(dateString);
const date = this.parseLocalDateTime(dateString);
if (!date) return '';
return date.toLocaleDateString('es-ES', {
year: 'numeric',
month: 'short',
@ -546,7 +639,8 @@ export class AutorizacionesPorFechaComponent implements OnInit {
}
formatDateTime(dateString: string): string {
const date = new Date(dateString);
const date = this.parseLocalDateTime(dateString);
if (!date) return '';
return date.toLocaleDateString('es-ES', {
year: 'numeric',
month: 'short',
@ -709,14 +803,33 @@ export class AutorizacionesPorFechaComponent implements OnInit {
}
validarFechas(): void {
const fechaInicio = this.filtroForm.get('fecha_inicio')?.value;
const fechaFin = this.filtroForm.get('fecha_fin')?.value;
const fechaInicioRaw = this.filtroForm.get('fecha_inicio')?.value;
const fechaFinRaw = this.filtroForm.get('fecha_fin')?.value;
const fechaInicio = this.normalizeDateInput(fechaInicioRaw);
const fechaFin = this.normalizeDateInput(fechaFinRaw);
const horaInicioRaw = this.filtroForm.get('hora_inicio')?.value;
const horaFinRaw = this.filtroForm.get('hora_fin')?.value;
const horaInicio = this.normalizeTimeInput(horaInicioRaw) || '00:00';
const horaFin = this.normalizeTimeInput(horaFinRaw) || '23:59';
if (fechaInicio && fechaFin && new Date(fechaFin) < new Date(fechaInicio)) {
if (
(horaInicioRaw && !this.normalizeTimeInput(horaInicioRaw)) ||
(horaFinRaw && !this.normalizeTimeInput(horaFinRaw))
) {
this.filtroForm.get('fecha_fin')?.setErrors({ fechaInvalida: true });
} else {
this.filtroForm.get('fecha_fin')?.setErrors(null);
return;
}
if (fechaInicio && fechaFin) {
const inicioStr = `${fechaInicio} ${horaInicio}`;
const finStr = `${fechaFin} ${horaFin}`;
if (inicioStr > finStr) {
this.filtroForm.get('fecha_fin')?.setErrors({ fechaInvalida: true });
return;
}
}
this.filtroForm.get('fecha_fin')?.setErrors(null);
}
// ========= EXPORTAR A CSV SIMPLE =========

View File

@ -72,6 +72,17 @@
flex: 1;
}
.cie10-row .cie10-input {
display: flex;
gap: 8px;
align-items: center;
flex: 1;
}
.cie10-row .cie10-input input {
flex: 1;
}
.cup-results {
margin-bottom: 12px;
}

View File

@ -377,14 +377,30 @@
{{ errorCups }}
</div>
<div class="form-row">
<div class="form-row cie10-row">
<label for="cie10Codigo">Codigo CIE-10:</label>
<input
id="cie10Codigo"
type="text"
[(ngModel)]="formAutorizacion.cie10_codigo"
placeholder="Ej: A00"
/>
<div class="cie10-input">
<input
id="cie10Codigo"
type="text"
[(ngModel)]="formAutorizacion.cie10_codigo"
placeholder="Ej: A00"
(keyup.enter)="buscarCie10()"
(input)="onCie10InputChange()"
/>
<button
type="button"
class="btn btn-secondary btn-sm"
(click)="buscarCie10()"
[disabled]="buscandoCie10"
>
{{ buscandoCie10 ? 'Buscando...' : 'Buscar' }}
</button>
</div>
</div>
<div class="status error" *ngIf="errorCie10">
{{ errorCie10 }}
</div>
<div class="form-row">
@ -404,6 +420,15 @@
[(ngModel)]="formAutorizacion.fecha_autorizacion"
/>
</div>
<div class="form-row">
<label for="horaAut">Hora autorizaci?n:</label>
<input
id="horaAut"
type="time"
[(ngModel)]="formAutorizacion.hora_autorizacion"
/>
</div>
<div class="form-row">
<label for="obs">Observación / servicios autorizados:</label>

View File

@ -35,6 +35,9 @@ export class AutorizacionesComponent {
cupSeleccionado: CupInfo | null = null;
buscandoCups = false;
errorCups: string | null = null;
buscandoCie10 = false;
errorCie10: string | null = null;
private cie10Timer: ReturnType<typeof setTimeout> | null = null;
verMasIps = false;
departamentoInterno = '';
observacionTraslado = '';
@ -50,6 +53,7 @@ export class AutorizacionesComponent {
id_ips: '',
numero_documento_autorizante: '',
fecha_autorizacion: '',
hora_autorizacion: '',
observacion: '',
cup_codigo: '',
cie10_codigo: '',
@ -146,6 +150,7 @@ export class AutorizacionesComponent {
id_ips: '',
numero_documento_autorizante: '',
fecha_autorizacion: '',
hora_autorizacion: '',
observacion: '',
cup_codigo: '',
cie10_codigo: '',
@ -155,6 +160,7 @@ export class AutorizacionesComponent {
ambito_atencion: '',
numero_orden: '',
};
this.setFechaHoraActual();
this.autorizacionesPaciente = [];
this.versionesPorAutorizacion = {};
@ -165,6 +171,11 @@ export class AutorizacionesComponent {
this.cupsDisponibles = [];
this.cupSeleccionado = null;
this.errorCups = null;
this.errorCie10 = null;
if (this.cie10Timer) {
clearTimeout(this.cie10Timer);
this.cie10Timer = null;
}
this.cargarIps(p.interno, this.verMasIps);
this.cargarAutorizantes();
@ -180,6 +191,11 @@ export class AutorizacionesComponent {
this.cupsDisponibles = [];
this.cupSeleccionado = null;
this.errorCups = null;
this.errorCie10 = null;
if (this.cie10Timer) {
clearTimeout(this.cie10Timer);
this.cie10Timer = null;
}
this.verMasIps = false;
this.departamentoInterno = '';
this.observacionTraslado = '';
@ -332,6 +348,63 @@ export class AutorizacionesComponent {
this.cupSeleccionado = null;
}
buscarCie10(): void {
const codigo = String(this.formAutorizacion.cie10_codigo || '')
.trim()
.toUpperCase();
if (!codigo) {
this.errorCie10 = 'Ingresa un codigo CIE-10 para buscar.';
return;
}
this.formAutorizacion.cie10_codigo = codigo;
this.buscandoCie10 = true;
this.errorCie10 = null;
this.pacienteService
.buscarCie10PorCodigo(codigo)
.pipe(
finalize(() => {
this.buscandoCie10 = false;
this.cdr.markForCheck();
})
)
.subscribe({
next: (data) => {
if (data?.descripcion) {
this.formAutorizacion.cie10_descripcion = data.descripcion;
} else {
this.errorCie10 = 'No se encontro el diagnostico CIE-10.';
}
},
error: (err) => {
console.error(err);
this.errorCie10 =
err?.status === 404
? 'No se encontro el diagnostico CIE-10.'
: 'Error consultando CIE-10.';
},
});
}
onCie10InputChange(): void {
this.errorCie10 = null;
this.formAutorizacion.cie10_descripcion = '';
if (this.cie10Timer) {
clearTimeout(this.cie10Timer);
}
const codigo = String(this.formAutorizacion.cie10_codigo || '').trim();
if (!codigo) {
return;
}
this.cie10Timer = setTimeout(() => {
this.buscarCie10();
}, 400);
}
// -------------------------
// Guardar autorización
// -------------------------
@ -459,6 +532,9 @@ export class AutorizacionesComponent {
if (resp?.fecha_ingreso_urgencias) {
observacionExtras.push(`Ingreso a urgencias: ${resp.fecha_ingreso_urgencias}`);
}
if (resp?.fecha_egreso) {
observacionExtras.push(`Fecha de egreso: ${resp.fecha_egreso}`);
}
if (observacionExtras.length) {
const actual = String(this.formAutorizacion.observacion || '').trim();
const nuevos = observacionExtras.filter(
@ -705,7 +781,42 @@ export class AutorizacionesComponent {
private formatDateInput(value: string | null | undefined): string {
if (!value) return '';
return value.substring(0, 10);
const raw = String(value).trim();
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
const [year, month, day] = raw.split('-').map(Number);
const dateOnly = new Date(year, month - 1, day);
return this.formatDateInputFromDate(dateOnly);
}
const date = new Date(raw);
if (Number.isNaN(date.getTime())) return '';
return this.formatDateInputFromDate(date);
}
private formatDateInputFromDate(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
private formatTimeInput(value: string | null | undefined): string {
if (!value) return '';
const raw = String(value).trim();
if (/^\d{2}:\d{2}/.test(raw) && !raw.includes('T')) {
return raw.substring(0, 5);
}
const date = new Date(raw);
if (Number.isNaN(date.getTime())) return '';
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${hours}:${minutes}`;
}
private setFechaHoraActual(): void {
const ahora = new Date();
this.formAutorizacion.fecha_autorizacion =
this.formatDateInputFromDate(ahora);
this.formAutorizacion.hora_autorizacion = this.formatTimeInput(ahora.toISOString());
}
iniciarEdicion(autorizacion: any): void {
@ -714,6 +825,7 @@ export class AutorizacionesComponent {
this.autorizacionEditando = autorizacion;
this.mensajeAutorizacion = null;
this.errorAutorizacion = null;
this.errorCie10 = null;
this.formAutorizacion = {
id_ips: String(autorizacion.id_ips || ''),
@ -721,6 +833,7 @@ export class AutorizacionesComponent {
autorizacion.numero_documento_autorizante || ''
),
fecha_autorizacion: this.formatDateInput(autorizacion.fecha_autorizacion),
hora_autorizacion: this.formatTimeInput(autorizacion.fecha_autorizacion),
observacion: autorizacion.observacion || '',
cup_codigo: autorizacion.cup_codigo || '',
cie10_codigo: autorizacion.cie10_codigo || '',
@ -745,10 +858,16 @@ export class AutorizacionesComponent {
this.autorizacionEditando = null;
this.observacionTraslado = '';
this.cupSeleccionado = null;
this.errorCie10 = null;
if (this.cie10Timer) {
clearTimeout(this.cie10Timer);
this.cie10Timer = null;
}
this.formAutorizacion = {
id_ips: '',
numero_documento_autorizante: '',
fecha_autorizacion: '',
hora_autorizacion: '',
observacion: '',
cup_codigo: '',
cie10_codigo: '',
@ -758,6 +877,7 @@ export class AutorizacionesComponent {
ambito_atencion: '',
numero_orden: '',
};
this.setFechaHoraActual();
this.limpiarArchivosHospitalarios();
}
@ -785,6 +905,14 @@ export class AutorizacionesComponent {
this.errorAutorizacion = 'Debe seleccionar quién autoriza.';
return;
}
if (!this.formAutorizacion.fecha_autorizacion) {
this.errorAutorizacion = 'Debe seleccionar la fecha de autorizacion.';
return;
}
if (!this.formAutorizacion.hora_autorizacion) {
this.errorAutorizacion = 'Debe seleccionar la hora de autorizacion.';
return;
}
if (!this.formAutorizacion.cup_codigo) {
this.errorAutorizacion = 'Debe seleccionar un CUPS.';
return;
@ -847,6 +975,8 @@ export class AutorizacionesComponent {
observacion: this.buildObservacionFinal(),
fecha_autorizacion:
this.formAutorizacion.fecha_autorizacion || undefined,
hora_autorizacion:
this.formAutorizacion.hora_autorizacion || undefined,
cup_codigo: this.formAutorizacion.cup_codigo,
cie10_codigo: this.formAutorizacion.cie10_codigo,
cie10_descripcion: this.formAutorizacion.cie10_descripcion,

View File

@ -45,5 +45,31 @@
<div class="status ok" *ngIf="statusMessage">{{ statusMessage }}</div>
<div class="status error" *ngIf="errorMessage">{{ errorMessage }}</div>
</div>
<div class="card cups-card">
<h2>Cargar CIE-10</h2>
<p class="subtitle">
Sube el archivo con el codigo y el diagnostico CIE-10.
</p>
<div class="form-row">
<label>Archivo CIE-10 (Excel):</label>
<input type="file" accept=".xlsx,.xls" (change)="onCie10Selected($event)" />
<span class="file-name" *ngIf="cie10File">{{ cie10File.name }}</span>
</div>
<div class="form-row acciones">
<button
class="btn btn-primary"
(click)="procesarCie10()"
[disabled]="cie10Loading || !cie10File"
>
{{ cie10Loading ? 'Procesando...' : 'Procesar CIE-10' }}
</button>
</div>
<div class="status ok" *ngIf="cie10StatusMessage">{{ cie10StatusMessage }}</div>
<div class="status error" *ngIf="cie10ErrorMessage">{{ cie10ErrorMessage }}</div>
</div>
</div>
</div>

View File

@ -16,9 +16,13 @@ import { JobsService } from '../../services/jobs';
export class CargarCupsComponent implements OnInit {
notaFile: File | null = null;
referenciaFile: File | null = null;
cie10File: File | null = null;
isLoading = false;
cie10Loading = false;
statusMessage: string | null = null;
errorMessage: string | null = null;
cie10StatusMessage: string | null = null;
cie10ErrorMessage: string | null = null;
constructor(
private authService: AuthService,
@ -62,6 +66,12 @@ export class CargarCupsComponent implements OnInit {
this.limpiarMensajes();
}
onCie10Selected(event: Event): void {
const input = event.target as HTMLInputElement;
this.cie10File = input.files?.[0] || null;
this.limpiarMensajesCie10();
}
procesarCups(): void {
this.limpiarMensajes();
@ -128,8 +138,81 @@ export class CargarCupsComponent implements OnInit {
});
}
procesarCie10(): void {
this.limpiarMensajesCie10();
if (!this.cie10File) {
this.cie10ErrorMessage = 'Debes seleccionar el archivo de CIE-10.';
return;
}
this.cie10Loading = true;
this.cie10StatusMessage = 'Subiendo archivo...';
const formData = new FormData();
formData.append('archivo', this.cie10File);
this.pacienteService.cargarExcelCie10(formData).subscribe({
next: (job) => {
this.cie10StatusMessage = 'Archivo en cola. Procesando...';
this.cdr.detectChanges();
this.jobsService.pollJob(job.id).subscribe({
next: (estado) => {
if (estado.status === 'completed') {
const partes: string[] = [];
if (estado.result?.mensaje) {
partes.push(estado.result.mensaje);
}
if (typeof estado.result?.procesadas === 'number') {
partes.push(`Procesadas: ${estado.result.procesadas}`);
}
if (typeof estado.result?.omitidas === 'number') {
partes.push(`Omitidas: ${estado.result.omitidas}`);
}
if (typeof estado.result?.total === 'number') {
partes.push(`Total: ${estado.result.total}`);
}
this.cie10StatusMessage =
partes.join(' - ') || 'CIE-10 procesado correctamente.';
this.cie10Loading = false;
}
if (estado.status === 'failed') {
this.cie10ErrorMessage =
estado.error?.message || 'Error procesando el archivo de CIE-10.';
this.cie10Loading = false;
}
this.cdr.detectChanges();
},
error: (error) => {
console.error(error);
this.cie10ErrorMessage =
'Error consultando el estado del procesamiento de CIE-10.';
this.cie10Loading = false;
this.cdr.detectChanges();
}
});
},
error: (err) => {
console.error(err);
this.cie10ErrorMessage =
err?.error?.error || 'Error subiendo el archivo de CIE-10.';
this.cie10Loading = false;
this.cdr.detectChanges();
}
});
}
private limpiarMensajes(): void {
this.errorMessage = null;
this.statusMessage = null;
}
private limpiarMensajesCie10(): void {
this.cie10ErrorMessage = null;
this.cie10StatusMessage = null;
}
}

View File

@ -177,7 +177,7 @@ export class EstadisticasAutorizacionesComponent implements OnInit {
this.isLoadingDetalle = true;
this.authService
.getAutorizacionesPorFecha(bucket.inicio, bucket.fin, 500, 0)
.getAutorizacionesPorFecha(bucket.inicio, bucket.fin, undefined, undefined, 500, 0)
.pipe(
finalize(() => {
this.isLoadingDetalle = false;

View File

@ -312,6 +312,8 @@ export class AuthService {
getAutorizacionesPorFecha(
fechaInicio: string,
fechaFin: string,
horaInicio?: string,
horaFin?: string,
limit = 100000,
offset = 0,
establecimiento?: string,
@ -320,6 +322,12 @@ export class AuthService {
): Observable<any[]> {
const headers = this.getAuthHeaders();
const params: any = { fecha_inicio: fechaInicio, fecha_fin: fechaFin, limit, offset };
if (horaInicio) {
params.hora_inicio = horaInicio;
}
if (horaFin) {
params.hora_fin = horaFin;
}
if (establecimiento) {
params.establecimiento = establecimiento;
}
@ -371,12 +379,20 @@ export class AuthService {
crearJobZipAutorizaciones(
fechaInicio: string,
fechaFin: string,
horaInicio?: string,
horaFin?: string,
establecimiento?: string,
ambito?: string,
ips?: string
): Observable<JobResponse> {
const headers = this.getAuthHeaders();
const payload: any = { fecha_inicio: fechaInicio, fecha_fin: fechaFin };
if (horaInicio) {
payload.hora_inicio = horaInicio;
}
if (horaFin) {
payload.hora_fin = horaFin;
}
if (establecimiento) {
payload.establecimiento = establecimiento;
}

View File

@ -21,6 +21,7 @@ export interface JobResult {
actualizados?: number | null;
omitidos?: number | null;
omitidas?: number | null;
procesadas?: number | null;
desactivados?: number | null;
duplicados?: number | null;
creadas?: number | null;

View File

@ -76,6 +76,7 @@ export interface CrearAutorizacionPayload {
estado_entrega?: string;
observacion?: string;
fecha_autorizacion?: string; // yyyy-MM-dd
hora_autorizacion?: string; // HH:mm
}
export interface RespuestaAutorizacion {
@ -136,6 +137,11 @@ export interface CupInfo {
cubierto: boolean;
}
export interface Cie10Info {
codigo: string;
descripcion: string;
}
// ====== Servicio ======
@Injectable({
@ -328,6 +334,19 @@ export class PacienteService {
);
}
cargarExcelCie10(formData: FormData): Observable<JobResponse> {
const token = this.authService.getToken();
const headers = new HttpHeaders({
'Authorization': `Bearer ${token}`
});
return this.http.post<JobResponse>(
`${this.API_URL}/cargar-cie10`,
formData,
{ headers }
);
}
cargarIps(formData: FormData): Observable<JobResponse> {
const token = this.authService.getToken();
const headers = new HttpHeaders({
@ -385,6 +404,14 @@ export class PacienteService {
});
}
buscarCie10PorCodigo(codigo: string): Observable<Cie10Info> {
const params = new HttpParams().set('codigo', codigo);
return this.http.get<Cie10Info>(`${this.API_URL}/cie10`, {
params,
headers: this.getAuthHeaders()
});
}
actualizarEstadoAutorizacion(
numeroAutorizacion: string,
estado: 'pendiente' | 'autorizado' | 'no_autorizado'
@ -407,6 +434,8 @@ export class PacienteService {
actualizarEstadoAutorizacionesMasivo(
fechaInicio: string,
fechaFin: string,
horaInicio: string | undefined,
horaFin: string | undefined,
estado: 'pendiente' | 'autorizado' | 'no_autorizado',
soloPendientes = false
): Observable<any> {
@ -415,6 +444,8 @@ export class PacienteService {
{
fecha_inicio: fechaInicio,
fecha_fin: fechaFin,
hora_inicio: horaInicio,
hora_fin: horaFin,
estado_autorizacion: estado,
solo_pendientes: soloPendientes,
},
@ -425,6 +456,8 @@ export class PacienteService {
actualizarAutorizanteMasivo(
fechaInicio: string,
fechaFin: string,
horaInicio: string | undefined,
horaFin: string | undefined,
numeroDocumentoAutorizante: number,
establecimiento?: string,
ambito?: string,
@ -433,6 +466,8 @@ export class PacienteService {
const payload: any = {
fecha_inicio: fechaInicio,
fecha_fin: fechaFin,
hora_inicio: horaInicio,
hora_fin: horaFin,
numero_documento_autorizante: numeroDocumentoAutorizante,
};