diff --git a/README.md b/README.md index c5a3a55..14725cb 100644 --- a/README.md +++ b/README.md @@ -343,5 +343,3 @@ Reinicia el backend: rc-service saludut-backend restart ``` - - diff --git a/backend/src/CIE-10.xlsx b/backend/src/CIE-10.xlsx new file mode 100644 index 0000000..660212a Binary files /dev/null and b/backend/src/CIE-10.xlsx differ diff --git a/backend/src/__pycache__/extraer_autorizacion_pdf.cpython-313.pyc b/backend/src/__pycache__/extraer_autorizacion_pdf.cpython-313.pyc index 30420a6..da10051 100644 Binary files a/backend/src/__pycache__/extraer_autorizacion_pdf.cpython-313.pyc and b/backend/src/__pycache__/extraer_autorizacion_pdf.cpython-313.pyc differ diff --git a/backend/src/extraer_autorizacion_pdf.py b/backend/src/extraer_autorizacion_pdf.py index 8922565..a2eb93e 100644 --- a/backend/src/extraer_autorizacion_pdf.py +++ b/backend/src/extraer_autorizacion_pdf.py @@ -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, } diff --git a/backend/src/plantilla_autorizacion.js b/backend/src/plantilla_autorizacion.js index 7cd1fe1..895faae 100644 --- a/backend/src/plantilla_autorizacion.js +++ b/backend/src/plantilla_autorizacion.js @@ -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'; }); } diff --git a/backend/src/plantilla_autorizacion_brigadas_ambulancias_hospitalarios.js b/backend/src/plantilla_autorizacion_brigadas_ambulancias_hospitalarios.js index 6a93375..7d1a84d 100644 --- a/backend/src/plantilla_autorizacion_brigadas_ambulancias_hospitalarios.js +++ b/backend/src/plantilla_autorizacion_brigadas_ambulancias_hospitalarios.js @@ -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'; }); } diff --git a/backend/src/schema.sql b/backend/src/schema.sql index 51ffc85..9af6c4a 100644 --- a/backend/src/schema.sql +++ b/backend/src/schema.sql @@ -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, diff --git a/backend/src/server.js b/backend/src/server.js index 3828100..92a3588 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -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); }); diff --git a/saludut-inpec/src/app/components/autorizaciones-por-fecha/autorizaciones-por-fecha.html b/saludut-inpec/src/app/components/autorizaciones-por-fecha/autorizaciones-por-fecha.html index 6bfdbc2..5820a27 100644 --- a/saludut-inpec/src/app/components/autorizaciones-por-fecha/autorizaciones-por-fecha.html +++ b/saludut-inpec/src/app/components/autorizaciones-por-fecha/autorizaciones-por-fecha.html @@ -60,6 +60,26 @@ +
+ Sube el archivo con el codigo y el diagnostico CIE-10. +
+ +