App-Custo / main.py
Copagril2026's picture
Upload 13 files
c272101 verified
Raw
History Blame Contribute Delete
52.3 kB
#Funções de bibliotecas exportadas
from asyncio import to_thread
import flet as ft
import pandas as pd
import asyncio
import copy
from datetime import datetime
from zoneinfo import ZoneInfo
#Importa modulos e suas funções
import Registros
import Login
import Qlik_sense
import Google_Sheets
#Var global para receber os dados do qlik
df=None
#Var de global para buscar o historico
df_hist=None
# Cria a fila indiana obrigatória para o Google Sheets
fila_de_salvamento = asyncio.Lock()
#Função mae, na qual chamamos as demais e oraganizamos a logica
def main(page: ft.Page):
#Definindo tamanho e parametos da pagina, nome etc
page.padding = 0
page.spacing = 0
page.bgcolor = ft.Colors.GREY_100
# Bloqueia o sistema no Modo Claro para que o Windows não mude as suas cores!
page.theme_mode = ft.ThemeMode.LIGHT
page.window_maximized = True
page.title = "CENTRAL DE CUSTOS"
# Força a barra de rolagem a aparecer em todo o sistema
page.theme = ft.Theme(
scrollbar_theme=ft.ScrollbarTheme(
thumb_visibility=True, # Mostra a barra (o "puxador")
track_visibility=True, # Mostra o trilho de fundo
thickness=12, # Deixa a barra mais grossa e fácil de clicar (padrão é ~8)
radius=10, # Deixa as pontas arredondadas
thumb_color=ft.Colors.GREY_500, # Cor da barra
track_color=ft.Colors.GREY_200, # Cor do fundo da barra
interactive=True
)
)
# 2. FUNÇÃO QUE RODA ESCONDIDA NO FUNDO
async def carregar_no_fundo():
global df, df_hist
await asyncio.sleep(2)
print("Iniciando download silencioso do Qlik... e sheets")
df, df_hist = Qlik_sense.extrair_dados_engine()
print("Download do Qlik e sheets concluído com sucesso!")
# Pega a hora exata de agora
hora_atual = datetime.now(ZoneInfo("America/Sao_Paulo")).strftime("%d/%m/%Y %H:%M:%S")
# Cria a variável de texto independente
texto_ultima_atualizacao = ft.Text(
value=f"Atualizado em: \n{hora_atual}",
color=ft.Colors.BLACK, # Um cinza escuro e elegante para não roubar a atenção
size=12,
italic=True
)
# 1. O NOVO BANNER DE CARREGAMENTO CONTÍNUO
banner_carregamento = ft.Container(
content=ft.Row(
controls=[
# A animação giratória!
ft.ProgressRing(width=20, height=20, stroke_width=3, color="#0D47A1"),
# O Texto avisando que ele pode continuar a mexer
ft.Text(
"Atualizando dados do Qlik em segundo plano... aguarde um momento!",
color="#0D47A1",
size=14,
weight="bold"
),
],
alignment=ft.MainAxisAlignment.CENTER, # Centraliza no meio da tela
spacing=15
),
bgcolor="#E3F2FD", # Um azul clarinho muito amigável e profissional
top=0, left=0, right=0,
padding=10,
visible=False, # Nasce invisível
shadow=ft.BoxShadow(blur_radius=10, color=ft.Colors.BLACK26) # Sombra para dar destaque flutuante
)
# Função para atualizar a tela de registros
# Função para atualizar a tela principal
async def atualizar_tela():
global df, df_hist
# 1. LIGA O BANNER E COMEÇA A GIRAR!
banner_carregamento.visible = True
page.update()
print("Iniciando download silencioso do Qlik e Sheets...")
try:
# 1. Pede a caixa ao Qlik, mas não a tenta abrir logo!
resultado = await asyncio.to_thread(Qlik_sense.extrair_dados_engine)
# 2. Verifica se a caixa NÃO está vazia e tem exatamente as 2 tabelas
if resultado is not None and len(resultado) == 2:
df, df_hist = resultado # Agora sim, abre a caixa com segurança
# Tira a nova foto da hora
nova_hora = datetime.now(ZoneInfo("America/Sao_Paulo")).strftime("%d/%m/%Y %H:%M:%S")
# Atualiza o rótulo de hora
texto_ultima_atualizacao.value = f"Atualizado em: \n{nova_hora}"
# 2. DESLIGA O BANNER (SUCESSO)
banner_carregamento.visible = False
page.update()
# 3. Reconstrói a tela principal com os dados novos
ir_para_principal()
else:
# 2. DESLIGA O BANNER (SUCESSO)
banner_carregamento.visible = False
page.update()
# Se o Qlik falhou e devolveu "Nada", avisa o utilizador sem quebrar a tela
print('N retornou nada')
except Exception as erro:
# Se a internet cair ou o código do Qlik der erro interno, ele cai aqui
print(f"❌ Erro de conexão: {erro}")
finally:
# 2. DESLIGA O BANNER (SUCESSO)
banner_carregamento.visible = False
# 3. TELA DE LOGIN
def ir_para_login(e=None):
tela = Login.montar_tela_login(ir_para_principal)
page.clean() # 1. Limpa a tela principal
page.add(tela)
page.update()
#Elimino toda a configuração atual de espaçamento da tela para eu poder configurar como desejar abaixo com flet
page.padding=0
page.spacing=0
page.expand=True
# --- FUNÇÃO 1: CONSTRÓI A TELA DE REGISTROS ---
def ir_para_registros(e):
global df_hist
print('Iniciado outro modulo')
# 1. Abre a mochila e tenta pegar os dados
dados_salvos = page.session.store.get("mochila_de_registros")
# 2. A verificação inteligente
if dados_salvos is not None:
print("🚀 Dados encontrados na mochila! Desenhando a tabela sem chamar o Sheets.")
# Aqui você manda a sua função de desenhar a tabela usar os 'dados_salvos'
# em vez de ler do Google Sheets de novo.
tela = Registros.montar_tela_registros(ir_para_login, ir_para_principal,dados_salvos, overlay_da_pagina=page.overlay, page=page)
else:
# ---> ADICIONE ESTA LINHA <---
# Já que a mochila estava vazia, vamos colocar o df_hist inicial lá dentro!
page.session.store.set("mochila_de_registros", df_hist)
# 2. Chama o módulo para desenhar a tela nova
# Passamos a função 'ir_para_principal' para ele saber como voltar
tela = Registros.montar_tela_registros(ir_para_login,ir_para_principal,df_hist,overlay_da_pagina=page.overlay,page=page)
page.clean() # 1. Limpa a tela principal
page.add(tela) # 3. Adiciona a tela nova
page.update()
# Dic que define tamanho das colunas
larguras_colunas = {
"DUPLICAR":90,
"SAFRA":100,
"CÓDIGO DO PRODUTO": 150,
"DERIVAÇÃO":80,
"DESCRIÇÃO PRODUTO": 200,
"MARCA": 180,
"CUSTO ATUAL": 120, # Maior porque tem muito texto
"DATA VENC ATUAL": 180,
"NOVO CUSTO":100,
"DATA DE VENC NOVA": 180,
"% BARTER":100,
"QUANTIDADE":100,
"Atualizar no Senior?":90,
"Valor de reposição?":90,
"Salvar alteração": 100,
}
def get_largura(nome_coluna):
return larguras_colunas.get(nome_coluna, 120)
#Essa def cria o esqueleto da pagina com a informações retornadas
def ir_para_principal(e=None):
print('Iniciando tela principal')
# 1. ABRE A MOCHILA E PEGA O FILTRO SALVO (Se não tiver, fica vazio "")
filtro_salvo = page.session.store.get("filtro_principal") or ""
#Uso a var globar para extrair o retorno do qlik
global df
if type(df).__name__ != 'DataFrame':
print("❌ Erro: df está vazio. Parando construção da tela.")
# 1. Transformamos em ASYNC
async def tentar_buscar_dados_novamente(e):
# Mostra o aviso e atualiza a tela ANTES de travar tudo
page.snack_bar = ft.SnackBar(
ft.Text("Buscando dados no Qlik... Aguarde! Isso pode levar alguns segundos."), duration=30000,
open=True)
page.update()
try:
import asyncio
import Qlik_sense
global df, hist
# 2. A MÁGICA: Joga a extração pesada para uma Thread separada!
# Assim o Flet não congela.
df, hist = await asyncio.to_thread(Qlik_sense.extrair_dados_engine)
if df is not None:
page.snack_bar = ft.SnackBar(
ft.Text("✅ Dados carregados com sucesso! Clique em 'Entrar' novamente."),
bgcolor="green", duration=6000, open=True)
page.update()
else:
page.snack_bar = ft.SnackBar(
ft.Text(
"❌ O erro persiste. O servidor do Qlik/Sheets pode estar instável - TAVA BOM AGORA PARECE QUE PIORO!"),
bgcolor="red", duration=6000, open=True)
page.update()
except Exception as erro_manual:
print(f"Erro na tentativa manual: {erro_manual}")
page.snack_bar = ft.SnackBar(
ft.Text("Erro fatal ao tentar reconectar - AGORA NÃO SEI O QUE FAZER!"), bgcolor="red",
open=True)
page.update()
# 3. O Alerta visual que o usuário vê primeiro
aviso_erro = ft.SnackBar(
content=ft.Row([
ft.Text("⚠️ Falha ao baixar dados. O servidor estava dormindo.", color="white"),
ft.TextButton("BORA ACORDAR ELE?", on_click=tentar_buscar_dados_novamente,
style=ft.ButtonStyle(color="blue200"))
]),
bgcolor="red",
duration=10000
)
page.overlay.append(aviso_erro)
aviso_erro.open = True
page.update()
return
# Criamos a tabela de dados como um objeto global dentro do main para podermos atualizar depois
tabela_dados_obj = ft.DataTable(
columns=[ft.DataColumn(ft.Text("")) for col in df.columns if col != "Chave_CC_PRO"],
rows=[], # Começa vazia, vamos preencher na função
column_spacing=9,
data_row_max_height=80,
data_row_min_height=50,
heading_row_height=0
)
#Empacoto a tabela acima que tem só colunas em uma coluna do flet
container_colum = ft.Column(
controls=[tabela_dados_obj], # Apenas a tabela diretamente!
scroll=ft.ScrollMode.ALWAYS,
expand=True,
)
# Cabeçalho Fixo (seu código atual) AQUI COLOCAMOS O CABEÇALHO EM NEGRITO COM LETRA PRETA
colunas_cabecalho = [
ft.DataColumn(
ft.Container(
content=ft.Text(col, weight="bold", color="black"),
width=get_largura(col),
alignment=ft.Alignment.CENTER
)
) for col in df.columns if col !="Chave_CC_PRO"
]
#AQUI LEMOS O CABEÇALHO ACIMA COLOCAMOS UM FUNDO TRASPARENTE E UM TAMANHO E ALTURA
tabela_cabecalho = ft.DataTable(
columns=colunas_cabecalho,
heading_row_color="#C8E6C9",
column_spacing=9,
heading_row_height=40,
)
# Colunas editavel listadas para usarmos em outras defs
COLUNA_EDITAVEL = ["DATA DE VENC NOVA","NOVO CUSTO","QUANTIDADE","% BARTER"]
# Colunas editavel para linhas duplicadas onde editamos mais campos
COLUNA_EDITAVEL_duplicado = ["UF","SAFRA","DATA DE VENC NOVA", "NOVO CUSTO", "QUANTIDADE", "% BARTER","DERIVAÇÃO"]
#Cria uma def para fechar nosso banner imporovisado
def fechar_banner_custom(e):
banner_customizado.visible = False
page.update()
#Cria um campo de texto vazio
texto_banner = ft.Text("", color="black", size=16, weight="bold")
#Cria nosso banner e recebe o texto vazio
banner_customizado = ft.Container(
content=ft.Row(
controls=[
texto_banner,
ft.Container(expand=True),
ft.TextButton("OK, ENTENDI", on_click=fechar_banner_custom, style=ft.ButtonStyle(color="black"))
]
),
bgcolor="green",
top=0, left=0, right=0,
padding=15,
visible=False,
)
# 2. AS FUNÇÕES QUE CONTROLAM O BANNER
def fechar_banner_sair(e):
banner_sair.visible = False
banner_sair.update()
def abrir_banner_sair(e):
banner_sair.visible = True
print('iniciando abertura do banner')
banner_sair.update()
# 1. CRIAMOS O BANNER COMO UMA VARIÁVEL LIVRE (Fora de um def)
# Assim, todas as funções abaixo conseguem enxergar ele!
banner_sair = ft.Container(
content=ft.Row(
controls=[
ft.Text("⚠️ ATENÇÃO VOCÊ IRÁ DESLOGAR DO SISTEMA ", color="#ff5e62", size=16, weight="bold"),
# A "mola" que empurra os botões para a direita
ft.Container(expand=True),
ft.TextButton("Sair do Site", on_click=lambda e: (fechar_banner_sair(e), ir_para_login(e)), style=ft.ButtonStyle(color="#ff4112")),
ft.TextButton("Voltar para o sistema", on_click=fechar_banner_sair,
style=ft.ButtonStyle(color="Black"))
],
spacing=15 # <--- AQUI ESTÁ O ESPAÇAMENTO ENTRE OS BOTÕES!
),
bgcolor="#fff7f7",
top=0, left=0, right=0,
padding=15,
# Dica: É melhor ele nascer invisível (False) e você ativar (True) só quando o usuário clicar em "Sair"
visible=False,
)
# --- FUNÇÃO QUE SALVA A LINHA TODA ---
async def salvar_edicao(df_index, caixas_da_linha,botao):
print("Iniciando verificação da linha completa...")
botao.disabled = True
botao.icon_color="grey"
botao.update()
valores_validados = {} # Vamos guardar aqui os valores que passarem no teste
# 1. VALIDAÇÃO: Vamos olhar cada caixinha daquela linha
for nome_coluna, campo_texto in caixas_da_linha.items():
valor_digitado = campo_texto.value
# Se o campo estiver vazio, pula e não altera nada
# Testa textos simples que também podem ser editados na linha duplicada
if nome_coluna in ["NOVO CUSTO","DATA DE VENC NOVA","DERIVAÇÃO"]:
if valor_digitado == "" or valor_digitado == " ":
banner_customizado.bgcolor = "orange"
texto_banner.value = f" ❌ O campo 'DATA DE VENC NOVA','NOVO CUSTO' e DERIVAÇÃO ,são obrigatorios !"
banner_customizado.visible = True
page.update()
# ... (final do seu código, depois de salvar no google) ...
botao.disabled = False
botao.icon_color = "green"
botao.update()
return # Aborta o salvamento da linha inteira!
else:
if valor_digitado == "" or valor_digitado == "-":
continue
# Testa os números
if nome_coluna in ["NOVO CUSTO","% BARTER","QUANTIDADE","DERIVAÇÃO"]:
try:
# 2. A Inteligência: Verifica de qual formato o número está vindo para n forçar ajustes em formatos já corretos
if ',' in valor_digitado:
# É padrão BR digitado pelo usuário (ex: 2.050,00)
valor_limpo = valor_digitado.replace('.', '').replace(',', '.')
else:
# Já é padrão Python / 2º clique (ex: 2050.0)
valor_limpo = valor_digitado
# --- O PEDÁGIO ADICIONADO AQUI ---
# Se tiver vazio ou for um traço, salva em branco e pula a conversão!
if valor_digitado.strip() in ["", "-"]:
valores_validados[nome_coluna] = "0"
continue
valores_validados[nome_coluna] = float(valor_limpo)
print('Salvo valor numerico com sucesso')
except ValueError:
print('deu erro na hora de validar campos de valor')
banner_customizado.bgcolor = "orange"
texto_banner.value = f" ❌ O campo NOVO CUSTO, % BARTER, QUANTIDADE, DERIVAÇÃO , precisa ser valor númerico valido "
banner_customizado.visible = True
page.update()
# ... (final do seu código, depois de salvar no google) ...
botao.disabled = False
botao.icon_color = "green"
botao.update()
return # Aborta o salvamento da linha inteira!
# Testa as datas
elif nome_coluna in ["DATA DE VENC NOVA"]:
try:
datetime.strptime(valor_digitado, "%d/%m/%Y")
valores_validados[nome_coluna] = valor_digitado
except ValueError:
print('Deu erro no momento de validar data')
banner_customizado.bgcolor = "orange"
texto_banner.value = f" ❌ O campo DATA DE VENC NOVA , precisa ser uma data valida "
banner_customizado.visible = True
page.update()
# ... (final do seu código, depois de salvar no google) ...
botao.disabled = False
botao.icon_color = "green"
botao.update()
return # Aborta o salvamento da linha inteira!
# Testa as Checkboxes (Elas não precisam de formatação, só precisamos pegar o True ou False)
elif nome_coluna in ["Valor de reposição?", "Atualizar no Senior?"]:
valores_validados[nome_coluna] = valor_digitado
# Testa textos simples que também podem ser editados na linha duplicada
elif nome_coluna in ["UF", "SAFRA"]:
# O .upper().strip() é um bônus para garantir que a pessoa digite " pr " e o sistema salve limpinho "PR"
valores_validados[nome_coluna] = str(valor_digitado).upper().strip()
# 2. SE PASSOU EM TUDO SEM ERROS, ATUALIZA O PANDAS
for nome_coluna, valor_novo in valores_validados.items():
if df[nome_coluna].dtype == 'string' or df[nome_coluna].dtype.name == 'string':
df[nome_coluna] = df[nome_coluna].astype('object')
df.at[df_index, nome_coluna] = valor_novo
print(f'Resposta salva no df:{df[nome_coluna]}')
# 3. Tira a foto da linha atualizada com TUDO o que foi digitado
linha_atualizada = df.loc[df_index]
#definimos o fuso horario de brasilia
Fuso_horario=ZoneInfo("America/Sao_Paulo")
# Pegamos a data ehora atual para enviar no registro
data_hora = datetime.now(Fuso_horario).strftime("%d/%m/%Y %H:%M:%S")
# ====================================================================
# TRATAMENTO VIP PARA O CUSTO ORIGINAL (CUSTO ATUAL)
# ====================================================================
custo_bruto = str(linha_atualizada['CUSTO ATUAL']).replace('R$', '').strip()
# Limpa qualquer formatação estranha que venha do Qlik
if ',' in custo_bruto and '.' in custo_bruto:
custo_bruto = custo_bruto.replace('.', '').replace(',', '.')
elif ',' in custo_bruto:
custo_bruto = custo_bruto.replace(',', '.')
try:
# Transforma em número real e formata com a vírgula perfeita para o Sheets
custo_original_perfeito = float(custo_bruto)
except ValueError:
# Se algo der muito errado, manda o que tiver
custo_original_perfeito = str(linha_atualizada['CUSTO ATUAL'])
# 4. Monta o Dicionário mestre para o Google Sheets
registro = {
"Chave_CC_PRO": linha_atualizada['Chave_CC_PRO'],
"Data/Hora": data_hora,
"User": page.session.store.get("user") or "Desconhecido",
"UF":linha_atualizada['UF'],
"Safra": linha_atualizada['SAFRA'],
# Colocamos o f"'{...}" para forçar o Google a ler como TEXTO PURO
"Produto": f"'{linha_atualizada['CÓDIGO DO PRODUTO']}",
#"Produto": linha_atualizada['CÓDIGO DO PRODUTO'],
"Derv": linha_atualizada['DERIVAÇÃO'],
"desc prod": linha_atualizada['DESCRIÇÃO PRODUTO'],
"Marca": linha_atualizada['MARCA'],
"Custo original": custo_original_perfeito,
"Data venc atual": linha_atualizada['DATA VENC ATUAL'],
"Data de venc nv": linha_atualizada['DATA DE VENC NOVA'],
"Novo custo": linha_atualizada['NOVO CUSTO'],
"Barter": linha_atualizada['% BARTER'],
"qtd": linha_atualizada['QUANTIDADE'],
"vlr_reposicao": linha_atualizada['Valor de reposição?'],
"Atualiza_senior": linha_atualizada['Atualizar no Senior?']
}
# Na hora de chamar o salvamento, use o copy.deepcopy:
registro_congelado = copy.deepcopy(registro)
#Enpacota de forma asincrona com with uma fila de salvamento
async with fila_de_salvamento:
retorno = await asyncio.to_thread(Google_Sheets.adicionar_linha_sheets, registro_congelado)
print(f'Retorno do mod sheets {retorno}')
#Verificando o retorno e status da api do google sheets
if retorno.get("status") == "sucesso":
print('Tentando atualizar tela sucesso')
banner_customizado.bgcolor = "#C8E6C9"
texto_banner.value = "Registro salvo com sucesso!"
banner_customizado.visible = True
banner_customizado.update()
else:
print('Tentando atualizar tela com erro')
banner_customizado.bgcolor = "orange"
# Agora a mensagem de erro é um texto limpo, não um objeto bizarro
texto_banner.value = f"Ops! Ocorreu um erro: {retorno.get('mensagem')}"
banner_customizado.visible = True
banner_customizado.update()
# ... (final do seu código, depois de salvar no google) ...
botao.disabled = False
botao.icon_color = "green"
botao.update()
#Cria a lista que irá receber os idex duplicados
Lista_duplicada: list[int] = []
#Cria uma variavel none apenas para receber o id da linha duplicada que desejarmos deletar depois
linha_selecionada_id = None
# --- 3. A NOVA FUNÇÃO DE DUPLICAR ---
def duplicar_linha(e):
global df
# 1. Pega o index de quem foi clicado (que nós guardamos no 'data' do botão)
index_original = e.control.data
print(f"Duplicando o registro index: {index_original}")
# 2. Faz a matemática do Pandas como você já fazia
linha_copiada = df.loc[index_original].copy()
campos_para_limpar = ["UF","SAFRA","CUSTO ATUAL","DATA VENC ATUAL","NOVO CUSTO", "DATA DE VENC NOVA", "% BARTER", "QUANTIDADE", "Valor de reposição?",
"Atualizar no Senior?"]
for campo in campos_para_limpar:
linha_copiada[campo] = ""
novo_index = df.index.max() + 1
df.loc[novo_index] = linha_copiada
Lista_duplicada.append(novo_index)
# ========================================================
# 3. O TRUQUE DE MESTRE: A INJEÇÃO NA TELA
# ========================================================
# Em vez de apagar tudo, pedimos à fábrica apenas UMA linha nova
nova_linha_flet = criar_linha_visual(novo_index, linha_copiada)
# Procuramos onde a linha original está desenhada na tela
for i, linha_visual in enumerate(tabela_dados_obj.rows):
if linha_visual.data == index_original:
# Encontrou! Agora empurra a linha nova EXATAMENTE abaixo dela (i + 1)
tabela_dados_obj.rows.insert(i + 1, nova_linha_flet)
break # Não precisa procurar mais
# Pede ao Flet para atualizar SÓ a tabela visual
tabela_dados_obj.update()
# --- 2. O PREENCHER TABELA (Super Rápido) ---
def preencher_tabela(dataframe):
linhas = []
# Ele só pede à fábrica para fazer as linhas e junta tudo!
for index, row in dataframe.iterrows():
linhas.append(criar_linha_visual(index, row))
# Atualizações adicionais de layout, se precisar...
tabela_dados_obj.rows = linhas
tabela_dados_obj.update()
# ==============================================================
# --- A NOVA CAIXA DE ALERTA DE EXCLUSÃO ---
# ==============================================================
def fechar_dialogo_delecao(e):
caixa_confirmacao.open = False
page.update()
def acionar_delecao_verdadeira(e):
global df
nonlocal linha_selecionada_id
# A Lógica de exclusão que ficava no teclado agora mora aqui!
# A) Remove do Pandas
df.drop(index=linha_selecionada_id, inplace=True)
# B) Remove da nossa Lista de controle
Lista_duplicada.remove(linha_selecionada_id)
# C) Remove Visualmente
for linha_visual in tabela_dados_obj.rows[:]:
if linha_visual.data == linha_selecionada_id:
tabela_dados_obj.rows.remove(linha_visual)
break
tabela_dados_obj.update()
# Limpa a mira e fecha a caixa
linha_selecionada_id = None
caixa_confirmacao.open = False
aviso_sucesso = ft.SnackBar(
content=ft.Text("🗑️ Linha duplicada apagada com sucesso!", weight="bold", color="black"),
bgcolor="blue", duration=5000
)
page.overlay.append(aviso_sucesso)
aviso_sucesso.open = True
page.update()
# O molde visual da caixa
caixa_confirmacao = ft.AlertDialog(
modal=True, # Impede de fechar clicando fora da caixa
title=ft.Text("Confirmar Exclusão", weight="bold"),
content=ft.Text(
"Você tem certeza que deseja excluir esta linha duplicada? Essa ação não poderá ser desfeita."),
actions=[
ft.TextButton("Sim, excluir", on_click=acionar_delecao_verdadeira,
style=ft.ButtonStyle(color="red")),
ft.TextButton("Não, cancelar", on_click=fechar_dialogo_delecao,
style=ft.ButtonStyle(color="black")),
],
actions_alignment=ft.MainAxisAlignment.END,
)
# ==============================================================
# --- A NOVA CAIXA DE ALERTA DE EXCLUSÃO ---
# ==============================================================
def fechar_dialogo_delecao(e):
caixa_confirmacao.open = False
page.update()
def acionar_delecao_verdadeira(e):
global df
nonlocal linha_selecionada_id
# A Lógica de exclusão que ficava no teclado agora mora aqui!
# A) Remove do Pandas
df.drop(index=linha_selecionada_id, inplace=True)
# B) Remove da nossa Lista de controle
Lista_duplicada.remove(linha_selecionada_id)
# C) Remove Visualmente
for linha_visual in tabela_dados_obj.rows[:]:
if linha_visual.data == linha_selecionada_id:
tabela_dados_obj.rows.remove(linha_visual)
break
tabela_dados_obj.update()
# Limpa a mira e fecha a caixa
linha_selecionada_id = None
caixa_confirmacao.open = False
aviso_sucesso = ft.SnackBar(
content=ft.Text("🗑️ Linha duplicada apagada com sucesso!", weight="bold", color="black"),
bgcolor="blue", duration=5000
)
page.overlay.append(aviso_sucesso)
aviso_sucesso.open = True
page.update()
# O molde visual da caixa
caixa_confirmacao = ft.AlertDialog(
modal=True, # Impede de fechar clicando fora da caixa
title=ft.Text("Confirmar Exclusão", weight="bold"),
content=ft.Text(
"Você tem certeza que deseja excluir esta linha duplicada? Essa ação não poderá ser desfeita."),
actions=[
ft.TextButton("Sim, excluir", on_click=acionar_delecao_verdadeira, style=ft.ButtonStyle(color="red")),
ft.TextButton("Não, cancelar", on_click=fechar_dialogo_delecao, style=ft.ButtonStyle(color="black")),
],
actions_alignment=ft.MainAxisAlignment.END,
)
# Def criada para escutar os movimentos do teclado
def deletar_linha_teclado(e: ft.KeyboardEvent):
global df
nonlocal linha_selecionada_id
# 1. Verifica se a pessoa apertou o atalho seguro (Shift + Delete)
if e.key == "Delete" and e.shift:
# 2. Verifica se existe alguma linha na mira
if linha_selecionada_id is None:
aviso_vazio = ft.SnackBar(
content=ft.Text(
"⚠️ Clique na linha que deseja deletar antes de mandar o comando SHIFT + DELETE",
color="white"),
bgcolor="blue", duration=5000
)
page.overlay.append(aviso_vazio)
aviso_vazio.open = True
page.update()
# 3. A REGRA DE OURO: Só continua se o ID estiver na Lista de Duplicadas!
if linha_selecionada_id in Lista_duplicada:
caixa_confirmacao.open = True
page.update()
# 4. O ESCUDO DOS ORIGINAIS
else:
aviso_erro = ft.SnackBar(
content=ft.Text("⚠️ Segurança: Não é possível apagar registros originais!", color="black"),
bgcolor="red", duration=5000
)
page.overlay.append(aviso_erro)
aviso_erro.open = True
page.update()
# ---> LIGAR O OUVINTE À PÁGINA <---
# Coloque este comando em qualquer lugar dentro da sua def principal (onde o 'page' existe)
page.on_keyboard_event = deletar_linha_teclado
# --- AS LISTAS PADRONIZADAS (Ajuste com os seus dados reais) ---
lista_ufs = ["MS","PR","MS (EXP)"]
lista_safras = ["26/26", "26/27", "27/27", "27/28", "28/28","28/29"]
# --- 1. A FÁBRICA: Fabrica apenas UMA linha visual ---
def criar_linha_visual(index, row):
celulas = []
cx_tx_linha = {}
# Lista com as colunas que precisam ser analisadas para tratamento (ajuste os nomes conforme seu main)
colunas_numericas = ["CUSTO ATUAL", "NOVO CUSTO", "% BARTER", "QUANTIDADE"]
for col_name in row.index:
valor = row[col_name]
if col_name == "Chave_CC_PRO":
continue
# ==============================================================================
# ETAPA 1: LIMPEZA E FORMATAÇÃO DO TEXTO (Antes de criar o visual)
# ==============================================================================
if valor in ["nan","NaT","", "-","None"]:
texto_formatado =""
elif col_name == 'CÓDIGO DO PRODUTO':
texto_formatado = str(valor)
elif col_name in colunas_numericas:
try:
# 1. Pega o valor bruto
texto_raw = str(valor).strip()
# 2. Se a biblioteca entregou o R$ ou a vírgula:
if "R$" in texto_raw or "," in texto_raw:
v_limpo = texto_raw.replace('R$', '').strip()
v_limpo = v_limpo.replace('.', '') # Remove ponto de milhar
v_limpo = v_limpo.replace(',', '.') # Troca vírgula decimal por ponto
valor_num = float(v_limpo)
else:
# 3. Se entregou um número puro
valor_num = float(texto_raw)
# 4. Reconstrói perfeitamente para o visual BR
texto_formatado = f"{valor_num:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
except Exception as erro:
texto_formatado = str(valor)
else:
texto_formatado = f"{valor:.2f}" if isinstance(valor, (int, float)) else str(valor)
# ==============================================================================
# ETAPA 2: CRIAÇÃO DO ELEMENTO VISUAL (Usando o texto_formatado já limpo)
# ==============================================================================
# 1. BOTÃO DE DUPLICAR
if col_name == "DUPLICAR":
botao_duplicar = ft.IconButton(
icon=ft.Icons.COPY, icon_color="blue", tooltip="Duplicar esta linha",
data=index,
on_click=duplicar_linha
)
celulas.append(ft.DataCell(ft.Container(content=botao_duplicar, width=get_largura(col_name))))
# 2. CHECKBOX 1
elif col_name == "Atualizar no Senior?":
Chackbox1 = ft.Checkbox(label="", value=False)
cx_tx_linha[col_name] = Chackbox1
celulas.append(ft.DataCell(ft.Container(content=Chackbox1, width=get_largura(col_name))))
# 3. CHECKBOX 2
elif col_name == "Valor de reposição?":
Chackbox2 = ft.Checkbox(label="", value=False)
cx_tx_linha[col_name] = Chackbox2
celulas.append(ft.DataCell(ft.Container(content=Chackbox2, width=get_largura(col_name))))
# 4. CAMPOS EDITÁVEIS (Agora recebe o texto_formatado já sem o R$)
elif (index in Lista_duplicada and col_name in COLUNA_EDITAVEL_duplicado) or (index not in Lista_duplicada and col_name in COLUNA_EDITAVEL):
# --- SE FOR UF: Cria a lista suspensa ---
if col_name == "UF":
campo_editavel = ft.Dropdown(
options=[ft.dropdown.Option(uf) for uf in lista_ufs],
# Se já tiver valor, ele vem selecionado. Se for novo, vem vazio (None)
value=texto_formatado if texto_formatado in lista_ufs else None,
text_size=12, height=40, content_padding=5,
border_color="transparent", focused_border_color="blue",
color="black"
)
# --- SE FOR SAFRA: Cria a lista suspensa ---
elif col_name == "SAFRA":
campo_editavel = ft.Dropdown(
options=[ft.dropdown.Option(safra) for safra in lista_safras],
value=texto_formatado if texto_formatado in lista_safras else None,
text_size=12, height=40, content_padding=5,
border_color="transparent", focused_border_color="blue",
color="black"
)
# --- SE FOREM OS OUTROS VALORES (Custos, Barter, Qtd): Mantém texto digitável ---
else:
campo_editavel = ft.TextField(
value=texto_formatado, text_size=12, height=35, content_padding=5,
border_color="transparent", focused_border_color="blue",
color="black", cursor_color="black", text_align=ft.TextAlign.CENTER
)
# Guarda na mochila para salvar depois e desenha na tela
cx_tx_linha[col_name] = campo_editavel
celulas.append(
ft.DataCell(
ft.Container(
content=campo_editavel,
width=get_largura(col_name),
alignment=ft.Alignment.CENTER
)
)
)
# 5. BOTÃO DE SALVAR
elif col_name == "Salvar alteração":
botao_salvar = ft.IconButton(
icon=ft.Icons.SAVE, icon_color="green", tooltip="Salvar registro",
on_click=lambda e, idx=index, cx=cx_tx_linha: page.run_task(salvar_edicao, idx, cx, e.control)
)
celulas.append(ft.DataCell(ft.Container(content=botao_salvar, width=get_largura(col_name))))
# 6. TEXTO BLOQUEADO (Também recebe o texto_formatado já sem o R$)
else:
texto_visual = ft.Text(texto_formatado, color="black", size=12)
celulas.append(ft.DataCell(ft.Container(content=texto_visual, width=get_largura(col_name),alignment=ft.Alignment.CENTER,
padding=ft.Padding.symmetric(vertical=10, horizontal=0))))
# Essa def serve apenas para receber o id da linha clicada
def acordar_linha(e, idx=index):
nonlocal linha_selecionada_id
linha_selecionada_id = idx
return ft.DataRow(
cells=celulas,
data=index,
color={
ft.ControlState.HOVERED: "#a7d498",
ft.ControlState.PRESSED: "#a7d498",
ft.ControlState.FOCUSED: "#a7d498",
ft.ControlState.SELECTED: "#a7d498",
ft.ControlState.DEFAULT: ft.Colors.TRANSPARENT,
},
on_select_change=acordar_linha
)
def executar_filtro_logica(texto):
page.session.store.set("filtro_principal", texto)
print('iniciando filtro no main')
texto = texto.lower().strip()
if texto == "" or texto == " ":
preencher_tabela(df.head(50))
print('sem dados para filtrar')
return
palavras_digitadas = texto.split()
df_filtrado = df.copy()
for palavra in palavras_digitadas:
print(f'Filtrando as palavras:{palavra}')
mascara_busca = df_filtrado.astype(str).apply(
lambda col: col.str.lower().str.contains(palavra, na=False))
df_filtrado = df_filtrado[mascara_busca.any(axis=1)]
preencher_tabela(df_filtrado.head(50))
# ========================================================
# 2. O EVENTO DA BARRA DE PESQUISA (Entrega o texto para o motor)
# ========================================================
def filtrar_tudo(e):
executar_filtro_logica(e.control.value)
# Campo de busca inteligente
campo_busca = ft.TextField(
label="Filtrar qualquer campo...",
value=filtro_salvo,
prefix_icon=ft.Icons.SEARCH,
width=250,
height=40,
text_size=14,
border_color="white24",
focused_border_color="green",
label_style=ft.TextStyle(color="white70"),
on_submit=filtrar_tudo # Chama a função quando clicamos no enter
)
# ==============================================================================
# --- ESTILOS PROFISSIONAIS PARA OS BOTÕES DE NAVEGAÇÃO ---
# ==============================================================================
# 1. ESTILO ATIVO (A tela onde o utilizador está agora)
# Cria um botão sólido, branco, com bordas arredondadas (estilo pílula)
estilo_ativo = ft.ButtonStyle(
color=ft.Colors.BLACK, # Texto escuro para contraste
bgcolor=ft.Colors.WHITE, # Fundo totalmente branco
shape=ft.RoundedRectangleBorder(radius=20), # Bordas bem redondas
padding=ft.Padding(left=20, right=20, top=10, bottom=10)
)
# 2. ESTILO INATIVO (A tela para onde ele pode ir)
# Fica transparente, mas ganha vida quando o mouse passa por cima
estilo_inativo = ft.ButtonStyle(
color={
ft.ControlState.DEFAULT: ft.Colors.WHITE70, # Branco levemente apagado
ft.ControlState.HOVERED: ft.Colors.WHITE # Fica 100% branco no hover
},
bgcolor=ft.Colors.TRANSPARENT,
overlay_color=ft.Colors.WHITE10, # Dá um leve fundo branco ao passar o mouse
shape=ft.RoundedRectangleBorder(radius=20),
padding=ft.Padding(left=20, right=20, top=10, bottom=10)
)
#Etapa onde definimos valores fora da tabela principal, como botões de atualizar, abrir novas abas etc
# ==============================================================================
# --- O CABEÇALHO RESPONSIVO (Balança Perfeita) ---
# ==============================================================================
linha_cabecalho = ft.Row(
controls=[
# 1. TEXTO DA ESQUERDA
ft.Text("Custo de Produtos - Lista Produtos", color="Black", weight="bold", size=18),
# 2. PRIMEIRA MOLA: Empurra o centro
ft.Container(expand=True),
# ==========================================================
# 3. O NOVO "PACOTE" CENTRAL NATIVO (A Pílula Verde do Figma!)
# ==========================================================
ft.Container(
content=ft.Row(
controls=[
# 1. O Logotipo (Agora é uma imagem solta)
ft.Image(src="logo.png", width=40, height=40, fit="contain"),
# Espaçamento entre logo e busca
ft.Container(width=10),
# 2. A barra de busca
campo_busca,
# Linha Divisória 1
ft.Container(width=1, height=30, bgcolor="white54",
margin=ft.Margin.symmetric(horizontal=10)),
# AQUI A "LISTA DE PRODUTOS" RECEBE O ESTILO INATIVO!
ft.TextButton(
"Lista De Produtos",
style=estilo_ativo
),
# Linha Divisória 2
ft.Container(width=1, height=30, bgcolor="white54",
margin=ft.Margin.symmetric(horizontal=10)),
# AQUI OS "MEUS REGISTROS" RECEBE O ESTILO ATIVO!
ft.TextButton(
"Meus Registros",
on_click=ir_para_registros,
style=estilo_inativo
),
],
spacing=5,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
alignment=ft.MainAxisAlignment.CENTER, # Centraliza os itens dentro do verde
),
# AQUI ESTÁ A MÁGICA VISUAL DO FIGMA!
bgcolor="#618963", # Substitua por seu código Hex exato do verde do Figma
padding=ft.Padding(left=20, right=20, top=5, bottom=5),
border_radius=30, # Deixa as pontas totalmente arredondadas!
# expand=True # (Opcional) Descomente se quiser que a pílula verde estique para preencher o meio
),
# 4. SEGUNDA MOLA: Empurra os botões de ação lá pro canto direito
ft.Container(expand=True),
# 5. PACOTE DA DIREITA: Agrupa o botão Atualizar e o Sair lado a lado
ft.Row(
controls=[
# O texto entra aqui!
texto_ultima_atualizacao,
# Botão Atualizar (Azul)
ft.Container(
content=ft.Icon(icon=ft.Icons.UPDATE_ROUNDED, color="white", size=20),
on_click=lambda _: page.run_task(atualizar_tela),
tooltip="Atualizar a tela!",
bgcolor="blue", width=35, height=35, border_radius=5, alignment=ft.Alignment.CENTER,
ink=True
),
# Botão Sair (Vermelho)
ft.Container(
content=ft.Icon(icon=ft.Icons.POWER_SETTINGS_NEW, color="white", size=20),
on_click=abrir_banner_sair,
tooltip="Sair do Sistema!",
bgcolor="red", width=35, height=35, border_radius=5, alignment=ft.Alignment.CENTER, ink=True
)
],
spacing=10 # Pequeno espaço entre o botão azul e o vermelho
)
],
alignment = ft.MainAxisAlignment.START,
vertical_alignment = ft.CrossAxisAlignment.CENTER,
)
# --- A ÁREA DA TABELA RESPONSIVA ---
area_tabela = ft.Container(
content=ft.Row(
controls=[
ft.Column(
controls=[
tabela_cabecalho,
container_colum
],
spacing=0,
expand=True # Permite que a coluna da tabela cresça verticalmente
)
],
scroll=ft.ScrollMode.ALWAYS, # Mantém o scroll horizontal essencial para tabelas grandes
vertical_alignment=ft.CrossAxisAlignment.START
),
bgcolor="white",
border_radius=10,
padding=5,
expand=True # Faz a caixa branca da tabela ocupar todo o espaço restante da tela
)
# --- O LAYOUT PRINCIPAL ---
conteudo = ft.Container(
# NOVA FORMA DE COLOCAR IMAGEM DE FUNDO:
image=ft.DecorationImage(
src="BP - Analítico-App-Custo-v2 (1).png",
fit="cover"
),
expand=True,
# NOVA FORMA DE FAZER O PADDING (Resolve o aviso amarelo):
padding=ft.Padding(top=20, left=30, right=30, bottom=20),
content=ft.Column(
controls=[
linha_cabecalho,
area_tabela # A tabela vai preencher todo o resto do espaço
],
expand=True,
spacing=20 # Espaçamento dinâmico entre o cabeçalho e a tabela
)
)
#Limpamos o overlay para poder encher ele novamente
page.overlay.clear()
# Como tiramos o Stack, o banner agora vai na "camada superior" da página (Overlay)
# Isso garante que ele sempre flutue no topo da tela, independente do tamanho!
page.overlay.append(banner_customizado)
page.overlay.append(banner_sair)
page.overlay.append(banner_carregamento)
page.overlay.append(caixa_confirmacao)
page.clean()
page.add(conteudo)
if filtro_salvo != "":
executar_filtro_logica(filtro_salvo)
else:
preencher_tabela(df.head(50))
# Assim que abrir a tela de login chamamos a def que irá buscar os dados do QLik
page.run_task(carregar_no_fundo)
ir_para_login()
#Caminhopara abir na minha maquina
#ft.run(main, view=ft.AppView.WEB_BROWSER, assets_dir=".") # Removido o modo browser para rodar como App Desktop nativo
#Caminho e porta para abrir no servidor do huggingface
ft.run(
main,
view=ft.AppView.WEB_BROWSER,
assets_dir=".",
host="0.0.0.0",
port=7860)