Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 195 additions & 42 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from tkinter import ttk, messagebox, scrolledtext
from lxml import html
from urllib.parse import urljoin
from datetime import datetime
from datetime import datetime, timedelta
import config

class SistemaBot:
Expand All @@ -22,32 +22,61 @@ def log(self, mensagem):
self.log_widget.insert(tk.END, mensagem + "\n")
self.log_widget.see(tk.END)

def executar(self, data_ini, data_fim, unidade):
def executar(self, data_ini, data_fim, unidade, medicos_excluir, especialidades_excluir):
try:
self.log(f"[{datetime.now().strftime('%H:%M:%S')}] Iniciando extração...")
self.log(f"Período: {data_ini} até {data_fim} | Unidade: {unidade}")

if medicos_excluir:
self.log(f"Médicos a excluir: {medicos_excluir}")
if especialidades_excluir:
self.log(f"Especialidades a excluir: {especialidades_excluir}")

login_url = f"{self.base_url}/Account/Login?ReturnUrl=/Medico/RelatorioEstatistica"
payload = {"login": config.LOGIN, "Senha": config.SENHA}
self.session.post(login_url, data=payload, headers=self.headers)
self.log("✓ Autenticação realizada")

report_url = f"{self.base_url}/Medico/ListaRelatorioAtendimentoMedico?inicio={data_ini}&fim={data_fim}&unidade={unidade}&usuario=undefined&medId=&dia=true&_=1768241465692"
res = self.session.get(report_url, headers=self.headers)
tree_report = html.fromstring(res.content)
# Gera lista de datas do período
data_inicio_obj = datetime.strptime(data_ini, "%Y-%m-%d")
data_fim_obj = datetime.strptime(data_fim, "%Y-%m-%d")

datas_para_processar = []
data_atual = data_inicio_obj
while data_atual <= data_fim_obj:
datas_para_processar.append(data_atual.strftime("%Y-%m-%d"))
data_atual += timedelta(days=1)

self.log(f"📅 Total de dias a processar: {len(datas_para_processar)}")

links = tree_report.xpath("//a[contains(@href, 'CentralPagamento')]/@href")
links_finais = [urljoin(self.base_url, l) for l in set(links)]
todos_links = set() # Usar set para evitar duplicatas

# Processa cada dia individualmente
for idx, data_processamento in enumerate(datas_para_processar, 1):
timestamp = int(time.time() * 1000)
report_url = f"{self.base_url}/Medico/ListaRelatorioAtendimentoMedico?inicio={data_processamento}&fim={data_processamento}&unidade={unidade}&usuario=undefined&medId=&dia=true&_={timestamp}"

self.log(f" Buscando médicos para {data_processamento} ({idx}/{len(datas_para_processar)})...")

res = self.session.get(report_url, headers=self.headers)
tree_report = html.fromstring(res.content)

links = tree_report.xpath("//a[contains(@href, 'CentralPagamento')]/@href")
links_completos = [urljoin(self.base_url, l) for l in links]
todos_links.update(links_completos)

time.sleep(config.REQUEST_DELAY)

links_finais = list(todos_links)

if not links_finais:
self.log("⚠ Nenhum médico encontrado no período")
return

self.log(f"✓ Encontrados {len(links_finais)} médicos")
self.log(f"✓ Encontrados {len(links_finais)} médicos únicos no período")

dados_relatorio = []
total_atend = 0
total_horar = 0
dados_por_dia = {} # Organiza dados por dia
excluidos = 0

for i, url in enumerate(links_finais, 1):
time.sleep(config.REQUEST_DELAY)
Expand All @@ -60,40 +89,138 @@ def executar(self, data_ini, data_fim, unidade):
especialidade = tree.xpath("//strong[@id='retnomemedico']/parent::label/following-sibling::label[1]/text()")
esp = especialidade[0].strip() if especialidade else "N/D"

linha = tree.xpath("//table[@id='tb_lista_dias']/tbody/tr[1]")
if linha:
qtd_h = linha[0].xpath("./td[1]/strong/text()")[0].replace('-', '').strip()
qtd_a = linha[0].xpath("./td[2]/strong/text()")[0].replace('-', '').strip()
hora_tr = linha[0].xpath("./td[2]/text()[1]")[0].strip()
# Verifica se deve excluir este médico ou especialidade
if medicos_excluir and any(m.strip().upper() in nome.upper() for m in medicos_excluir):
self.log(f"[{i}/{len(links_finais)}] ⊗ Excluído: {nome}")
excluidos += 1
continue

if especialidades_excluir and any(e.strip().upper() in esp.upper() for e in especialidades_excluir):
self.log(f"[{i}/{len(links_finais)}] ⊗ Excluído: {nome} ({esp})")
excluidos += 1
continue

val_h = int(qtd_h) if qtd_h.isdigit() else 0
val_a = int(qtd_a) if qtd_a.isdigit() else 0
total_horar += val_h
total_atend += val_a
linhas = tree.xpath("//table[@id='tb_lista_dias']/tbody/tr")
if linhas:
dias_processados = 0

