Geoeasy commited on
Commit
6a7fb3c
·
verified ·
1 Parent(s): a8bb5ba

Upload 7 files

Browse files
Files changed (7) hide show
  1. .gitignore +36 -0
  2. .python-version +1 -0
  3. 00_criar_bd_e_user_teste.py +120 -0
  4. README.md +2 -1
  5. app.py +843 -0
  6. init_db.py +122 -0
  7. requirements.txt +5 -0
.gitignore ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .Python
6
+ build/
7
+ develop-eggs/
8
+ dist/
9
+ downloads/
10
+ eggs/
11
+ .eggs/
12
+ lib/
13
+ lib64/
14
+ parts/
15
+ sdist/
16
+ var/
17
+ wheels/
18
+ *.egg-info/
19
+ .installed.cfg
20
+ *.egg
21
+
22
+ # Virtual environments
23
+ venv/
24
+ ENV/
25
+ env/
26
+
27
+ # IDE
28
+ .vscode/
29
+ .idea/
30
+ *.swp
31
+ *.swo
32
+ *~
33
+
34
+ # OS
35
+ .DS_Store
36
+ Thumbs.db
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.11
00_criar_bd_e_user_teste.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script de inicialização da base de dados IA Mining
4
+ Versão com USERNAME em vez de user_id
5
+ """
6
+
7
+ import sqlite3
8
+ import bcrypt
9
+ from pathlib import Path
10
+
11
+ # Paths
12
+ BASE_DIR = Path(__file__).parent
13
+ DB_PATH = BASE_DIR / "db" / "app.db"
14
+ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
15
+
16
+ print("=" * 60)
17
+ print("IA MINING - Inicialização da Base de Dados")
18
+ print("Versão: USERNAME (em vez de user_id)")
19
+ print("=" * 60)
20
+
21
+ # Conectar à BD
22
+ conn = sqlite3.connect(DB_PATH)
23
+ conn.execute("PRAGMA foreign_keys = ON") # Ativar foreign keys
24
+ cur = conn.cursor()
25
+
26
+ # Criar tabela users
27
+ print("\n[1/5] Criando tabela 'users'...")
28
+ cur.execute("""
29
+ CREATE TABLE IF NOT EXISTS users (
30
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
31
+ username TEXT UNIQUE NOT NULL,
32
+ password_hash TEXT NOT NULL,
33
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
34
+ )
35
+ """)
36
+ print("✅ Tabela 'users' criada")
37
+
38
+ # Criar tabela tasks (com username em vez de user_id)
39
+ print("\n[2/5] Criando tabela 'tasks' (com username)...")
40
+ cur.execute("""
41
+ CREATE TABLE IF NOT EXISTS tasks (
42
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
43
+ username TEXT NOT NULL,
44
+ task_code TEXT UNIQUE NOT NULL,
45
+ task_name TEXT NOT NULL,
46
+ task_date DATE NOT NULL,
47
+ start_time TIMESTAMP NOT NULL,
48
+ end_time TIMESTAMP NOT NULL,
49
+ planned_hours REAL NOT NULL,
50
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
51
+ FOREIGN KEY (username) REFERENCES users(username) ON DELETE CASCADE
52
+ )
53
+ """)
54
+ print("✅ Tabela 'tasks' criada (FK: username)")
55
+
56
+ # Criar tabela subtasks
57
+ print("\n[3/5] Criando tabela 'subtasks'...")
58
+ cur.execute("""
59
+ CREATE TABLE IF NOT EXISTS subtasks (
60
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
61
+ task_id INTEGER NOT NULL,
62
+ subtask_code TEXT UNIQUE NOT NULL,
63
+ subtask_name TEXT NOT NULL,
64
+ start_time TIMESTAMP NOT NULL,
65
+ end_time TIMESTAMP NOT NULL,
66
+ planned_hours REAL NOT NULL,
67
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
68
+ FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
69
+ )
70
+ """)
71
+ print("✅ Tabela 'subtasks' criada")
72
+
73
+ # Criar tabela task_logs (com username)
74
+ print("\n[4/5] Criando tabela 'task_logs' (com username)...")
75
+ cur.execute("""
76
+ CREATE TABLE IF NOT EXISTS task_logs (
77
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
78
+ username TEXT NOT NULL,
79
+ task_id INTEGER,
80
+ activity TEXT NOT NULL,
81
+ event_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
82
+ FOREIGN KEY (username) REFERENCES users(username) ON DELETE CASCADE,
83
+ FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
84
+ )
85
+ """)
86
+ print("✅ Tabela 'task_logs' criada")
87
+
88
+ # Criar utilizador de teste
89
+ print("\n[5/5] Criando utilizador de teste...")
90
+ username = "admin"
91
+ password = "admin123"
92
+ pw_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
93
+
94
+ try:
95
+ cur.execute(
96
+ "INSERT INTO users (username, password_hash) VALUES (?, ?)",
97
+ (username, pw_hash)
98
+ )
99
+ print(f"✅ Utilizador de teste criado:")
100
+ print(f" Username: {username}")
101
+ print(f" Password: {password}")
102
+ except sqlite3.IntegrityError:
103
+ print(f"⚠️ Utilizador '{username}' já existe")
104
+
105
+ # Commit e fechar
106
+ conn.commit()
107
+ conn.close()
108
+
109
+ print("\n" + "=" * 60)
110
+ print("✅ BASE DE DADOS INICIALIZADA COM SUCESSO!")
111
+ print("=" * 60)
112
+ print("\n📋 Estrutura:")
113
+ print(" - tasks.username → FK para users.username")
114
+ print(" - task_logs.username → FK para users.username")
115
+ print(" - Códigos de tarefa: TAR_{username}_{seq}")
116
+ print("\nPróximos passos:")
117
+ print("1. Execute: python app.py")
118
+ print("2. Aceda: http://localhost:7860")
119
+ print(f"3. Faça login com: {username} / {password}")
120
+ print("=" * 60)
README.md CHANGED
@@ -4,8 +4,9 @@ emoji: ⛏️
4
  colorFrom: blue
5
  colorTo: green
6
  sdk: gradio
7
- sdk_version: "5.0.0"
8
  app_file: app.py
 
9
  pinned: false
10
  ---
11
 
 
4
  colorFrom: blue
5
  colorTo: green
6
  sdk: gradio
7
+ sdk_version: 4.44.0
8
  app_file: app.py
9
+ python_version: 3.11
10
  pinned: false
11
  ---
12
 
app.py ADDED
@@ -0,0 +1,843 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import sqlite3
3
+ import bcrypt
4
+ import pandas as pd
5
+ from pathlib import Path
6
+ from datetime import datetime
7
+ import plotly.figure_factory as ff
8
+ import plotly.graph_objects as go
9
+
10
+ # ======================================================
11
+ # Configuração
12
+ # ======================================================
13
+ BASE_DIR = Path(__file__).parent
14
+ DB_PATH = BASE_DIR / "db" / "app.db"
15
+ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
16
+
17
+ # Inicializar base de dados automaticamente
18
+ try:
19
+ from init_db import init_database
20
+ init_database()
21
+ except Exception as e:
22
+ print(f"Aviso ao inicializar BD: {e}")
23
+
24
+ # ======================================================
25
+ # Funções de Base de Dados
26
+ # ======================================================
27
+ def get_conn():
28
+ """Retorna conexão com BD"""
29
+ conn = sqlite3.connect(DB_PATH, timeout=30)
30
+ conn.execute("PRAGMA foreign_keys = ON")
31
+ return conn
32
+
33
+ # ======================================================
34
+ # Utilitários
35
+ # ======================================================
36
+ def slots_30_min():
37
+ """Gera slots de 30 em 30 minutos"""
38
+ return [f"{h:02d}:{m:02d}" for h in range(24) for m in (0, 30)]
39
+
40
+ def calcular_horas(start_date, start_time, end_date, end_time):
41
+ """Calcula horas entre duas datas/horas"""
42
+ if not all([start_date, start_time, end_date, end_time]):
43
+ raise ValueError("Preencha todos os campos")
44
+
45
+ try:
46
+ # Aceitar formato de gr.DateTime (pode vir como "2026-02-03" ou "2026-02-03 00:00:00")
47
+ if isinstance(start_date, str) and " " in start_date:
48
+ start_date = start_date.split()[0]
49
+ if isinstance(end_date, str) and " " in end_date:
50
+ end_date = end_date.split()[0]
51
+
52
+ inicio = datetime.strptime(f"{start_date} {start_time}", "%Y-%m-%d %H:%M")
53
+ fim = datetime.strptime(f"{end_date} {end_time}", "%Y-%m-%d %H:%M")
54
+ except ValueError as e:
55
+ raise ValueError(f"Formato inválido: {e}")
56
+
57
+ if fim <= inicio:
58
+ raise ValueError("Data/hora fim deve ser posterior ao início")
59
+
60
+ return round((fim - inicio).total_seconds() / 3600, 2)
61
+
62
+ def calcular_horas_preview(start_date, start_time, end_date, end_time):
63
+ """Preview de horas calculadas"""
64
+ try:
65
+ if not all([start_date, start_time, end_date, end_time]):
66
+ return ""
67
+ horas = calcular_horas(start_date, start_time, end_date, end_time)
68
+ return f"{horas} h"
69
+ except:
70
+ return ""
71
+
72
+ # ======================================================
73
+ # Autenticação
74
+ # ======================================================
75
+ def login_user(username, password):
76
+ """Faz login do utilizador - retorna username"""
77
+ if not username or not password:
78
+ return None, "❌ Preencha todos os campos", "⚠️ **Não autenticado**"
79
+
80
+ conn = get_conn()
81
+ cur = conn.cursor()
82
+ cur.execute("SELECT username, password_hash FROM users WHERE username = ?", (username,))
83
+ row = cur.fetchone()
84
+
85
+ if not row:
86
+ conn.close()
87
+ return None, "❌ Utilizador não encontrado", "⚠️ **Não autenticado**"
88
+
89
+ db_username, pw_hash = row
90
+
91
+ if bcrypt.checkpw(password.encode(), pw_hash.encode()):
92
+ conn.close()
93
+ indicator = f"## ✅ Logado: **{db_username}**"
94
+ return db_username, f"✅ Bem-vindo, {db_username}!", indicator
95
+
96
+ conn.close()
97
+ return None, "❌ Password incorreta", "⚠️ **Não autenticado**"
98
+
99
+ def create_user(username, password):
100
+ """Cria novo utilizador"""
101
+ if not username or not password:
102
+ return "❌ Preencha todos os campos"
103
+
104
+ if len(password) < 6:
105
+ return "❌ Password deve ter pelo menos 6 caracteres"
106
+
107
+ conn = get_conn()
108
+ cur = conn.cursor()
109
+ pw_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
110
+
111
+ try:
112
+ cur.execute("INSERT INTO users (username, password_hash) VALUES (?, ?)", (username, pw_hash))
113
+ conn.commit()
114
+ return f"✅ Utilizador '{username}' criado! Faça login agora."
115
+ except sqlite3.IntegrityError:
116
+ return f"⚠️ Utilizador '{username}' já existe"
117
+ finally:
118
+ conn.close()
119
+
120
+ # ======================================================
121
+ # CRUD Tarefas
122
+ # ======================================================
123
+ def get_task_codes(username):
124
+ """Lista códigos de tarefas do utilizador"""
125
+ if username is None:
126
+ return []
127
+ conn = get_conn()
128
+ cur = conn.cursor()
129
+ cur.execute("SELECT task_code FROM tasks WHERE username = ? ORDER BY id DESC", (username,))
130
+ codes = [row[0] for row in cur.fetchall()]
131
+ conn.close()
132
+ return codes
133
+
134
+ def add_task(username, task_name, start_date, start_time, end_date, end_time):
135
+ """Adiciona nova tarefa"""
136
+ if username is None:
137
+ return "", "❌ Faça login primeiro", gr.Dropdown(choices=[])
138
+
139
+ if not task_name or not task_name.strip():
140
+ return "", "❌ Preencha a descrição da tarefa", gr.Dropdown(choices=[])
141
+
142
+ try:
143
+ planned_hours = calcular_horas(start_date, start_time, end_date, end_time)
144
+ except ValueError as e:
145
+ return "", f"❌ {str(e)}", gr.Dropdown(choices=[])
146
+
147
+ conn = get_conn()
148
+ cur = conn.cursor()
149
+
150
+ try:
151
+ # Gerar código com username
152
+ cur.execute("SELECT COUNT(*) FROM tasks WHERE username = ?", (username,))
153
+ seq = cur.fetchone()[0] + 1
154
+ task_code = f"TAR_{username}_{seq:04d}"
155
+
156
+ # Limpar formato de data
157
+ if isinstance(start_date, str) and " " in start_date:
158
+ start_date = start_date.split()[0]
159
+ if isinstance(end_date, str) and " " in end_date:
160
+ end_date = end_date.split()[0]
161
+
162
+ cur.execute("""
163
+ INSERT INTO tasks (username, task_code, task_name, task_date, start_time, end_time, planned_hours)
164
+ VALUES (?, ?, ?, ?, ?, ?, ?)
165
+ """, (username, task_code, task_name, start_date,
166
+ f"{start_date} {start_time}:00",
167
+ f"{end_date} {end_time}:00",
168
+ planned_hours))
169
+
170
+ conn.commit()
171
+
172
+ # Atualizar lista
173
+ task_codes = get_task_codes(username)
174
+
175
+ return f"{planned_hours} h", f"✅ Tarefa {task_code} criada!", gr.Dropdown(choices=task_codes, value=task_code)
176
+
177
+ except Exception as e:
178
+ conn.rollback()
179
+ return "", f"❌ Erro: {e}", gr.Dropdown(choices=[])
180
+ finally:
181
+ conn.close()
182
+
183
+ def get_task_by_code(username, task_code):
184
+ """Busca tarefa por código"""
185
+ if not task_code or username is None:
186
+ return "", "", "09:00", "", "17:00", ""
187
+
188
+ conn = get_conn()
189
+ cur = conn.cursor()
190
+ cur.execute("""
191
+ SELECT task_name, start_time, end_time, planned_hours
192
+ FROM tasks
193
+ WHERE username = ? AND task_code = ?
194
+ """, (username, task_code))
195
+ row = cur.fetchone()
196
+ conn.close()
197
+
198
+ if not row:
199
+ return "", "", "09:00", "", "17:00", ""
200
+
201
+ task_name, start_time, end_time, planned_hours = row
202
+
203
+ # Parse timestamps
204
+ start_dt = datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S")
205
+ end_dt = datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S")
206
+
207
+ return (
208
+ task_name,
209
+ start_dt.strftime("%Y-%m-%d"),
210
+ start_dt.strftime("%H:%M"),
211
+ end_dt.strftime("%Y-%m-%d"),
212
+ end_dt.strftime("%H:%M"),
213
+ f"{planned_hours} h"
214
+ )
215
+
216
+ def update_task(username, task_code, task_name, start_date, start_time, end_date, end_time):
217
+ """Atualiza tarefa existente"""
218
+ if username is None:
219
+ return "❌ Faça login primeiro"
220
+
221
+ if not task_code:
222
+ return "❌ Selecione uma tarefa"
223
+
224
+ if not task_name or not task_name.strip():
225
+ return "❌ Preencha a descrição"
226
+
227
+ try:
228
+ planned_hours = calcular_horas(start_date, start_time, end_date, end_time)
229
+ except ValueError as e:
230
+ return f"❌ {str(e)}"
231
+
232
+ conn = get_conn()
233
+ cur = conn.cursor()
234
+
235
+ try:
236
+ # Limpar formato de data
237
+ if isinstance(start_date, str) and " " in start_date:
238
+ start_date = start_date.split()[0]
239
+ if isinstance(end_date, str) and " " in end_date:
240
+ end_date = end_date.split()[0]
241
+
242
+ cur.execute("""
243
+ UPDATE tasks
244
+ SET task_name = ?, task_date = ?, start_time = ?, end_time = ?, planned_hours = ?
245
+ WHERE username = ? AND task_code = ?
246
+ """, (task_name, start_date,
247
+ f"{start_date} {start_time}:00",
248
+ f"{end_date} {end_time}:00",
249
+ planned_hours, username, task_code))
250
+
251
+ if cur.rowcount == 0:
252
+ return "❌ Tarefa não encontrada"
253
+
254
+ conn.commit()
255
+ return f"✅ Tarefa {task_code} atualizada!"
256
+
257
+ except Exception as e:
258
+ conn.rollback()
259
+ return f"❌ Erro: {e}"
260
+ finally:
261
+ conn.close()
262
+
263
+ def delete_task(username, task_code):
264
+ """Elimina tarefa"""
265
+ if username is None:
266
+ return "❌ Faça login primeiro"
267
+
268
+ if not task_code:
269
+ return "❌ Selecione uma tarefa"
270
+
271
+ conn = get_conn()
272
+ cur = conn.cursor()
273
+
274
+ try:
275
+ cur.execute("DELETE FROM tasks WHERE username = ? AND task_code = ?", (username, task_code))
276
+
277
+ if cur.rowcount == 0:
278
+ return "❌ Tarefa não encontrada"
279
+
280
+ conn.commit()
281
+ return f"✅ Tarefa {task_code} eliminada!"
282
+
283
+ except Exception as e:
284
+ conn.rollback()
285
+ return f"❌ Erro: {e}"
286
+ finally:
287
+ conn.close()
288
+
289
+ def list_tasks(username):
290
+ """Lista tarefas do utilizador"""
291
+ if username is None:
292
+ return pd.DataFrame()
293
+
294
+ conn = get_conn()
295
+
296
+ df = pd.read_sql_query("""
297
+ SELECT
298
+ t.task_code AS 'Código',
299
+ t.task_name AS 'Tarefa',
300
+ t.start_time AS 'Início',
301
+ t.end_time AS 'Fim',
302
+ t.planned_hours AS 'Horas Previstas',
303
+ COALESCE(SUM(s.planned_hours), 0) AS 'Horas Subtarefas',
304
+ COUNT(s.id) AS 'Nº Subtarefas'
305
+ FROM tasks t
306
+ LEFT JOIN subtasks s ON t.id = s.task_id
307
+ WHERE t.username = ?
308
+ GROUP BY t.id
309
+ ORDER BY t.id DESC
310
+ """, conn, params=(username,))
311
+
312
+ conn.close()
313
+
314
+ if not df.empty:
315
+ df['Status'] = df.apply(
316
+ lambda row: '🔴 EXCEDIDO' if row['Horas Subtarefas'] > row['Horas Previstas'] else '🟢 OK',
317
+ axis=1
318
+ )
319
+
320
+ return df
321
+
322
+ # ======================================================
323
+ # CRUD Subtarefas
324
+ # ======================================================
325
+ def get_subtask_codes(username, task_code):
326
+ """Lista códigos de subtarefas"""
327
+ if username is None or not task_code:
328
+ return []
329
+
330
+ conn = get_conn()
331
+ cur = conn.cursor()
332
+ cur.execute("""
333
+ SELECT s.subtask_code
334
+ FROM subtasks s
335
+ JOIN tasks t ON s.task_id = t.id
336
+ WHERE t.username = ? AND t.task_code = ?
337
+ ORDER BY s.id DESC
338
+ """, (username, task_code))
339
+ codes = [row[0] for row in cur.fetchall()]
340
+ conn.close()
341
+ return codes
342
+
343
+ def add_subtask(username, task_code, sub_name, start_date, start_time, end_date, end_time):
344
+ """Adiciona subtarefa"""
345
+ if username is None:
346
+ return "", "❌ Faça login primeiro"
347
+
348
+ if not task_code:
349
+ return "", "❌ Selecione uma tarefa"
350
+
351
+ if not sub_name or not sub_name.strip():
352
+ return "", "❌ Preencha a descrição"
353
+
354
+ try:
355
+ planned_hours = calcular_horas(start_date, start_time, end_date, end_time)
356
+ except ValueError as e:
357
+ return "", f"❌ {str(e)}"
358
+
359
+ conn = get_conn()
360
+ cur = conn.cursor()
361
+
362
+ try:
363
+ # Buscar task_id e horas
364
+ cur.execute("SELECT id, planned_hours FROM tasks WHERE username = ? AND task_code = ?",
365
+ (username, task_code))
366
+ row = cur.fetchone()
367
+ if not row:
368
+ return "", "❌ Tarefa não encontrada"
369
+
370
+ task_id, task_hours = row
371
+
372
+ # Verificar limite de 10
373
+ cur.execute("SELECT COUNT(*) FROM subtasks WHERE task_id = ?", (task_id,))
374
+ count = cur.fetchone()[0]
375
+ if count >= 10:
376
+ return "", "❌ Limite de 10 subtarefas atingido"
377
+
378
+ # Gerar código
379
+ subtask_code = f"SUB_{task_id}_{count+1:02d}"
380
+
381
+ # Limpar formato de data
382
+ if isinstance(start_date, str) and " " in start_date:
383
+ start_date = start_date.split()[0]
384
+ if isinstance(end_date, str) and " " in end_date:
385
+ end_date = end_date.split()[0]
386
+
387
+ cur.execute("""
388
+ INSERT INTO subtasks (task_id, subtask_code, subtask_name, start_time, end_time, planned_hours)
389
+ VALUES (?, ?, ?, ?, ?, ?)
390
+ """, (task_id, subtask_code, sub_name,
391
+ f"{start_date} {start_time}:00",
392
+ f"{end_date} {end_time}:00",
393
+ planned_hours))
394
+
395
+ conn.commit()
396
+
397
+ # Verificar total
398
+ cur.execute("SELECT COALESCE(SUM(planned_hours), 0) FROM subtasks WHERE task_id = ?", (task_id,))
399
+ total_sub = cur.fetchone()[0]
400
+
401
+ if total_sub > task_hours:
402
+ msg = f"⚠️ Subtarefa {subtask_code} criada, mas TOTAL EXCEDE ({total_sub}h / {task_hours}h)"
403
+ else:
404
+ msg = f"✅ Subtarefa {subtask_code} criada! ({total_sub}h / {task_hours}h)"
405
+
406
+ return f"{planned_hours} h", msg
407
+
408
+ except Exception as e:
409
+ conn.rollback()
410
+ return "", f"❌ Erro: {e}"
411
+ finally:
412
+ conn.close()
413
+
414
+ def delete_subtask(username, task_code, subtask_code):
415
+ """Elimina subtarefa"""
416
+ if username is None:
417
+ return "❌ Faça login primeiro"
418
+
419
+ if not task_code or not subtask_code:
420
+ return "❌ Selecione tarefa e subtarefa"
421
+
422
+ conn = get_conn()
423
+ cur = conn.cursor()
424
+
425
+ try:
426
+ # Buscar task_id
427
+ cur.execute("SELECT id FROM tasks WHERE username = ? AND task_code = ?", (username, task_code))
428
+ row = cur.fetchone()
429
+ if not row:
430
+ return "❌ Tarefa não encontrada"
431
+
432
+ task_id = row[0]
433
+
434
+ cur.execute("DELETE FROM subtasks WHERE task_id = ? AND subtask_code = ?", (task_id, subtask_code))
435
+
436
+ if cur.rowcount == 0:
437
+ return "❌ Subtarefa não encontrada"
438
+
439
+ conn.commit()
440
+ return f"✅ Subtarefa {subtask_code} eliminada!"
441
+
442
+ except Exception as e:
443
+ conn.rollback()
444
+ return f"❌ Erro: {e}"
445
+ finally:
446
+ conn.close()
447
+
448
+ def list_subtasks(username, task_code):
449
+ """Lista subtarefas de uma tarefa"""
450
+ if username is None or not task_code:
451
+ return pd.DataFrame()
452
+
453
+ conn = get_conn()
454
+ cur = conn.cursor()
455
+
456
+ # Buscar task_id e horas
457
+ cur.execute("SELECT id, planned_hours FROM tasks WHERE username = ? AND task_code = ?",
458
+ (username, task_code))
459
+ row = cur.fetchone()
460
+ if not row:
461
+ conn.close()
462
+ return pd.DataFrame()
463
+
464
+ task_id, task_hours = row
465
+
466
+ df = pd.read_sql_query("""
467
+ SELECT
468
+ subtask_code AS 'Código',
469
+ subtask_name AS 'Subtarefa',
470
+ start_time AS 'Início',
471
+ end_time AS 'Fim',
472
+ planned_hours AS 'Horas'
473
+ FROM subtasks
474
+ WHERE task_id = ?
475
+ ORDER BY id DESC
476
+ """, conn, params=(task_id,))
477
+
478
+ conn.close()
479
+
480
+ if not df.empty:
481
+ total_sub = df['Horas'].sum()
482
+ exceeded = total_sub > task_hours
483
+ df['Status'] = '🔴 EXCEDIDO' if exceeded else '🟢 OK'
484
+
485
+ return df
486
+
487
+ # ======================================================
488
+ # Gráfico Gantt
489
+ # ======================================================
490
+ def generate_gantt(username, selected_task_code=None):
491
+ """Gera gráfico Gantt - mostra tarefas ou subtarefas"""
492
+ if username is None:
493
+ return go.Figure().add_annotation(
494
+ text="⚠️ Faça login primeiro",
495
+ xref="paper", yref="paper",
496
+ x=0.5, y=0.5, showarrow=False,
497
+ font=dict(size=20)
498
+ )
499
+
500
+ conn = get_conn()
501
+
502
+ # Se nenhuma tarefa selecionada, mostrar todas as tarefas
503
+ if not selected_task_code:
504
+ df = pd.read_sql_query("""
505
+ SELECT
506
+ task_code,
507
+ task_name,
508
+ start_time,
509
+ end_time
510
+ FROM tasks
511
+ WHERE username = ?
512
+ ORDER BY start_time
513
+ """, conn, params=(username,))
514
+
515
+ conn.close()
516
+
517
+ if df.empty:
518
+ return go.Figure().add_annotation(
519
+ text="Nenhuma tarefa encontrada",
520
+ xref="paper", yref="paper",
521
+ x=0.5, y=0.5, showarrow=False,
522
+ font=dict(size=16)
523
+ )
524
+
525
+ # Preparar dados para Gantt
526
+ gantt_data = []
527
+ for _, row in df.iterrows():
528
+ start = datetime.strptime(row['start_time'], "%Y-%m-%d %H:%M:%S")
529
+ end = datetime.strptime(row['end_time'], "%Y-%m-%d %H:%M:%S")
530
+
531
+ gantt_data.append(dict(
532
+ Task=row['task_name'][:30], # Limitar tamanho
533
+ Start=start.strftime("%Y-%m-%d"),
534
+ Finish=end.strftime("%Y-%m-%d"),
535
+ Resource=row['task_code']
536
+ ))
537
+
538
+ fig = ff.create_gantt(
539
+ gantt_data,
540
+ index_col='Resource',
541
+ show_colorbar=True,
542
+ group_tasks=True,
543
+ title=f"📊 Gantt - Tarefas de {username}"
544
+ )
545
+
546
+ fig.update_layout(
547
+ xaxis_title="Data",
548
+ height=max(500, 200 + len(gantt_data) * 50),
549
+ margin=dict(l=400, r=50, t=80, b=50),
550
+ hovermode='closest',
551
+ yaxis=dict(automargin=True)
552
+ )
553
+
554
+ return fig
555
+
556
+ # Se tarefa selecionada, mostrar tarefa + subtarefas
557
+ else:
558
+ # Buscar tarefa
559
+ cur = conn.cursor()
560
+ cur.execute("""
561
+ SELECT id, task_name, start_time, end_time
562
+ FROM tasks
563
+ WHERE username = ? AND task_code = ?
564
+ """, (username, selected_task_code))
565
+ task_row = cur.fetchone()
566
+
567
+ if not task_row:
568
+ conn.close()
569
+ return go.Figure().add_annotation(
570
+ text="Tarefa não encontrada",
571
+ xref="paper", yref="paper",
572
+ x=0.5, y=0.5, showarrow=False
573
+ )
574
+
575
+ task_id, task_name, task_start, task_end = task_row
576
+
577
+ # Buscar subtarefas
578
+ df_sub = pd.read_sql_query("""
579
+ SELECT
580
+ subtask_code,
581
+ subtask_name,
582
+ start_time,
583
+ end_time
584
+ FROM subtasks
585
+ WHERE task_id = ?
586
+ ORDER BY start_time
587
+ """, conn, params=(task_id,))
588
+
589
+ conn.close()
590
+
591
+ # Preparar dados
592
+ gantt_data = []
593
+
594
+ # Adicionar tarefa principal
595
+ task_start_dt = datetime.strptime(task_start, "%Y-%m-%d %H:%M:%S")
596
+ task_end_dt = datetime.strptime(task_end, "%Y-%m-%d %H:%M:%S")
597
+
598
+ gantt_data.append(dict(
599
+ Task=f"📌 {task_name[:30]}",
600
+ Start=task_start_dt.strftime("%Y-%m-%d"),
601
+ Finish=task_end_dt.strftime("%Y-%m-%d"),
602
+ Resource=selected_task_code
603
+ ))
604
+
605
+ # Adicionar subtarefas
606
+ for _, row in df_sub.iterrows():
607
+ start = datetime.strptime(row['start_time'], "%Y-%m-%d %H:%M:%S")
608
+ end = datetime.strptime(row['end_time'], "%Y-%m-%d %H:%M:%S")
609
+
610
+ gantt_data.append(dict(
611
+ Task=f" └─ {row['subtask_name'][:25]}",
612
+ Start=start.strftime("%Y-%m-%d"),
613
+ Finish=end.strftime("%Y-%m-%d"),
614
+ Resource=row['subtask_code']
615
+ ))
616
+
617
+ if len(gantt_data) == 1:
618
+ # Só tarefa, sem subtarefas
619
+ fig = ff.create_gantt(
620
+ gantt_data,
621
+ index_col='Resource',
622
+ show_colorbar=False,
623
+ title=f"📊 Gantt - {selected_task_code} (sem subtarefas)"
624
+ )
625
+ else:
626
+ fig = ff.create_gantt(
627
+ gantt_data,
628
+ index_col='Resource',
629
+ show_colorbar=True,
630
+ group_tasks=True,
631
+ title=f"📊 Gantt - {selected_task_code} com {len(gantt_data)-1} subtarefas"
632
+ )
633
+
634
+ fig.update_layout(
635
+ xaxis_title="Data",
636
+ height=max(500, 200 + len(gantt_data) * 50),
637
+ margin=dict(l=600, r=50, t=80, b=50),
638
+ hovermode='closest',
639
+ yaxis=dict(
640
+ automargin=False,
641
+ tickfont=dict(size=10),
642
+ tickmode='linear',
643
+ side='left'
644
+ ),
645
+ xaxis=dict(domain=[0, 1])
646
+ )
647
+
648
+ return fig
649
+
650
+
651
+
652
+ # ======================================================
653
+ # Interface Gradio
654
+ # ======================================================
655
+ with gr.Blocks(title="Time Tracking") as demo:
656
+
657
+ gr.Markdown("# ⛏️ Time Tracking")
658
+
659
+ user_state = gr.State(None) # Armazena username
660
+ login_indicator = gr.Markdown("## ⚠️ **Não autenticado**")
661
+
662
+ # ===== TAB: Login =====
663
+ with gr.Tab("🔐 Login"):
664
+ with gr.Row():
665
+ with gr.Column():
666
+ gr.Markdown("### Entrar")
667
+ login_user_input = gr.Textbox(label="Utilizador")
668
+ login_pass_input = gr.Textbox(label="Password", type="password")
669
+ login_msg = gr.Markdown()
670
+ login_btn = gr.Button("Entrar", variant="primary")
671
+
672
+ with gr.Column():
673
+ gr.Markdown("### Criar Conta")
674
+ new_user_input = gr.Textbox(label="Novo utilizador")
675
+ new_pass_input = gr.Textbox(label="Password", type="password")
676
+ create_msg = gr.Markdown()
677
+ create_btn = gr.Button("Criar", variant="secondary")
678
+
679
+ login_btn.click(
680
+ login_user,
681
+ [login_user_input, login_pass_input],
682
+ [user_state, login_msg, login_indicator]
683
+ )
684
+
685
+ create_btn.click(
686
+ create_user,
687
+ [new_user_input, new_pass_input],
688
+ create_msg
689
+ )
690
+
691
+ # ===== TAB: Gerir Tarefas =====
692
+ with gr.Tab("📝 Gerir Tarefas"):
693
+ with gr.Row():
694
+ edit_task_code = gr.Dropdown(label="Selecionar tarefa (vazio = nova)", choices=[])
695
+ refresh_tasks_btn = gr.Button("🔄", size="sm")
696
+
697
+ task_name = gr.Textbox(label="Descrição", lines=2)
698
+
699
+ with gr.Row():
700
+ with gr.Column():
701
+ task_start_date = gr.DateTime(label="📅 Data início", value=datetime.now().strftime("%Y-%m-%d"), include_time=False, type="string")
702
+ task_start_time = gr.Dropdown(label="Hora início", choices=slots_30_min(), value="09:00")
703
+ with gr.Column():
704
+ task_end_date = gr.DateTime(label="📅 Data fim", value=datetime.now().strftime("%Y-%m-%d"), include_time=False, type="string")
705
+ task_end_time = gr.Dropdown(label="Hora fim", choices=slots_30_min(), value="17:00")
706
+
707
+ task_hours_box = gr.Textbox(label="Horas previstas", interactive=False)
708
+ task_msg = gr.Markdown()
709
+
710
+ with gr.Row():
711
+ save_task_btn = gr.Button("💾 Guardar Nova", variant="primary")
712
+ update_task_btn = gr.Button("✏️ Atualizar", variant="secondary")
713
+ delete_task_btn = gr.Button("🗑️ Eliminar", variant="stop")
714
+
715
+ # Eventos
716
+ for comp in [task_start_date, task_start_time, task_end_date, task_end_time]:
717
+ comp.change(calcular_horas_preview, [task_start_date, task_start_time, task_end_date, task_end_time], task_hours_box)
718
+
719
+ edit_task_code.change(
720
+ get_task_by_code,
721
+ [user_state, edit_task_code],
722
+ [task_name, task_start_date, task_start_time, task_end_date, task_end_time, task_hours_box]
723
+ )
724
+
725
+ refresh_tasks_btn.click(lambda u: gr.Dropdown(choices=get_task_codes(u)), user_state, edit_task_code)
726
+
727
+ save_task_btn.click(
728
+ add_task,
729
+ [user_state, task_name, task_start_date, task_start_time, task_end_date, task_end_time],
730
+ [task_hours_box, task_msg, edit_task_code]
731
+ )
732
+
733
+ update_task_btn.click(
734
+ update_task,
735
+ [user_state, edit_task_code, task_name, task_start_date, task_start_time, task_end_date, task_end_time],
736
+ task_msg
737
+ )
738
+
739
+ delete_task_btn.click(delete_task, [user_state, edit_task_code], task_msg)
740
+
741
+ # ===== TAB: Gerir Subtarefas =====
742
+ with gr.Tab("📋 Gerir Subtarefas"):
743
+ with gr.Row():
744
+ sub_task_code = gr.Dropdown(label="Tarefa", choices=[])
745
+ refresh_sub_btn = gr.Button("🔄", size="sm")
746
+
747
+ sub_name = gr.Textbox(label="Descrição", lines=2)
748
+
749
+ with gr.Row():
750
+ with gr.Column():
751
+ sub_start_date = gr.DateTime(label="📅 Data início", value=datetime.now().strftime("%Y-%m-%d"), include_time=False, type="string")
752
+ sub_start_time = gr.Dropdown(label="Hora início", choices=slots_30_min(), value="09:00")
753
+ with gr.Column():
754
+ sub_end_date = gr.DateTime(label="📅 Data fim", value=datetime.now().strftime("%Y-%m-%d"), include_time=False, type="string")
755
+ sub_end_time = gr.Dropdown(label="Hora fim", choices=slots_30_min(), value="10:00")
756
+
757
+ sub_hours_box = gr.Textbox(label="Horas", interactive=False)
758
+ sub_msg = gr.Markdown()
759
+ save_sub_btn = gr.Button("💾 Guardar Subtarefa", variant="primary")
760
+
761
+ gr.Markdown("---\n### Eliminar Subtarefa")
762
+ with gr.Row():
763
+ del_task_code = gr.Dropdown(label="Tarefa", choices=[])
764
+ del_sub_code = gr.Dropdown(label="Subtarefa", choices=[])
765
+ refresh_del_btn = gr.Button("🔄", size="sm")
766
+
767
+ del_msg = gr.Markdown()
768
+ del_sub_btn = gr.Button("🗑️ Eliminar", variant="stop")
769
+
770
+ # Eventos
771
+ for comp in [sub_start_date, sub_start_time, sub_end_date, sub_end_time]:
772
+ comp.change(calcular_horas_preview, [sub_start_date, sub_start_time, sub_end_date, sub_end_time], sub_hours_box)
773
+
774
+ refresh_sub_btn.click(lambda u: gr.Dropdown(choices=get_task_codes(u)), user_state, sub_task_code)
775
+
776
+ save_sub_btn.click(
777
+ add_subtask,
778
+ [user_state, sub_task_code, sub_name, sub_start_date, sub_start_time, sub_end_date, sub_end_time],
779
+ [sub_hours_box, sub_msg]
780
+ )
781
+
782
+ refresh_del_btn.click(lambda u: gr.Dropdown(choices=get_task_codes(u)), user_state, del_task_code)
783
+ del_task_code.change(lambda u, tc: gr.Dropdown(choices=get_subtask_codes(u, tc)), [user_state, del_task_code], del_sub_code)
784
+
785
+ del_sub_btn.click(delete_subtask, [user_state, del_task_code, del_sub_code], del_msg)
786
+
787
+ # ===== TAB: Visualizar Tarefas =====
788
+ with gr.Tab("📊 Visualizar Tarefas"):
789
+ tasks_table = gr.Dataframe()
790
+ refresh_view_btn = gr.Button("🔄 Atualizar")
791
+ refresh_view_btn.click(list_tasks, user_state, tasks_table)
792
+
793
+ # ===== TAB: Visualizar Subtarefas =====
794
+ with gr.Tab("📋 Visualizar Subtarefas"):
795
+ view_task_code = gr.Dropdown(label="Tarefa", choices=[])
796
+ refresh_view_sub_btn = gr.Button("🔄 Atualizar lista")
797
+ subtasks_table = gr.Dataframe()
798
+
799
+ refresh_view_sub_btn.click(lambda u: gr.Dropdown(choices=get_task_codes(u)), user_state, view_task_code)
800
+ view_task_code.change(list_subtasks, [user_state, view_task_code], subtasks_table)
801
+
802
+ # ===== TAB: Gráfico Gantt =====
803
+ with gr.Tab("📊 Gráfico Gantt"):
804
+ gr.Markdown("""
805
+ ### 📊 Gráfico de Gantt
806
+
807
+ **Modo 1**: Deixe vazio para ver **todas as tarefas**
808
+ **Modo 2**: Selecione uma tarefa para ver **tarefa + subtarefas**
809
+ """)
810
+
811
+ gantt_task_select = gr.Dropdown(label="🔍 Selecionar tarefa (opcional - deixe vazio para ver todas)", choices=[], allow_custom_value=False)
812
+
813
+ with gr.Row():
814
+ gantt_refresh_btn = gr.Button("🔄 Atualizar lista")
815
+ gantt_clear_btn = gr.Button("❌ Limpar seleção")
816
+ gantt_generate_btn = gr.Button("📊 Gerar Gantt", variant="primary")
817
+
818
+ gantt_plot = gr.Plot(label="Gráfico")
819
+
820
+ # Atualizar dropdown de tarefas
821
+ gantt_refresh_btn.click(
822
+ lambda u: gr.Dropdown(choices=get_task_codes(u)),
823
+ user_state,
824
+ gantt_task_select
825
+ )
826
+
827
+ # Limpar seleção (volta para modo "todas as tarefas")
828
+ gantt_clear_btn.click(
829
+ lambda: gr.Dropdown(value=None),
830
+ None,
831
+ gantt_task_select
832
+ )
833
+
834
+ # Gerar Gantt (todas as tarefas ou tarefa específica)
835
+ gantt_generate_btn.click(
836
+ generate_gantt,
837
+ [user_state, gantt_task_select],
838
+ gantt_plot
839
+ )
840
+
841
+ if __name__ == "__main__":
842
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=False)
843
+
init_db.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script de inicialização automática da base de dados
4
+ Executado automaticamente ao iniciar a aplicação no Hugging Face Spaces
5
+ """
6
+
7
+ import sqlite3
8
+ import bcrypt
9
+ from pathlib import Path
10
+
11
+ def init_database():
12
+ """Inicializa a base de dados se não existir"""
13
+
14
+ BASE_DIR = Path(__file__).parent
15
+ DB_PATH = BASE_DIR / "db" / "app.db"
16
+
17
+ # Se a base de dados já existe, não fazer nada
18
+ if DB_PATH.exists():
19
+ print("✅ Base de dados já existe")
20
+ return
21
+
22
+ # Criar diretório
23
+ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
24
+
25
+ print("=" * 60)
26
+ print("Inicialização da Base de Dados")
27
+ print("=" * 60)
28
+
29
+ # Conectar à BD
30
+ conn = sqlite3.connect(DB_PATH)
31
+ conn.execute("PRAGMA foreign_keys = ON")
32
+ cur = conn.cursor()
33
+
34
+ # Criar tabela users
35
+ print("\n[1/4] Criando tabela 'users'...")
36
+ cur.execute("""
37
+ CREATE TABLE IF NOT EXISTS users (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ username TEXT UNIQUE NOT NULL,
40
+ password_hash TEXT NOT NULL,
41
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
42
+ )
43
+ """)
44
+ print("✅ Tabela 'users' criada")
45
+
46
+ # Criar tabela tasks
47
+ print("\n[2/4] Criando tabela 'tasks'...")
48
+ cur.execute("""
49
+ CREATE TABLE IF NOT EXISTS tasks (
50
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
51
+ username TEXT NOT NULL,
52
+ task_code TEXT UNIQUE NOT NULL,
53
+ task_name TEXT NOT NULL,
54
+ task_date DATE NOT NULL,
55
+ start_time TIMESTAMP NOT NULL,
56
+ end_time TIMESTAMP NOT NULL,
57
+ planned_hours REAL NOT NULL,
58
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
59
+ FOREIGN KEY (username) REFERENCES users(username) ON DELETE CASCADE
60
+ )
61
+ """)
62
+ print("✅ Tabela 'tasks' criada")
63
+
64
+ # Criar tabela subtasks
65
+ print("\n[3/4] Criando tabela 'subtasks'...")
66
+ cur.execute("""
67
+ CREATE TABLE IF NOT EXISTS subtasks (
68
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
69
+ task_id INTEGER NOT NULL,
70
+ subtask_code TEXT UNIQUE NOT NULL,
71
+ subtask_name TEXT NOT NULL,
72
+ start_time TIMESTAMP NOT NULL,
73
+ end_time TIMESTAMP NOT NULL,
74
+ planned_hours REAL NOT NULL,
75
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
76
+ FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
77
+ )
78
+ """)
79
+ print("✅ Tabela 'subtasks' criada")
80
+
81
+ # Criar tabela task_logs
82
+ print("\n[4/4] Criando tabela 'task_logs'...")
83
+ cur.execute("""
84
+ CREATE TABLE IF NOT EXISTS task_logs (
85
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
86
+ username TEXT NOT NULL,
87
+ task_id INTEGER,
88
+ activity TEXT NOT NULL,
89
+ event_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
90
+ FOREIGN KEY (username) REFERENCES users(username) ON DELETE CASCADE,
91
+ FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
92
+ )
93
+ """)
94
+ print("✅ Tabela 'task_logs' criada")
95
+
96
+ # Criar utilizador de teste
97
+ print("\n[5/5] Criando utilizador de teste...")
98
+ username = "admin"
99
+ password = "admin123"
100
+ pw_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
101
+
102
+ try:
103
+ cur.execute(
104
+ "INSERT INTO users (username, password_hash) VALUES (?, ?)",
105
+ (username, pw_hash)
106
+ )
107
+ print(f"✅ Utilizador de teste criado:")
108
+ print(f" Username: {username}")
109
+ print(f" Password: {password}")
110
+ except sqlite3.IntegrityError:
111
+ print(f"⚠️ Utilizador '{username}' já existe")
112
+
113
+ # Commit e fechar
114
+ conn.commit()
115
+ conn.close()
116
+
117
+ print("\n" + "=" * 60)
118
+ print("✅ BASE DE DADOS INICIALIZADA COM SUCESSO!")
119
+ print("=" * 60)
120
+
121
+ if __name__ == "__main__":
122
+ init_database()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio==4.44.0
2
+ huggingface-hub==0.20.0
3
+ bcrypt
4
+ pandas
5
+ plotly