cmbtest commited on
Commit
ee66aa9
·
verified ·
1 Parent(s): 6da971b

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +729 -0
app.py ADDED
@@ -0,0 +1,729 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from supabase import create_client, Client
3
+ import random
4
+ import time
5
+ import io
6
+ import uuid
7
+ from PIL import Image, ImageDraw, ImageFont
8
+ import pandas as pd
9
+ from datetime import datetime
10
+ import os
11
+
12
+ # --- Configuração Inicial ---
13
+ st.set_page_config(
14
+ page_title="Number Assignment & Form System",
15
+ layout="centered",
16
+ initial_sidebar_state="expanded"
17
+ )
18
+
19
+ # --- Estilização CSS ---
20
+ st.markdown("""
21
+ <style>
22
+ .main-header {text-align: center; margin-bottom: 30px;}
23
+ .number-display {font-size: 72px; text-align: center; margin: 30px 0;}
24
+ .success-msg {background-color: #d4edda; color: #155724; padding: 10px; border-radius: 5px;}
25
+ .error-msg {background-color: #f8d7da; color: #721c24; padding: 10px; border-radius: 5px;}
26
+ .sub-header {font-size: 1.5em; color: #2196F3;}
27
+ </style>
28
+ """, unsafe_allow_html=True)
29
+
30
+ # --- Funções ---
31
+
32
+ def get_supabase_client() -> Client:
33
+ """Estabelece conexão com o Supabase usando variáveis de ambiente."""
34
+ supabase_url = os.getenv("SUPABASE_URL")
35
+ supabase_key = os.getenv("SUPABASE_KEY")
36
+ if not supabase_url or not supabase_key:
37
+ st.error("Credenciais do Supabase não configuradas no ambiente.")
38
+ return None
39
+ try:
40
+ client = create_client(supabase_url, supabase_key)
41
+ client.table("_dummy").select("*").limit(1).execute()
42
+ return client
43
+ except Exception as e:
44
+ st.error(f"Erro ao conectar ao Supabase: {str(e)}")
45
+ return None
46
+
47
+ def check_table_exists(supabase, table_name):
48
+ """Verifica se uma tabela específica existe no Supabase."""
49
+ try:
50
+ supabase.table(table_name).select("*").limit(1).execute()
51
+ return True
52
+ except Exception:
53
+ return False
54
+
55
+ def create_meeting_table(supabase, table_name, meeting_name, max_number=999, selected_forms=None):
56
+ """Cria uma nova tabela para uma reunião no Supabase e registra metadados."""
57
+ try:
58
+ response_metadata = supabase.table("meetings_metadata").insert({
59
+ "table_name": table_name,
60
+ "meeting_name": meeting_name,
61
+ "created_at": datetime.now().isoformat(),
62
+ "max_number": max_number
63
+ }).execute()
64
+ meeting_id = response_metadata.data[0]["id"]
65
+
66
+ create_table_query = f"""
67
+ CREATE TABLE public.{table_name} (
68
+ id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
69
+ number INTEGER NOT NULL,
70
+ assigned BOOLEAN DEFAULT FALSE,
71
+ assigned_at TIMESTAMPTZ,
72
+ user_id TEXT
73
+ );
74
+ """
75
+ supabase.rpc("execute_sql", {"query": create_table_query}).execute()
76
+
77
+ time.sleep(1)
78
+ if not check_table_exists(supabase, table_name):
79
+ raise Exception(f"Tabela {table_name} não foi criada com sucesso no Supabase.")
80
+
81
+ batch_size = 100
82
+ for i in range(0, max_number, batch_size):
83
+ end = min(i + batch_size, max_number)
84
+ data = [{"number": j, "assigned": False, "assigned_at": None, "user_id": None}
85
+ for j in range(i+1, end+1)]
86
+ supabase.table(table_name).insert(data).execute()
87
+
88
+ # Associar formulários à reunião
89
+ if selected_forms:
90
+ for form_id in selected_forms:
91
+ supabase.table("meeting_forms").insert({
92
+ "meeting_id": meeting_id,
93
+ "form_id": form_id
94
+ }).execute()
95
+
96
+ return True
97
+ except Exception as e:
98
+ st.error(f"Erro ao criar tabela de reunião: {str(e)}")
99
+ try:
100
+ supabase.table("meetings_metadata").delete().eq("table_name", table_name).execute()
101
+ supabase.rpc("execute_sql", {"query": f"DROP TABLE IF EXISTS public.{table_name}"}).execute()
102
+ except Exception as rollback_e:
103
+ st.error(f"Erro no rollback: {str(rollback_e)}")
104
+ return False
105
+
106
+ def get_available_meetings(supabase):
107
+ """Recupera a lista de reuniões disponíveis da tabela de metadados."""
108
+ try:
109
+ response = supabase.table("meetings_metadata").select("*").execute()
110
+ return response.data if response.data else []
111
+ except Exception as e:
112
+ st.error(f"Erro ao recuperar reuniões: {str(e)}")
113
+ return []
114
+
115
+ def get_available_forms(supabase):
116
+ """Recupera a lista de formulários disponíveis da tabela de metadados."""
117
+ try:
118
+ response = supabase.table("forms_metadata").select("*").execute()
119
+ return response.data if response.data else []
120
+ except Exception as e:
121
+ st.error(f"Erro ao recuperar formulários: {str(e)}")
122
+ return []
123
+
124
+ def get_forms_for_meeting(supabase, meeting_id):
125
+ """Recupera os formulários associados a uma reunião específica."""
126
+ try:
127
+ response = supabase.table("meeting_forms").select("form_id").eq("meeting_id", meeting_id).execute()
128
+ form_ids = [row["form_id"] for row in response.data]
129
+ if form_ids:
130
+ forms = supabase.table("forms_metadata").select("*").in_("id", form_ids).execute()
131
+ return forms.data if forms.data else []
132
+ return []
133
+ except Exception as e:
134
+ st.error(f"Erro ao recuperar formulários da reunião: {str(e)}")
135
+ return []
136
+
137
+ def get_answered_forms(supabase, participant_id):
138
+ """Recupera os IDs dos formulários já respondidos por um participant_id."""
139
+ try:
140
+ response = supabase.table("responses").select("form_id").eq("participant_id", participant_id).execute()
141
+ return set(row["form_id"] for row in response.data) if response.data else set()
142
+ except Exception as e:
143
+ st.error(f"Erro ao verificar formulários respondidos: {str(e)}")
144
+ return set()
145
+
146
+ def generate_number_image(number):
147
+ """Gera uma imagem com o número atribuído."""
148
+ width, height = 600, 300
149
+ img = Image.new("RGB", (width, height), color=(255, 255, 255))
150
+ draw = ImageDraw.Draw(img)
151
+
152
+ for y in range(height):
153
+ r = int(220 - y/3)
154
+ g = int(240 - y/3)
155
+ b = 255
156
+ for x in range(width):
157
+ draw.point((x, y), fill=(r, g, b))
158
+
159
+ try:
160
+ font = ImageFont.truetype("Arial.ttf", 200)
161
+ except IOError:
162
+ font = ImageFont.load_default()
163
+
164
+ number_text = str(number)
165
+ bbox = draw.textbbox((0, 0), number_text, font=font)
166
+ text_width = bbox[2] - bbox[0]
167
+ text_height = bbox[3] - bbox[1]
168
+ text_position = ((width - text_width) // 2, (height - text_height) // 2)
169
+ draw.text(text_position, number_text, font=font, fill=(0, 0, 100))
170
+
171
+ img_buffer = io.BytesIO()
172
+ img.save(img_buffer, format="PNG")
173
+ img_buffer.seek(0)
174
+ return img_buffer
175
+
176
+ def generate_participant_link(table_name, user_id=None, mode="participant"):
177
+ """Gera um link para participantes acessarem a reunião ou formulário."""
178
+ base_url = "https://mynumber.streamlit.app"
179
+ if user_id:
180
+ return f"{base_url}/?table={table_name}&mode={mode}&user_id={user_id}"
181
+ return f"{base_url}/?table={table_name}&mode={mode}"
182
+
183
+ # --- Verifica Modo (Master, Participant ou Participant_Form) ---
184
+ query_params = st.query_params
185
+ mode = query_params.get("mode", "master")
186
+ table_name_from_url = query_params.get("table", None)
187
+
188
+ if "user_id" not in st.session_state:
189
+ user_id_from_url = query_params.get("user_id", None)
190
+ if user_id_from_url:
191
+ st.session_state["user_id"] = user_id_from_url
192
+ else:
193
+ st.session_state["user_id"] = str(uuid.uuid4())
194
+
195
+ if mode == "participant" and table_name_from_url:
196
+ # --- Modo Participante para Reuniões ---
197
+ st.markdown("<h1 class='main-header'>Obtenha Seu Número</h1>", unsafe_allow_html=True)
198
+ supabase = get_supabase_client()
199
+ if not supabase:
200
+ st.stop()
201
+
202
+ if not check_table_exists(supabase, table_name_from_url):
203
+ st.error("Reunião não encontrada ou inválida.")
204
+ st.stop()
205
+
206
+ try:
207
+ meeting_info = supabase.table("meetings_metadata").select("*").eq("table_name", table_name_from_url).execute()
208
+ meeting_name = meeting_info.data[0]["meeting_name"] if meeting_info.data else "Reunião"
209
+ meeting_id = meeting_info.data[0]["id"]
210
+ st.subheader(f"Reunião: {meeting_name}")
211
+ except Exception:
212
+ st.subheader("Obtenha um número para a reunião")
213
+ st.stop()
214
+
215
+ user_id = st.session_state["user_id"]
216
+
217
+ participant_link = generate_participant_link(table_name_from_url, user_id, mode="participant")
218
+ st.markdown(f"**Seu Link Persistente para Reunião:** [{participant_link}]({participant_link})")
219
+ st.write("Guarde este link para acessar sempre o mesmo número!")
220
+
221
+ try:
222
+ existing = supabase.table(table_name_from_url).select("number").eq("user_id", user_id).execute()
223
+ if existing.data:
224
+ st.session_state["assigned_number"] = existing.data[0]["number"]
225
+ else:
226
+ with st.spinner("Atribuindo um número..."):
227
+ response = supabase.table(table_name_from_url).select("*").eq("assigned", False).execute()
228
+ if response.data:
229
+ available_numbers = [row["number"] for row in response.data]
230
+ if available_numbers:
231
+ assigned_number = random.choice(available_numbers)
232
+ supabase.table(table_name_from_url).update({
233
+ "assigned": True,
234
+ "assigned_at": datetime.now().isoformat(),
235
+ "user_id": user_id
236
+ }).eq("number", assigned_number).execute()
237
+ st.session_state["assigned_number"] = assigned_number
238
+ else:
239
+ st.error("Todos os números foram atribuídos!")
240
+ st.stop()
241
+ else:
242
+ st.error("Todos os números foram atribuídos!")
243
+ st.stop()
244
+
245
+ st.markdown(f"""
246
+ <div class='success-msg'>
247
+ <p>Seu número atribuído é:</p>
248
+ <div class='number-display'>{st.session_state['assigned_number']}</div>
249
+ </div>
250
+ """, unsafe_allow_html=True)
251
+
252
+ # Mostrar links de formulários disponíveis para esta reunião com status
253
+ st.subheader("Formulários Disponíveis para Você")
254
+ forms = get_forms_for_meeting(supabase, meeting_id)
255
+ participant_id = str(st.session_state["assigned_number"])
256
+ answered_forms = get_answered_forms(supabase, participant_id)
257
+ if forms:
258
+ for form in forms:
259
+ form_id = form["id"]
260
+ form_link = generate_participant_link(form["table_name"], user_id, mode="participant_form")
261
+ status = "✅ Respondido" if form_id in answered_forms else "⏳ Pendente"
262
+ st.markdown(f"- **{form['form_name']}** ({status}): [{form_link}]({form_link})")
263
+ else:
264
+ st.info("Nenhum formulário disponível para esta reunião.")
265
+
266
+ except Exception as e:
267
+ st.error(f"Erro ao atribuir número: {str(e)}")
268
+ st.stop()
269
+
270
+ if st.button("Salvar como Imagem"):
271
+ with st.spinner("Gerando imagem..."):
272
+ img_buffer = generate_number_image(st.session_state["assigned_number"])
273
+ st.image(img_buffer)
274
+ st.download_button(
275
+ "Baixar Imagem",
276
+ img_buffer,
277
+ file_name=f"meu_numero_{st.session_state['assigned_number']}.png",
278
+ mime="image/png"
279
+ )
280
+
281
+ elif mode == "participant_form" and table_name_from_url:
282
+ # --- Modo Participante para Formulários ---
283
+ st.markdown("<h1 class='main-header'>Responder Formulário</h1>", unsafe_allow_html=True)
284
+ supabase = get_supabase_client()
285
+ if not supabase:
286
+ st.stop()
287
+
288
+ form_info = supabase.table("forms_metadata").select("*").eq("table_name", table_name_from_url).execute()
289
+ if not form_info.data:
290
+ st.error("Formulário não encontrado.")
291
+ st.stop()
292
+
293
+ form_id = form_info.data[0]['id']
294
+ st.subheader(f"Formulário: {form_info.data[0]['form_name']}")
295
+
296
+ questions = supabase.table("questions").select("*").eq("form_id", form_id).execute()
297
+ if not questions.data:
298
+ st.error("Nenhuma pergunta encontrada para este formulário.")
299
+ st.stop()
300
+
301
+ user_id = st.session_state["user_id"]
302
+ participant_id_default = ""
303
+ meeting_table_name = ""
304
+ for meeting in get_available_meetings(supabase):
305
+ assigned = supabase.table(meeting["table_name"]).select("number").eq("user_id", user_id).execute()
306
+ if assigned.data:
307
+ participant_id_default = str(assigned.data[0]["number"])
308
+ meeting_table_name = meeting["table_name"]
309
+ break
310
+
311
+ if not participant_id_default:
312
+ st.error("Você precisa ter um número atribuído para responder formulários.")
313
+ st.stop()
314
+
315
+ # Verificar se o formulário já foi respondido
316
+ participant_id = participant_id_default
317
+ answered_forms = get_answered_forms(supabase, participant_id)
318
+ if form_id in answered_forms:
319
+ st.warning("Você já respondeu este formulário. Cada participante só pode responder uma vez.")
320
+ st.stop()
321
+
322
+ with st.form("form_submission"):
323
+ responses = {}
324
+ for q in questions.data:
325
+ st.write(f"{q['question_text']}")
326
+ if q['question_type'] == 'text':
327
+ responses[q['id']] = st.text_input("Sua resposta", key=f"resp_{q['id']}")
328
+ elif q['question_type'] == 'multiple_choice':
329
+ options = supabase.table("options").select("*").eq("question_id", q['id']).execute()
330
+ option_texts = [opt['option_text'] for opt in options.data]
331
+ option_ids = [opt['id'] for opt in options.data]
332
+ selected_option = st.selectbox("Escolha uma opção", option_texts, key=f"resp_{q['id']}")
333
+ responses[q['id']] = option_ids[option_texts.index(selected_option)]
334
+
335
+ st.text_input("Seu Nome ou ID", value=participant_id, key="participant_id", disabled=True)
336
+ if st.form_submit_button("Enviar"):
337
+ if all(responses.values()):
338
+ for q_id, answer in responses.items():
339
+ response_data = {
340
+ "form_id": form_id,
341
+ "participant_id": participant_id,
342
+ "question_id": q_id,
343
+ "answer": str(answer)
344
+ }
345
+ supabase.table("responses").insert(response_data).execute()
346
+ st.success("Respostas enviadas com sucesso!")
347
+ st.markdown(f"Voltando para sua página de participante em 3 segundos...")
348
+ time.sleep(3)
349
+ st.query_params.update({
350
+ "table": meeting_table_name,
351
+ "mode": "participant",
352
+ "user_id": user_id
353
+ })
354
+ st.rerun()
355
+ else:
356
+ st.warning("Preencha todas as respostas.")
357
+
358
+ else:
359
+ # --- Modo Master ---
360
+ valid_pages = ["Gerenciar Reuniões", "Compartilhar Link da Reunião", "Ver Estatísticas", "Gerenciar Formulários", "Compartilhar Link do Formulário"]
361
+ if "page" not in st.session_state or st.session_state["page"] not in valid_pages:
362
+ st.session_state["page"] = "Gerenciar Reuniões"
363
+
364
+ st.sidebar.title("Menu (Master)")
365
+ page = st.sidebar.radio("Escolha uma opção", valid_pages, index=valid_pages.index(st.session_state["page"]))
366
+
367
+ # --- Página 1: Gerenciar Reuniões ---
368
+ if page == "Gerenciar Reuniões":
369
+ st.session_state["page"] = "Gerenciar Reuniões"
370
+ st.markdown("<h1 class='main-header'>Gerenciar Reuniões</h1>", unsafe_allow_html=True)
371
+ supabase = get_supabase_client()
372
+ if not supabase:
373
+ st.stop()
374
+
375
+ with st.form("create_meeting_form"):
376
+ st.subheader("Criar Nova Reunião")
377
+ meeting_name = st.text_input("Nome da Reunião")
378
+ max_number = st.number_input("Número Máximo", min_value=10, max_value=10000, value=999)
379
+
380
+ # Seleção de formulários disponíveis para a reunião
381
+ forms = get_available_forms(supabase)
382
+ form_options = {f"{f['form_name']} ({f['table_name']})": f["id"] for f in forms}
383
+ selected_forms = st.multiselect("Formulários Disponíveis nesta Reunião", list(form_options.keys()))
384
+ selected_form_ids = [form_options[form] for form in selected_forms] if selected_forms else None
385
+
386
+ submit_button = st.form_submit_button("Criar Reunião")
387
+
388
+ if submit_button:
389
+ if meeting_name:
390
+ table_name = f"meeting_{int(time.time())}_{meeting_name.lower().replace(' ', '_')}"
391
+ if check_table_exists(supabase, table_name):
392
+ st.error("Uma reunião com esse nome já existe. Tente outro nome.")
393
+ else:
394
+ with st.spinner("Criando reunião..."):
395
+ success = create_meeting_table(supabase, table_name, meeting_name, max_number, selected_form_ids)
396
+ if success:
397
+ participant_link = generate_participant_link(table_name, mode="participant")
398
+ st.success(f"Reunião '{meeting_name}' criada com sucesso!")
399
+ st.markdown(f"**Link para Participantes:** [{participant_link}]({participant_link})")
400
+ st.session_state["selected_table"] = table_name
401
+ st.session_state["page"] = "Compartilhar Link da Reunião"
402
+ st.rerun()
403
+ else:
404
+ st.error("Falha ao criar a reunião.")
405
+ else:
406
+ st.warning("Por favor, insira um nome para a reunião.")
407
+
408
+ st.subheader("Reuniões Existentes")
409
+ meetings = get_available_meetings(supabase)
410
+ if meetings:
411
+ meeting_data = []
412
+ for meeting in meetings:
413
+ if "table_name" in meeting and "meeting_name" in meeting:
414
+ table_name = meeting["table_name"]
415
+ if check_table_exists(supabase, table_name):
416
+ try:
417
+ count_response = supabase.table(table_name).select("*", count="exact").eq("assigned", True).execute()
418
+ assigned_count = count_response.count if hasattr(count_response, 'count') else 0
419
+ participant_link = generate_participant_link(table_name, mode="participant")
420
+ meeting_data.append({
421
+ "Nome": meeting.get("meeting_name", "Sem nome"),
422
+ "Tabela": table_name,
423
+ "Link": participant_link,
424
+ "Criada em": meeting.get("created_at", "")[:16].replace("T", " "),
425
+ "Números Atribuídos": assigned_count,
426
+ "Total de Números": meeting.get("max_number", 0)
427
+ })
428
+ except Exception as e:
429
+ st.warning(f"Erro ao processar reunião {table_name}: {str(e)}")
430
+ if meeting_data:
431
+ df = pd.DataFrame(meeting_data)
432
+ st.dataframe(df)
433
+ else:
434
+ st.info("Nenhuma reunião válida encontrada.")
435
+ else:
436
+ st.info("Nenhuma reunião disponível ou erro ao acessar o Supabase.")
437
+
438
+ # --- Página 2: Compartilhar Link da Reunião ---
439
+ elif page == "Compartilhar Link da Reunião":
440
+ st.session_state["page"] = "Compartilhar Link da Reunião"
441
+ st.markdown("<h1 class='main-header'>Compartilhar Link da Reunião</h1>", unsafe_allow_html=True)
442
+
443
+ supabase = get_supabase_client()
444
+ if not supabase:
445
+ st.stop()
446
+
447
+ meetings = get_available_meetings(supabase)
448
+ if not meetings:
449
+ st.info("Nenhuma reunião disponível. Crie uma reunião primeiro.")
450
+ st.stop()
451
+
452
+ options = {f"{m['meeting_name']} ({m['table_name']})": m["table_name"]
453
+ for m in meetings if "table_name" in m and "meeting_name" in m}
454
+ selected = st.selectbox("Selecione uma reunião para compartilhar:", list(options.keys()))
455
+
456
+ if selected:
457
+ selected_table = options[selected]
458
+ participant_link = generate_participant_link(selected_table, mode="participant")
459
+ st.markdown(f"**Link para Participantes:** [{participant_link}]({participant_link})")
460
+ if st.button("Copiar Link"):
461
+ st.write("Link copiado para a área de transferência!")
462
+ st.code(participant_link)
463
+
464
+ # --- Página 3: Ver Estatísticas ---
465
+ elif page == "Ver Estatísticas":
466
+ st.session_state["page"] = "Ver Estatísticas"
467
+ st.markdown("<h1 class='main-header'>Estatísticas da Reunião</h1>", unsafe_allow_html=True)
468
+ supabase = get_supabase_client()
469
+ if not supabase:
470
+ st.stop()
471
+
472
+ meetings = get_available_meetings(supabase)
473
+ if not meetings:
474
+ st.info("Nenhuma reunião disponível para análise.")
475
+ st.stop()
476
+
477
+ options = {f"{m['meeting_name']} ({m['table_name']})": m["table_name"]
478
+ for m in meetings if "table_name" in m and "meeting_name" in m}
479
+ selected = st.selectbox("Selecione uma reunião:", list(options.keys()))
480
+
481
+ if selected:
482
+ selected_table = options[selected]
483
+ meeting_info = supabase.table("meetings_metadata").select("id").eq("table_name", selected_table).execute()
484
+ meeting_id = meeting_info.data[0]["id"]
485
+
486
+ # Estatísticas de números
487
+ try:
488
+ total_response = supabase.table(selected_table).select("*", count="exact").execute()
489
+ total_numbers = total_response.count if hasattr(total_response, 'count') else 0
490
+ assigned_response = supabase.table(selected_table).select("*", count="exact").eq("assigned", True).execute()
491
+ assigned_numbers = assigned_response.count if hasattr(assigned_response, 'count') else 0
492
+ percentage = (assigned_numbers / total_numbers) * 100 if total_numbers > 0 else 0
493
+
494
+ col1, col2, col3 = st.columns(3)
495
+ with col1:
496
+ st.metric("Total de Números", total_numbers)
497
+ with col2:
498
+ st.metric("Números Atribuídos", assigned_numbers)
499
+ with col3:
500
+ st.metric("Porcentagem Atribuída", f"{percentage:.1f}%")
501
+
502
+ try:
503
+ time_data_response = supabase.table(selected_table).select("*").eq("assigned", True).order("assigned_at").execute()
504
+ if time_data_response.data:
505
+ time_data = []
506
+ for item in time_data_response.data:
507
+ if item.get("assigned_at"):
508
+ time_data.append({
509
+ "time": item.get("assigned_at")[:16].replace("T", " "),
510
+ "count": 1
511
+ })
512
+ if time_data:
513
+ df = pd.DataFrame(time_data)
514
+ df["time"] = pd.to_datetime(df["time"])
515
+ df["hour"] = df["time"].dt.floor("H")
516
+ hourly_counts = df.groupby("hour").count().reset_index()
517
+ hourly_counts["hour_str"] = hourly_counts["hour"].dt.strftime("%m/%d %H:00")
518
+ st.subheader("Atribuições de Números por Hora")
519
+ st.bar_chart(data=hourly_counts, x="hour_str", y="count")
520
+ except Exception:
521
+ st.info("Dados temporais não disponíveis para esta reunião.")
522
+
523
+ if st.button("Exportar Dados de Números"):
524
+ try:
525
+ all_data_response = supabase.table(selected_table).select("*").execute()
526
+ if all_data_response.data:
527
+ df = pd.DataFrame(all_data_response.data)
528
+ csv = df.to_csv(index=False)
529
+ st.download_button(
530
+ "Baixar CSV",
531
+ csv,
532
+ file_name=f"{selected_table}_numeros_export.csv",
533
+ mime="text/csv"
534
+ )
535
+ except Exception as e:
536
+ st.error(f"Erro ao exportar dados: {str(e)}")
537
+ except Exception as e:
538
+ st.error(f"Erro ao recuperar estatísticas de números: {str(e)}")
539
+
540
+ # Estatísticas de respostas dos formulários
541
+ st.subheader("Respostas dos Formulários")
542
+ forms = get_forms_for_meeting(supabase, meeting_id)
543
+ if forms:
544
+ form_ids = [f["id"] for f in forms]
545
+ responses = supabase.table("responses").select("participant_id, form_id, question_id, answer").in_("form_id", form_ids).execute()
546
+ if responses.data:
547
+ response_data = []
548
+ for resp in responses.data:
549
+ form = next((f for f in forms if f["id"] == resp["form_id"]), None)
550
+ question = supabase.table("questions").select("question_text").eq("id", resp["question_id"]).execute().data[0]
551
+ response_data.append({
552
+ "Participante": resp["participant_id"],
553
+ "Formulário": form["form_name"] if form else "Desconhecido",
554
+ "Pergunta": question["question_text"],
555
+ "Resposta": resp["answer"]
556
+ })
557
+ df = pd.DataFrame(response_data)
558
+ st.dataframe(df)
559
+
560
+ if st.button("Exportar Respostas dos Formulários"):
561
+ csv = df.to_csv(index=False)
562
+ st.download_button(
563
+ "Baixar CSV",
564
+ csv,
565
+ file_name=f"{selected_table}_respostas_export.csv",
566
+ mime="text/csv"
567
+ )
568
+ else:
569
+ st.info("Nenhuma resposta registrada para os formulários desta reunião.")
570
+ else:
571
+ st.info("Nenhum formulário associado a esta reunião.")
572
+
573
+ # --- Página 4: Gerenciar Formulários ---
574
+ elif page == "Gerenciar Formulários":
575
+ st.session_state["page"] = "Gerenciar Formulários"
576
+ st.markdown("<h1 class='main-header'>Gerenciar Formulários</h1>", unsafe_allow_html=True)
577
+ supabase = get_supabase_client()
578
+ if not supabase:
579
+ st.stop()
580
+
581
+ with st.form("create_form_form"):
582
+ st.subheader("Criar Novo Formulário")
583
+ form_name = st.text_input("Nome do Formulário", key="form_name")
584
+
585
+ if 'questions' not in st.session_state:
586
+ st.session_state['questions'] = []
587
+
588
+ st.markdown("<h3 class='sub-header'>Adicionar Pergunta</h3>", unsafe_allow_html=True)
589
+ question_type = st.selectbox("Tipo da Pergunta", ["Texto", "Múltipla Escolha"], key="q_type")
590
+ question_text = st.text_input("Texto da Pergunta", key="q_text")
591
+
592
+ correct_answer = None
593
+ options = []
594
+ if question_type == "Múltipla Escolha":
595
+ num_options = st.number_input("Número de Opções", min_value=2, max_value=10, value=2, key="num_opts")
596
+ for i in range(num_options):
597
+ option_text = st.text_input(f"Opção {i+1}", key=f"opt_{i}")
598
+ if option_text:
599
+ options.append(option_text)
600
+ correct_option = st.selectbox("Opção Correta (opcional)", ["Nenhuma"] + options, key="correct_opt")
601
+ if correct_option != "Nenhuma":
602
+ correct_answer = correct_option
603
+ else:
604
+ correct_answer = st.text_input("Resposta Correta (opcional)", key="correct_text")
605
+
606
+ if st.form_submit_button("Adicionar Pergunta"):
607
+ if question_text:
608
+ if question_type == "Múltipla Escolha" and len(options) >= 2:
609
+ st.session_state['questions'].append({
610
+ 'type': 'multiple_choice',
611
+ 'text': question_text,
612
+ 'options': options,
613
+ 'correct': correct_answer
614
+ })
615
+ st.success("Pergunta de múltipla escolha adicionada!")
616
+ elif question_type == "Texto":
617
+ st.session_state['questions'].append({
618
+ 'type': 'text',
619
+ 'text': question_text,
620
+ 'correct': correct_answer if correct_answer else None
621
+ })
622
+ st.success("Pergunta de texto adicionada!")
623
+ else:
624
+ st.warning("Adicione pelo menos 2 opções para perguntas de múltipla escolha.")
625
+ else:
626
+ st.warning("O texto da pergunta é obrigatório.")
627
+
628
+ if st.session_state['questions']:
629
+ st.markdown("<h3 class='sub-header'>Perguntas Adicionadas</h3>", unsafe_allow_html=True)
630
+ for i, q in enumerate(st.session_state['questions']):
631
+ st.write(f"{i+1}. {q['text']} ({q['type']})")
632
+ if q['type'] == 'multiple_choice':
633
+ st.write("Opções:", ", ".join(q['options']))
634
+ st.write(f"Correta: {q['correct'] if q['correct'] else 'Nenhuma'}")
635
+ else:
636
+ st.write(f"Correta: {q['correct'] if q['correct'] else 'Nenhuma'}")
637
+
638
+ if st.form_submit_button("Criar Formulário"):
639
+ if form_name and st.session_state['questions']:
640
+ table_name = f"form_{int(time.time())}_{form_name.lower().replace(' ', '_')}"
641
+ form_data = {"form_name": form_name, "table_name": table_name, "created_at": datetime.now().isoformat()}
642
+ form_response = supabase.table("forms_metadata").insert(form_data).execute()
643
+ form_id = form_response.data[0]['id']
644
+
645
+ for q in st.session_state['questions']:
646
+ question_data = {
647
+ "form_id": form_id,
648
+ "question_text": q['text'],
649
+ "question_type": q['type'],
650
+ "correct_answer": q['correct']
651
+ }
652
+ q_response = supabase.table("questions").insert(question_data).execute()
653
+ question_id = q_response.data[0]['id']
654
+
655
+ if q['type'] == 'multiple_choice':
656
+ for opt in q['options']:
657
+ opt_data = {"question_id": question_id, "option_text": opt}
658
+ opt_response = supabase.table("options").insert(opt_data).execute()
659
+ if opt == q['correct']:
660
+ supabase.table("questions").update({"correct_answer": str(opt_response.data[0]['id'])}).eq("id", question_id).execute()
661
+
662
+ participant_link = generate_participant_link(table_name, mode="participant_form")
663
+ st.success(f"Formulário '{form_name}' criado com sucesso!")
664
+ st.markdown(f"**Link Geral para Participantes:** [{participant_link}]({participant_link})")
665
+ st.session_state['questions'] = []
666
+ st.session_state["selected_form_table"] = table_name
667
+ st.session_state["page"] = "Compartilhar Link do Formulário"
668
+ st.rerun()
669
+ else:
670
+ st.warning("Insira um nome para o formulário e pelo menos uma pergunta.")
671
+
672
+ st.subheader("Formulários Disponíveis")
673
+ forms = get_available_forms(supabase)
674
+ if forms:
675
+ form_data = []
676
+ for form in forms:
677
+ participant_link = generate_participant_link(form["table_name"], mode="participant_form")
678
+ form_data.append({
679
+ "Nome": form["form_name"],
680
+ "Link Geral": participant_link,
681
+ "Criado em": form["created_at"][:16].replace("T", " ")
682
+ })
683
+ df = pd.DataFrame(form_data)
684
+ st.dataframe(df, column_config={"Link Geral": st.column_config.LinkColumn("Link Geral")})
685
+ else:
686
+ st.info("Nenhum formulário disponível.")
687
+
688
+ # --- Página 5: Compartilhar Link do Formulário ---
689
+ elif page == "Compartilhar Link do Formulário":
690
+ st.session_state["page"] = "Compartilhar Link do Formulário"
691
+ st.markdown("<h1 class='main-header'>Compartilhar Link do Formulário</h1>", unsafe_allow_html=True)
692
+
693
+ supabase = get_supabase_client()
694
+ if not supabase:
695
+ st.stop()
696
+
697
+ forms = get_available_forms(supabase)
698
+ if not forms:
699
+ st.info("Nenhum formulário disponível. Crie um formulário primeiro.")
700
+ st.stop()
701
+
702
+ options = {f"{f['form_name']} ({f['table_name']})": f["table_name"]
703
+ for f in forms if "table_name" in f and "form_name" in f}
704
+ selected = st.selectbox("Selecione um formulário para compartilhar:", list(options.keys()))
705
+
706
+ if selected:
707
+ selected_table = options[selected]
708
+ participant_link = generate_participant_link(selected_table, mode="participant_form")
709
+ st.markdown(f"**Link Geral para Participantes:** [{participant_link}]({participant_link})")
710
+ if st.button("Copiar Link Geral"):
711
+ st.write("Link copiado para a área de transferência!")
712
+ st.code(participant_link)
713
+
714
+ st.subheader("Links Únicos por Usuário")
715
+ meetings = get_available_meetings(supabase)
716
+ user_links = []
717
+ for meeting in meetings:
718
+ assigned_users = supabase.table(meeting["table_name"]).select("user_id, number").eq("assigned", True).execute()
719
+ for user in assigned_users.data:
720
+ user_link = generate_participant_link(selected_table, user["user_id"], mode="participant_form")
721
+ user_links.append({"Número": user["number"], "Link": user_link})
722
+ if user_links:
723
+ df = pd.DataFrame(user_links)
724
+ st.dataframe(df, column_config={"Link": st.column_config.LinkColumn("Link")})
725
+ else:
726
+ st.info("Nenhum usuário com número atribuído encontrado.")
727
+
728
+ if __name__ == "__main__":
729
+ pass