perc = (val_a / val_h * 100) if val_h > 0 else 0
# Processa cada dia de atendimento
for idx, linha in enumerate(linhas):
try:
# Extrai a data do atendimento (primeiro text() da primeira coluna)
data_atend = linha.xpath("./td[1]/text()[1]")
data_atend = data_atend[0].strip() if data_atend else ""

# Extrai quantidade de horários
qtd_h = linha.xpath("./td[1]/strong/text()")
qtd_h = qtd_h[0].replace('-', '').strip() if qtd_h else "0"

# Extrai quantidade de atendimentos
qtd_a = linha.xpath("./td[2]/strong/text()")
qtd_a = qtd_a[0].replace('-', '').strip() if qtd_a else "0"

# Extrai horário de trabalho
hora_tr = linha.xpath("./td[2]/text()[1]")
hora_tr = hora_tr[0].strip() if hora_tr else "N/D"

val_h = int(qtd_h) if qtd_h.isdigit() else 0
val_a = int(qtd_a) if qtd_a.isdigit() else 0

perc = (val_a / val_h * 100) if val_h > 0 else 0

# Organiza por dia
if data_atend not in dados_por_dia:
dados_por_dia[data_atend] = []

dados_por_dia[data_atend].append({
"Data": data_atend,
"Médico": nome,
"Especialidade": esp,
"Horário Trabalho": hora_tr,
"Qtd Horários": val_h,
"Qtd Atendidos": val_a,
"% Eficiência": f"{perc:.2f}%"
})

dias_processados += 1

except Exception as e:
self.log(f" Aviso: Erro ao processar linha {idx+1} - {str(e)}")
continue

dados_relatorio.append({
"Médico": nome, "Especialidade": esp, "Horas": hora_tr,
"Qtd Horários": val_h, "Qtd Atendidos": val_a, "% Eficiência": f"{perc:.2f}%"
})
self.log(f"[{i}/{len(links_finais)}] {nome} - {esp}")
self.log(f"[{i}/{len(links_finais)}] {nome} - {esp} ({dias_processados} dia(s))")

arquivo = f"Relatorio_{data_ini.replace('-', '')}.csv"
with open(arquivo, mode='w', newline='', encoding='utf-8-sig') as f:
colunas = ["Médico", "Especialidade", "Horas", "Qtd Horários", "Qtd Atendidos", "% Eficiência"]
if excluidos > 0:
self.log(f"\n⊗ Total de registros excluídos: {excluidos}")

# Formata o nome do arquivo com o período
if data_ini == data_fim:
nome_arquivo = f"Relatorio_{data_ini.replace('-', '')}.csv"
periodo_desc = data_ini
else:
nome_arquivo = f"Relatorio_{data_ini.replace('-', '')}_ate_{data_fim.replace('-', '')}.csv"
periodo_desc = f"{data_ini} até {data_fim}"

# Gera o CSV organizado por dia
with open(nome_arquivo, mode='w', newline='', encoding='utf-8-sig') as f:
colunas = ["Data", "Médico", "Especialidade", "Horário Trabalho", "Qtd Horários", "Qtd Atendidos", "% Eficiência"]
writer = csv.DictWriter(f, fieldnames=colunas, delimiter=';')

# Cabeçalho com informações do período
writer.writerow({"Data": f"RELATÓRIO DE ATENDIMENTOS - PERÍODO: {periodo_desc}"})
writer.writerow({"Data": f"Gerado em: {datetime.now().strftime('%d/%m/%Y às %H:%M:%S')}"})
writer.writerow({})

writer.writeheader()
writer.writerows(dados_relatorio)

perc_geral = (total_atend / total_horar * 100) if total_horar > 0 else 0
# Ordena as datas e escreve os dados
total_horar_geral = 0
total_atend_geral = 0

for data in sorted(dados_por_dia.keys()):
registros_dia = dados_por_dia[data]
total_horar_dia = 0
total_atend_dia = 0

# Escreve os registros do dia
for registro in registros_dia:
writer.writerow(registro)
total_horar_dia += registro["Qtd Horários"]
total_atend_dia += registro["Qtd Atendidos"]

# Subtotal do dia
perc_dia = (total_atend_dia / total_horar_dia * 100) if total_horar_dia > 0 else 0
writer.writerow({})
writer.writerow({
"Data": f"SUBTOTAL {data}",
"Qtd Horários": total_horar_dia,
"Qtd Atendidos": total_atend_dia,
"% Eficiência": f"{perc_dia:.2f}%"
})
writer.writerow({})

total_horar_geral += total_horar_dia
total_atend_geral += total_atend_dia

# Total geral
perc_geral = (total_atend_geral / total_horar_geral * 100) if total_horar_geral > 0 else 0
writer.writerow({})
writer.writerow({"Médico": "TOTAIS GERAIS", "Qtd Horários": total_horar, "Qtd Atendidos": total_atend})
writer.writerow({"Médico": "PORCENTAGEM GERAL", "% Eficiência": f"{perc_geral:.2f}%"})
writer.writerow({
"Data": "TOTAIS GERAIS",
"Qtd Horários": total_horar_geral,
"Qtd Atendidos": total_atend_geral,
"% Eficiência": f"{perc_geral:.2f}%"
})

