hedtorresca's picture
Update app.py
f5adf1a verified
"""
Gestor de Actividades – Equipo de Investigación – Clanes & CoCrea
Autor: (tu nombre)
Última actualización: 2025-07-09
"""
# ╭───────────────────────────── IMPORTS ─────────────────────────────╮
import gradio as gr
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from datetime import datetime, timedelta
import pathlib, io, openpyxl
from openpyxl.styles import PatternFill
from openpyxl.formatting.rule import DataBarRule
# ╭────────────────────────── CONFIGURACIÓN ──────────────────────────╮
CSV_FILE = "data/cronograma.csv"
RESPONSABLES_FIJOS = ['Todos', 'Juliana', 'María Angélica', 'Hernán David']
ESTADOS_FIJOS = ['No Iniciada', 'En Progreso', 'Completada',
'Retrasada', 'Bloqueada']
# Colores consistentes (hex RGB sin '#')
# Paleta única, ahora con ‘#’ ☑️ (válida para Plotly)
ESTADO_COLOR = {
"No Iniciada": "#d3d3d3", # gris claro
"En Progreso": "#1f77b4", # azul
"Completada" : "#2ca02c", # verde
"Retrasada" : "#ff7f0e", # naranja
"Bloqueada" : "#d62728", # rojo
}
# ╭─────────────────────────── UTILIDADES ────────────────────────────╮
def _iso(d):
"""Convierte None/str/datetime a 'YYYY-MM-DD' o ''."""
if d in (None, "", pd.NaT): return ""
return pd.to_datetime(d).strftime("%Y-%m-%d")
def create_empty_dataframe():
cols = ['ID','Fase','Tarea','Responsable',
'Fecha de Inicio','Fecha de Fin',
'Estado','Progreso (%)','Descripción']
return pd.DataFrame(columns=cols).astype({"ID":"Int64"})
def _split_resp(s):
if pd.isna(s) or s=="":
return set()
return set(map(str.strip, str(s).split(';')))
def load_data_from_csv():
if not pathlib.Path(CSV_FILE).is_file():
return create_empty_dataframe()
df = pd.read_csv(CSV_FILE)[[*create_empty_dataframe().columns]]
df["ID"] = pd.to_numeric(df["ID"], errors="coerce").astype("Int64")
df["Fecha de Inicio"] = pd.to_datetime(df["Fecha de Inicio"], errors="coerce")
df["Fecha de Fin"] = pd.to_datetime(df["Fecha de Fin"], errors="coerce")
df["Progreso (%)"] = pd.to_numeric(df["Progreso (%)"], errors="coerce").fillna(0)
df.dropna(subset=["ID"], inplace=True)
return df
def save_data_to_csv(df):
out = df.copy()
out["Fecha de Inicio"] = out["Fecha de Inicio"].apply(_iso)
out["Fecha de Fin"] = out["Fecha de Fin"].apply(_iso)
out.to_csv(CSV_FILE, index=False)
print(f"[{datetime.now():%Y-%m-%d %H:%M:%S}] Guardado en {CSV_FILE}")
def get_dynamic_choices(df):
fases = sorted(df['Fase'].dropna().unique().tolist())
extras = sorted({r for cell in df['Responsable'].dropna()
for r in _split_resp(cell)})
resps = sorted(set(RESPONSABLES_FIJOS) | set(extras))
return fases, resps
# ╭────────────────────── SOPORTE EXCEL (auto-ancho) ──────────────────╮
def _autosize(ws, min_width=12, padding=2):
"""Ajusta el ancho de cada columna al contenido más largo + padding."""
for col_cells in ws.columns:
max_len = max(len(str(c.value)) if c.value is not None else 0
for c in col_cells)
new_w = max(max_len + padding, min_width)
ws.column_dimensions[col_cells[0].column_letter].width = new_w
def _apply_estado_fill(cell, estado):
"""Rellena la celda según estado usando la misma paleta."""
hex_rgb = ESTADO_COLOR.get(estado, "#d3d3d3").lstrip("#")
cell.fill = PatternFill("solid", fgColor="FF" + hex_rgb.upper())
def _excel_week_cols(min_date, max_date):
base = (min_date - timedelta(days=min_date.weekday()))
weeks = []
while base <= max_date:
weeks.append(base)
base += timedelta(days=7)
return weeks
# ╭──────────────────────────── GRÁFICAS ─────────────────────────────╮
def create_gantt_chart(df):
base = go.Figure(layout_title_text="Cronograma del Proyecto")
if df.empty or df.dropna(subset=['Fecha de Inicio','Fecha de Fin']).empty:
base.add_annotation(text="No hay tareas para mostrar",
xref="paper",yref="paper",showarrow=False,font_size=16)
base.update_xaxes(visible=False); base.update_yaxes(visible=False)
return base
tmp = df.dropna(subset=['Fecha de Inicio','Fecha de Fin']).copy()
tmp['Fecha de Inicio'] = pd.to_datetime(tmp['Fecha de Inicio'])
tmp['Fecha de Fin'] = pd.to_datetime(tmp['Fecha de Fin'])
fig = px.timeline(tmp,
x_start="Fecha de Inicio", x_end="Fecha de Fin",
y="Tarea", color="Estado",
color_discrete_map=ESTADO_COLOR,
title="Cronograma del Proyecto")
fig.update_yaxes(autorange="reversed")
fig.add_vline(x=datetime.now(), line_dash="dash", line_color="red")
return fig
def create_progress_pie_chart(df, sel_id):
if df.empty:
pct, title = 0, "Progreso General"
else:
df["Progreso (%)"] = pd.to_numeric(df["Progreso (%)"],
errors="coerce").fillna(0)
if sel_id and sel_id in df["ID"].values:
row = df[df["ID"]==sel_id].iloc[0]
pct, title = row["Progreso (%)"], f"Progreso: {row['Tarea'][:30]}"
else:
pct, title = df["Progreso (%)"].mean(), "Progreso General"
pct = round(pct)
fig = go.Figure(
data=[go.Pie(
labels=['Completado','Pendiente'],
values=[pct,100-pct], hole=.4,
marker_colors=['#2ca02c','#d3d3d3'],
textinfo='none', hoverinfo='label+percent'
)]
)
fig.update_layout(title_text=title,
annotations=[dict(text=f'{pct}%',
x=0.5,y=0.5,
font_size=20,showarrow=False)],
showlegend=False, margin=dict(t=60,b=20,l=20,r=20))
return fig
# ╭───────────────────────────── LÓGICA ──────────────────────────────╮
def apply_filters(df, fases, responsables):
tmp = df.copy()
if fases: # por fase
tmp = tmp[tmp['Fase'].isin(fases)]
if responsables and 'Todos' not in responsables:
tmp = tmp[tmp['Responsable'].apply(
lambda cell: bool(_split_resp(cell) & set(responsables))
)]
tmp = tmp.sort_values(["Fase","Fecha de Inicio","Tarea"])
gantt = create_gantt_chart(tmp)
pie = create_progress_pie_chart(tmp, None)
return tmp, gantt, pie
def manage_tasks(df, action, sel_id,
nombre, fase, resp_list, f_ini, f_fin,
estado, avance, desc):
work = df.copy()
f_ini, f_fin = _iso(f_ini), _iso(f_fin)
resp_str = ";".join(sorted(set(resp_list))) if resp_list else ""
if action == "add":
if not nombre or not f_ini or not f_fin:
gr.Warning("Nombre y fechas son obligatorios.")
return df, df, *apply_filters(df, [], []), *[gr.update()]*3
new_id = (work["ID"].max()+1) if not work.empty else 1
new = {'ID':new_id,'Fase':fase,'Tarea':nombre,'Responsable':resp_str,
'Fecha de Inicio':f_ini,'Fecha de Fin':f_fin,
'Estado':estado,'Progreso (%)':avance,'Descripción':desc}
work = pd.concat([work,pd.DataFrame([new])], ignore_index=True)\
.sort_values(["Fase","Fecha de Inicio","Tarea"])
gr.Info(f"Tarea “{nombre}” añadida.")
elif action == "update":
if sel_id is None:
gr.Warning("Selecciona una tarea para actualizar.")
return df, df, *apply_filters(df, [], []), *[gr.update()]*3
mask = work["ID"]==sel_id
work.loc[mask,['Fase','Tarea','Responsable','Fecha de Inicio',
'Fecha de Fin','Estado','Progreso (%)','Descripción']] = \
[fase,nombre,resp_str,f_ini,f_fin,estado,avance,desc]
gr.Info(f"Tarea ID {sel_id} actualizada.")
elif action == "delete":
if sel_id is None:
gr.Warning("Selecciona una tarea para eliminar.")
return df, df, *apply_filters(df, [], []), *[gr.update()]*3
work = work[work["ID"]!=sel_id].reset_index(drop=True)
gr.Info("Tarea eliminada.")
save_data_to_csv(work)
fases,resps = get_dynamic_choices(work)
fil,gantt,pie = apply_filters(work, [], [])
return (work, fil, gantt, pie,
gr.update(choices=fases), # dd_fase
gr.update(choices=fases), # fl_fase
gr.update(choices=resps), # dd_resp
gr.update(choices=resps)) # fl_resp
def populate_form_on_select(df_state, df_visible, sel: gr.SelectData):
if sel is None:
return None,"","","",[],None,None,"No Iniciada",0,""
dv = pd.DataFrame(df_visible, columns=df_state.columns)
sel_id = dv.iloc[sel.index[0]]["ID"]
row = df_state.loc[df_state["ID"] == sel_id].iloc[0]
return (
sel_id,
row["Tarea"], row["Fase"],
row["Responsable"],
_iso(row["Fecha de Inicio"]), _iso(row["Fecha de Fin"]),
row["Estado"], row["Progreso (%)"], row["Descripción"]
)
#╭───────────────────────── EXPORTADORES ────────────────────────────╮
def generate_full_excel(df):
if df.empty:
gr.Warning("No hay datos para exportar."); return None
out = io.BytesIO()
with pd.ExcelWriter(out, engine='openpyxl') as w:
# ------- Hoja Detalle -------
cols_det = ['Fase','Tarea','Responsable','Fecha de Inicio',
'Fecha de Fin','Estado','Progreso (%)','Descripción']
df_det = df[cols_det]
df_det["Progreso (%)"] = pd.to_numeric(df_det["Progreso (%)"], errors="coerce").fillna(0).astype(int)
df_det.to_excel(w, index=False, sheet_name="Detalle")
ws_det = w.book["Detalle"]
# colores + barra de datos
col_estado = cols_det.index('Estado')+1
for r in range(2, ws_det.max_row+1):
_apply_estado_fill(ws_det.cell(r, col_estado),
ws_det.cell(r, col_estado).value)
col_prog = cols_det.index("Progreso (%)") + 1
bar = DataBarRule(
start_type="num", start_value=0,
end_type="num", end_value=100,
color="FF2CA02C", # ← verde (#2ca02c) con canal alfa FF
showValue=True
)
ws_det.conditional_formatting.add(
f"{openpyxl.utils.get_column_letter(col_prog)}2:"
f"{openpyxl.utils.get_column_letter(col_prog)}{ws_det.max_row}",
bar
)
_autosize(ws_det)
# ------- Hoja Gantt Semanal -------
min_d, max_d = df['Fecha de Inicio'].min(), df['Fecha de Fin'].max()
weeks = _excel_week_cols(min_d, max_d)
gantt_cols = ['Fase','Tarea'] + [wk.strftime("W%Y-%m-%d") for wk in weeks]
gdf = pd.DataFrame(columns=gantt_cols)
for _, row in df.iterrows():
line = {'Fase':row['Fase'], 'Tarea':row['Tarea']}
for i, wk in enumerate(weeks):
if row['Fecha de Inicio'] <= wk+timedelta(days=6) and row['Fecha de Fin'] >= wk:
line[gantt_cols[2+i]] = row['Estado']
else:
line[gantt_cols[2+i]] = ""
gdf = pd.concat([gdf, pd.DataFrame([line])], ignore_index=True)
gdf.to_excel(w, index=False, sheet_name="GanttSemanal")
ws_g = w.book["GanttSemanal"]
for r in range(2, ws_g.max_row+1):
for c in range(3, ws_g.max_column+1):
est = ws_g.cell(r,c).value
if est: _apply_estado_fill(ws_g.cell(r,c), est)
_autosize(ws_g, min_width=10)
out.seek(0)
fname = f"cronograma_detalle_{datetime.now():%Y%m%d_%H%M%S}.xlsx"
with open(fname,"wb") as f:
f.write(out.read())
gr.Info("Reporte Excel (detalle + gantt semanal) generado.")
return gr.update(value=fname, visible=True)
def generate_monthly_excel(df):
if df.empty:
gr.Warning("No hay datos para exportar."); return None
out = io.BytesIO()
with pd.ExcelWriter(out, engine='openpyxl') as w:
min_d = df['Fecha de Inicio'].min().replace(day=1)
max_d = df['Fecha de Fin'].max().replace(day=1)
months = []
cur = min_d
while cur <= max_d:
months.append(cur)
cur = (cur + timedelta(days=32)).replace(day=1)
gantt_cols = ['Fase','Tarea'] + [m.strftime("%Y-%m") for m in months]
gdf = pd.DataFrame(columns=gantt_cols)
for _, row in df.iterrows():
line = {'Fase':row['Fase'], 'Tarea':row['Tarea']}
for i, m in enumerate(months):
last = (m + timedelta(days=32)).replace(day=1) - timedelta(days=1)
line[gantt_cols[2+i]] = row['Estado'] \
if row['Fecha de Inicio'] <= last and row['Fecha de Fin'] >= m else ""
gdf = pd.concat([gdf, pd.DataFrame([line])], ignore_index=True)
gdf.to_excel(w, index=False, sheet_name="GanttMensual")
ws = w.book["GanttMensual"]
for r in range(2, ws.max_row+1):
for c in range(3, ws.max_column+1):
est = ws.cell(r,c).value
if est: _apply_estado_fill(ws.cell(r,c), est)
_autosize(ws, min_width=10)
out.seek(0)
fname = f"cronograma_mensual_{datetime.now():%Y%m%d_%H%M%S}.xlsx"
with open(fname,"wb") as f:
f.write(out.read())
gr.Info("Reporte Excel mensual generado.")
return gr.update(value=fname, visible=True)
def export_csv(df):
if df.empty:
gr.Warning("No hay datos para exportar."); return None
fname = f"cronograma_export_{datetime.now():%Y%m%d_%H%M%S}.csv"
df.to_csv(fname, index=False)
gr.Info("CSV exportado.")
return gr.update(value=fname, visible=True)
# ╭────────────────────────── INTERFAZ UI ────────────────────────────╮
with gr.Blocks(theme=gr.themes.Soft(),
title="Gestor de Actividades - Equipo de Investigación - Clanes & CoCrea") as demo:
df_state = gr.State(create_empty_dataframe())
selected_task = gr.State()
gr.Markdown("# 🚀 Gestor de Actividades – Equipo de Investigación – Clanes & CoCrea")
with gr.Row():
# ----- PANEL EDICIÓN -----
with gr.Column(scale=1):
with gr.Accordion("📝 Gestión de tareas", open=True):
t_nombre = gr.Textbox(label="Nombre de la tarea")
dd_fase = gr.Dropdown(label="Fase", allow_custom_value=True)
dd_resp = gr.Dropdown(label="Responsable(s)",
multiselect=True, allow_custom_value=True)
with gr.Row():
f_ini = gr.DateTime(label="Fecha inicio",
include_time=False, type="string")
f_fin = gr.DateTime(label="Fecha fin",
include_time=False, type="string")
dd_estado = gr.Dropdown(label="Estado",
choices=ESTADOS_FIJOS, value="No Iniciada")
s_avance = gr.Slider(label="Progreso (%)",
minimum=0, maximum=100, step=1)
t_desc = gr.Textbox(label="Descripción / notas", lines=3)
with gr.Row():
b_add = gr.Button("✔️ Añadir", variant="primary")
b_upd = gr.Button("🔄 Actualizar", variant="secondary")
b_del = gr.Button("❌ Eliminar", variant="stop")
with gr.Accordion("⬇️ Exportar / Reportes", open=True):
b_rep_full = gr.Button("Excel Detalle + Gantt Semanal")
b_rep_month = gr.Button("Excel Gantt Mensual")
b_csv = gr.Button("CSV (cronograma)")
f_out_full = gr.File(label="Reporte completo", visible=True)
f_out_month = gr.File(label="Reporte mensual", visible=True)
f_out_csv = gr.File(label="CSV exportado", visible=True)
# ----- PANEL VISUALIZACIÓN -----
with gr.Column(scale=3):
with gr.Row():
with gr.Column(scale=2):
gr.Markdown("### 🔍 Filtros")
with gr.Row():
fl_fase = gr.Dropdown(label="Fase", multiselect=True)
fl_resp = gr.Dropdown(label="Responsable", multiselect=True)
with gr.Column(scale=1):
gr.Markdown("### 📊 Progreso")
p_pie = gr.Plot()
gr.Markdown("### 🗓️ Tabla de actividades")
t_df = gr.DataFrame(headers=create_empty_dataframe().columns.tolist(),
interactive=True, wrap=True)
gr.Markdown("### 📈 Diagrama de Gantt")
p_gantt = gr.Plot()
# ----- CONEXIONES -----
form_inputs = [t_nombre, dd_fase, dd_resp, f_ini, f_fin,
dd_estado, s_avance, t_desc]
task_outputs = [df_state, t_df, p_gantt, p_pie,
dd_fase, fl_fase, dd_resp, fl_resp]
b_add.click(fn=manage_tasks,
inputs=[df_state, gr.State("add"), selected_task] + form_inputs,
outputs=task_outputs)
b_upd.click(fn=manage_tasks,
inputs=[df_state, gr.State("update"), selected_task] + form_inputs,
outputs=task_outputs)
b_del.click(fn=manage_tasks,
inputs=[df_state, gr.State("delete"), selected_task] + form_inputs,
outputs=task_outputs)
t_df.select(fn=populate_form_on_select,
inputs=[df_state, t_df],
outputs=[selected_task] + form_inputs, show_progress="hidden")\
.then(fn=create_progress_pie_chart,
inputs=[df_state, selected_task], outputs=p_pie)
for fl in (fl_fase, fl_resp):
fl.change(fn=lambda *args: apply_filters(*args)[0:3],
inputs=[df_state, fl_fase, fl_resp],
outputs=[t_df, p_gantt, p_pie])
b_rep_full.click(fn=generate_full_excel,
inputs=[df_state], outputs=[f_out_full])
b_rep_month.click(fn=generate_monthly_excel,
inputs=[df_state], outputs=[f_out_month])
b_csv.click(fn=export_csv, inputs=[df_state], outputs=[f_out_csv])
def _init():
df = load_data_from_csv()
fases,resps = get_dynamic_choices(df)
fil,gantt,pie = apply_filters(df, [], [])
return (df, fil, gantt, pie,
gr.update(choices=fases),
gr.update(choices=fases),
gr.update(choices=resps),
gr.update(choices=resps))
demo.load(fn=_init,
outputs=[df_state, t_df, p_gantt, p_pie,
dd_fase, fl_fase, dd_resp, fl_resp])
if __name__ == "__main__":
demo.launch()