| """Utilidades de exportación a Excel y PDF.""" |
| import io |
| import os |
| from datetime import datetime |
| import pandas as pd |
|
|
| try: |
| from fpdf import FPDF |
| PDF_AVAILABLE = True |
| except ImportError: |
| PDF_AVAILABLE = False |
|
|
| try: |
| import openpyxl |
| EXCEL_AVAILABLE = True |
| except ImportError: |
| EXCEL_AVAILABLE = False |
|
|
|
|
| def _pdf_safe(s) -> str: |
| """La fuente core del PDF (Helvetica) solo soporta latin-1. |
| Quitamos emojis y reemplazamos caracteres no soportados (guiones largos, etc.).""" |
| if s is None: |
| return "" |
| s = (str(s) |
| .replace("–", "-").replace("—", "-").replace("•", "-") |
| .replace("✅", "[OK]").replace("👤", "(C)").replace("🆘", "").replace("…", "...")) |
| return s.encode("latin-1", "ignore").decode("latin-1").strip() |
|
|
|
|
| def _df_para_export(rows: list[dict]) -> pd.DataFrame: |
| if not rows: |
| return pd.DataFrame() |
| df = pd.DataFrame(rows) |
| cols_rename = { |
| "nombre": "Nombre", |
| "cedula": "Cédula", |
| "edad": "Edad", |
| "hospital": "Hospital", |
| "condicion": "Condición", |
| "descripcion": "Descripción (IA)", |
| "fecha_ingreso": "Fecha de ingreso", |
| "fecha_update": "Última actualización", |
| "verificado": "Verificado", |
| "contacto": "Contacto familiar", |
| "notas": "Notas", |
| "fuente": "Fuente del reporte", |
| } |
| df = df.rename(columns={k: v for k, v in cols_rename.items() if k in df.columns}) |
| df["Verificado"] = df["Verificado"].apply(lambda x: "Sí" if x else "No") |
| cols_orden = ["Nombre", "Cédula", "Edad", "Hospital", "Condición", |
| "Última actualización", "Verificado", "Contacto familiar", |
| "Notas", "Descripción (IA)", "Fuente del reporte", "Fecha de ingreso"] |
| cols_presentes = [c for c in cols_orden if c in df.columns] |
| return df[cols_presentes] |
|
|
|
|
| def exportar_excel(rows: list[dict]) -> str | None: |
| if not rows: |
| return None |
| df = _df_para_export(rows) |
| ts = datetime.now().strftime("%Y%m%d_%H%M%S") |
| path = f"/tmp/busqueda_familiar_{ts}.xlsx" |
| with pd.ExcelWriter(path, engine="openpyxl") as writer: |
| df.to_excel(writer, index=False, sheet_name="Personas") |
| ws = writer.sheets["Personas"] |
| |
| anchos = {"Nombre": 28, "Cédula": 14, "Edad": 6, "Hospital": 32, |
| "Condición": 16, "Última actualización": 20, "Verificado": 10, |
| "Contacto familiar": 18, "Notas": 30, "Descripción (IA)": 40, |
| "Fuente del reporte": 18, "Fecha de ingreso": 20} |
| for i, col in enumerate(df.columns, 1): |
| ws.column_dimensions[ws.cell(1, i).column_letter].width = anchos.get(col, 15) |
| return path |
|
|
|
|
| class _PDF(FPDF): |
| def __init__(self, titulo: str): |
| super().__init__(orientation="L", unit="mm", format="A4") |
| self._titulo = titulo |
|
|
| def header(self): |
| self.set_font("Helvetica", "B", 13) |
| self.set_fill_color(185, 28, 28) |
| self.set_text_color(255, 255, 255) |
| self.cell(0, 10, _pdf_safe(self._titulo), align="C", fill=True, new_x="LMARGIN", new_y="NEXT") |
| self.set_text_color(0, 0, 0) |
| self.set_font("Helvetica", size=8) |
| self.cell(0, 6, _pdf_safe(f"Generado: {datetime.now().strftime('%d/%m/%Y %H:%M')}"), |
| align="R", new_x="LMARGIN", new_y="NEXT") |
| self.ln(2) |
|
|
| def footer(self): |
| self.set_y(-12) |
| self.set_font("Helvetica", "I", 8) |
| self.set_text_color(120, 120, 120) |
| self.cell(0, 6, _pdf_safe(f"Plataforma Buscador Familiar - Terremoto Venezuela | Pag. {self.page_no()}"), |
| align="C") |
|
|
|
|
| def exportar_pdf(rows: list[dict]) -> str | None: |
| if not rows or not PDF_AVAILABLE: |
| return None |
|
|
| df = _df_para_export(rows) |
|
|
| |
| cols_pdf = ["Nombre", "Cédula", "Edad", "Hospital", "Condición", |
| "Última actualización", "Verificado"] |
| cols_pdf = [c for c in cols_pdf if c in df.columns] |
|
|
| anchos_pdf = { |
| "Nombre": 55, "Cédula": 26, "Edad": 12, "Hospital": 60, |
| "Condición": 28, "Última actualización": 36, "Verificado": 18, |
| } |
|
|
| pdf = _PDF("Busqueda de Familiares - Terremoto Venezuela") |
| pdf.add_page() |
|
|
| |
| pdf.set_font("Helvetica", "B", 9) |
| pdf.set_fill_color(220, 38, 38) |
| pdf.set_text_color(255, 255, 255) |
| for col in cols_pdf: |
| pdf.cell(anchos_pdf.get(col, 30), 8, _pdf_safe(col), border=1, fill=True, align="C") |
| pdf.ln() |
|
|
| |
| pdf.set_font("Helvetica", size=8) |
| pdf.set_text_color(0, 0, 0) |
| fill = False |
| for _, row in df.iterrows(): |
| pdf.set_fill_color(255, 235, 235) if fill else pdf.set_fill_color(255, 255, 255) |
| for col in cols_pdf: |
| val = _pdf_safe(row.get(col, "")) |
| if len(val) > 35: |
| val = val[:33] + "..." |
| pdf.cell(anchos_pdf.get(col, 30), 7, val, border=1, fill=True) |
| pdf.ln() |
| fill = not fill |
|
|
| |
| pdf.ln(4) |
| pdf.set_font("Helvetica", "I", 7) |
| pdf.set_text_color(100, 100, 100) |
| pdf.multi_cell(0, 5, _pdf_safe( |
| "AVISO: Esta informacion es de caracter orientativo. Verifique siempre con el personal " |
| "del hospital. [OK] = Verificado por personal medico - No = Reportado por ciudadano.")) |
|
|
| ts = datetime.now().strftime("%Y%m%d_%H%M%S") |
| path = f"/tmp/busqueda_familiar_{ts}.pdf" |
| pdf.output(path) |
| return path |
|
|