self.log(f"\n✓ Relatório gerado: {arquivo}")
self.log(f"\n✓ Relatório gerado: {nome_arquivo}")
self.log(f"Total de dias com atendimento: {len(dados_por_dia)}")
self.log(f"Eficiência geral: {perc_geral:.2f}%")
messagebox.showinfo("Concluído", f"Relatório salvo com sucesso!\n\nArquivo: {arquivo}\nEficiência: {perc_geral:.2f}%")
messagebox.showinfo("Concluído",
f"Relatório salvo com sucesso!\n\n"
f"Arquivo: {nome_arquivo}\n"
f"Período: {periodo_desc}\n"
f"Dias com atendimento: {len(dados_por_dia)}\n"
f"Eficiência: {perc_geral:.2f}%")

except Exception as e:
self.log(f"\n✗ ERRO: {str(e)}")
Expand All @@ -104,7 +231,7 @@ class App:
def __init__(self, root):
self.root = root
self.root.title("Gerador de Relatórios")
self.root.geometry("600x560")
self.root.geometry("500x700")
self.root.resizable(False, False)

self.configurar_estilos()
Expand Down Expand Up @@ -136,9 +263,9 @@ def criar_interface(self):
form_frame.pack(fill="x", pady=(0, 15))

campos = [
("Data Início:", "2026-01-01", "inicio"),
("Data Fim:", "2026-01-01", "fim"),
("Unidade:", "1", "unidade")
("Data Início:", "2026-01-09", "inicio"),
("Data Fim:", "2026-01-09", "fim"),
("ID Unidade:", "1", "unidade")
]

self.entries = {}
Expand All @@ -148,9 +275,31 @@ def criar_interface(self):
entry.insert(0, valor_padrao)
entry.grid(row=i, column=1, sticky="ew", pady=8)
self.entries[nome] = entry

form_frame.columnconfigure(1, weight=1)

# Frame de exclusões
excl_frame = ttk.LabelFrame(main_frame, text=" Filtros de Exclusão (opcional) ", padding="15")
excl_frame.pack(fill="x", pady=(0, 15))

ttk.Label(excl_frame, text="Excluir Médicos:",
font=('Segoe UI', 9, 'italic')).grid(row=0, column=0, sticky="w", pady=(0, 5))
ttk.Label(excl_frame, text="(separar por vírgula)",
font=('Segoe UI', 8), foreground='#7f8c8d').grid(row=0, column=1, sticky="w", pady=(0, 5))

self.entry_medicos_excluir = ttk.Entry(excl_frame, width=50, font=('Segoe UI', 9))
self.entry_medicos_excluir.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(0, 10))

ttk.Label(excl_frame, text="Excluir Especialidades:",
font=('Segoe UI', 9, 'italic')).grid(row=2, column=0, sticky="w", pady=(0, 5))
ttk.Label(excl_frame, text="(separar por vírgula)",
font=('Segoe UI', 8), foreground='#7f8c8d').grid(row=2, column=1, sticky="w", pady=(0, 5))

self.entry_especialidades_excluir = ttk.Entry(excl_frame, width=50, font=('Segoe UI', 9))
self.entry_especialidades_excluir.grid(row=3, column=0, columnspan=2, sticky="ew")

excl_frame.columnconfigure(0, weight=1)

btn_frame = ttk.Frame(main_frame)
btn_frame.pack(fill="x", pady=10)

Expand All @@ -162,7 +311,7 @@ def criar_interface(self):
log_frame = ttk.LabelFrame(main_frame, text=" Log de Execução ", padding="10")
log_frame.pack(fill="both", expand=True)

self.txt_log = scrolledtext.ScrolledText(log_frame, height=12, width=70,
self.txt_log = scrolledtext.ScrolledText(log_frame, height=32, width=70,
font=('Consolas', 9),
bg='#2c3e50', fg='#ecf0f1',
insertbackground='white')
Expand All @@ -179,11 +328,15 @@ def iniciar_processamento(self):
messagebox.showwarning("Atenção", "Preencha todos os campos obrigatórios!")
return

# Processa as exclusões
medicos_excluir = [m.strip() for m in self.entry_medicos_excluir.get().split(',') if m.strip()]
especialidades_excluir = [e.strip() for e in self.entry_especialidades_excluir.get().split(',') if e.strip()]

self.btn_gerar.state(['disabled'])
self.txt_log.delete(1.0, tk.END)

def executar_e_habilitar():
self.bot.executar(d_ini, d_fim, unid)
self.bot.executar(d_ini, d_fim, unid, medicos_excluir, especialidades_excluir)
self.btn_gerar.state(['!disabled'])

threading.Thread(target=executar_e_habilitar, daemon=True).start()
Expand Down