Spaces:
Running
Running
| #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) | |