pm-dashboard/dashboard.py
José Manuel Gómez 663f058c00 Initial commit: pm-dashboard scaffold conectado a Airtable
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>
2026-07-03 19:02:41 +02:00

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,
)