Project / app.py
Geoeasy's picture
Upload 7 files
6a7fb3c verified
import gradio as gr
import sqlite3
import bcrypt
import pandas as pd
from pathlib import Path
from datetime import datetime
import plotly.figure_factory as ff
import plotly.graph_objects as go
# ======================================================
# Configuração
# ======================================================
BASE_DIR = Path(__file__).parent
DB_PATH = BASE_DIR / "db" / "app.db"
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
# Inicializar base de dados automaticamente
try:
from init_db import init_database
init_database()
except Exception as e:
print(f"Aviso ao inicializar BD: {e}")
# ======================================================
# Funções de Base de Dados
# ======================================================
def get_conn():
"""Retorna conexão com BD"""
conn = sqlite3.connect(DB_PATH, timeout=30)
conn.execute("PRAGMA foreign_keys = ON")
return conn
# ======================================================
# Utilitários
# ======================================================
def slots_30_min():
"""Gera slots de 30 em 30 minutos"""
return [f"{h:02d}:{m:02d}" for h in range(24) for m in (0, 30)]
def calcular_horas(start_date, start_time, end_date, end_time):
"""Calcula horas entre duas datas/horas"""
if not all([start_date, start_time, end_date, end_time]):
raise ValueError("Preencha todos os campos")
try:
# Aceitar formato de gr.DateTime (pode vir como "2026-02-03" ou "2026-02-03 00:00:00")
if isinstance(start_date, str) and " " in start_date:
start_date = start_date.split()[0]
if isinstance(end_date, str) and " " in end_date:
end_date = end_date.split()[0]
inicio = datetime.strptime(f"{start_date} {start_time}", "%Y-%m-%d %H:%M")
fim = datetime.strptime(f"{end_date} {end_time}", "%Y-%m-%d %H:%M")
except ValueError as e:
raise ValueError(f"Formato inválido: {e}")
if fim <= inicio:
raise ValueError("Data/hora fim deve ser posterior ao início")
return round((fim - inicio).total_seconds() / 3600, 2)
def calcular_horas_preview(start_date, start_time, end_date, end_time):
"""Preview de horas calculadas"""
try:
if not all([start_date, start_time, end_date, end_time]):
return ""
horas = calcular_horas(start_date, start_time, end_date, end_time)
return f"{horas} h"
except:
return ""
# ======================================================
# Autenticação
# ======================================================
def login_user(username, password):
"""Faz login do utilizador - retorna username"""
if not username or not password:
return None, "❌ Preencha todos os campos", "⚠️ **Não autenticado**"
conn = get_conn()
cur = conn.cursor()
cur.execute("SELECT username, password_hash FROM users WHERE username = ?", (username,))
row = cur.fetchone()
if not row:
conn.close()
return None, "❌ Utilizador não encontrado", "⚠️ **Não autenticado**"
db_username, pw_hash = row
if bcrypt.checkpw(password.encode(), pw_hash.encode()):
conn.close()
indicator = f"## ✅ Logado: **{db_username}**"
return db_username, f"✅ Bem-vindo, {db_username}!", indicator
conn.close()
return None, "❌ Password incorreta", "⚠️ **Não autenticado**"
def create_user(username, password):
"""Cria novo utilizador"""
if not username or not password:
return "❌ Preencha todos os campos"
if len(password) < 6:
return "❌ Password deve ter pelo menos 6 caracteres"
conn = get_conn()
cur = conn.cursor()
pw_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
try:
cur.execute("INSERT INTO users (username, password_hash) VALUES (?, ?)", (username, pw_hash))
conn.commit()
return f"✅ Utilizador '{username}' criado! Faça login agora."
except sqlite3.IntegrityError:
return f"⚠️ Utilizador '{username}' já existe"
finally:
conn.close()
# ======================================================
# CRUD Tarefas
# ======================================================
def get_task_codes(username):
"""Lista códigos de tarefas do utilizador"""
if username is None:
return []
conn = get_conn()
cur = conn.cursor()
cur.execute("SELECT task_code FROM tasks WHERE username = ? ORDER BY id DESC", (username,))
codes = [row[0] for row in cur.fetchall()]
conn.close()
return codes
def add_task(username, task_name, start_date, start_time, end_date, end_time):
"""Adiciona nova tarefa"""
if username is None:
return "", "❌ Faça login primeiro", gr.Dropdown(choices=[])
if not task_name or not task_name.strip():
return "", "❌ Preencha a descrição da tarefa", gr.Dropdown(choices=[])
try:
planned_hours = calcular_horas(start_date, start_time, end_date, end_time)
except ValueError as e:
return "", f"❌ {str(e)}", gr.Dropdown(choices=[])
conn = get_conn()
cur = conn.cursor()
try:
# Gerar código com username
cur.execute("SELECT COUNT(*) FROM tasks WHERE username = ?", (username,))
seq = cur.fetchone()[0] + 1
task_code = f"TAR_{username}_{seq:04d}"
# Limpar formato de data
if isinstance(start_date, str) and " " in start_date:
start_date = start_date.split()[0]
if isinstance(end_date, str) and " " in end_date:
end_date = end_date.split()[0]
cur.execute("""
INSERT INTO tasks (username, task_code, task_name, task_date, start_time, end_time, planned_hours)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (username, task_code, task_name, start_date,
f"{start_date} {start_time}:00",
f"{end_date} {end_time}:00",
planned_hours))
conn.commit()
# Atualizar lista
task_codes = get_task_codes(username)
return f"{planned_hours} h", f"✅ Tarefa {task_code} criada!", gr.Dropdown(choices=task_codes, value=task_code)
except Exception as e:
conn.rollback()
return "", f"❌ Erro: {e}", gr.Dropdown(choices=[])
finally:
conn.close()
def get_task_by_code(username, task_code):
"""Busca tarefa por código"""
if not task_code or username is None:
return "", "", "09:00", "", "17:00", ""
conn = get_conn()
cur = conn.cursor()
cur.execute("""
SELECT task_name, start_time, end_time, planned_hours
FROM tasks
WHERE username = ? AND task_code = ?
""", (username, task_code))
row = cur.fetchone()
conn.close()
if not row:
return "", "", "09:00", "", "17:00", ""
task_name, start_time, end_time, planned_hours = row
# Parse timestamps
start_dt = datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S")
end_dt = datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S")
return (
task_name,
start_dt.strftime("%Y-%m-%d"),
start_dt.strftime("%H:%M"),
end_dt.strftime("%Y-%m-%d"),
end_dt.strftime("%H:%M"),
f"{planned_hours} h"
)
def update_task(username, task_code, task_name, start_date, start_time, end_date, end_time):
"""Atualiza tarefa existente"""
if username is None:
return "❌ Faça login primeiro"
if not task_code:
return "❌ Selecione uma tarefa"
if not task_name or not task_name.strip():
return "❌ Preencha a descrição"
try:
planned_hours = calcular_horas(start_date, start_time, end_date, end_time)
except ValueError as e:
return f"❌ {str(e)}"
conn = get_conn()
cur = conn.cursor()
try:
# Limpar formato de data
if isinstance(start_date, str) and " " in start_date:
start_date = start_date.split()[0]
if isinstance(end_date, str) and " " in end_date:
end_date = end_date.split()[0]
cur.execute("""
UPDATE tasks
SET task_name = ?, task_date = ?, start_time = ?, end_time = ?, planned_hours = ?
WHERE username = ? AND task_code = ?
""", (task_name, start_date,
f"{start_date} {start_time}:00",
f"{end_date} {end_time}:00",
planned_hours, username, task_code))
if cur.rowcount == 0:
return "❌ Tarefa não encontrada"
conn.commit()
return f"✅ Tarefa {task_code} atualizada!"
except Exception as e:
conn.rollback()
return f"❌ Erro: {e}"
finally:
conn.close()
def delete_task(username, task_code):
"""Elimina tarefa"""
if username is None:
return "❌ Faça login primeiro"
if not task_code:
return "❌ Selecione uma tarefa"
conn = get_conn()
cur = conn.cursor()
try:
cur.execute("DELETE FROM tasks WHERE username = ? AND task_code = ?", (username, task_code))
if cur.rowcount == 0:
return "❌ Tarefa não encontrada"
conn.commit()
return f"✅ Tarefa {task_code} eliminada!"
except Exception as e:
conn.rollback()
return f"❌ Erro: {e}"
finally:
conn.close()
def list_tasks(username):
"""Lista tarefas do utilizador"""
if username is None:
return pd.DataFrame()
conn = get_conn()
df = pd.read_sql_query("""
SELECT
t.task_code AS 'Código',
t.task_name AS 'Tarefa',
t.start_time AS 'Início',
t.end_time AS 'Fim',
t.planned_hours AS 'Horas Previstas',
COALESCE(SUM(s.planned_hours), 0) AS 'Horas Subtarefas',
COUNT(s.id) AS 'Nº Subtarefas'
FROM tasks t
LEFT JOIN subtasks s ON t.id = s.task_id
WHERE t.username = ?
GROUP BY t.id
ORDER BY t.id DESC
""", conn, params=(username,))
conn.close()
if not df.empty:
df['Status'] = df.apply(
lambda row: '🔴 EXCEDIDO' if row['Horas Subtarefas'] > row['Horas Previstas'] else '🟢 OK',
axis=1
)
return df
# ======================================================
# CRUD Subtarefas
# ======================================================
def get_subtask_codes(username, task_code):
"""Lista códigos de subtarefas"""
if username is None or not task_code:
return []
conn = get_conn()
cur = conn.cursor()
cur.execute("""
SELECT s.subtask_code
FROM subtasks s
JOIN tasks t ON s.task_id = t.id
WHERE t.username = ? AND t.task_code = ?
ORDER BY s.id DESC
""", (username, task_code))
codes = [row[0] for row in cur.fetchall()]
conn.close()
return codes
def add_subtask(username, task_code, sub_name, start_date, start_time, end_date, end_time):
"""Adiciona subtarefa"""
if username is None:
return "", "❌ Faça login primeiro"
if not task_code:
return "", "❌ Selecione uma tarefa"
if not sub_name or not sub_name.strip():
return "", "❌ Preencha a descrição"
try:
planned_hours = calcular_horas(start_date, start_time, end_date, end_time)
except ValueError as e:
return "", f"❌ {str(e)}"
conn = get_conn()
cur = conn.cursor()
try:
# Buscar task_id e horas
cur.execute("SELECT id, planned_hours FROM tasks WHERE username = ? AND task_code = ?",
(username, task_code))
row = cur.fetchone()
if not row:
return "", "❌ Tarefa não encontrada"
task_id, task_hours = row
# Verificar limite de 10
cur.execute("SELECT COUNT(*) FROM subtasks WHERE task_id = ?", (task_id,))
count = cur.fetchone()[0]
if count >= 10:
return "", "❌ Limite de 10 subtarefas atingido"
# Gerar código
subtask_code = f"SUB_{task_id}_{count+1:02d}"
# Limpar formato de data
if isinstance(start_date, str) and " " in start_date:
start_date = start_date.split()[0]
if isinstance(end_date, str) and " " in end_date:
end_date = end_date.split()[0]
cur.execute("""
INSERT INTO subtasks (task_id, subtask_code, subtask_name, start_time, end_time, planned_hours)
VALUES (?, ?, ?, ?, ?, ?)
""", (task_id, subtask_code, sub_name,
f"{start_date} {start_time}:00",
f"{end_date} {end_time}:00",
planned_hours))
conn.commit()
# Verificar total
cur.execute("SELECT COALESCE(SUM(planned_hours), 0) FROM subtasks WHERE task_id = ?", (task_id,))
total_sub = cur.fetchone()[0]
if total_sub > task_hours:
msg = f"⚠️ Subtarefa {subtask_code} criada, mas TOTAL EXCEDE ({total_sub}h / {task_hours}h)"
else:
msg = f"✅ Subtarefa {subtask_code} criada! ({total_sub}h / {task_hours}h)"
return f"{planned_hours} h", msg
except Exception as e:
conn.rollback()
return "", f"❌ Erro: {e}"
finally:
conn.close()
def delete_subtask(username, task_code, subtask_code):
"""Elimina subtarefa"""
if username is None:
return "❌ Faça login primeiro"
if not task_code or not subtask_code:
return "❌ Selecione tarefa e subtarefa"
conn = get_conn()
cur = conn.cursor()
try:
# Buscar task_id
cur.execute("SELECT id FROM tasks WHERE username = ? AND task_code = ?", (username, task_code))
row = cur.fetchone()
if not row:
return "❌ Tarefa não encontrada"
task_id = row[0]
cur.execute("DELETE FROM subtasks WHERE task_id = ? AND subtask_code = ?", (task_id, subtask_code))
if cur.rowcount == 0:
return "❌ Subtarefa não encontrada"
conn.commit()
return f"✅ Subtarefa {subtask_code} eliminada!"
except Exception as e:
conn.rollback()
return f"❌ Erro: {e}"
finally:
conn.close()
def list_subtasks(username, task_code):
"""Lista subtarefas de uma tarefa"""
if username is None or not task_code:
return pd.DataFrame()
conn = get_conn()
cur = conn.cursor()
# Buscar task_id e horas
cur.execute("SELECT id, planned_hours FROM tasks WHERE username = ? AND task_code = ?",
(username, task_code))
row = cur.fetchone()
if not row:
conn.close()
return pd.DataFrame()
task_id, task_hours = row
df = pd.read_sql_query("""
SELECT
subtask_code AS 'Código',
subtask_name AS 'Subtarefa',
start_time AS 'Início',
end_time AS 'Fim',
planned_hours AS 'Horas'
FROM subtasks
WHERE task_id = ?
ORDER BY id DESC
""", conn, params=(task_id,))
conn.close()
if not df.empty:
total_sub = df['Horas'].sum()
exceeded = total_sub > task_hours
df['Status'] = '🔴 EXCEDIDO' if exceeded else '🟢 OK'
return df
# ======================================================
# Gráfico Gantt
# ======================================================
def generate_gantt(username, selected_task_code=None):
"""Gera gráfico Gantt - mostra tarefas ou subtarefas"""
if username is None:
return go.Figure().add_annotation(
text="⚠️ Faça login primeiro",
xref="paper", yref="paper",
x=0.5, y=0.5, showarrow=False,
font=dict(size=20)
)
conn = get_conn()
# Se nenhuma tarefa selecionada, mostrar todas as tarefas
if not selected_task_code:
df = pd.read_sql_query("""
SELECT
task_code,
task_name,
start_time,
end_time
FROM tasks
WHERE username = ?
ORDER BY start_time
""", conn, params=(username,))
conn.close()
if df.empty:
return go.Figure().add_annotation(
text="Nenhuma tarefa encontrada",
xref="paper", yref="paper",
x=0.5, y=0.5, showarrow=False,
font=dict(size=16)
)
# Preparar dados para Gantt
gantt_data = []
for _, row in df.iterrows():
start = datetime.strptime(row['start_time'], "%Y-%m-%d %H:%M:%S")
end = datetime.strptime(row['end_time'], "%Y-%m-%d %H:%M:%S")
gantt_data.append(dict(
Task=row['task_name'][:30], # Limitar tamanho
Start=start.strftime("%Y-%m-%d"),
Finish=end.strftime("%Y-%m-%d"),
Resource=row['task_code']
))
fig = ff.create_gantt(
gantt_data,
index_col='Resource',
show_colorbar=True,
group_tasks=True,
title=f"📊 Gantt - Tarefas de {username}"
)
fig.update_layout(
xaxis_title="Data",
height=max(500, 200 + len(gantt_data) * 50),
margin=dict(l=400, r=50, t=80, b=50),
hovermode='closest',
yaxis=dict(automargin=True)
)
return fig
# Se tarefa selecionada, mostrar tarefa + subtarefas
else:
# Buscar tarefa
cur = conn.cursor()
cur.execute("""
SELECT id, task_name, start_time, end_time
FROM tasks
WHERE username = ? AND task_code = ?
""", (username, selected_task_code))
task_row = cur.fetchone()
if not task_row:
conn.close()
return go.Figure().add_annotation(
text="Tarefa não encontrada",
xref="paper", yref="paper",
x=0.5, y=0.5, showarrow=False
)
task_id, task_name, task_start, task_end = task_row
# Buscar subtarefas
df_sub = pd.read_sql_query("""
SELECT
subtask_code,
subtask_name,
start_time,
end_time
FROM subtasks
WHERE task_id = ?
ORDER BY start_time
""", conn, params=(task_id,))
conn.close()
# Preparar dados
gantt_data = []
# Adicionar tarefa principal
task_start_dt = datetime.strptime(task_start, "%Y-%m-%d %H:%M:%S")
task_end_dt = datetime.strptime(task_end, "%Y-%m-%d %H:%M:%S")
gantt_data.append(dict(
Task=f"📌 {task_name[:30]}",
Start=task_start_dt.strftime("%Y-%m-%d"),
Finish=task_end_dt.strftime("%Y-%m-%d"),
Resource=selected_task_code
))
# Adicionar subtarefas
for _, row in df_sub.iterrows():
start = datetime.strptime(row['start_time'], "%Y-%m-%d %H:%M:%S")
end = datetime.strptime(row['end_time'], "%Y-%m-%d %H:%M:%S")
gantt_data.append(dict(
Task=f" └─ {row['subtask_name'][:25]}",
Start=start.strftime("%Y-%m-%d"),
Finish=end.strftime("%Y-%m-%d"),
Resource=row['subtask_code']
))
if len(gantt_data) == 1:
# Só tarefa, sem subtarefas
fig = ff.create_gantt(
gantt_data,
index_col='Resource',
show_colorbar=False,
title=f"📊 Gantt - {selected_task_code} (sem subtarefas)"
)
else:
fig = ff.create_gantt(
gantt_data,
index_col='Resource',
show_colorbar=True,
group_tasks=True,
title=f"📊 Gantt - {selected_task_code} com {len(gantt_data)-1} subtarefas"
)
fig.update_layout(
xaxis_title="Data",
height=max(500, 200 + len(gantt_data) * 50),
margin=dict(l=600, r=50, t=80, b=50),
hovermode='closest',
yaxis=dict(
automargin=False,
tickfont=dict(size=10),
tickmode='linear',
side='left'
),
xaxis=dict(domain=[0, 1])
)
return fig
# ======================================================
# Interface Gradio
# ======================================================
with gr.Blocks(title="Time Tracking") as demo:
gr.Markdown("# ⛏️ Time Tracking")
user_state = gr.State(None) # Armazena username
login_indicator = gr.Markdown("## ⚠️ **Não autenticado**")
# ===== TAB: Login =====
with gr.Tab("🔐 Login"):
with gr.Row():
with gr.Column():
gr.Markdown("### Entrar")
login_user_input = gr.Textbox(label="Utilizador")
login_pass_input = gr.Textbox(label="Password", type="password")
login_msg = gr.Markdown()
login_btn = gr.Button("Entrar", variant="primary")
with gr.Column():
gr.Markdown("### Criar Conta")
new_user_input = gr.Textbox(label="Novo utilizador")
new_pass_input = gr.Textbox(label="Password", type="password")
create_msg = gr.Markdown()
create_btn = gr.Button("Criar", variant="secondary")
login_btn.click(
login_user,
[login_user_input, login_pass_input],
[user_state, login_msg, login_indicator]
)
create_btn.click(
create_user,
[new_user_input, new_pass_input],
create_msg
)
# ===== TAB: Gerir Tarefas =====
with gr.Tab("📝 Gerir Tarefas"):
with gr.Row():
edit_task_code = gr.Dropdown(label="Selecionar tarefa (vazio = nova)", choices=[])
refresh_tasks_btn = gr.Button("🔄", size="sm")
task_name = gr.Textbox(label="Descrição", lines=2)
with gr.Row():
with gr.Column():
task_start_date = gr.DateTime(label="📅 Data início", value=datetime.now().strftime("%Y-%m-%d"), include_time=False, type="string")
task_start_time = gr.Dropdown(label="Hora início", choices=slots_30_min(), value="09:00")
with gr.Column():
task_end_date = gr.DateTime(label="📅 Data fim", value=datetime.now().strftime("%Y-%m-%d"), include_time=False, type="string")
task_end_time = gr.Dropdown(label="Hora fim", choices=slots_30_min(), value="17:00")
task_hours_box = gr.Textbox(label="Horas previstas", interactive=False)
task_msg = gr.Markdown()
with gr.Row():
save_task_btn = gr.Button("💾 Guardar Nova", variant="primary")
update_task_btn = gr.Button("✏️ Atualizar", variant="secondary")
delete_task_btn = gr.Button("🗑️ Eliminar", variant="stop")
# Eventos
for comp in [task_start_date, task_start_time, task_end_date, task_end_time]:
comp.change(calcular_horas_preview, [task_start_date, task_start_time, task_end_date, task_end_time], task_hours_box)
edit_task_code.change(
get_task_by_code,
[user_state, edit_task_code],
[task_name, task_start_date, task_start_time, task_end_date, task_end_time, task_hours_box]
)
refresh_tasks_btn.click(lambda u: gr.Dropdown(choices=get_task_codes(u)), user_state, edit_task_code)
save_task_btn.click(
add_task,
[user_state, task_name, task_start_date, task_start_time, task_end_date, task_end_time],
[task_hours_box, task_msg, edit_task_code]
)
update_task_btn.click(
update_task,
[user_state, edit_task_code, task_name, task_start_date, task_start_time, task_end_date, task_end_time],
task_msg
)
delete_task_btn.click(delete_task, [user_state, edit_task_code], task_msg)
# ===== TAB: Gerir Subtarefas =====
with gr.Tab("📋 Gerir Subtarefas"):
with gr.Row():
sub_task_code = gr.Dropdown(label="Tarefa", choices=[])
refresh_sub_btn = gr.Button("🔄", size="sm")
sub_name = gr.Textbox(label="Descrição", lines=2)
with gr.Row():
with gr.Column():
sub_start_date = gr.DateTime(label="📅 Data início", value=datetime.now().strftime("%Y-%m-%d"), include_time=False, type="string")
sub_start_time = gr.Dropdown(label="Hora início", choices=slots_30_min(), value="09:00")
with gr.Column():
sub_end_date = gr.DateTime(label="📅 Data fim", value=datetime.now().strftime("%Y-%m-%d"), include_time=False, type="string")
sub_end_time = gr.Dropdown(label="Hora fim", choices=slots_30_min(), value="10:00")
sub_hours_box = gr.Textbox(label="Horas", interactive=False)
sub_msg = gr.Markdown()
save_sub_btn = gr.Button("💾 Guardar Subtarefa", variant="primary")
gr.Markdown("---\n### Eliminar Subtarefa")
with gr.Row():
del_task_code = gr.Dropdown(label="Tarefa", choices=[])
del_sub_code = gr.Dropdown(label="Subtarefa", choices=[])
refresh_del_btn = gr.Button("🔄", size="sm")
del_msg = gr.Markdown()
del_sub_btn = gr.Button("🗑️ Eliminar", variant="stop")
# Eventos
for comp in [sub_start_date, sub_start_time, sub_end_date, sub_end_time]:
comp.change(calcular_horas_preview, [sub_start_date, sub_start_time, sub_end_date, sub_end_time], sub_hours_box)
refresh_sub_btn.click(lambda u: gr.Dropdown(choices=get_task_codes(u)), user_state, sub_task_code)
save_sub_btn.click(
add_subtask,
[user_state, sub_task_code, sub_name, sub_start_date, sub_start_time, sub_end_date, sub_end_time],
[sub_hours_box, sub_msg]
)
refresh_del_btn.click(lambda u: gr.Dropdown(choices=get_task_codes(u)), user_state, del_task_code)
del_task_code.change(lambda u, tc: gr.Dropdown(choices=get_subtask_codes(u, tc)), [user_state, del_task_code], del_sub_code)
del_sub_btn.click(delete_subtask, [user_state, del_task_code, del_sub_code], del_msg)
# ===== TAB: Visualizar Tarefas =====
with gr.Tab("📊 Visualizar Tarefas"):
tasks_table = gr.Dataframe()
refresh_view_btn = gr.Button("🔄 Atualizar")
refresh_view_btn.click(list_tasks, user_state, tasks_table)
# ===== TAB: Visualizar Subtarefas =====
with gr.Tab("📋 Visualizar Subtarefas"):
view_task_code = gr.Dropdown(label="Tarefa", choices=[])
refresh_view_sub_btn = gr.Button("🔄 Atualizar lista")
subtasks_table = gr.Dataframe()
refresh_view_sub_btn.click(lambda u: gr.Dropdown(choices=get_task_codes(u)), user_state, view_task_code)
view_task_code.change(list_subtasks, [user_state, view_task_code], subtasks_table)
# ===== TAB: Gráfico Gantt =====
with gr.Tab("📊 Gráfico Gantt"):
gr.Markdown("""
### 📊 Gráfico de Gantt
**Modo 1**: Deixe vazio para ver **todas as tarefas**
**Modo 2**: Selecione uma tarefa para ver **tarefa + subtarefas**
""")
gantt_task_select = gr.Dropdown(label="🔍 Selecionar tarefa (opcional - deixe vazio para ver todas)", choices=[], allow_custom_value=False)
with gr.Row():
gantt_refresh_btn = gr.Button("🔄 Atualizar lista")
gantt_clear_btn = gr.Button("❌ Limpar seleção")
gantt_generate_btn = gr.Button("📊 Gerar Gantt", variant="primary")
gantt_plot = gr.Plot(label="Gráfico")
# Atualizar dropdown de tarefas
gantt_refresh_btn.click(
lambda u: gr.Dropdown(choices=get_task_codes(u)),
user_state,
gantt_task_select
)
# Limpar seleção (volta para modo "todas as tarefas")
gantt_clear_btn.click(
lambda: gr.Dropdown(value=None),
None,
gantt_task_select
)
# Gerar Gantt (todas as tarefas ou tarefa específica)
gantt_generate_btn.click(
generate_gantt,
[user_state, gantt_task_select],
gantt_plot
)
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860, share=False)