#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)