hedtorresca commited on
Commit
bf5515d
·
verified ·
1 Parent(s): 024264c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +88 -185
app.py CHANGED
@@ -1,213 +1,131 @@
1
  import gradio as gr
2
  import pandas as pd
3
  import plotly.express as px
4
- import requests
5
- import msal
6
- import io
7
- import os # Importante: Módulo para leer variables de entorno
8
  from datetime import datetime
 
9
 
10
- # --- 1. CLASE HELPER PARA LA CONEXIÓN CON ONEDRIVE (MODIFICADA) ---
11
-
12
- class OneDriveHelper:
13
- """
14
- Gestiona la autenticación y operaciones con la API de MS Graph,
15
- leyendo la configuración desde las variables de entorno (Secrets de HF).
16
- """
17
- def __init__(self):
18
- # Leer configuración desde los "Secrets" de Hugging Face Spaces
19
- self.client_id = os.getenv('CLIENT_ID')
20
- self.client_secret = os.getenv('CLIENT_SECRET')
21
- self.tenant_id = os.getenv('TENANT_ID')
22
- self.user_id = os.getenv('USER_ID')
23
- self.file_path = os.getenv('ONEDRIVE_FILE_PATH')
24
-
25
- if not all([self.client_id, self.client_secret, self.tenant_id, self.user_id, self.file_path]):
26
- raise ValueError("Faltan una o más variables de entorno (Secrets) para la configuración de OneDrive.")
27
-
28
- self.authority = f"https://login.microsoftonline.com/{self.tenant_id}"
29
- self.graph_endpoint = "https://graph.microsoft.com/v1.0"
30
-
31
- self.app = msal.ConfidentialClientApplication(
32
- self.client_id,
33
- authority=self.authority,
34
- client_credential=self.client_secret
35
  )
36
- self.token = None
37
-
38
- def get_access_token(self):
39
- """Obtiene un token de acceso para la API de Graph."""
40
- # Siempre se adquiere un nuevo token para evitar problemas de expiración en entornos serverless
41
- result = self.app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
42
- if "access_token" in result:
43
- self.token = result['access_token']
44
- return self.token
45
- else:
46
- raise Exception("No se pudo obtener el token de acceso: " + result.get("error_description", ""))
47
-
48
- def download_excel_to_dataframe(self):
49
- """Descarga el archivo Excel de OneDrive y lo carga en un DataFrame."""
50
- try:
51
- token = self.get_access_token()
52
- headers = {'Authorization': 'Bearer ' + token}
53
- url = f"{self.graph_endpoint}/users/{self.user_id}/drive/root:{self.file_path}:/content"
54
- response = requests.get(url, headers=headers)
55
-
56
- if response.status_code == 200:
57
- excel_data = io.BytesIO(response.content)
58
- df = pd.read_excel(excel_data, engine='openpyxl')
59
- df['Fecha de Inicio'] = pd.to_datetime(df['Fecha de Inicio'])
60
- df['Fecha de Fin'] = pd.to_datetime(df['Fecha de Fin'])
61
- return df
62
- elif response.status_code == 404:
63
- print("Advertencia: Archivo no encontrado. Se creará uno nuevo al guardar.")
64
- return self.create_empty_dataframe()
65
- else:
66
- response.raise_for_status()
67
- except Exception as e:
68
- print(f"Error al descargar el archivo: {e}")
69
- return self.create_empty_dataframe()
70
-
71
- def upload_dataframe_to_excel(self, df):
72
- """Sube un DataFrame a OneDrive, sobrescribiendo el archivo Excel."""
73
- try:
74
- token = self.get_access_token()
75
- headers = {'Authorization': 'Bearer ' + token, 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}
76
-
77
- output_buffer = io.BytesIO()
78
- df.to_excel(output_buffer, index=False, engine='openpyxl')
79
- output_buffer.seek(0)
80
-
81
- url = f"{self.graph_endpoint}/users/{self.user_id}/drive/root:{self.file_path}:/content"
82
- response = requests.put(url, headers=headers, data=output_buffer.read())
83
- response.raise_for_status()
84
- print("Archivo guardado exitosamente en OneDrive.")
85
- return True
86
- except Exception as e:
87
- gr.Error(f"Error al guardar en OneDrive: {e}")
88
- return False
89
-
90
- @staticmethod
91
- def create_empty_dataframe():
92
- """Crea la estructura del DataFrame si el archivo no existe."""
93
- return pd.DataFrame(columns=[
94
- 'ID', 'Tarea', 'Fase', 'Responsable', 'Fecha de Inicio',
95
- 'Fecha de Fin', 'Estado', 'Progreso (%)', 'Descripción'
96
- ])
97
-
98
- # --- El resto del código de la aplicación (lógica de UI y funciones) es idéntico ---
99
-
100
- # --- 2. INSTANCIACIÓN Y CARGA INICIAL DE DATOS ---
101
- try:
102
- onedrive = OneDriveHelper()
103
- initial_df = onedrive.download_excel_to_dataframe()
104
- except ValueError as e:
105
- # Manejar error si los secrets no están configurados
106
- gr.Warning(str(e))
107
- initial_df = OneDriveHelper.create_empty_dataframe()
108
-
109
-
110
- # --- 3. FUNCIONES DE LÓGICA DE LA APLICACIÓN (SIN CAMBIOS) ---
111
 
