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>
130 lines
4.3 KiB
Python
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",
|
|
)
|