This commit is contained in:
Jhonathan Guevara 2025-12-31 06:35:59 -05:00
parent 2d29bd88a1
commit 0287cd6931
Signed by: jhonathan_guevara
GPG Key ID: 619239F12DCBE55B
27 changed files with 2293 additions and 260 deletions

View File

@ -299,3 +299,45 @@ pip3 install --break-system-packages pdfplumber pypdf reportlab pillow
# verificacion
python3 -c "import pandas, openpyxl, dotenv; print('OK', pandas.__version__, openpyxl.__version__, dotenv.__version__)"
```
#### 7.7) OCR (PDF real + escaneado) con Tesseract (Alpine)
Si necesitas leer PDFs escaneados para el autorrelleno, usa este flujo.
```bash
# Uso:
# sh -c '...este bloque...' archivo.pdf
# Si no pasas argumento, usa "archivo.pdf"
# 1) Asegura repo community (donde esta ocrmypdf/tesseract-data)
ALPINE_VER="$(cut -d. -f1,2 /etc/alpine-release)"
sed -i 's/^#\(.*\/community\)/\1/' /etc/apk/repositories
grep -q "/v${ALPINE_VER}/community" /etc/apk/repositories || \
echo "https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VER}/community" >> /etc/apk/repositories
# 2) Instala herramientas: pdftotext (poppler), ocrmypdf, tesseract + idiomas
apk update
apk add --no-cache \
poppler-utils \
ocrmypdf \
tesseract-ocr-data-spa \
tesseract-ocr-data-eng
# 3) Procesa el PDF (real/escaneado/mixto)
IN="${1:-archivo.pdf}"
BASE="${IN%.pdf}"
TXT="${BASE}.txt"
OCRPDF="${BASE}.ocr.pdf"
# Intenta extraer texto directo
pdftotext -layout "$IN" - > "$TXT" 2>/dev/null || true
# Si el texto es muy corto, probablemente es escaneado (o casi todo imagen) -> OCR
LEN="$(tr -d '[:space:]' < "$TXT" | wc -c | tr -d ' ')"
if [ "${LEN:-0}" -lt 80 ]; then
ocrmypdf -l spa+eng --skip-text --rotate-pages --deskew --optimize 3 "$IN" "$OCRPDF"
pdftotext -layout "$OCRPDF" "$TXT"
echo "OK (OCR aplicado cuando hacia falta): $OCRPDF | Texto: $TXT"
else
echo "OK (PDF con texto): Texto: $TXT"
fi
```

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,292 @@
import json
import os
import re
import sys
import unicodedata
def normalize(text):
text = text or ""
text = unicodedata.normalize("NFD", text)
text = "".join(ch for ch in text if not unicodedata.combining(ch))
text = re.sub(r"\s+", " ", text).strip()
return text.upper()
def extract_text_pdfplumber(path):
try:
import pdfplumber # type: ignore
except Exception:
return ""
parts = []
with pdfplumber.open(path) as pdf:
for page in pdf.pages:
parts.append(page.extract_text() or "")
return "\n".join(parts)
def extract_text_fitz(path):
try:
import fitz # type: ignore
except Exception:
return ""
parts = []
doc = fitz.open(path)
try:
for page in doc:
parts.append(page.get_text() or "")
finally:
doc.close()
return "\n".join(parts)
def configure_tesseract():
try:
import pytesseract # type: ignore
except Exception:
return None
candidates = []
env_path = os.environ.get("TESSERACT_PATH")
if env_path:
candidates.append(env_path)
candidates.extend(
[
r"C:\Program Files\Tesseract-OCR\tesseract.exe",
r"C:\Program Files (x86)\Tesseract-OCR\tesseract.exe",
]
)
for path in candidates:
if path and os.path.isfile(path):
pytesseract.pytesseract.tesseract_cmd = path
return path
return None
def is_tesseract_available():
try:
import pytesseract # type: ignore
except Exception:
return False
configure_tesseract()
try:
_ = pytesseract.get_tesseract_version()
return True
except Exception:
return False
def extract_text_ocr(path, max_pages=2):
try:
import fitz # type: ignore
import pytesseract # type: ignore
from PIL import Image # type: ignore
except Exception:
return "", "ocr_modules_missing"
configure_tesseract()
try:
_ = pytesseract.get_tesseract_version()
except Exception:
return "", "tesseract_not_found"
text_parts = []
ocr_error = ""
doc = fitz.open(path)
try:
total_pages = min(max_pages, doc.page_count)
for i in range(total_pages):
page = doc.load_page(i)
pix = page.get_pixmap(dpi=200)
img = Image.frombytes("RGB", (pix.width, pix.height), pix.samples)
try:
text = pytesseract.image_to_string(img, lang="spa") or ""
except Exception:
try:
text = pytesseract.image_to_string(img, lang="eng") or ""
if not ocr_error:
ocr_error = "tesseract_lang_missing_spa"
except Exception:
return "", "tesseract_lang_missing"
text_parts.append(text)
finally:
doc.close()
return "\n".join(text_parts), ocr_error
def extract_name(lines, norm_lines):
keys = ["DATOS DEL USUARIO", "DATOS DEL PACIENTE"]
idx = -1
for key in keys:
for i, line in enumerate(norm_lines):
if key in line:
idx = i
break
if idx != -1:
break
if idx == -1:
return None
skip_tokens = [
"1ER APELLIDO",
"2DO APELLIDO",
"1ER NOMBRE",
"2DO NOMBRE",
"TIPO DOCUMENTO",
"DOCUMENTO DE IDENTIFICACION",
"REGISTRO CIVIL",
"TARJETA",
"CEDULA",
"NUIP",
]
for j in range(idx + 1, min(idx + 6, len(lines))):
nline = norm_lines[j]
if any(token in nline for token in skip_tokens):
continue
if len(lines[j].split()) >= 2:
return lines[j]
return None
def extract_document(lines, norm_lines):
idx = -1
for i, line in enumerate(norm_lines):
if "TIPO DOCUMENTO" in line or "TIPO DE DOCUMENTO" in line:
idx = i
break
if idx != -1:
for j in range(idx + 1, min(idx + 8, len(lines))):
digits = re.findall(r"\d{6,}", lines[j])
if digits:
return digits[-1]
for i, line in enumerate(norm_lines):
if "CEDULA" in line or "DOCUMENTO" in line or "NUIP" in line:
digits = re.findall(r"\d{6,}", lines[i])
if digits:
return digits[-1]
return None
def extract_cups(lines, norm_lines):
idx = -1
for i, line in enumerate(norm_lines):
if "CODIGO CUPS" in line:
idx = i
break
if idx == -1:
return None, None
for j in range(idx + 1, min(idx + 6, len(lines))):
tokens = lines[j].split()
if not tokens:
continue
code = tokens[0]
if not re.match(r"^[A-Z0-9]{4,10}$", code, re.IGNORECASE):
continue
desc = ""
if len(tokens) >= 2 and re.match(r"^\d+[,.]\d+$", tokens[1]):
desc = " ".join(tokens[2:])
else:
desc = " ".join(tokens[1:])
return code, desc.strip() or None
return None, None
def extract_cie10(lines, norm_lines):
for i, nline in enumerate(norm_lines):
if "DIAGNOSTICO PRINCIPAL" in nline:
match = re.search(r"DIAGNOSTICO PRINCIPAL\s+([A-Z0-9]{3,6})\s*(.*)", nline)
if match:
code = match.group(1)
desc = match.group(2).strip().title()
return code, desc or None
return None, None
def detect_format(norm_text, norm_lines):
if "ANEXO TECNICO" in norm_text or "SOLICITUD DE AUTORIZACION" in norm_text:
return "ANEXO_TECNICO"
for line in norm_lines:
if "ATENCION INICIAL DE URGENCIAS" in line:
return "ANEXO_URGENCIAS"
return "DESCONOCIDO"
def build_response(text, ocr_used, ocr_available, ocr_error):
lines = [line.strip() for line in (text or "").split("\n") if line.strip()]
norm_lines = [normalize(line) for line in lines]
norm_text = normalize(text)
nombre = extract_name(lines, norm_lines)
documento = extract_document(lines, norm_lines)
cup_codigo, cup_desc = extract_cups(lines, norm_lines)
cie_codigo, cie_desc = extract_cie10(lines, norm_lines)
formato = detect_format(norm_text, norm_lines)
warnings = []
if not text:
warnings.append("no_text_extracted")
if not cup_codigo:
warnings.append("cups_not_found")
if not cie_codigo:
warnings.append("cie10_not_found")
return {
"ok": True,
"text_length": len(norm_text),
"ocr_usado": ocr_used,
"ocr_disponible": ocr_available,
"ocr_error": ocr_error or None,
"formato": formato,
"nombre_paciente": nombre,
"numero_documento": documento,
"cup_codigo": cup_codigo,
"cup_descripcion": cup_desc,
"cie10_codigo": cie_codigo,
"cie10_descripcion": cie_desc,
"warnings": warnings,
}
def main():
if len(sys.argv) < 2:
print(json.dumps({"ok": False, "error": "missing_file"}, ensure_ascii=True))
return
path = sys.argv[1]
text = extract_text_pdfplumber(path)
if not text:
text = extract_text_fitz(path)
normalized_len = len(normalize(text))
ocr_used = False
ocr_error = ""
ocr_available = is_tesseract_available()
if normalized_len < 50 and ocr_available:
ocr_text, ocr_error = extract_text_ocr(path)
if ocr_text:
text = ocr_text
ocr_used = True
response = build_response(text, ocr_used, ocr_available, ocr_error)
print(json.dumps(response, ensure_ascii=True))
if __name__ == "__main__":
main()

View File

@ -1436,27 +1436,27 @@ async function crearLibroAutorizacion(a) {
sheet.getCell('L21').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('L21').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('L21').alignment = {"horizontal":"center","vertical":"middle"};
sheet.getCell('M21').value = "CODIGO INTERNO";
sheet.getCell('M21').value = "CODIGO CIE-10";
sheet.getCell('M21').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('M21').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('M21').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('N21').value = "CODIGO INTERNO";
sheet.getCell('N21').value = "DIAGNOSTICO";
sheet.getCell('N21').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('N21').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('N21').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('O21').value = "CODIGO INTERNO";
sheet.getCell('O21').value = "DIAGNOSTICO";
sheet.getCell('O21').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('O21').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('O21').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('P21').value = "CODIGO INTERNO";
sheet.getCell('P21').value = "DIAGNOSTICO";
sheet.getCell('P21').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('P21').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('P21').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('Q21').value = "CODIGO INTERNO";
sheet.getCell('Q21').value = "DIAGNOSTICO";
sheet.getCell('Q21').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('Q21').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('Q21').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('R21').value = "CODIGO INTERNO";
sheet.getCell('R21').value = "DIAGNOSTICO";
sheet.getCell('R21').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('R21').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"double"},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('R21').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
@ -1510,27 +1510,27 @@ async function crearLibroAutorizacion(a) {
sheet.getCell('L22').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('L22').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('L22').alignment = {"horizontal":"center","vertical":"middle"};
sheet.getCell('M22').value = "CODIGO INTERNO";
sheet.getCell('M22').value = "CODIGO CIE-10";
sheet.getCell('M22').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('M22').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('M22').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('N22').value = "CODIGO INTERNO";
sheet.getCell('N22').value = "DIAGNOSTICO";
sheet.getCell('N22').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('N22').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('N22').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('O22').value = "CODIGO INTERNO";
sheet.getCell('O22').value = "DIAGNOSTICO";
sheet.getCell('O22').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('O22').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('O22').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('P22').value = "CODIGO INTERNO";
sheet.getCell('P22').value = "DIAGNOSTICO";
sheet.getCell('P22').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('P22').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('P22').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('Q22').value = "CODIGO INTERNO";
sheet.getCell('Q22').value = "DIAGNOSTICO";
sheet.getCell('Q22').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('Q22').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('Q22').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('R22').value = "CODIGO INTERNO";
sheet.getCell('R22').value = "DIAGNOSTICO";
sheet.getCell('R22').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('R22').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"double"},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('R22').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
@ -2697,9 +2697,8 @@ async function crearLibroAutorizacion(a) {
sheet.getCell(celda).value = nitIps;
});
['M21', 'N21', 'R21'].forEach(celda => {
sheet.getCell(celda).value = a.interno || '';
});
sheet.getCell('M21').value = a.cie10_codigo || '';
sheet.getCell('N21').value = a.cie10_descripcion || '';
['N20', 'R20'].forEach(celda => {
sheet.getCell(celda).value = a.sexo || '';
@ -2715,12 +2714,12 @@ async function crearLibroAutorizacion(a) {
});
['M16', 'N16'].forEach(celda => {
sheet.getCell(celda).value = a.departamento || '';
sheet.getCell(celda).value = a.municipio || '';
});
// 6) Municipio IPS (Q16:R16)
// 6) Departamento IPS (Q16:R16)
['Q16', 'R16'].forEach(celda => {
sheet.getCell(celda).value = a.municipio || '';
sheet.getCell(celda).value = a.departamento || '';
});
// 7) Nombre completo paciente (H18:R18)
@ -2755,6 +2754,12 @@ async function crearLibroAutorizacion(a) {
if (nivelTexto) cupInfoParts.push(nivelTexto);
const cupInfo = cupInfoParts.join(' - ');
const observacionBase = a.observacion || '';
const solicitanteNombre = String(a.nombre_solicitante || '').trim();
const observacionLower = observacionBase.toLowerCase();
const solicitanteInfo =
solicitanteNombre && !observacionLower.includes('solicitante')
? `Solicitante: ${solicitanteNombre}`
: '';
const tipoAutorizacion = (a.tipo_autorizacion || 'consultas_externas').toLowerCase();
const tipoServicioRaw = String(a.tipo_servicio || '').trim();
let tipoServicioTexto = '';
@ -2763,7 +2768,7 @@ async function crearLibroAutorizacion(a) {
} else if (tipoAutorizacion === 'consultas_externas') {
tipoServicioTexto = 'Tipo servicio: Consulta externa';
}
const observacion = [cupInfo, tipoServicioTexto, observacionBase]
const observacion = [cupInfo, tipoServicioTexto, observacionBase, solicitanteInfo]
.filter(Boolean)
.join(' | ');
['H31','R31'].forEach(celda => {

View File

@ -1279,27 +1279,27 @@ async function crearLibroAutorizacionBrigadasAmbulanciasHospitalarios(a) {
sheet.getCell('L21').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('L21').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('L21').alignment = {"horizontal":"center","vertical":"middle"};
sheet.getCell('M21').value = "CODIGO INTERNO";
sheet.getCell('M21').value = "CODIGO CIE-10";
sheet.getCell('M21').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('M21').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('M21').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('N21').value = "CODIGO INTERNO";
sheet.getCell('N21').value = "DIAGNOSTICO";
sheet.getCell('N21').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('N21').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('N21').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('O21').value = "CODIGO INTERNO";
sheet.getCell('O21').value = "DIAGNOSTICO";
sheet.getCell('O21').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('O21').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('O21').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('P21').value = "CODIGO INTERNO";
sheet.getCell('P21').value = "DIAGNOSTICO";
sheet.getCell('P21').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('P21').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('P21').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('Q21').value = "CODIGO INTERNO";
sheet.getCell('Q21').value = "DIAGNOSTICO";
sheet.getCell('Q21').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('Q21').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('Q21').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('R21').value = "CODIGO INTERNO";
sheet.getCell('R21').value = "DIAGNOSTICO";
sheet.getCell('R21').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('R21').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"double"},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('R21').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
@ -1347,27 +1347,27 @@ async function crearLibroAutorizacionBrigadasAmbulanciasHospitalarios(a) {
sheet.getCell('L22').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('L22').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('L22').alignment = {"horizontal":"center","vertical":"middle"};
sheet.getCell('M22').value = "CODIGO INTERNO";
sheet.getCell('M22').value = "CODIGO CIE-10";
sheet.getCell('M22').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('M22').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('M22').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('N22').value = "CODIGO INTERNO";
sheet.getCell('N22').value = "DIAGNOSTICO";
sheet.getCell('N22').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('N22').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('N22').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('O22').value = "CODIGO INTERNO";
sheet.getCell('O22').value = "DIAGNOSTICO";
sheet.getCell('O22').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('O22').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('O22').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('P22').value = "CODIGO INTERNO";
sheet.getCell('P22').value = "DIAGNOSTICO";
sheet.getCell('P22').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('P22').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('P22').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('Q22').value = "CODIGO INTERNO";
sheet.getCell('Q22').value = "DIAGNOSTICO";
sheet.getCell('Q22').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('Q22').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"medium","color":{"argb":"FF000000"}},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('Q22').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
sheet.getCell('R22').value = "CODIGO INTERNO";
sheet.getCell('R22').value = "DIAGNOSTICO";
sheet.getCell('R22').fill = {"type":"pattern","pattern":"none"};
sheet.getCell('R22').border = {"left":{"style":"medium","color":{"argb":"FF000000"}},"right":{"style":"double"},"top":{"style":"medium","color":{"argb":"FF000000"}},"bottom":{"style":"medium","color":{"argb":"FF000000"}}};
sheet.getCell('R22').alignment = {"horizontal":"center","vertical":"middle","wrapText":true};
@ -2373,9 +2373,8 @@ async function crearLibroAutorizacionBrigadasAmbulanciasHospitalarios(a) {
sheet.getCell(celda).value = nitIps;
});
['M21', 'N21', 'R21'].forEach((celda) => {
sheet.getCell(celda).value = a.interno || '';
});
sheet.getCell('M21').value = a.cie10_codigo || '';
sheet.getCell('N21').value = a.cie10_descripcion || '';
['N20', 'R20'].forEach((celda) => {
sheet.getCell(celda).value = a.sexo || '';
@ -2390,11 +2389,11 @@ async function crearLibroAutorizacionBrigadasAmbulanciasHospitalarios(a) {
});
['M16', 'N16'].forEach((celda) => {
sheet.getCell(celda).value = a.departamento || '';
sheet.getCell(celda).value = a.municipio || '';
});
['Q16', 'R16'].forEach((celda) => {
sheet.getCell(celda).value = a.municipio || '';
sheet.getCell(celda).value = a.departamento || '';
});
['H18', 'I18', 'J18', 'K18', 'L18', 'M18', 'N18', 'O18', 'P18', 'Q18', 'R18'].forEach((celda) => {
@ -2425,7 +2424,13 @@ async function crearLibroAutorizacionBrigadasAmbulanciasHospitalarios(a) {
if (nivelTexto) cupInfoParts.push(nivelTexto);
const cupInfo = cupInfoParts.join(' - ');
const observacionBase = a.observacion || '';
const observacion = [cupInfo, observacionBase].filter(Boolean).join(' | ');
const solicitanteNombre = String(a.nombre_solicitante || '').trim();
const observacionLower = observacionBase.toLowerCase();
const solicitanteInfo =
solicitanteNombre && !observacionLower.includes('solicitante')
? 'Solicitante: ' + solicitanteNombre
: '';
const observacion = [cupInfo, observacionBase, solicitanteInfo].filter(Boolean).join(' | ');
['H31', 'R31'].forEach((celda) => {
sheet.getCell(celda).value = observacion;
});

View File

@ -180,13 +180,26 @@ CREATE TABLE IF NOT EXISTS autorizacion (
interno text NOT NULL REFERENCES paciente(interno),
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,
observacion text,
cup_codigo varchar(20),
cie10_codigo varchar(20),
cie10_descripcion text,
tipo_autorizacion varchar(50) NOT NULL DEFAULT 'consultas_externas',
tipo_servicio varchar(50),
ambito_atencion varchar(20),
numero_orden varchar(50),
archivo_historial_clinico text,
archivo_historial_clinico_nombre text,
archivo_anexo text,
archivo_anexo_nombre text,
estado_entrega varchar(20) NOT NULL DEFAULT 'pendiente_entrega',
estado_autorizacion varchar(20) NOT NULL DEFAULT 'pendiente',
version integer NOT NULL DEFAULT 1
version integer NOT NULL DEFAULT 1,
correo_inpec_pendiente boolean NOT NULL DEFAULT false,
correo_inpec_enviado boolean NOT NULL DEFAULT false,
correo_inpec_respuesta boolean NOT NULL DEFAULT false
);
DO $$
@ -202,6 +215,32 @@ BEGIN
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'autorizacion_estado_entrega_chk'
) THEN
ALTER TABLE autorizacion
ADD CONSTRAINT autorizacion_estado_entrega_chk
CHECK (estado_entrega IN ('pendiente_entrega', 'entregado', 'programado', 'atendido', 'cancelado'));
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'autorizacion_ambito_atencion_chk'
) THEN
ALTER TABLE autorizacion
ADD CONSTRAINT autorizacion_ambito_atencion_chk
CHECK (ambito_atencion IN ('intramural', 'extramural'));
END IF;
END $$;
INSERT INTO consecutivo_autorizacion (id, codigo)
VALUES (1, COALESCE((SELECT MAX(numero_autorizacion) FROM autorizacion), 'UTUSCPGB00'))
ON CONFLICT (id) DO NOTHING;
@ -229,11 +268,17 @@ CREATE TABLE IF NOT EXISTS autorizacion_version (
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),
cie10_codigo VARCHAR(20),
cie10_descripcion TEXT,
tipo_autorizacion VARCHAR(50),
tipo_servicio VARCHAR(50),
ambito_atencion VARCHAR(20),
numero_orden VARCHAR(50),
estado_entrega VARCHAR(20),
fecha_version TIMESTAMP NOT NULL DEFAULT NOW()
);

File diff suppressed because it is too large Load Diff

View File

@ -87,7 +87,8 @@
font-size: 0.9rem;
}
.form-group input {
.form-group input,
.form-group select {
width: 100%;
padding: 12px 16px;
border: 2px solid var(--color-input-border);
@ -98,7 +99,8 @@
color: var(--color-text-main);
}
.form-group input:focus {
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #1976d2;
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
@ -284,6 +286,12 @@
font-family: "Courier New", monospace;
}
.numero-orden {
font-weight: 600;
font-family: "Courier New", monospace;
text-align: center;
}
.cup-codigo {
font-weight: 600;
font-family: "Courier New", monospace;
@ -321,8 +329,10 @@
.nombre-paciente,
.ips,
.autorizante,
.solicitante,
.establecimiento,
.estado {
.estado,
.estado-entrega {
max-width: 180px;
line-height: 1.3;
}
@ -340,7 +350,8 @@
text-transform: uppercase;
}
.estado select {
.estado select,
.estado-entrega select {
width: 100%;
min-width: 140px;
padding: 10px 12px;
@ -500,8 +511,10 @@
.nombre-paciente,
.ips,
.autorizante,
.solicitante,
.establecimiento,
.estado {
.estado,
.estado-entrega {
max-width: 120px;
}
}

View File

@ -74,6 +74,37 @@
</button>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="establecimiento">Centro penitenciario:</label>
<input
id="establecimiento"
type="text"
formControlName="establecimiento"
placeholder="Codigo o nombre"
/>
</div>
<div class="form-group">
<label for="ips">IPS:</label>
<input
id="ips"
type="text"
formControlName="ips"
placeholder="Nombre o NIT"
/>
</div>
<div class="form-group">
<label for="ambito">Ambito:</label>
<select id="ambito" formControlName="ambito">
<option value="">Todos</option>
<option value="intramural">Intramural</option>
<option value="extramural">Extramural</option>
</select>
</div>
</div>
</form>
</div>
@ -97,8 +128,8 @@
placeholder="Numero de autorizacion"
/>
</div>
<div class="resultados-actions" *ngIf="esAdmin">
<div class="estado-masivo">
<div class="resultados-actions" *ngIf="esAdmin || puedeDescargarMasivo">
<div class="estado-masivo" *ngIf="esAdmin">
<label for="estadoMasivo">Estado masivo:</label>
<select
id="estadoMasivo"
@ -120,20 +151,22 @@
</div>
<button
class="btn btn-secondary btn-exportar"
*ngIf="puedeDescargarMasivo"
(click)="exportarAExcel()"
title="Exportar a Excel"
[disabled]="descargandoZip || descargandoPdf"
>
Exportar Excel
</button>
<button
class="btn btn-secondary btn-descargar-todos"
(click)="descargarTodosLosPdfs()"
title="Descargar todos los PDFs"
[disabled]="descargandoZip"
>
Descargar todos los PDFs
</button>
<button
class="btn btn-secondary btn-descargar-todos"
*ngIf="puedeDescargarMasivo"
(click)="descargarTodosLosPdfs()"
title="Descargar todos los PDFs"
[disabled]="descargandoZip"
>
Descargar todos los PDFs
</button>
</div>
</div>
@ -149,6 +182,8 @@
<th>Version</th>
<th>Tipo autorizacion</th>
<th>Tipo servicio</th>
<th>Ambito</th>
<th>Numero orden</th>
<th>CUPS</th>
<th>Cubre</th>
<th>Nivel</th>
@ -156,14 +191,16 @@
<th>Municipio</th>
<th>Departamento</th>
<th>Autorizante</th>
<th>Solicitante</th>
<th>Establecimiento</th>
<th>Estado</th>
<th>Estado entrega</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<tr *ngIf="autorizacionesFiltradas.length === 0">
<td class="no-result" colspan="17">
<td class="no-result" colspan="21">
No hay autorizaciones para ese numero.
</td>
</tr>
@ -184,6 +221,10 @@
<td class="tipo-servicio">
{{ getTipoServicioLabel(aut.tipo_servicio) }}
</td>
<td class="ambito">{{ getAmbitoLabel(aut.ambito_atencion) }}</td>
<td class="numero-orden">
{{ aut.ambito_atencion === 'intramural' ? (aut.numero_orden || '-') : '-' }}
</td>
<td class="cup-codigo">{{ aut.cup_codigo }}</td>
<td class="cup-cobertura">
<span
@ -209,6 +250,7 @@
<td class="municipio">{{ aut.municipio || '-' }}</td>
<td class="departamento">{{ aut.departamento || '-' }}</td>
<td class="autorizante">{{ aut.nombre_autorizante }}</td>
<td class="solicitante">{{ aut.nombre_solicitante || '-' }}</td>
<td class="establecimiento">{{ aut.nombre_establecimiento }}</td>
<td class="estado">
<ng-container *ngIf="esAdmin; else estadoTexto">
@ -234,6 +276,32 @@
</span>
</ng-template>
</td>
<td class="estado-entrega">
<ng-container *ngIf="puedeGestionarEntrega; else estadoEntregaTexto">
<select
[ngModel]="aut.estado_entrega || 'pendiente_entrega'"
(ngModelChange)="actualizarEstadoEntrega(aut, $event)"
[disabled]="actualizandoEntrega[aut.numero_autorizacion]"
>
<option value="pendiente_entrega">Pendiente por entrega</option>
<option value="entregado">Entregado</option>
<option value="programado">Programado</option>
<option value="atendido">Atendido</option>
<option value="cancelado">Cancelado</option>
</select>
<span
class="mini-status"
*ngIf="actualizandoEntrega[aut.numero_autorizacion]"
>
Guardando...
</span>
</ng-container>
<ng-template #estadoEntregaTexto>
<span class="estado-label">
{{ getEstadoEntregaLabel(aut.estado_entrega) }}
</span>
</ng-template>
</td>
<td class="acciones">
<button
class="btn btn-success btn-sm btn-descargar"

View File

@ -28,8 +28,14 @@ export class AutorizacionesPorFechaComponent implements OnInit {
actualizandoMasivo = false;
estadoMasivo: 'pendiente' | 'autorizado' | 'no_autorizado' = 'autorizado';
esAdmin = false;
puedeGestionarEntrega = false;
puedeDescargarMasivo = false;
filtroNumero = '';
establecimientoFiltro = '';
ambitoFiltro = '';
ipsFiltro = '';
autorizacionesFiltradas: any[] = [];
actualizandoEntrega: Record<string, boolean> = {};
// Para saber si ya buscamos algo
hayResultados = false;
@ -48,12 +54,18 @@ export class AutorizacionesPorFechaComponent implements OnInit {
) {
this.filtroForm = this.fb.group({
fecha_inicio: ['', Validators.required],
fecha_fin: ['', Validators.required]
fecha_fin: ['', Validators.required],
establecimiento: [''],
ambito: [''],
ips: [''],
});
}
ngOnInit(): void {
this.esAdmin = this.authService.isAdministrador();
this.puedeGestionarEntrega =
this.esAdmin || this.authService.isAdministrativoSede();
this.puedeDescargarMasivo = this.authService.puedeDescargarPdfs();
// Rango por defecto: últimos 30 días
const hoy = new Date();
@ -85,6 +97,9 @@ export class AutorizacionesPorFechaComponent implements OnInit {
// Guardamos las fechas ya formateadas para reutilizarlas
this.fechaInicioApi = this.formatDateForInput(inicio);
this.fechaFinApi = this.formatDateForInput(fin);
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();
this.isLoading = true;
this.autorizaciones = [];
@ -96,7 +111,10 @@ export class AutorizacionesPorFechaComponent implements OnInit {
this.fechaInicioApi!,
this.fechaFinApi!,
this.limiteAutorizaciones,
0
0,
this.establecimientoFiltro || undefined,
this.ambitoFiltro || undefined,
this.ipsFiltro || undefined
)
.pipe(finalize(() => {
this.isLoading = false;
@ -284,6 +302,45 @@ export class AutorizacionesPorFechaComponent implements OnInit {
});
}
actualizarEstadoEntrega(autorizacion: any, estado: string): void {
if (!this.puedeGestionarEntrega || !autorizacion) {
return;
}
const numero = String(autorizacion.numero_autorizacion || '');
if (!numero) return;
if (this.actualizandoEntrega[numero]) {
return;
}
const estadoPrevio = String(autorizacion.estado_entrega || 'pendiente_entrega');
autorizacion.estado_entrega = estado;
this.actualizandoEntrega[numero] = true;
this.pacienteService
.actualizarEstadoEntrega(numero, estado)
.pipe(
finalize(() => {
this.actualizandoEntrega[numero] = false;
this.cdr.detectChanges();
})
)
.subscribe({
next: (resp) => {
const data = resp?.autorizacion;
if (data) {
autorizacion.estado_entrega = data.estado_entrega;
}
},
error: (err) => {
console.error(err);
autorizacion.estado_entrega = estadoPrevio;
this.errorMessage = err?.error?.error || 'Error actualizando estado de entrega.';
}
});
}
// ========= USUARIO =========
logout(): void {
this.authService.logout();
@ -309,8 +366,8 @@ export class AutorizacionesPorFechaComponent implements OnInit {
descargarTodosLosPdfs(): void {
this.limpiarMensajes();
if (!this.esAdmin) {
this.errorMessage = 'No tienes permisos para descargar todos los PDFs.';
if (!this.puedeDescargarMasivo) {
this.errorMessage = 'No tienes permisos para descargar los PDFs.';
return;
}
@ -326,8 +383,14 @@ export class AutorizacionesPorFechaComponent implements OnInit {
this.descargandoZip = true;
this.authService
.crearJobZipAutorizaciones(this.fechaInicioApi, this.fechaFinApi)
this.authService
.crearJobZipAutorizaciones(
this.fechaInicioApi,
this.fechaFinApi,
this.establecimientoFiltro || undefined,
this.ambitoFiltro || undefined,
this.ipsFiltro || undefined
)
.subscribe({
next: (job) => {
this.jobsService.pollJob(job.id).subscribe({
@ -439,6 +502,17 @@ export class AutorizacionesPorFechaComponent implements OnInit {
return tipo ? String(tipo) : '';
}
getAmbitoLabel(ambito: string | null | undefined): string {
const normalizado = String(ambito || '').toLowerCase();
if (normalizado === 'intramural') {
return 'Intramural';
}
if (normalizado === 'extramural') {
return 'Extramural';
}
return ambito ? String(ambito) : '';
}
getEstadoAutorizacionLabel(estado: string | null | undefined): string {
const normalizado = String(estado || 'pendiente').toLowerCase();
if (normalizado === 'autorizado') {
@ -450,6 +524,23 @@ export class AutorizacionesPorFechaComponent implements OnInit {
return 'Pendiente';
}
getEstadoEntregaLabel(estado: string | null | undefined): string {
const normalizado = String(estado || 'pendiente_entrega').toLowerCase();
if (normalizado === 'entregado') {
return 'Entregado';
}
if (normalizado === 'programado') {
return 'Programado';
}
if (normalizado === 'atendido') {
return 'Atendido';
}
if (normalizado === 'cancelado') {
return 'Cancelado';
}
return 'Pendiente por entrega';
}
getCoberturaLabel(autorizacion: any): string {
if (autorizacion?.cup_cubierto === true) {
return 'Cubre';
@ -528,7 +619,7 @@ export class AutorizacionesPorFechaComponent implements OnInit {
// ========= EXPORTAR A CSV SIMPLE =========
exportarAExcel(): void {
if (!this.esAdmin) {
if (!this.puedeDescargarMasivo) {
this.errorMessage = 'No tienes permisos para exportar.';
return;
}
@ -549,13 +640,19 @@ export class AutorizacionesPorFechaComponent implements OnInit {
'Tipo autorizacion',
'Tipo servicio',
'CUPS',
'CIE-10',
'Diagnostico',
'Ambito',
'Numero orden',
'Nivel',
'IPS',
'Municipio',
'Departamento',
'Autorizante',
'Solicitante',
'Establecimiento',
'Estado'
'Estado',
'Estado entrega'
];
const separator = ';';
@ -571,13 +668,19 @@ export class AutorizacionesPorFechaComponent implements OnInit {
this.csvValue(this.getTipoAutorizacionLabel(aut.tipo_autorizacion)),
this.csvValue(this.getTipoServicioLabel(aut.tipo_servicio)),
this.csvValue(aut.cup_codigo || ''),
this.csvValue(aut.cie10_codigo || ''),
this.csvValue(aut.cie10_descripcion || ''),
this.csvValue(this.getAmbitoLabel(aut.ambito_atencion)),
this.csvValue(aut.ambito_atencion === 'intramural' ? (aut.numero_orden || '') : ''),
this.csvValue(aut.cup_nivel || ''),
this.csvValue(aut.nombre_ips),
this.csvValue(aut.municipio),
this.csvValue(aut.departamento),
this.csvValue(aut.nombre_autorizante),
this.csvValue(aut.nombre_solicitante || ''),
this.csvValue(aut.nombre_establecimiento),
this.csvValue(this.getEstadoAutorizacionLabel(aut.estado_autorizacion))
this.csvValue(this.getEstadoAutorizacionLabel(aut.estado_autorizacion)),
this.csvValue(this.getEstadoEntregaLabel(aut.estado_entrega))
].join(separator)
)
].join('\n');

View File

@ -30,6 +30,30 @@
color: var(--color-text-main);
}
.file-field {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.file-field input[type='file'] {
padding: 6px 0;
}
.file-name {
font-size: 0.8rem;
color: var(--color-text-muted);
}
.file-actions {
flex: 1;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.form-row input:focus,
.form-row select:focus,
.form-row textarea:focus {

View File

@ -164,7 +164,11 @@
"
>
<label for="tipoServicio">Tipo de servicio:</label>
<select id="tipoServicio" [(ngModel)]="formAutorizacion.tipo_servicio">
<select
id="tipoServicio"
[(ngModel)]="formAutorizacion.tipo_servicio"
(change)="onTipoServicioChange()"
>
<option value="">-- Seleccione --</option>
<option value="brigadas">Brigadas</option>
<option value="ambulancias">Ambulancias</option>
@ -172,6 +176,90 @@
</select>
</div>
<div
class="form-row"
*ngIf="formAutorizacion.tipo_servicio === 'hospitalarios'"
>
<label for="historialClinico">Historial clinico (HC):</label>
<div class="file-field">
<input
id="historialClinico"
type="file"
accept=".pdf,application/pdf"
(change)="onHistorialClinicoChange($event)"
/>
<span class="file-name" *ngIf="archivoHistorialClinico">
{{ archivoHistorialClinico.name }}
</span>
</div>
</div>
<div
class="form-row"
*ngIf="formAutorizacion.tipo_servicio === 'hospitalarios'"
>
<label for="anexoHospitalario">Anexo:</label>
<div class="file-field">
<input
id="anexoHospitalario"
type="file"
accept=".pdf,application/pdf"
(change)="onAnexoChange($event)"
/>
<span class="file-name" *ngIf="archivoAnexo">
{{ archivoAnexo.name }}
</span>
</div>
</div>
<div
class="form-row"
*ngIf="formAutorizacion.tipo_servicio === 'hospitalarios'"
>
<label></label>
<div class="file-actions">
<button
type="button"
class="btn btn-secondary btn-sm"
(click)="autorrellenarDesdePdf()"
[disabled]="autorrellenandoPdf || (!archivoAnexo && !archivoHistorialClinico)"
>
Autorrellenar desde PDF
</button>
<span class="mini-status" *ngIf="autorrellenandoPdf">Leyendo PDF...</span>
</div>
</div>
<div class="status ok" *ngIf="autorrellenoInfo">
{{ autorrellenoInfo }}
</div>
<div class="status error" *ngIf="autorrellenoError">
{{ autorrellenoError }}
</div>
<div class="form-row">
<label for="ambitoAtencion">Ambito:</label>
<select
id="ambitoAtencion"
[(ngModel)]="formAutorizacion.ambito_atencion"
(change)="onAmbitoChange()"
>
<option value="">-- Seleccione --</option>
<option value="intramural">Intramural</option>
<option value="extramural">Extramural</option>
</select>
</div>
<div class="form-row" *ngIf="formAutorizacion.ambito_atencion === 'intramural'">
<label for="numeroOrden">Numero de orden:</label>
<input
id="numeroOrden"
type="text"
[(ngModel)]="formAutorizacion.numero_orden"
placeholder="Ej: 12345"
/>
</div>
<div class="form-row">
<label for="ips">IPS / Hospital:</label>
<div class="ips-field">
@ -285,6 +373,25 @@
{{ errorCups }}
</div>
<div class="form-row">
<label for="cie10Codigo">Codigo CIE-10:</label>
<input
id="cie10Codigo"
type="text"
[(ngModel)]="formAutorizacion.cie10_codigo"
placeholder="Ej: A00"
/>
</div>
<div class="form-row">
<label for="cie10Descripcion">Diagnostico:</label>
<textarea
id="cie10Descripcion"
rows="2"
[(ngModel)]="formAutorizacion.cie10_descripcion"
></textarea>
</div>
<div class="form-row">
<label for="fechaAut">Fecha autorización:</label>
<input
@ -335,6 +442,10 @@
</button>
</div>
<div class="status" *ngIf="subiendoArchivosHospitalarios">
Subiendo archivos hospitalarios...
</div>
<div class="status ok" *ngIf="mensajeAutorizacion">
{{ mensajeAutorizacion }}
</div>
@ -356,6 +467,7 @@
<th>Nivel</th>
<th>Tipo autorizacion</th>
<th>Tipo servicio</th>
<th>Ambito</th>
<th>Version</th>
<th>IPS</th>
<th>Autoriza</th>
@ -371,6 +483,7 @@
<td>{{ a.cup_nivel }}</td>
<td>{{ getTipoAutorizacionLabel(a.tipo_autorizacion) }}</td>
<td>{{ getTipoServicioLabel(a.tipo_servicio) }}</td>
<td>{{ getAmbitoLabel(a.ambito_atencion) }}</td>
<td class="version-cell">
<div class="version-stack">
<span>v{{ a.version || 1 }}</span>

View File

@ -4,7 +4,8 @@ import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AuthService } from '../../services/auth';
import { PacienteService, AutorizacionVersion, CupInfo } from '../../services/paciente';
import { finalize } from 'rxjs/operators';
import { finalize, switchMap, catchError, map } from 'rxjs/operators';
import { of } from 'rxjs';
import { AppHeaderComponent } from '../shared/app-header/app-header';
import { JobsService } from '../../services/jobs';
@ -51,8 +52,12 @@ export class AutorizacionesComponent {
fecha_autorizacion: '',
observacion: '',
cup_codigo: '',
cie10_codigo: '',
cie10_descripcion: '',
tipo_autorizacion: 'consultas_externas',
tipo_servicio: '',
ambito_atencion: '',
numero_orden: '',
};
guardandoAutorizacion = false;
@ -64,6 +69,12 @@ export class AutorizacionesComponent {
errorAutLista: string | null = null;
descargandoPdf = false;
archivoHistorialClinico: File | null = null;
archivoAnexo: File | null = null;
subiendoArchivosHospitalarios = false;
autorrellenandoPdf = false;
autorrellenoInfo: string | null = null;
autorrellenoError: string | null = null;
constructor(
private authService: AuthService,
@ -137,8 +148,12 @@ export class AutorizacionesComponent {
fecha_autorizacion: '',
observacion: '',
cup_codigo: '',
cie10_codigo: '',
cie10_descripcion: '',
tipo_autorizacion: 'consultas_externas',
tipo_servicio: '',
ambito_atencion: '',
numero_orden: '',
};
this.autorizacionesPaciente = [];
@ -153,6 +168,7 @@ export class AutorizacionesComponent {
this.cargarIps(p.interno, this.verMasIps);
this.cargarAutorizantes();
this.limpiarArchivosHospitalarios();
}
cerrarAutorizacion(): void {
@ -172,6 +188,7 @@ export class AutorizacionesComponent {
this.versionActualPorAutorizacion = {};
this.versionSeleccionada = {};
this.cargandoVersiones = {};
this.limpiarArchivosHospitalarios();
this.cdr.markForCheck();
}
@ -320,9 +337,174 @@ export class AutorizacionesComponent {
'brigadas_ambulancias_hospitalarios'
) {
this.formAutorizacion.tipo_servicio = '';
this.limpiarArchivosHospitalarios();
}
}
onTipoServicioChange(): void {
const servicio = String(this.formAutorizacion.tipo_servicio || '')
.trim()
.toLowerCase();
if (servicio !== 'hospitalarios') {
this.limpiarArchivosHospitalarios();
}
}
onAmbitoChange(): void {
const ambito = String(this.formAutorizacion.ambito_atencion || '')
.trim()
.toLowerCase();
if (ambito !== 'intramural') {
this.formAutorizacion.numero_orden = '';
}
}
onHistorialClinicoChange(event: Event): void {
this.setPdfFile(event, 'historial');
}
onAnexoChange(event: Event): void {
this.setPdfFile(event, 'anexo');
}
autorrellenarDesdePdf(): void {
const archivo = this.archivoAnexo || this.archivoHistorialClinico;
if (!archivo) {
this.autorrellenoError = 'Adjunta un PDF para autorrellenar.';
return;
}
this.autorrellenoInfo = null;
this.autorrellenoError = null;
this.autorrellenandoPdf = true;
this.pacienteService
.autorrellenarAutorizacionPdf(archivo)
.pipe(
finalize(() => {
this.autorrellenandoPdf = false;
this.cdr.markForCheck();
})
)
.subscribe({
next: (resp: any) => {
if (resp?.cup_codigo) {
this.formAutorizacion.cup_codigo = resp.cup_codigo;
this.buscarCups();
}
if (resp?.cie10_codigo) {
this.formAutorizacion.cie10_codigo = resp.cie10_codigo;
}
if (resp?.cie10_descripcion) {
this.formAutorizacion.cie10_descripcion = resp.cie10_descripcion;
}
const infoParts = [];
if (resp?.nombre_paciente) {
infoParts.push(`Paciente: ${resp.nombre_paciente}`);
}
if (resp?.numero_documento) {
infoParts.push(`Documento: ${resp.numero_documento}`);
}
if (resp?.formato) {
infoParts.push(`Formato: ${this.getFormatoPdfLabel(resp.formato)}`);
}
if (resp?.ocr_usado) {
infoParts.push('OCR usado');
}
const warnings = Array.isArray(resp?.warnings) ? resp.warnings : [];
if (warnings.length) {
infoParts.push(`Avisos: ${warnings.join(', ')}`);
}
this.autorrellenoInfo = infoParts.join(' | ') || 'PDF procesado.';
const docPdf = String(resp?.numero_documento || '').trim();
const docPaciente = String(this.pacienteSeleccionado?.numero_documento || '').trim();
if (docPdf && docPaciente && docPdf !== docPaciente) {
this.autorrellenoError =
'El documento del PDF no coincide con el paciente seleccionado.';
}
if (!this.autorrellenoError && warnings.includes('no_text_extracted')) {
if (resp?.ocr_disponible) {
this.autorrellenoError =
'No se pudo leer texto del PDF. Revisa que el archivo sea legible.';
} else {
this.autorrellenoError =
'No se pudo leer texto del PDF. OCR no disponible en el servidor.';
}
}
},
error: (err) => {
console.error(err);
this.autorrellenoError =
err?.error?.error || 'Error leyendo datos del PDF.';
},
});
}
private setPdfFile(event: Event, tipo: 'historial' | 'anexo'): void {
const input = event.target as HTMLInputElement | null;
const file = input?.files?.[0] || null;
if (!file) {
if (tipo === 'historial') {
this.archivoHistorialClinico = null;
} else {
this.archivoAnexo = null;
}
return;
}
if (!this.isPdfFile(file)) {
if (input) {
input.value = '';
}
this.errorAutorizacion = 'Solo se permiten archivos PDF.';
if (tipo === 'historial') {
this.archivoHistorialClinico = null;
} else {
this.archivoAnexo = null;
}
return;
}
if (tipo === 'historial') {
this.archivoHistorialClinico = file;
} else {
this.archivoAnexo = file;
}
}
private isPdfFile(file: File): boolean {
const nombre = String(file.name || '').toLowerCase();
const tipo = String(file.type || '').toLowerCase();
return tipo === 'application/pdf' || nombre.endsWith('.pdf');
}
private getFormatoPdfLabel(formato: string): string {
if (formato === 'ANEXO_TECNICO') {
return 'Anexo tecnico';
}
if (formato === 'ANEXO_URGENCIAS') {
return 'Anexo urgencias';
}
return 'Desconocido';
}
private limpiarArchivosHospitalarios(): void {
this.archivoHistorialClinico = null;
this.archivoAnexo = null;
this.resetAutorrelleno();
}
private resetAutorrelleno(): void {
this.autorrellenandoPdf = false;
this.autorrellenoInfo = null;
this.autorrellenoError = null;
}
getTipoAutorizacionLabel(tipo: string | null | undefined): string {
const normalizado = String(tipo || '').toLowerCase();
if (normalizado === 'consultas_externas') {
@ -348,6 +530,17 @@ export class AutorizacionesComponent {
return tipo ? String(tipo) : '';
}
getAmbitoLabel(ambito: string | null | undefined): string {
const normalizado = String(ambito || '').toLowerCase();
if (normalizado === 'intramural') {
return 'Intramural';
}
if (normalizado === 'extramural') {
return 'Extramural';
}
return ambito ? String(ambito) : '';
}
getEstadoAutorizacionLabel(estado: string | null | undefined): string {
const normalizado = String(estado || 'pendiente').toLowerCase();
if (normalizado === 'autorizado') {
@ -454,10 +647,15 @@ export class AutorizacionesComponent {
fecha_autorizacion: this.formatDateInput(autorizacion.fecha_autorizacion),
observacion: autorizacion.observacion || '',
cup_codigo: autorizacion.cup_codigo || '',
cie10_codigo: autorizacion.cie10_codigo || '',
cie10_descripcion: autorizacion.cie10_descripcion || '',
tipo_autorizacion: autorizacion.tipo_autorizacion || 'consultas_externas',
tipo_servicio: autorizacion.tipo_servicio || '',
ambito_atencion: autorizacion.ambito_atencion || '',
numero_orden: autorizacion.numero_orden || '',
};
this.cupSeleccionado = null;
this.limpiarArchivosHospitalarios();
this.onTipoAutorizacionChange();
this.onIpsChange();
@ -476,9 +674,14 @@ export class AutorizacionesComponent {
fecha_autorizacion: '',
observacion: '',
cup_codigo: '',
cie10_codigo: '',
cie10_descripcion: '',
tipo_autorizacion: 'consultas_externas',
tipo_servicio: '',
ambito_atencion: '',
numero_orden: '',
};
this.limpiarArchivosHospitalarios();
}
guardarAutorizacion(): void {
@ -501,6 +704,26 @@ export class AutorizacionesComponent {
this.errorAutorizacion = 'Debe seleccionar un CUPS.';
return;
}
if (!this.formAutorizacion.cie10_codigo) {
this.errorAutorizacion = 'Debe ingresar el codigo CIE-10.';
return;
}
if (!this.formAutorizacion.cie10_descripcion) {
this.errorAutorizacion = 'Debe ingresar el diagnostico.';
return;
}
if (!this.formAutorizacion.ambito_atencion) {
this.errorAutorizacion = 'Debe seleccionar el ambito (intramural o extramural).';
return;
}
const ambitoSeleccionado = String(this.formAutorizacion.ambito_atencion || '')
.trim()
.toLowerCase();
const numeroOrdenInput = String(this.formAutorizacion.numero_orden || '').trim();
if (ambitoSeleccionado === 'intramural' && !numeroOrdenInput) {
this.errorAutorizacion = 'Debe ingresar el numero de orden para intramural.';
return;
}
const tipoAutorizacion = String(
this.formAutorizacion.tipo_autorizacion || 'consultas_externas'
).toLowerCase();
@ -515,6 +738,21 @@ export class AutorizacionesComponent {
return;
}
const esHospitalario =
requiereServicio && String(tipoServicio).toLowerCase() === 'hospitalarios';
const requiereAdjuntos = esHospitalario && !this.autorizacionEditando;
const tieneAdjuntos =
!!this.archivoHistorialClinico || !!this.archivoAnexo;
if (
(requiereAdjuntos || tieneAdjuntos) &&
(!this.archivoHistorialClinico || !this.archivoAnexo)
) {
this.errorAutorizacion =
'Debe adjuntar historial clinico (HC) y anexo en hospitalarios.';
return;
}
const payload = {
interno: this.pacienteSeleccionado.interno,
id_ips: Number(this.formAutorizacion.id_ips),
@ -525,8 +763,15 @@ export class AutorizacionesComponent {
fecha_autorizacion:
this.formAutorizacion.fecha_autorizacion || undefined,
cup_codigo: this.formAutorizacion.cup_codigo,
cie10_codigo: this.formAutorizacion.cie10_codigo,
cie10_descripcion: this.formAutorizacion.cie10_descripcion,
tipo_autorizacion: tipoAutorizacion,
tipo_servicio: requiereServicio ? tipoServicio : undefined,
ambito_atencion: this.formAutorizacion.ambito_atencion,
numero_orden:
ambitoSeleccionado === 'intramural' && numeroOrdenInput
? numeroOrdenInput
: undefined,
};
this.guardandoAutorizacion = true;
@ -540,13 +785,36 @@ export class AutorizacionesComponent {
request$
.pipe(
switchMap((resp: any) => {
const numeroAutorizacion = resp?.numero_autorizacion;
if (
esHospitalario &&
this.archivoHistorialClinico &&
this.archivoAnexo &&
numeroAutorizacion
) {
this.subiendoArchivosHospitalarios = true;
return this.pacienteService
.subirArchivosHospitalarios(
numeroAutorizacion,
this.archivoHistorialClinico,
this.archivoAnexo
)
.pipe(
map(() => ({ resp, archivosOk: true, error: null })),
catchError((error) => of({ resp, archivosOk: false, error }))
);
}
return of({ resp, archivosOk: true, error: null });
}),
finalize(() => {
this.guardandoAutorizacion = false;
this.subiendoArchivosHospitalarios = false;
this.cdr.markForCheck();
})
)
.subscribe({
next: (resp: any) => {
next: ({ resp, archivosOk, error }) => {
if (this.autorizacionEditando) {
const versionTexto = resp?.version ? ` (v${resp.version})` : '';
this.mensajeAutorizacion = `Autorizacion ${resp.numero_autorizacion} actualizada${versionTexto}.`;
@ -555,6 +823,16 @@ export class AutorizacionesComponent {
} else {
this.mensajeAutorizacion = `Autorizacion N ${resp.numero_autorizacion} creada el ${resp.fecha_autorizacion}.`;
}
if (esHospitalario && this.archivoHistorialClinico && this.archivoAnexo) {
if (archivosOk) {
this.limpiarArchivosHospitalarios();
} else {
this.errorAutorizacion =
error?.error?.error ||
'Autorizacion guardada, pero fallo la carga de archivos hospitalarios.';
}
}
},
error: (err) => {
console.error(err);

View File

@ -76,6 +76,14 @@
<span class="summary-label">Sin IPS</span>
<span class="summary-value">{{ resumen.sin_ips || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Sin diagnostico</span>
<span class="summary-value">{{ resumen.sin_diagnostico || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Sin ambito</span>
<span class="summary-value">{{ resumen.sin_ambito || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">CUPS no cubiertos</span>
<span class="summary-value">{{ resumen.cups_no_cubiertos || 0 }}</span>

View File

@ -313,10 +313,22 @@ export class AuthService {
fechaInicio: string,
fechaFin: string,
limit = 500,
offset = 0
offset = 0,
establecimiento?: string,
ambito?: string,
ips?: string
): Observable<any[]> {
const headers = this.getAuthHeaders();
const params = { fecha_inicio: fechaInicio, fecha_fin: fechaFin, limit, offset };
const params: any = { fecha_inicio: fechaInicio, fecha_fin: fechaFin, limit, offset };
if (establecimiento) {
params.establecimiento = establecimiento;
}
if (ambito) {
params.ambito = ambito;
}
if (ips) {
params.ips = ips;
}
return this.http.get<any[]>(`${this.API_URL}/autorizaciones-por-fecha`, { headers, params });
}
@ -356,11 +368,27 @@ export class AuthService {
);
}
crearJobZipAutorizaciones(fechaInicio: string, fechaFin: string): Observable<JobResponse> {
crearJobZipAutorizaciones(
fechaInicio: string,
fechaFin: string,
establecimiento?: string,
ambito?: string,
ips?: string
): Observable<JobResponse> {
const headers = this.getAuthHeaders();
const payload: any = { fecha_inicio: fechaInicio, fecha_fin: fechaFin };
if (establecimiento) {
payload.establecimiento = establecimiento;
}
if (ambito) {
payload.ambito = ambito;
}
if (ips) {
payload.ips = ips;
}
return this.http.post<JobResponse>(
`${this.API_URL}/jobs/autorizaciones-zip`,
{ fecha_inicio: fechaInicio, fecha_fin: fechaFin },
payload,
{ headers }
);
}

View File

@ -25,6 +25,8 @@ export interface JobResult {
duplicados?: number | null;
creadas?: number | null;
sin_paciente?: number | null;
sin_diagnostico?: number | null;
sin_ambito?: number | null;
sin_cups?: number | null;
sin_ips?: number | null;
cups_no_cubiertos?: number | null;

View File

@ -67,8 +67,13 @@ export interface CrearAutorizacionPayload {
id_ips: number;
numero_documento_autorizante: number;
cup_codigo: string;
cie10_codigo?: string;
cie10_descripcion?: string;
tipo_autorizacion?: string;
tipo_servicio?: string;
ambito_atencion?: string;
numero_orden?: string;
estado_entrega?: string;
observacion?: string;
fecha_autorizacion?: string; // yyyy-MM-dd
}
@ -78,6 +83,11 @@ export interface RespuestaAutorizacion {
fecha_autorizacion: string;
version?: number;
estado_autorizacion?: string;
cie10_codigo?: string | null;
cie10_descripcion?: string | null;
ambito_atencion?: string | null;
numero_orden?: string | null;
estado_entrega?: string | null;
}
export interface AutorizacionListado {
@ -85,6 +95,8 @@ export interface AutorizacionListado {
fecha_autorizacion: string;
observacion: string | null;
cup_codigo?: string | null;
cie10_codigo?: string | null;
cie10_descripcion?: string | null;
cup_descripcion?: string | null;
cup_nivel?: string | null;
cup_especialidad?: string | null;
@ -92,6 +104,9 @@ export interface AutorizacionListado {
tipo_servicio?: string | null;
version?: number | null;
estado_autorizacion?: string | null;
ambito_atencion?: string | null;
numero_orden?: string | null;
estado_entrega?: string | null;
id_ips?: number | null;
numero_documento_autorizante?: number | null;
nombre_ips: string;
@ -99,6 +114,7 @@ export interface AutorizacionListado {
departamento?: string | null;
ips_tiene_convenio?: boolean | null;
nombre_autorizante: string;
nombre_solicitante?: string | null;
}
export interface AutorizacionVersion {
@ -229,6 +245,41 @@ export class PacienteService {
);
}
subirArchivosHospitalarios(
numeroAutorizacion: string,
historialClinico: File,
anexo: File
): Observable<any> {
const formData = new FormData();
formData.append('historial_clinico', historialClinico);
formData.append('anexo', anexo);
const token = this.authService.getToken();
const headers = new HttpHeaders({
Authorization: `Bearer ${token}`,
});
return this.http.post(
`${this.API_URL}/autorizaciones/${numeroAutorizacion}/archivos`,
formData,
{ headers }
);
}
autorrellenarAutorizacionPdf(archivo: File): Observable<any> {
const formData = new FormData();
formData.append('archivo', archivo);
const token = this.authService.getToken();
const headers = new HttpHeaders({
Authorization: `Bearer ${token}`,
});
return this.http.post(`${this.API_URL}/autorizaciones/autorrellenar`, formData, {
headers,
});
}
obtenerVersionesAutorizacion(numeroAutorizacion: string): Observable<AutorizacionVersionResponse> {
return this.http.get<AutorizacionVersionResponse>(
`${this.API_URL}/autorizaciones/${numeroAutorizacion}/versiones`,
@ -345,6 +396,14 @@ export class PacienteService {
);
}
actualizarEstadoEntrega(numeroAutorizacion: string, estado: string): Observable<any> {
return this.http.patch(
`${this.API_URL}/autorizaciones/${numeroAutorizacion}/estado-entrega`,
{ estado_entrega: estado },
{ headers: this.getAuthHeaders() }
);
}
actualizarEstadoAutorizacionesMasivo(
fechaInicio: string,
fechaFin: string,