112
  def create_gantt_chart(df):
113
- """Genera un diagrama de Gantt interactivo con Plotly."""
114
- if df.empty or 'Fecha de Inicio' not in df.columns or 'Fecha de Fin' not in df.columns:
115
- return px.timeline(title="Cronograma de Proyecto (Sin datos)")
116
-
117
- status_colors = {
118
- 'No Iniciada': 'lightgrey', 'En Progreso': 'blue', 'Completada': 'green',
119
- 'Retrasada': 'orange', 'Bloqueada': 'red'
120
- }
121
- fig = px.timeline(
122
- df, x_start="Fecha de Inicio", x_end="Fecha de Fin", y="Tarea",
123
- color="Estado", color_discrete_map=status_colors, title="Diagrama de Gantt del Proyecto",
124
- labels={"Tarea": "Actividades"}
125
- )
126
  fig.update_yaxes(autorange="reversed")
127
- fig.update_layout(title_x=0.5, font=dict(size=12))
128
  return fig
129
 
130
- def filter_data(df, phases, responsibles, statuses):
131
- """Filtra el DataFrame según los criterios seleccionados."""
132
- filtered_df = df.copy()
133
- if phases: filtered_df = filtered_df[filtered_df['Fase'].isin(phases)]
134
- if responsibles: filtered_df = filtered_df[filtered_df['Responsable'].isin(responsibles)]
135
- if statuses: filtered_df = filtered_df[filtered_df['Estado'].isin(statuses)]
136
- return filtered_df
137
 
138
  def manage_tasks(df_state, *args):
139
- """Función central para añadir, actualizar o eliminar tareas."""
140
- action, selected_id, task_id, name, phase, responsible, start_date, end_date, status, progress, desc, new_phase, new_resp = args
141
  df = df_state.copy()
142
 
 
143
  if action == "add":
144
  if not name or not start_date or not end_date:
145
  gr.Warning("Nombre, Fecha de Inicio y Fecha de Fin son obligatorios.")
146
  return df, create_gantt_chart(df), gr.update(), gr.update()
147
- new_id = df['ID'].max() + 1 if not df.empty else 1
148
- final_phase = new_phase if new_phase else phase
149
- final_resp = new_resp if new_resp else responsible
150
- new_task = pd.DataFrame([{'ID': new_id, 'Tarea': name, 'Fase': final_phase, 'Responsable': final_resp,
151
- 'Fecha de Inicio': start_date, 'Fecha de Fin': end_date, 'Estado': status,
152
- 'Progreso (%)': progress, 'Descripción': desc}])
153
  df = pd.concat([df, new_task], ignore_index=True)
154
- gr.Info(f"Tarea '{name}' añadida con éxito.")
155
 
156
  elif action == "update":
157
  if selected_id is None:
158
- gr.Warning("Selecciona una tarea de la tabla para actualizar.")
159
  return df, create_gantt_chart(df), gr.update(), gr.update()
160
- final_phase = new_phase if new_phase else phase
161
- final_resp = new_resp if new_resp else responsible
162
- df.loc[df['ID'] == selected_id, ['Tarea', 'Fase', 'Responsable', 'Fecha de Inicio', 'Fecha de Fin', 'Estado', 'Progreso (%)', 'Descripción']] = \
163
- [name, final_phase, final_resp, start_date, end_date, status, progress, desc]
164
  gr.Info(f"Tarea ID {selected_id} actualizada.")
