Spaces:
Sleeping
Sleeping
""" | |
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() |