Streamlit dashboard con pestañas Proyectos/Tareas/Clientes/Áreas leyendo directamente de la base de Airtable de gestión de proyectos. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
134 lines
4.9 KiB
Python
134 lines
4.9 KiB
Python
"""Dashboard interactivo para pm-dashboard — Streamlit."""
|
|
import streamlit as st
|
|
import pandas as pd
|
|
import sys
|
|
import os
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
|
|
from airtable_client import AirtableClient
|
|
|
|
st.set_page_config(
|
|
page_title="PM Dashboard",
|
|
layout="wide",
|
|
initial_sidebar_state="expanded",
|
|
)
|
|
|
|
|
|
@st.cache_data(ttl=300, show_spinner="Cargando datos de Airtable...")
|
|
def _load_data():
|
|
at = AirtableClient()
|
|
return {
|
|
"projects": at.get_projects(),
|
|
"tasks": at.get_tasks(),
|
|
"clients": at.get_clients(),
|
|
"areas": at.get_areas(),
|
|
}
|
|
|
|
|
|
data = _load_data()
|
|
|
|
client_name_by_id = {c["id"]: (c["display_name"] or c["company_name"]) for c in data["clients"]}
|
|
project_name_by_id = {p["id"]: p["project"] for p in data["projects"]}
|
|
area_name_by_id = {a["id"]: a["area"] for a in data["areas"]}
|
|
|
|
st.title("📊 PM Dashboard")
|
|
|
|
tab_proyectos, tab_tareas, tab_clientes, tab_areas = st.tabs(
|
|
["📁 Proyectos", "✅ Tareas", "👥 Clientes", "🗂️ Áreas"]
|
|
)
|
|
|
|
# ── Proyectos ─────────────────────────────────────────────────────────────── #
|
|
|
|
with tab_proyectos:
|
|
df = pd.DataFrame(data["projects"])
|
|
if df.empty:
|
|
st.info("No hay proyectos.")
|
|
else:
|
|
df["client"] = df["client_ids"].apply(
|
|
lambda ids: ", ".join(client_name_by_id.get(i, "") for i in ids)
|
|
)
|
|
|
|
col1, col2 = st.columns(2)
|
|
status_filter = col1.multiselect("Status", sorted(df["status"].dropna().unique()))
|
|
client_filter = col2.multiselect("Cliente", sorted(client_name_by_id.values()))
|
|
|
|
view = df.copy()
|
|
if status_filter:
|
|
view = view[view["status"].isin(status_filter)]
|
|
if client_filter:
|
|
view = view[view["client"].apply(lambda c: any(cf in c for cf in client_filter))]
|
|
|
|
st.dataframe(
|
|
view[["project", "status", "client", "project_lead", "kickoff_date", "due_date", "budget", "internal"]],
|
|
use_container_width=True,
|
|
hide_index=True,
|
|
)
|
|
st.caption(f"{len(view)} de {len(df)} proyectos")
|
|
|
|
# ── Tareas ────────────────────────────────────────────────────────────────── #
|
|
|
|
with tab_tareas:
|
|
df = pd.DataFrame(data["tasks"])
|
|
if df.empty:
|
|
st.info("No hay tareas.")
|
|
else:
|
|
df["project"] = df["project_ids"].apply(
|
|
lambda ids: ", ".join(project_name_by_id.get(i, "") for i in ids)
|
|
)
|
|
df["area"] = df["area_ids"].apply(
|
|
lambda ids: ", ".join(area_name_by_id.get(i, "") for i in ids)
|
|
)
|
|
df["assigned"] = df["assigned_to"].apply(lambda names: ", ".join(names))
|
|
|
|
col1, col2, col3 = st.columns(3)
|
|
status_filter = col1.multiselect("Status", sorted(df["status"].dropna().unique()))
|
|
prioridad_filter = col2.multiselect("Prioridad", sorted(df["prioridad"].dropna().unique()))
|
|
assigned_filter = col3.multiselect(
|
|
"Asignado a", sorted({n for names in df["assigned_to"] for n in names})
|
|
)
|
|
|
|
view = df.copy()
|
|
if status_filter:
|
|
view = view[view["status"].isin(status_filter)]
|
|
if prioridad_filter:
|
|
view = view[view["prioridad"].isin(prioridad_filter)]
|
|
if assigned_filter:
|
|
view = view[view["assigned"].apply(lambda a: any(af in a for af in assigned_filter))]
|
|
|
|
st.dataframe(
|
|
view[["task", "project", "area", "status", "prioridad", "assigned", "due_date", "horas_estimadas"]],
|
|
use_container_width=True,
|
|
hide_index=True,
|
|
)
|
|
st.caption(f"{len(view)} de {len(df)} tareas")
|
|
|
|
# ── Clientes ──────────────────────────────────────────────────────────────── #
|
|
|
|
with tab_clientes:
|
|
df = pd.DataFrame(data["clients"])
|
|
if df.empty:
|
|
st.info("No hay clientes.")
|
|
else:
|
|
df["n_proyectos"] = df["project_ids"].apply(len)
|
|
df["n_tareas"] = df["task_ids"].apply(len)
|
|
st.dataframe(
|
|
df[["display_name", "company_name", "n_proyectos", "n_tareas"]],
|
|
use_container_width=True,
|
|
hide_index=True,
|
|
)
|
|
|
|
# ── Áreas ─────────────────────────────────────────────────────────────────── #
|
|
|
|
with tab_areas:
|
|
df = pd.DataFrame(data["areas"])
|
|
if df.empty:
|
|
st.info("No hay áreas.")
|
|
else:
|
|
df["n_tareas"] = df["task_ids"].apply(len)
|
|
st.dataframe(
|
|
df[["area", "descripcion", "n_tareas"]],
|
|
use_container_width=True,
|
|
hide_index=True,
|
|
)
|