165
 
166
  elif action == "delete":
167
  if selected_id is None:
168
- gr.Warning("Selecciona una tarea de la tabla para eliminar.")
169
  return df, create_gantt_chart(df), gr.update(), gr.update()
170
  task_name = df.loc[df['ID'] == selected_id, 'Tarea'].iloc[0]
171
- df = df[df['ID'] != selected_id]
172
  gr.Info(f"Tarea '{task_name}' eliminada.")
173
 
174
- onedrive.upload_dataframe_to_excel(df)
175
- all_phases = sorted(df['Fase'].unique().tolist())
176
- all_responsibles = sorted(df['Responsable'].unique().tolist())
 
 
 
 
 
 
177
  return df, create_gantt_chart(df), gr.update(choices=all_phases), gr.update(choices=all_responsibles)
178
 
179
- def populate_form_on_select(df, evt: gr.SelectData):
180
- """Al seleccionar una fila de la tabla, llena el formulario de edición."""
181
- if evt.index is None:
182
- return None, "", "", "", None, None, "No Iniciada", 0, ""
183
- selected_row = df.iloc[evt.index[0]]
184
- return (selected_row['ID'], selected_row['Tarea'], selected_row['Fase'],
185
- selected_row['Responsable'], selected_row['Fecha de Inicio'].to_pydatetime().date(),
186
- selected_row['Fecha de Fin'].to_pydatetime().date(), selected_row['Estado'],
187
- selected_row['Progreso (%)'], selected_row['Descripción'])
188
-
189
- # --- 4. DEFINICIÓN DE LA INTERFAZ CON GRADIO (SIN CAMBIOS) ---
190
 
191
- with gr.Blocks(theme=gr.themes.Soft(), title="Gestor de Proyectos con OneDrive") as demo:
 
192
  df_state = gr.State(initial_df)
193
  selected_task_id = gr.State(None)
194
- initial_phases = sorted(initial_df['Fase'].unique().tolist())
195
- initial_responsibles = sorted(initial_df['Responsable'].unique().tolist())
 
196
  status_options = ['No Iniciada', 'En Progreso', 'Completada', 'Retrasada', 'Bloqueada']
197
 
198
- gr.Markdown("# 📊 Gestor de Cronogramas de Proyectos (Desplegado en HF Spaces)")
 
199
 
200
  with gr.Row():
201
  with gr.Column(scale=1):
202
  with gr.Accordion("📝 Panel de Gestión de Tareas", open=True):
203
  txt_task_id = gr.Textbox(label="ID Tarea", interactive=False, visible=False)
204
  txt_task_name = gr.Textbox(label="Nombre de la Tarea")
205
- with gr.Row():
206
- dd_phase = gr.Dropdown(label="Fase del Proyecto", choices=initial_phases)
207
- txt_new_phase = gr.Textbox(label="O añadir nueva fase...")
208
- with gr.Row():
209
- dd_responsible = gr.Dropdown(label="Responsable", choices=initial_responsibles)
210
- txt_new_responsible = gr.Textbox(label="O añadir nuevo responsable...")
211
  with gr.Row():
212
  date_start = gr.DatePicker(label="Fecha de Inicio")
213
  date_end = gr.DatePicker(label="Fecha de Fin")
@@ -219,36 +137,21 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Gestor de Proyectos con OneDrive")
219
  btn_update = gr.Button("🔄 Actualizar Tarea", variant="secondary")
220
  btn_delete = gr.Button("❌ Eliminar Tarea", variant="stop")
221
  with gr.Column(scale=2):
222
- with gr.Accordion("🔍 Filtros de Visualización"):
223
- with gr.Row():
224
- chk_filter_phase = gr.CheckboxGroup(label="Filtrar por Fase", choices=initial_phases)
225
- chk_filter_resp = gr.CheckboxGroup(label="Filtrar por Responsable", choices=initial_responsibles)
226
- chk_filter_status = gr.CheckboxGroup(label="Filtrar por Estado", choices=status_options)
227
  gr.Markdown("### 🗓️ Tabla de Actividades")
228
  df_table = gr.DataFrame(value=initial_df, headers=list(initial_df.columns), interactive=True, height=300)
