Añade login con Google y CRUD de proyectos/tareas
Restringe el acceso a colaboradores conocidos en Airtable, añade una vista de tareas propias filtrable por fecha, y permite crear, editar y borrar proyectos y tareas desde el dashboard. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
663f058c00
commit
7f0b08d3dd
15
.streamlit/secrets.toml.example
Normal file
15
.streamlit/secrets.toml.example
Normal file
@ -0,0 +1,15 @@
|
||||
# Copia este archivo a .streamlit/secrets.toml (ya está en .gitignore) y
|
||||
# rellena los valores reales de tu OAuth Client de Google Cloud Console.
|
||||
#
|
||||
# Pasos en Google Cloud Console:
|
||||
# 1. Crea/configura la pantalla de consentimiento OAuth ("Google Auth Platform").
|
||||
# 2. Crea credenciales -> OAuth client ID -> tipo "Web application".
|
||||
# 3. En "Authorized redirect URIs" añade exactamente la redirect_uri de abajo
|
||||
# (debe terminar en /oauth2callback). Para producción, usa el dominio real.
|
||||
|
||||
[auth]
|
||||
redirect_uri = "http://localhost:8501/oauth2callback"
|
||||
cookie_secret = "cambia-esto-por-una-cadena-aleatoria-larga"
|
||||
client_id = "tu-client-id.apps.googleusercontent.com"
|
||||
client_secret = "tu-client-secret"
|
||||
server_metadata_url = "https://accounts.google.com/.well-known/openid-configuration"
|
||||
@ -2,16 +2,20 @@ from pyairtable import Api
|
||||
import config
|
||||
|
||||
|
||||
def _collaborator_name(value):
|
||||
def _collaborator(value):
|
||||
if isinstance(value, dict):
|
||||
return value.get("name") or value.get("email") or ""
|
||||
return ""
|
||||
return {
|
||||
"id": value.get("id", ""),
|
||||
"name": value.get("name") or value.get("email") or "",
|
||||
"email": value.get("email", ""),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def _collaborator_names(values):
|
||||
def _collaborators(values):
|
||||
if not values:
|
||||
return []
|
||||
return [_collaborator_name(v) for v in values]
|
||||
return [c for c in (_collaborator(v) for v in values) if c]
|
||||
|
||||
|
||||
class AirtableClient:
|
||||
@ -32,14 +36,18 @@ class AirtableClient:
|
||||
result = []
|
||||
for r in records:
|
||||
f = r["fields"]
|
||||
project_lead = _collaborator(f.get("Project lead"))
|
||||
working_team = _collaborators(f.get("Working team"))
|
||||
result.append({
|
||||
"id": r["id"],
|
||||
"project": f.get("Project", ""),
|
||||
"tags": f.get("Tags", []),
|
||||
"status": f.get("Status", ""),
|
||||
"client_ids": f.get("Client", []),
|
||||
"project_lead": _collaborator_name(f.get("Project lead")),
|
||||
"working_team": _collaborator_names(f.get("Working team")),
|
||||
"project_lead": project_lead,
|
||||
"project_lead_name": project_lead["name"] if project_lead else "",
|
||||
"working_team": working_team,
|
||||
"working_team_names": [c["name"] for c in working_team],
|
||||
"kickoff_date": f.get("Kickoff date"),
|
||||
"due_date": f.get("Due date"),
|
||||
"budget": f.get("Budget", 0),
|
||||
@ -59,13 +67,15 @@ class AirtableClient:
|
||||
result = []
|
||||
for r in records:
|
||||
f = r["fields"]
|
||||
assigned_to = _collaborators(f.get("Assigned to"))
|
||||
result.append({
|
||||
"id": r["id"],
|
||||
"task": f.get("Task", ""),
|
||||
"project_ids": f.get("Project", []),
|
||||
"area_ids": f.get("Area", []),
|
||||
"status": f.get("Status Task", ""),
|
||||
"assigned_to": _collaborator_names(f.get("Assigned to")),
|
||||
"assigned_to": assigned_to,
|
||||
"assigned_to_names": [c["name"] for c in assigned_to],
|
||||
"kick_off": f.get("Kick off"),
|
||||
"due_date": f.get("Due date"),
|
||||
"days_to_complete": f.get("Days to complete"),
|
||||
@ -103,3 +113,40 @@ class AirtableClient:
|
||||
"task_ids": f.get("Tasks", []),
|
||||
})
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_known_collaborators(projects: list[dict], tasks: list[dict]) -> dict:
|
||||
"""Colaboradores conocidos por haber aparecido en algún proyecto o tarea, indexados por email."""
|
||||
known = {}
|
||||
for p in projects:
|
||||
candidates = list(p["working_team"])
|
||||
if p["project_lead"]:
|
||||
candidates.append(p["project_lead"])
|
||||
for c in candidates:
|
||||
if c["email"]:
|
||||
known[c["email"]] = c
|
||||
for t in tasks:
|
||||
for c in t["assigned_to"]:
|
||||
if c["email"]:
|
||||
known[c["email"]] = c
|
||||
return known
|
||||
|
||||
# ── Escritura ─────────────────────────────────────────────────────────── #
|
||||
|
||||
def create_project(self, fields: dict) -> dict:
|
||||
return self.projects.create(fields, typecast=True)
|
||||
|
||||
def update_project(self, record_id: str, fields: dict) -> dict:
|
||||
return self.projects.update(record_id, fields, typecast=True)
|
||||
|
||||
def delete_project(self, record_id: str) -> None:
|
||||
self.projects.delete(record_id)
|
||||
|
||||
def create_task(self, fields: dict) -> dict:
|
||||
return self.tasks.create(fields, typecast=True)
|
||||
|
||||
def update_task(self, record_id: str, fields: dict) -> dict:
|
||||
return self.tasks.update(record_id, fields, typecast=True)
|
||||
|
||||
def delete_task(self, record_id: str) -> None:
|
||||
self.tasks.delete(record_id)
|
||||
|
||||
318
dashboard.py
318
dashboard.py
@ -14,10 +14,38 @@ st.set_page_config(
|
||||
initial_sidebar_state="expanded",
|
||||
)
|
||||
|
||||
# ── Autenticación ─────────────────────────────────────────────────────────── #
|
||||
|
||||
try:
|
||||
_auth_configured = "auth" in st.secrets
|
||||
except Exception:
|
||||
_auth_configured = False
|
||||
|
||||
if not _auth_configured:
|
||||
st.title("📊 PM Dashboard")
|
||||
st.error(
|
||||
"Falta configurar el login. Copia `.streamlit/secrets.toml.example` a "
|
||||
"`.streamlit/secrets.toml` y rellena las credenciales de tu OAuth Client de Google "
|
||||
"(ver instrucciones dentro del archivo)."
|
||||
)
|
||||
st.stop()
|
||||
|
||||
if not st.user.is_logged_in:
|
||||
st.title("📊 PM Dashboard")
|
||||
st.button("Iniciar sesión con Google", on_click=st.login)
|
||||
st.stop()
|
||||
|
||||
|
||||
@st.cache_resource
|
||||
def _get_client():
|
||||
return AirtableClient()
|
||||
|
||||
|
||||
at = _get_client()
|
||||
|
||||
|
||||
@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(),
|
||||
@ -27,17 +55,99 @@ def _load_data():
|
||||
|
||||
|
||||
data = _load_data()
|
||||
known_collaborators = AirtableClient.get_known_collaborators(data["projects"], data["tasks"])
|
||||
|
||||
if st.user.email not in known_collaborators:
|
||||
st.error(
|
||||
f"Tu email ({st.user.email}) no está registrado como colaborador en Airtable. "
|
||||
"Pide a un administrador que te asigne a un proyecto o tarea primero."
|
||||
)
|
||||
st.button("Cerrar sesión", on_click=st.logout)
|
||||
st.stop()
|
||||
|
||||
st.sidebar.write(f"👤 {st.user.name or st.user.email}")
|
||||
st.sidebar.button("Cerrar sesión", on_click=st.logout)
|
||||
|
||||
client_name_by_id = {c["id"]: (c["display_name"] or c["company_name"]) for c in data["clients"]}
|
||||
client_id_by_name = {v: k for k, v in client_name_by_id.items()}
|
||||
project_name_by_id = {p["id"]: p["project"] for p in data["projects"]}
|
||||
project_id_by_name = {v: k for k, v in project_name_by_id.items()}
|
||||
area_name_by_id = {a["id"]: a["area"] for a in data["areas"]}
|
||||
area_id_by_name = {v: k for k, v in area_name_by_id.items()}
|
||||
|
||||
collaborator_labels = {
|
||||
f"{c['name']} <{email}>": email for email, c in sorted(known_collaborators.items())
|
||||
}
|
||||
NONE_OPTION = "(ninguno)"
|
||||
|
||||
|
||||
def _collaborator_fields(emails):
|
||||
return [{"email": e} for e in emails]
|
||||
|
||||
|
||||
def _parse_date(value):
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
return pd.to_datetime(value).date()
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def _confirm_delete(key_prefix, label, on_confirm):
|
||||
confirmed = st.checkbox(f"Confirmar borrado de «{label}»", key=f"{key_prefix}_confirm")
|
||||
if st.button("🗑️ Borrar", key=f"{key_prefix}_delete_btn", disabled=not confirmed):
|
||||
on_confirm()
|
||||
st.cache_data.clear()
|
||||
st.success("Borrado.")
|
||||
st.rerun()
|
||||
|
||||
|
||||
st.title("📊 PM Dashboard")
|
||||
|
||||
tab_proyectos, tab_tareas, tab_clientes, tab_areas = st.tabs(
|
||||
["📁 Proyectos", "✅ Tareas", "👥 Clientes", "🗂️ Áreas"]
|
||||
tab_mis_tareas, tab_proyectos, tab_tareas, tab_clientes, tab_areas = st.tabs(
|
||||
["🙋 Mis tareas", "📁 Proyectos", "✅ Tareas", "👥 Clientes", "🗂️ Áreas"]
|
||||
)
|
||||
|
||||
# ── Mis tareas ────────────────────────────────────────────────────────────── #
|
||||
|
||||
with tab_mis_tareas:
|
||||
df = pd.DataFrame(data["tasks"])
|
||||
if df.empty:
|
||||
st.info("No hay tareas.")
|
||||
else:
|
||||
mine = df[
|
||||
df["assigned_to"].apply(lambda people: any(p["email"] == st.user.email for p in people))
|
||||
].copy()
|
||||
if mine.empty:
|
||||
st.info("No tienes tareas asignadas.")
|
||||
else:
|
||||
mine["project"] = mine["project_ids"].apply(
|
||||
lambda ids: ", ".join(project_name_by_id.get(i, "") for i in ids)
|
||||
)
|
||||
mine["area"] = mine["area_ids"].apply(
|
||||
lambda ids: ", ".join(area_name_by_id.get(i, "") for i in ids)
|
||||
)
|
||||
mine["due_date_parsed"] = mine["due_date"].apply(_parse_date)
|
||||
|
||||
dated = mine[mine["due_date_parsed"].notna()]
|
||||
if not dated.empty:
|
||||
min_d, max_d = dated["due_date_parsed"].min(), dated["due_date_parsed"].max()
|
||||
date_range = st.date_input("Rango de vencimiento (Due date)", value=(min_d, max_d))
|
||||
if isinstance(date_range, tuple) and len(date_range) == 2:
|
||||
start, end = date_range
|
||||
mine = mine[
|
||||
mine["due_date_parsed"].isna()
|
||||
| ((mine["due_date_parsed"] >= start) & (mine["due_date_parsed"] <= end))
|
||||
]
|
||||
|
||||
st.dataframe(
|
||||
mine[["task", "project", "area", "status", "prioridad", "due_date", "horas_estimadas"]],
|
||||
use_container_width=True,
|
||||
hide_index=True,
|
||||
)
|
||||
st.caption(f"{len(mine)} tareas asignadas a ti")
|
||||
|
||||
# ── Proyectos ─────────────────────────────────────────────────────────────── #
|
||||
|
||||
with tab_proyectos:
|
||||
@ -50,21 +160,106 @@ with tab_proyectos:
|
||||
)
|
||||
|
||||
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()))
|
||||
status_options = sorted(df["status"].dropna().unique())
|
||||
status_filter = col1.multiselect("Status", status_options, key="proj_status_filter")
|
||||
client_filter = col2.multiselect("Cliente", sorted(client_name_by_id.values()), key="proj_client_filter")
|
||||
|
||||
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))]
|
||||
view = view.reset_index(drop=True)
|
||||
|
||||
st.dataframe(
|
||||
view[["project", "status", "client", "project_lead", "kickoff_date", "due_date", "budget", "internal"]],
|
||||
selection = st.dataframe(
|
||||
view[["project", "status", "client", "project_lead_name", "kickoff_date", "due_date", "budget", "internal"]],
|
||||
use_container_width=True,
|
||||
hide_index=True,
|
||||
on_select="rerun",
|
||||
selection_mode="single-row",
|
||||
key="proj_table",
|
||||
)
|
||||
st.caption(f"{len(view)} de {len(df)} proyectos")
|
||||
st.caption(f"{len(view)} de {len(df)} proyectos — selecciona una fila para editarla o borrarla")
|
||||
|
||||
with st.expander("➕ Nuevo proyecto"):
|
||||
with st.form("new_project_form", clear_on_submit=True):
|
||||
name = st.text_input("Project")
|
||||
status = st.selectbox("Status", options=status_options + ["(otro)"])
|
||||
if status == "(otro)":
|
||||
status = st.text_input("Nuevo status")
|
||||
client_sel = st.multiselect("Cliente", options=sorted(client_id_by_name))
|
||||
lead_sel = st.selectbox("Project lead", options=[NONE_OPTION] + sorted(collaborator_labels))
|
||||
team_sel = st.multiselect("Working team", options=sorted(collaborator_labels))
|
||||
kickoff = st.date_input("Kickoff date", value=None)
|
||||
due = st.date_input("Due date", value=None)
|
||||
budget = st.number_input("Budget", min_value=0.0, step=100.0)
|
||||
internal = st.checkbox("Internal")
|
||||
code = st.text_input("Code")
|
||||
submitted = st.form_submit_button("Crear proyecto")
|
||||
if submitted:
|
||||
if not name:
|
||||
st.error("El nombre del proyecto es obligatorio.")
|
||||
else:
|
||||
fields = {"Project": name, "Status": status, "Internal": internal, "Code": code}
|
||||
if client_sel:
|
||||
fields["Client"] = [client_id_by_name[c] for c in client_sel]
|
||||
if lead_sel != NONE_OPTION:
|
||||
fields["Project lead"] = {"email": collaborator_labels[lead_sel]}
|
||||
if team_sel:
|
||||
fields["Working team"] = _collaborator_fields([collaborator_labels[t] for t in team_sel])
|
||||
if kickoff:
|
||||
fields["Kickoff date"] = kickoff.isoformat()
|
||||
if due:
|
||||
fields["Due date"] = due.isoformat()
|
||||
if budget:
|
||||
fields["Budget"] = budget
|
||||
at.create_project(fields)
|
||||
st.cache_data.clear()
|
||||
st.success("Proyecto creado.")
|
||||
st.rerun()
|
||||
|
||||
selected_rows = selection.selection.rows if selection and selection.selection else []
|
||||
if selected_rows:
|
||||
sel = view.iloc[selected_rows[0]]
|
||||
st.markdown(f"#### ✏️ Editar «{sel['project']}»")
|
||||
with st.form(f"edit_project_{sel['id']}"):
|
||||
name = st.text_input("Project", value=sel["project"])
|
||||
status_idx = status_options.index(sel["status"]) if sel["status"] in status_options else 0
|
||||
status = st.selectbox("Status", options=status_options, index=status_idx)
|
||||
current_client_names = [client_name_by_id.get(i) for i in sel["client_ids"] if client_name_by_id.get(i)]
|
||||
client_sel = st.multiselect("Cliente", options=sorted(client_id_by_name), default=current_client_names)
|
||||
lead_email = sel["project_lead"]["email"] if sel["project_lead"] else None
|
||||
lead_options = [NONE_OPTION] + sorted(collaborator_labels)
|
||||
current_lead_label = next((lbl for lbl, e in collaborator_labels.items() if e == lead_email), NONE_OPTION)
|
||||
lead_sel = st.selectbox("Project lead", options=lead_options, index=lead_options.index(current_lead_label))
|
||||
team_emails = {c["email"] for c in sel["working_team"]}
|
||||
current_team_labels = [lbl for lbl, e in collaborator_labels.items() if e in team_emails]
|
||||
team_sel = st.multiselect("Working team", options=sorted(collaborator_labels), default=current_team_labels)
|
||||
kickoff = st.date_input("Kickoff date", value=_parse_date(sel["kickoff_date"]))
|
||||
due = st.date_input("Due date", value=_parse_date(sel["due_date"]))
|
||||
budget = st.number_input("Budget", min_value=0.0, step=100.0, value=float(sel["budget"] or 0))
|
||||
internal = st.checkbox("Internal", value=bool(sel["internal"]))
|
||||
code = st.text_input("Code", value=sel["code"])
|
||||
save = st.form_submit_button("💾 Guardar cambios")
|
||||
if save:
|
||||
fields = {
|
||||
"Project": name,
|
||||
"Status": status,
|
||||
"Client": [client_id_by_name[c] for c in client_sel],
|
||||
"Project lead": {"email": collaborator_labels[lead_sel]} if lead_sel != NONE_OPTION else [],
|
||||
"Working team": _collaborator_fields([collaborator_labels[t] for t in team_sel]),
|
||||
"Kickoff date": kickoff.isoformat() if kickoff else None,
|
||||
"Due date": due.isoformat() if due else None,
|
||||
"Budget": budget,
|
||||
"Internal": internal,
|
||||
"Code": code,
|
||||
}
|
||||
at.update_project(sel["id"], fields)
|
||||
st.cache_data.clear()
|
||||
st.success("Proyecto actualizado.")
|
||||
st.rerun()
|
||||
|
||||
_confirm_delete(f"proj_{sel['id']}", sel["project"], lambda rid=sel["id"]: at.delete_project(rid))
|
||||
|
||||
# ── Tareas ────────────────────────────────────────────────────────────────── #
|
||||
|
||||
@ -79,13 +274,15 @@ with tab_tareas:
|
||||
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))
|
||||
df["assigned"] = df["assigned_to_names"].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()))
|
||||
status_options = sorted(df["status"].dropna().unique())
|
||||
prioridad_options = sorted(df["prioridad"].dropna().unique())
|
||||
status_filter = col1.multiselect("Status", status_options, key="task_status_filter")
|
||||
prioridad_filter = col2.multiselect("Prioridad", prioridad_options, key="task_prioridad_filter")
|
||||
assigned_filter = col3.multiselect(
|
||||
"Asignado a", sorted({n for names in df["assigned_to"] for n in names})
|
||||
"Asignado a", sorted({n for names in df["assigned_to_names"] for n in names}), key="task_assigned_filter"
|
||||
)
|
||||
|
||||
view = df.copy()
|
||||
@ -95,13 +292,106 @@ with tab_tareas:
|
||||
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))]
|
||||
view = view.reset_index(drop=True)
|
||||
|
||||
st.dataframe(
|
||||
selection = st.dataframe(
|
||||
view[["task", "project", "area", "status", "prioridad", "assigned", "due_date", "horas_estimadas"]],
|
||||
use_container_width=True,
|
||||
hide_index=True,
|
||||
on_select="rerun",
|
||||
selection_mode="single-row",
|
||||
key="task_table",
|
||||
)
|
||||
st.caption(f"{len(view)} de {len(df)} tareas")
|
||||
st.caption(f"{len(view)} de {len(df)} tareas — selecciona una fila para editarla o borrarla")
|
||||
|
||||
with st.expander("➕ Nueva tarea"):
|
||||
with st.form("new_task_form", clear_on_submit=True):
|
||||
task_name = st.text_input("Task")
|
||||
project_sel = st.multiselect("Project", options=sorted(project_id_by_name))
|
||||
area_sel = st.multiselect("Area", options=sorted(area_id_by_name))
|
||||
status = st.selectbox("Status Task", options=status_options + ["(otro)"])
|
||||
if status == "(otro)":
|
||||
status = st.text_input("Nuevo status")
|
||||
prioridad = st.selectbox("Prioridad", options=prioridad_options + ["(otro)"])
|
||||
if prioridad == "(otro)":
|
||||
prioridad = st.text_input("Nueva prioridad")
|
||||
assigned_sel = st.multiselect("Assigned to", options=sorted(collaborator_labels))
|
||||
kick_off = st.date_input("Kick off", value=None)
|
||||
due = st.date_input("Due date", value=None)
|
||||
horas = st.number_input("Horas estimadas", min_value=0.0, step=1.0)
|
||||
interno_externo = st.text_input("INTERNO/EXTERNO")
|
||||
recurrencia = st.text_input("Recurrencia")
|
||||
submitted = st.form_submit_button("Crear tarea")
|
||||
if submitted:
|
||||
if not task_name:
|
||||
st.error("El nombre de la tarea es obligatorio.")
|
||||
else:
|
||||
fields = {
|
||||
"Task": task_name,
|
||||
"Status Task": status,
|
||||
"Prioridad": prioridad,
|
||||
"Horas estimadas": horas,
|
||||
"INTERNO/EXTERNO": interno_externo,
|
||||
"Recurrencia": recurrencia,
|
||||
}
|
||||
if project_sel:
|
||||
fields["Project"] = [project_id_by_name[p] for p in project_sel]
|
||||
if area_sel:
|
||||
fields["Area"] = [area_id_by_name[a] for a in area_sel]
|
||||
if assigned_sel:
|
||||
fields["Assigned to"] = _collaborator_fields([collaborator_labels[a] for a in assigned_sel])
|
||||
if kick_off:
|
||||
fields["Kick off"] = kick_off.isoformat()
|
||||
if due:
|
||||
fields["Due date"] = due.isoformat()
|
||||
at.create_task(fields)
|
||||
st.cache_data.clear()
|
||||
st.success("Tarea creada.")
|
||||
st.rerun()
|
||||
|
||||
selected_rows = selection.selection.rows if selection and selection.selection else []
|
||||
if selected_rows:
|
||||
sel = view.iloc[selected_rows[0]]
|
||||
st.markdown(f"#### ✏️ Editar «{sel['task']}»")
|
||||
with st.form(f"edit_task_{sel['id']}"):
|
||||
task_name = st.text_input("Task", value=sel["task"])
|
||||
current_project_names = [project_name_by_id.get(i) for i in sel["project_ids"] if project_name_by_id.get(i)]
|
||||
project_sel = st.multiselect("Project", options=sorted(project_id_by_name), default=current_project_names)
|
||||
current_area_names = [area_name_by_id.get(i) for i in sel["area_ids"] if area_name_by_id.get(i)]
|
||||
area_sel = st.multiselect("Area", options=sorted(area_id_by_name), default=current_area_names)
|
||||
status_idx = status_options.index(sel["status"]) if sel["status"] in status_options else 0
|
||||
status = st.selectbox("Status Task", options=status_options, index=status_idx)
|
||||
prioridad_idx = prioridad_options.index(sel["prioridad"]) if sel["prioridad"] in prioridad_options else 0
|
||||
prioridad = st.selectbox("Prioridad", options=prioridad_options, index=prioridad_idx)
|
||||
assigned_emails = {c["email"] for c in sel["assigned_to"]}
|
||||
current_assigned_labels = [lbl for lbl, e in collaborator_labels.items() if e in assigned_emails]
|
||||
assigned_sel = st.multiselect("Assigned to", options=sorted(collaborator_labels), default=current_assigned_labels)
|
||||
kick_off = st.date_input("Kick off", value=_parse_date(sel["kick_off"]))
|
||||
due = st.date_input("Due date", value=_parse_date(sel["due_date"]))
|
||||
horas = st.number_input("Horas estimadas", min_value=0.0, step=1.0, value=float(sel["horas_estimadas"] or 0))
|
||||
interno_externo = st.text_input("INTERNO/EXTERNO", value=sel["interno_externo"])
|
||||
recurrencia = st.text_input("Recurrencia", value=sel["recurrencia"])
|
||||
save = st.form_submit_button("💾 Guardar cambios")
|
||||
if save:
|
||||
fields = {
|
||||
"Task": task_name,
|
||||
"Project": [project_id_by_name[p] for p in project_sel],
|
||||
"Area": [area_id_by_name[a] for a in area_sel],
|
||||
"Status Task": status,
|
||||
"Prioridad": prioridad,
|
||||
"Assigned to": _collaborator_fields([collaborator_labels[a] for a in assigned_sel]),
|
||||
"Kick off": kick_off.isoformat() if kick_off else None,
|
||||
"Due date": due.isoformat() if due else None,
|
||||
"Horas estimadas": horas,
|
||||
"INTERNO/EXTERNO": interno_externo,
|
||||
"Recurrencia": recurrencia,
|
||||
}
|
||||
at.update_task(sel["id"], fields)
|
||||
st.cache_data.clear()
|
||||
st.success("Tarea actualizada.")
|
||||
st.rerun()
|
||||
|
||||
_confirm_delete(f"task_{sel['id']}", sel["task"], lambda rid=sel["id"]: at.delete_task(rid))
|
||||
|
||||
# ── Clientes ──────────────────────────────────────────────────────────────── #
|
||||
|
||||
|
||||
@ -2,3 +2,5 @@ pyairtable==3.3.0
|
||||
python-dotenv==1.2.2
|
||||
streamlit>=1.35.0
|
||||
pandas>=2.0.0
|
||||
Authlib>=1.3.2
|
||||
httpx
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user