excel-planner/dashboard/app.py
juan.pelaez 27d759ace8 Agregar integracion Excel-Planner con tablero RTC Sapian.
Puente Power Automate, servidor local del tablero HTML v7 y exportacion a Excel/TSV para monitoreo de proyectos en Microsoft Planner.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 13:44:38 -05:00

130 lines
4.3 KiB
Python

import pandas as pd
import plotly.express as px
import streamlit as st
from config import DATA_SOURCE, data_source_label, validate_config
from export.excel_exporter import export_to_excel
from services.data_factory import create_data_service
st.set_page_config(
page_title="Monitoreo Planner",
page_icon="📊",
layout="wide",
)
st.title("📊 Monitoreo de Proyectos — Microsoft Planner")
st.caption(f"Fuente de datos: **{data_source_label()}**")
missing = validate_config()
if missing:
st.error(f"Faltan variables en `.env`: **{', '.join(missing)}**")
if DATA_SOURCE == "powerautomate":
st.info("Configura `POWER_AUTOMATE_URL` con la URL del trigger HTTP de tu flujo. "
"Guia: `docs/POWER_AUTOMATE_FLUJO.md`")
else:
st.info("Copia `.env.example` a `.env` y completa las credenciales de Azure.")
st.stop()
@st.cache_data(ttl=300, show_spinner="Obteniendo datos...")
def load_projects():
service = create_data_service()
return service.get_all_projects()
if st.button("🔄 Actualizar datos", type="primary"):
load_projects.clear()
with st.spinner(f"Conectando via {data_source_label()}..."):
try:
projects = load_projects()
except Exception as e:
st.error(f"Error al obtener datos: {e}")
if DATA_SOURCE == "powerautomate":
st.info("Verifica que el flujo de Power Automate este **activado** y que la URL del trigger sea correcta.")
else:
st.info("Verifica permisos de la app en Azure (`Tasks.Read`, `User.Read`).")
st.stop()
if not projects:
st.warning("No se encontraron proyectos.")
st.stop()
all_tasks = []
for p in projects:
all_tasks.extend(p["tareas"])
total_tasks = len(all_tasks)
completed = sum(1 for t in all_tasks if t["porcentaje"] == 100)
overdue = sum(1 for t in all_tasks if t["vencida"])
progress_pct = round((completed / total_tasks) * 100, 1) if total_tasks else 0
col1, col2, col3, col4 = st.columns(4)
col1.metric("Proyectos", len(projects))
col2.metric("Tareas totales", total_tasks)
col3.metric("Avance global", f"{progress_pct}%")
col4.metric("Tareas vencidas", overdue)
st.divider()
col_filter1, col_filter2 = st.columns(2)
project_names = sorted({p["proyecto"] for p in projects})
selected_projects = col_filter1.multiselect("Filtrar por proyecto", project_names, default=project_names)
statuses = ["Por hacer", "En progreso", "Completada"]
selected_statuses = col_filter2.multiselect("Filtrar por estado", statuses, default=statuses)
filtered_projects = [p for p in projects if p["proyecto"] in selected_projects]
filtered_tasks = [
t for p in filtered_projects for t in p["tareas"]
if t["estado"] in selected_statuses
]
chart_col1, chart_col2 = st.columns(2)
summary_df = pd.DataFrame([{
"proyecto": p["proyecto"],
"completadas": p["completadas"],
"en_progreso": p["en_progreso"],
"por_hacer": p["por_hacer"],
} for p in filtered_projects])
if not summary_df.empty:
melted = summary_df.melt(id_vars="proyecto", var_name="estado", value_name="cantidad")
estado_labels = {
"completadas": "Completadas",
"en_progreso": "En progreso",
"por_hacer": "Por hacer",
}
melted["estado"] = melted["estado"].map(estado_labels)
fig_bar = px.bar(
melted, x="proyecto", y="cantidad", color="estado",
title="Tareas por proyecto y estado",
barmode="stack",
)
fig_bar.update_layout(xaxis_tickangle=-45)
chart_col1.plotly_chart(fig_bar, use_container_width=True)
fig_pie = px.pie(
summary_df, values="completadas", names="proyecto",
title="Tareas completadas por proyecto",
)
chart_col2.plotly_chart(fig_pie, use_container_width=True)
st.subheader("Detalle de tareas")
tasks_df = pd.DataFrame(filtered_tasks)
if not tasks_df.empty:
display_cols = ["proyecto", "tarea", "estado", "porcentaje", "prioridad",
"fecha_vencimiento", "asignados", "vencida"]
st.dataframe(tasks_df[display_cols], use_container_width=True, hide_index=True)
else:
st.info("No hay tareas con los filtros seleccionados.")
st.divider()
excel_bytes = export_to_excel(filtered_projects if filtered_projects else projects)
st.download_button(
label="📥 Descargar Excel",
data=excel_bytes,
file_name="reporte_planner.xlsx",
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
type="primary",
)