229
  gr.Markdown("### 📈 Diagrama de Gantt")
230
  gantt_plot = gr.Plot(create_gantt_chart(initial_df))
231
 
232
- # --- 5. LÓGICA DE EVENTOS (SIN CAMBIOS) ---
233
- task_inputs = [txt_task_id, txt_task_name, dd_phase, dd_responsible, date_start, date_end,
234
- dd_status, slider_progress, txt_description, txt_new_phase, txt_new_responsible]
235
- filter_inputs = [df_state, chk_filter_phase, chk_filter_resp, chk_filter_status]
236
-
237
- btn_add.click(fn=lambda *args: manage_tasks(*args), inputs=[gr.State("add"), df_state] + task_inputs, outputs=[df_state, gantt_plot, dd_phase, dd_responsible])
238
  btn_update.click(fn=lambda *args: manage_tasks(*args), inputs=[gr.State("update"), df_state, selected_task_id] + task_inputs, outputs=[df_state, gantt_plot, dd_phase, dd_responsible])
239
  btn_delete.click(fn=lambda *args: manage_tasks(*args), inputs=[gr.State("delete"), df_state, selected_task_id] + task_inputs, outputs=[df_state, gantt_plot, dd_phase, dd_responsible])
240
 
241
  df_table.select(fn=populate_form_on_select, inputs=[df_state], outputs=[selected_task_id, txt_task_name, dd_phase, dd_responsible, date_start, date_end, dd_status, slider_progress, txt_description])
242
 
243
- def apply_filters_and_update_ui(df, phases, responsibles, statuses):
244
- filtered = filter_data(df, phases, responsibles, statuses)
245
- return filtered, create_gantt_chart(filtered)
246
-
247
- for chk_filter in [chk_filter_phase, chk_filter_resp, chk_filter_status]:
248
- chk_filter.change(fn=apply_filters_and_update_ui, inputs=filter_inputs, outputs=[df_table, gantt_plot])
249
-
250
- df_state.change(fn=lambda df: (df, create_gantt_chart(df)), inputs=[df_state], outputs=[df_table, gantt_plot])
251
 
252
- # --- El lanzador se gestiona automáticamente por HF Spaces ---
253
  if __name__ == "__main__":
254
  demo.launch(debug=True)
 
1
  import gradio as gr
2
  import pandas as pd
3
  import plotly.express as px
 
 
 
 
4
  from datetime import datetime
5
+ import os
6
 
7
+ # --- 1. CONFIGURACIÓN DEL ARCHIVO CSV ---
8
+ CSV_FILE = "cronograma.csv"
9
+
10
+ # --- 2. LÓGICA DE CARGA Y GUARDADO DE DATOS ---
11
+
12
+ def load_data_from_csv():
13
+ """Carga los datos desde el archivo CSV al iniciar la aplicación."""
14
+ try:
15
+ # Lee el CSV y convierte las columnas de fecha al formato correcto
16
+ df = pd.read_csv(
17
+ CSV_FILE,
18
+ parse_dates=['Fecha de Inicio', 'Fecha de Fin']
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  )
20
+ return df
21
+ except FileNotFoundError:
22
+ # Si el archivo no existe, crea un DataFrame vacío con las columnas correctas
23
+ return create_empty_dataframe()
24
+ except Exception as e:
25
+ print(f"Error al leer el CSV: {e}")
26
+ return create_empty_dataframe()
27
+
28
+ def save_data_to_csv(df):
29
+ """Guarda el DataFrame completo en el archivo CSV."""
30
+ try:
31
+ df.to_csv(CSV_FILE, index=False)
32
+ print(f"Datos guardados exitosamente en {CSV_FILE}")
33
+ except Exception as e:
34
+ gr.Error(f"No se pudo guardar el archivo CSV: {e}")
35
+
36
+ def create_empty_dataframe():
37
+ """Crea la estructura del DataFrame si el archivo no existe o está vacío."""
38
+ columns = ['ID', 'Tarea', 'Fase', 'Responsable', 'Fecha de Inicio', 'Fecha de Fin', 'Estado', 'Progreso (%)', 'Descripción']
39
+ df = pd.DataFrame(columns=columns)
40
+ # Asegurar tipos de datos correctos para un df vacío
41
+ df['ID'] = df['ID'].astype(int)
42
+ df['Progreso (%)'] = df['Progreso (%)'].astype(int)
43
+ return df
44
+
45
+ # --- Carga inicial de datos ---
46
+ initial_df = load_data_from_csv()
47
+
48
+ # --- 3. FUNCIONES DE LA APLICACIÓN (LÓGICA DE UI) ---
49
+ # (Las funciones create_gantt_chart y populate_form_on_select no cambian)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
  def create_gantt_chart(df):
52
+ if df.empty or df['Fecha de Inicio'].isnull().all() or df['Fecha de Fin'].isnull().all():
53
+ return px.timeline(title="Cronograma de Proyecto")
54
+
55
+ status_colors = {'No Iniciada': 'lightgrey', 'En Progreso': 'blue', 'Completada': 'green', 'Retrasada': 'orange', 'Bloqueada': 'red'}
56
+ df_filtered = df.dropna(subset=['Fecha de Inicio', 'Fecha de Fin'])
57
+ fig = px.timeline(df_filtered, x_start="Fecha de Inicio", x_end="Fecha de Fin", y="Tarea", color="Estado", color_discrete_map=status_colors, title="Diagrama de Gantt del Proyecto")
 
 
 
 
 
 
 
58
  fig.update_yaxes(autorange="reversed")
 
59
  return fig
60
 
61
+ def populate_form_on_select(df, evt: gr.SelectData):
62
+ if evt.index is None: return None, "", "", "", None, None, "No Iniciada", 0, ""
63
+ selected_row = df.iloc[evt.index[0]]
64
+ start_date = selected_row['Fecha de Inicio'].to_pydatetime().date() if pd.notna(selected_row['Fecha de Inicio']) else None
65
+ end_date = selected_row['Fecha de Fin'].to_pydatetime().date() if pd.notna(selected_row['Fecha de Fin']) else None
66
+ return (selected_row['ID'], selected_row['Tarea'], selected_row['Fase'], selected_row['Responsable'], start_date, end_date, selected_row['Estado'], selected_row['Progreso (%)'], selected_row['Descripción'])
 
67
 
68
  def manage_tasks(df_state, *args):
69
+ """Función central que ahora también guarda los cambios en el CSV."""
70
+ action, selected_id, task_id, name, phase, responsible, start_date, end_date, status, progress, desc = args
71
  df = df_state.copy()
72
 
73
+ # Lógica para añadir, actualizar o eliminar
74
  if action == "add":
75
  if not name or not start_date or not end_date:
76
  gr.Warning("Nombre, Fecha de Inicio y Fecha de Fin son obligatorios.")
77
  return df, create_gantt_chart(df), gr.update(), gr.update()
78
+ new_id = int(df['ID'].max() + 1) if not df.empty else 1
79
+ new_task = pd.DataFrame([{'ID': new_id, 'Tarea': name, 'Fase': phase, 'Responsable': responsible, 'Fecha de Inicio': start_date, 'Fecha de Fin': end_date, 'Estado': status, 'Progreso (%)': progress, 'Descripción': desc}])
 
 
 
 
80
  df = pd.concat([df, new_task], ignore_index=True)
81
+ gr.Info(f"Tarea '{name}' añadida.")
82
 
83
  elif action == "update":
84
  if selected_id is None:
85
+ gr.Warning("Selecciona una tarea para actualizar.")
86
  return df, create_gantt_chart(df), gr.update(), gr.update()
87
+ df.loc[df['ID'] == selected_id, ['Tarea', 'Fase', 'Responsable', 'Fecha de Inicio', 'Fecha de Fin', 'Estado', 'Progreso (%)', 'Descripción']] = [name, phase, responsible, start_date, end_date, status, progress, desc]
 
 
 
88
  gr.Info(f"Tarea ID {selected_id} actualizada.")
89
 
90
  elif action == "delete":
91
  if selected_id is None:
92
+ gr.Warning("Selecciona una tarea para eliminar.")
93
  return df, create_gantt_chart(df), gr.update(), gr.update()
94
  task_name = df.loc[df['ID'] == selected_id, 'Tarea'].iloc[0]
95
+ df = df[df['ID'] != selected_id].reset_index(drop=True)
96
  gr.Info(f"Tarea '{task_name}' eliminada.")
97
 
98
+ # Asegurar tipos de datos correctos antes de guardar
99
+ df['Fecha de Inicio'] = pd.to_datetime(df['Fecha de Inicio'])
100
+ df['Fecha de Fin'] = pd.to_datetime(df['Fecha de Fin'])
101
+
102
+ # **Paso clave: Guardar el DataFrame modificado en el archivo CSV**
103
+ save_data_to_csv(df)
104
+
105
+ all_phases = sorted(df['Fase'].dropna().unique().tolist())
106
+ all_responsibles = sorted(df['Responsable'].dropna().unique().tolist())
107
  return df, create_gantt_chart(df), gr.update(choices=all_phases), gr.update(choices=all_responsibles)
108
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
+ # --- 4. INTERFAZ DE GRADIO ---
111
+ with gr.Blocks(theme=gr.themes.Soft(), title="Gestor de Proyectos con CSV") as demo:
112
  df_state = gr.State(initial_df)
113
  selected_task_id = gr.State(None)
114
+
115
+ initial_phases = sorted(initial_df['Fase'].dropna().unique().tolist())
116
+ initial_responsibles = sorted(initial_df['Responsable'].dropna().unique().tolist())
117
  status_options = ['No Iniciada', 'En Progreso', 'Completada', 'Retrasada', 'Bloqueada']
118
 
119
+ gr.Markdown("# 💾 Gestor de Proyectos (con Guardado en CSV)")
120
+ gr.Markdown("Los datos se guardan en el 'Space'. **Nota:** Los datos se reiniciarán si el Space está inactivo por mucho tiempo.")
121
 
122
  with gr.Row():
123
  with gr.Column(scale=1):
124
  with gr.Accordion("📝 Panel de Gestión de Tareas", open=True):
125
  txt_task_id = gr.Textbox(label="ID Tarea", interactive=False, visible=False)
126
  txt_task_name = gr.Textbox(label="Nombre de la Tarea")
127
+ dd_phase = gr.Dropdown(label="Fase del Proyecto", choices=initial_phases, allow_custom_value=True)
128
+ dd_responsible = gr.Dropdown(label="Responsable", choices=initial_responsibles, allow_custom_value=True)
 
 
 
 
129
  with gr.Row():
130
  date_start = gr.DatePicker(label="Fecha de Inicio")
131
  date_end = gr.DatePicker(label="Fecha de Fin")
 
137
  btn_update = gr.Button("🔄 Actualizar Tarea", variant="secondary")
138
  btn_delete = gr.Button("❌ Eliminar Tarea", variant="stop")
139
  with gr.Column(scale=2):
 
 
 
 
 
140
  gr.Markdown("### 🗓️ Tabla de Actividades")
141
  df_table = gr.DataFrame(value=initial_df, headers=list(initial_df.columns), interactive=True, height=300)
142
  gr.Markdown("### 📈 Diagrama de Gantt")
143
  gantt_plot = gr.Plot(create_gantt_chart(initial_df))
144
 
145
+ # --- 5. LÓGICA DE EVENTOS ---
146
+ task_inputs = [txt_task_id, txt_task_name, dd_phase, dd_responsible, date_start, date_end, dd_status, slider_progress, txt_description]
147
+ btn_add.click(fn=lambda *args: manage_tasks(*args), inputs=[gr.State("add"), df_state, selected_task_id] + task_inputs, outputs=[df_state, gantt_plot, dd_phase, dd_responsible])
 
 
 
148
  btn_update.click(fn=lambda *args: manage_tasks(*args), inputs=[gr.State("update"), df_state, selected_task_id] + task_inputs, outputs=[df_state, gantt_plot, dd_phase, dd_responsible])
149
  btn_delete.click(fn=lambda *args: manage_tasks(*args), inputs=[gr.State("delete"), df_state, selected_task_id] + task_inputs, outputs=[df_state, gantt_plot, dd_phase, dd_responsible])
150
 
151
  df_table.select(fn=populate_form_on_select, inputs=[df_state], outputs=[selected_task_id, txt_task_name, dd_phase, dd_responsible, date_start, date_end, dd_status, slider_progress, txt_description])
152
 
153
+ df_state.change(fn=lambda df: df, inputs=[df_state], outputs=[df_table])
154
+ df_state.change(fn=create_gantt_chart, inputs=[df_state], outputs=[gantt_plot])
 
 
 
 
 
 
155
 
 
156
  if __name__ == "__main__":
157
  demo.launch(debug=True)