Copagril2026 commited on
Commit
60fbba0
·
verified ·
1 Parent(s): 952d69f

App Custo

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ BP[[:space:]]-[[:space:]]Analítico-App-Custo-v2[[:space:]](1).png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
File without changes
BP - Analítico-App-Custo-v2 (1).png ADDED

Git LFS Details

  • SHA256: d733bcda886f86dcc4cdfd34e6587b2cf2a4e52f82351ea6e47bbb4349e10eaf
  • Pointer size: 131 Bytes
  • Size of remote file: 103 kB
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 1. Pega um "computador" Linux com Python 3.11 já instalado
2
+ FROM python:3.11-slim
3
+
4
+ # 2. Cria uma pasta chamada /app lá dentro e entra nela
5
+ WORKDIR /app
6
+
7
+ # 3. Copia a nossa lista de compras e manda o Python instalar tudo
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ # 4. Copia TODO o resto dos seus arquivos para dentro da nuvem
12
+ # (Isso vai copiar o main.py, suas imagens PNG, o Login.py, etc)
13
+ COPY . .
14
+
15
+ # 5. Avisa o Hugging Face que o nosso sistema vai sair pela porta 7860
16
+ EXPOSE 7860
17
+
18
+ # 6. O comando final: Dá o play no sistema!
19
+ CMD ["python", "main.py"]
Google_Sheets.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+ import os
3
+ import gspread
4
+ from google.oauth2.service_account import Credentials
5
+ import pandas as pd
6
+ import json # <-- NOVO IMPORT OBRIGATÓRIO
7
+
8
+ # Abrindo o cofre .env para pegar as chaves (funciona no PC, é ignorado no Hugging Face)
9
+ load_dotenv()
10
+
11
+ # --- 1. CONFIGURAÇÕES ---
12
+ ID_PLANILHA = os.getenv("ID_PLANILHA")
13
+
14
+ # Escopos (permissões) que o robô vai pedir
15
+ ESCOPOS = [
16
+ "https://www.googleapis.com/auth/spreadsheets",
17
+ "https://www.googleapis.com/auth/drive"
18
+ ]
19
+
20
+ def conectar_sheets():
21
+ """Lê as credenciais (da nuvem ou local) e abre a conexão definitiva com o Google"""
22
+ try:
23
+ # Tenta puxar o JSON gigante direto do cofre (Hugging Face)
24
+ segredo_google = os.environ.get("GOOGLE_JSON")
25
+
26
+ if segredo_google:
27
+ # --- MODO NUVEM (Hugging Face) ---
28
+ print("Tentando conectar pelo MODO NUVEM...")
29
+ credenciais_dict = json.loads(segredo_google)
30
+ # Usa o from_service_account_info em vez do _file
31
+ credenciais = Credentials.from_service_account_info(credenciais_dict, scopes=ESCOPOS)
32
+ else:
33
+ # --- MODO LOCAL (Seu Computador) ---
34
+ print("Tentando conectar pelo MODO LOCAL...")
35
+ CAMINHO_JSON = os.getenv("CAMINHO_JSON")
36
+ credenciais = Credentials.from_service_account_file(CAMINHO_JSON, scopes=ESCOPOS)
37
+
38
+ # Autoriza e cria o cliente final
39
+ cliente = gspread.authorize(credenciais)
40
+ print("✅ Conectado ao Google com sucesso!")
41
+ return cliente
42
+
43
+ except Exception as e:
44
+ print(f"❌ Erro ao conectar: {e}")
45
+ return None
46
+
47
+ # ==============================================================================
48
+ # AS FUNÇÕES ABAIXO NÃO PRECISARAM DE MUDANÇAS, POIS A LÓGICA ACIMA RESOLVE TUDO!
49
+ # ==============================================================================
50
+
51
+ def adicionar_linha_sheets(historico):
52
+ cliente = conectar_sheets()
53
+ if not cliente:
54
+ return
55
+
56
+ try:
57
+ print("1. Abrindo a planilha...")
58
+ # Abre a planilha pelo ID e seleciona a primeira aba (Página1)
59
+ planilha = cliente.open_by_key(ID_PLANILHA)
60
+ aba = planilha.sheet1
61
+
62
+ print("3. Preparando a nova linha...")
63
+ # Convertemos o dic em list pegamos seus valroes e enviamos como uma nova linha
64
+ linha_formato_lista = list(historico.values())
65
+ aba.append_row(linha_formato_lista)
66
+
67
+ aviso='1'
68
+
69
+ return aviso
70
+
71
+ except Exception as e:
72
+ aviso=e
73
+ return aviso
74
+
75
+ def ler_historico():
76
+ print("Baixando registros do Google Sheets...")
77
+ cliente = conectar_sheets()
78
+ planilha = cliente.open_by_key(ID_PLANILHA)
79
+ aba = planilha.sheet1
80
+
81
+ # O COMANDO MÁGICO AQUI:
82
+ dados = aba.get_all_records()
83
+ df = pd.DataFrame(dados)
84
+ return df
85
+
86
+ def senhas():
87
+ cliente = conectar_sheets()
88
+ planilha = cliente.open_by_key(ID_PLANILHA)
89
+ aba = planilha.worksheet("acessos")
90
+
91
+ # O COMANDO MÁGICO AQUI:
92
+ dados = aba.get_all_records()
93
+ df = pd.DataFrame(dados)
94
+ return df
Login.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import flet as ft
2
+ import Google_Sheets
3
+ def montar_tela_login(funcao_entrar_no_sistema):
4
+
5
+
6
+ texto_aviso = ft.TextField(
7
+ label=None,
8
+ width=0,
9
+ height=0,
10
+ border_radius=10,
11
+ border_color="white",
12
+
13
+
14
+ )
15
+
16
+ # 1. Campos de Texto
17
+ campo_usuario = ft.TextField(
18
+ label="Usuário",
19
+ width=280,
20
+ height=50,
21
+ border_radius=10,
22
+ prefix_icon=ft.Icons.PERSON,
23
+ border_color="green",
24
+ focused_border_color="#2E7D32",
25
+ )
26
+
27
+ campo_senha = ft.TextField(
28
+ label="Senha",
29
+ width=280,
30
+ height=50,
31
+ border_radius=10,
32
+ password=True,
33
+ can_reveal_password=True,
34
+ prefix_icon=ft.Icons.LOCK,
35
+ border_color="green",
36
+ focused_border_color="#2E7D32",
37
+ )
38
+
39
+ # Chama a planilha para verificar o login e senha
40
+ retorno = Google_Sheets.senhas()
41
+ print(f'Retorno da planilha se senhas:{retorno}')
42
+
43
+
44
+ # 3. A Lógica de Validação
45
+ def fazer_login(e):
46
+ user_digitado = (campo_usuario.value or "").strip()
47
+ senha_digitada = (campo_senha.value or "").strip()
48
+
49
+
50
+ # Filtra os user e senhas do excel
51
+ tabela_filtrada = retorno[
52
+ (retorno['Usuário'].astype(str) == user_digitado) &
53
+ (retorno['Senha'].astype(str) == senha_digitada)
54
+ ]
55
+
56
+
57
+
58
+
59
+
60
+ # Validação Básica: Deixou em branco?
61
+ if not user_digitado or not senha_digitada:
62
+ print("Preencha os campos!")
63
+ texto_aviso.value=f"❌ Preencha os campos! "
64
+ texto_aviso.bgcolor="yellow"
65
+ texto_aviso.border_color="black"
66
+ texto_aviso.height=50
67
+ texto_aviso.width=290
68
+ return
69
+
70
+ # Se o Pandas achou uma linha, ele entra! Se a tabela vier vazia, credencial inválida.
71
+ if not tabela_filtrada.empty:
72
+ print("Login com sucesso!")
73
+
74
+ e.page.session.store.set("user",user_digitado)
75
+ funcao_entrar_no_sistema(e) # Libera a passagem para o main.py!
76
+ else:
77
+ print("Tentativa de acesso negada.")
78
+ texto_aviso.value = f"❌ Acesso Incorreto ! "
79
+ texto_aviso.bgcolor = "orange"
80
+ texto_aviso.border_color = "black"
81
+ texto_aviso.height = 50
82
+ texto_aviso.width = 290
83
+
84
+
85
+
86
+ # 2. Botão de Entrar
87
+ btn_entrar = ft.ElevatedButton(
88
+ content=ft.Text("ACESSAR SISTEMA", color="white"),
89
+ width=280,
90
+ height=40,
91
+ style=ft.ButtonStyle(
92
+ bgcolor="#2E7D32",
93
+ shape=ft.RoundedRectangleBorder(radius=10),
94
+ elevation=5,
95
+ ),
96
+ on_click=fazer_login
97
+ )
98
+
99
+ # 3. Definimos o Formulário Branco
100
+ formulario_branco = ft.Container(
101
+ content=ft.Column(
102
+ controls=[
103
+ # Colocando o texto de aviso na tela
104
+ texto_aviso,
105
+
106
+ ft.Divider(height=10, color="transparent"),
107
+
108
+ ft.Icon(ft.Icons.ACCOUNT_CIRCLE, size=80, color="#2E7D32"),
109
+ ft.Text("Bem-vindo", size=24, weight="bold", color="black"),
110
+ ft.Text("Faça login para continuar", size=14, color="grey"),
111
+ ft.Divider(height=20, color="transparent"),
112
+
113
+ campo_usuario,
114
+ ft.Divider(height=10, color="transparent"),
115
+ campo_senha,
116
+ ft.Divider(height=30, color="transparent"),
117
+ btn_entrar
118
+ ],
119
+ horizontal_alignment=ft.CrossAxisAlignment.CENTER,
120
+ spacing=5
121
+ ),
122
+ width=380, # Aumentei um pouco para caber os inputs (280) + o padding (80)
123
+ height=500,
124
+ padding=40,
125
+ bgcolor="white",
126
+ border_radius=20,
127
+ shadow=ft.BoxShadow(
128
+ spread_radius=1,
129
+ blur_radius=15,
130
+ color=ft.Colors.BLACK45,
131
+ offset=ft.Offset(0, 0),
132
+ blur_style=ft.BlurStyle.OUTER,
133
+ ),
134
+ )
135
+
136
+ # ==============================================================================
137
+ # 4. LAYOUT FINAL (Totalmente Responsivo)
138
+ # ==============================================================================
139
+ layout = ft.Container(
140
+ # Coloca a imagem de fundo perfeitamente ajustada
141
+ image=ft.DecorationImage(
142
+ src="login fundo.webp",
143
+ fit="cover" # Faz a imagem cobrir toda a tela
144
+ ),
145
+ expand=True, # Força o container a ocupar a tela inteira do navegador/monitor
146
+
147
+ # A MÁGICA ESTÁ AQUI: Alinha o conteúdo (o formulário) exatamente no centro de tudo!
148
+ alignment=ft.Alignment.CENTER,
149
+
150
+ content=formulario_branco
151
+ )
152
+
153
+ return layout
Qlik_sense.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+ import os
3
+ import requests # NOVO: Biblioteca nativa para requisições na web
4
+ from qlik_sdk import AuthType, Config, Apps
5
+ import pandas as pd
6
+ import Google_Sheets
7
+
8
+
9
+ # Abrindo o cofre.env para pegar as chaves
10
+ load_dotenv()
11
+
12
+ # --- 1. CONFIGURAÇÕES ---
13
+ TENANT_URL = os.getenv("TENANT_URL")
14
+ APP_ID = os.getenv("APP_ID")
15
+ CLIENT_ID = os.getenv("CLIENT_ID")
16
+ CLIENT_SECRET = os.getenv("CLIENT_SECRET")
17
+
18
+ # --- 2. A AUTENTICAÇÃO BLINDADA (BUSCA MANUAL DO TOKEN) ---
19
+ print("Buscando autorização M2M no Qlik...")
20
+
21
+ # Fazemos o pedido oficial do Token direto na "porta" do Qlik Cloud
22
+ resposta = requests.post(
23
+ f"{TENANT_URL}/oauth/token",
24
+ json={
25
+ "client_id": CLIENT_ID,
26
+ "client_secret": CLIENT_SECRET,
27
+ "grant_type": "client_credentials"
28
+ },
29
+ headers={"Accept": "application/json", "Content-Type": "application/json"}
30
+ )
31
+
32
+ # Se der erro de senha/ID ou de permissão, ele avisa na hora
33
+ if resposta.status_code != 200:
34
+ print(f"❌ Erro na autenticação com o Qlik: {resposta.text}")
35
+ exit()
36
+
37
+ # Extraímos o código do token que acabamos de receber!
38
+ token_seguro = resposta.json().get("access_token")
39
+ print("✔️ Token gerado com sucesso!")
40
+
41
+
42
+ # O PULO DO GATO: Agora nós enganamos o SDK!
43
+ # Nós passamos o 'auth_type' como APIKey, e colocamos o token fresquinho ali dentro.
44
+ config = Config(
45
+ host=TENANT_URL,
46
+ auth_type=AuthType.APIKey,
47
+ api_key=token_seguro
48
+ )
49
+
50
+ # Instanciamos a classe Apps normalmente. O SDK vai achar que é uma chave normal e abrir!
51
+ apps = Apps(config)
52
+
53
+ # =========================================================
54
+
55
+ def extrair_dados_engine():
56
+ print("Conectando ao App do Qlik...")
57
+
58
+ # Pega a referência do Aplicativo
59
+ app = apps.get(APP_ID)
60
+ try:
61
+ # Abre a sessão com a Engine (memória RAM do Qlik)
62
+ with app.open():
63
+ print(f"App '{app.attributes.name}' aberto! Montando a tabela virtual...")
64
+
65
+ # --- 3. DEFINA AS COLUNAS AQUI ---
66
+ campos_desejados = ["SAFRA","UF","CÓDIGO DO PRODUTO","DERIVAÇÃO","DESCRIÇÃO PRODUTO","MARCA","CUSTO ATUAL","DATA VENC ATUAL","DATA DE VENC NOVA","NOVO CUSTO","% BARTER","QUANTIDADE"]
67
+
68
+ limite_linhas = int(10000 / len(campos_desejados))
69
+
70
+ qDimensions = [{"qDef": {"qFieldDefs": [campo]}} for campo in campos_desejados]
71
+
72
+ properties = {
73
+ "qInfo": {"qType": "StraightTable"},
74
+ "qHyperCubeDef": {
75
+ "qDimensions": qDimensions,
76
+ "qInitialDataFetch": [{
77
+ "qTop": 0,
78
+ "qLeft": 0,
79
+ "qHeight": limite_linhas,
80
+ "qWidth": len(campos_desejados)
81
+ }]
82
+ }
83
+ }
84
+
85
+ # Cria o objeto na sessão e puxa o layout
86
+ obj = app.create_session_object(properties)
87
+ layout = obj.get_layout()
88
+
89
+ # Acessa a matriz de dados que a Engine devolveu
90
+ matriz_dados = layout.qHyperCube.qDataPages[0].qMatrix
91
+
92
+ # --- 4. CONVERTE PARA PANDAS ---
93
+ dados_limpos = []
94
+ for linha in matriz_dados:
95
+ registro = {}
96
+ for i, campo in enumerate(campos_desejados):
97
+ registro[campo] = linha[i].qText
98
+ dados_limpos.append(registro)
99
+
100
+ df = pd.DataFrame(dados_limpos)
101
+
102
+ #Crio uma nova coluna onde add a opção de salvar o registro
103
+ df["Salvar alteração"]=""
104
+
105
+ #Chamando a leitura do google sheets para colocar os valores na tela
106
+ hist=Google_Sheets.ler_historico()
107
+
108
+ # 2. Só faz a mágica se tiver dados no Sheets
109
+ if not hist.empty:
110
+ print("Cruzando dados do Qlik com o Google Sheets...")
111
+
112
+ # Passo A: Transforma a coluna de texto em DATA DE VERDADE para o Python saber quem é mais novo
113
+ hist['DATA REGISTRADA'] = pd.to_datetime(hist['DATA REGISTRADA'], format="%d/%m/%Y %H:%M:%S")
114
+
115
+ #Vou forçar as 2 colunas de produto tanto do excel quanto do qlik a serem textos sem espaços
116
+ hist['PRODUTO']=hist['PRODUTO'].astype(str).str.strip()
117
+ df['CÓDIGO DO PRODUTO']=df['CÓDIGO DO PRODUTO'].astype(str).str.strip()
118
+
119
+ # Passo B: Ordena do mais novo pro mais velho, e remove produtos repetidos (fica só o último salvo!)
120
+ hist_recente = hist.sort_values(by='DATA REGISTRADA', ascending=False).drop_duplicates(subset=['PRODUTO'],keep='first')
121
+
122
+ # Passo C: Transforma o 'Cod Produto' na chave de busca (como se fosse a 1ª coluna do PROCV no Excel)
123
+ hist_recente.set_index('PRODUTO', inplace=True)
124
+
125
+ # Passo D: Faz o PROCV mapeando o Código do Produto do Qlik com o do Sheets
126
+ # Se não achar nada (.fillna("")), deixa a caixinha em branco ("")
127
+ df["DATA DE VENC NOVA"] = df['CÓDIGO DO PRODUTO'].map(hist_recente['DATA VENC NOVA']).fillna("")
128
+ df["NOVO CUSTO"] = df['CÓDIGO DO PRODUTO'].map(hist_recente['NOVO CUSTO']).fillna("")
129
+ df["% BARTER"] = df['CÓDIGO DO PRODUTO'].map(hist_recente['BARTER %']).fillna("")
130
+ df["QUANTIDADE"] = df['CÓDIGO DO PRODUTO'].map(hist_recente['QUANTIDADE']).fillna("")
131
+
132
+ # 3. Se a planilha do Sheets estiver totalmente vazia (primeiro dia de uso)
133
+ else:
134
+ print("Planilha vazia. Criando colunas em branco.")
135
+ df["DATA DE VENC NOVA"] = ""
136
+ df["NOVO CUSTO"] = ""
137
+ df["% BARTER"] = ""
138
+ df["QUANTIDADE"] = ""
139
+
140
+
141
+ print(f"Sucesso! Extraídas {df} linhas do Qlik.")
142
+ return df,hist
143
+
144
+ except Exception as e:
145
+ print(f"Erro ao extrair dados do Qlik: {e}")
146
+ return None
147
+
Registros.py ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import flet as ft
2
+ import Google_Sheets
3
+
4
+ #Cria uma variavel global para atualziar a base quando atualizada
5
+ upd=None
6
+
7
+ def montar_tela_registros(loogof,retorno,base):
8
+ print('Iniciando modulo registros (Lendo direto do Sheets)')
9
+
10
+ #Recebe a base lida no inicio do código
11
+ global upd
12
+
13
+ upd=base
14
+
15
+ #Logica para retroanr a tela de login
16
+ def ir_para_login(e=None):
17
+
18
+ loogof()
19
+
20
+ #Função para voltar a tela principal
21
+ def tela_principal(e=None):
22
+
23
+ retorno()
24
+
25
+ #Função para atualizar a tela de registros
26
+ def atualizar(e):
27
+ global upd
28
+ upd = Google_Sheets.ler_historico()
29
+
30
+ preencher_tabela(upd)
31
+
32
+
33
+ # 2. SE A PLANILHA ESTIVER VAZIA
34
+ if upd.empty:
35
+ conteudo_tabela = ft.Text("Nenhuma alteração realizada.", color="black", size=20)
36
+
37
+ # 3. SE TIVER DADOS, MONTA A TABELA
38
+ else:
39
+ print('Lendo os registros e montando a tela...')
40
+
41
+
42
+ # Pega os nomes das colunas da Linha 1 do Sheets
43
+ nomes_colunas = upd.columns.tolist()
44
+ colunas_flet = [ft.DataColumn(ft.Text(str(nome), weight="bold", color="black")) for nome in nomes_colunas]
45
+
46
+ tabela_obj = ft.DataTable(
47
+ columns=colunas_flet,
48
+ rows=[], # Deixamos vazio para a função abaixo preencher
49
+ heading_row_color="#C8E6C9",
50
+ column_spacing=20,
51
+ data_row_min_height=40,
52
+ )
53
+ conteudo_tabela = ft.Column([tabela_obj], scroll=ft.ScrollMode.AUTO, expand=True)
54
+
55
+ # --- FUNÇÃO DE PREENCHER A TABELA (TOTALMENTE CONVERTIDA PARA PANDAS) ---
56
+ def preencher_tabela(dataframe,precisa_atualizar=True):
57
+ linhas_flet = []
58
+
59
+ # O jeito certo de ler um DataFrame linha a linha:
60
+ for index, row in dataframe.iterrows():
61
+ celulas = []
62
+
63
+ # Para cada nome de coluna, pega o valor exato daquela célula
64
+ for col in nomes_colunas:
65
+ valor = row[col]
66
+ celulas.append(ft.DataCell(ft.Text(str(valor), color="black", size=12)))
67
+
68
+ linhas_flet.append(ft.DataRow(cells=celulas,
69
+
70
+ #Criamos uma configuração de cores para ele ter o visual de houver na tela
71
+ color={
72
+ #Cor quando ele passa o moouse por cima
73
+ ft.ControlState.HOVERED:"#a7d498",
74
+
75
+ # Cor para o exato momento do clique
76
+ ft.ControlState.PRESSED: "#a7d498",
77
+
78
+ # Cor de quando você está com o cursor piscando lá dentro digitando
79
+ ft.ControlState.FOCUSED: "#a7d498",
80
+
81
+ # Outra cor para quando ele clica
82
+ ft.ControlState.SELECTED:"#a7d498",
83
+
84
+ #Outra cor para quando ele não passa o mouse nem clica
85
+ ft.ControlState.DEFAULT:ft.Colors.TRANSPARENT,
86
+
87
+ },
88
+ #Essa função serve apenas para avisar ao flet que essa linha deve ser ativa, assim o hover funciona
89
+ on_select_change=lambda e: print('Acordando a linha')
90
+
91
+
92
+ ))
93
+ tabela_obj.rows = linhas_flet
94
+
95
+ if precisa_atualizar:
96
+ tabela_obj.update()
97
+ conteudo_tabela.update()
98
+
99
+ # --- A SUPER BUSCA GLOBAL (ESTILO GOOGLE) ---
100
+
101
+ def filtrar_tudo(e):
102
+
103
+ print('iniciando filtro')
104
+ # 1. Pega o texto, deixa minúsculo e tira espaços sobrando nas pontas
105
+ texto = e.control.value.lower().strip()
106
+
107
+ # 2. Se apagou tudo, devolve a tabela original
108
+ if texto == "" or texto == " ":
109
+ preencher_tabela(upd.head(50))
110
+ print('sem dados para filtrar')
111
+ return
112
+
113
+ # 3. O SEGREDO: Corta o texto nos espaços!
114
+ # Se ele digitou "AGRO 3000", vira uma lista: ["agro", "3000"]
115
+ palavras_digitadas = texto.split()
116
+
117
+ # Fazemos uma cópia da tabela inteira para ir "espremendo" ela
118
+ df_filtrado = upd.copy()
119
+
120
+ # 4. O Loop do Afunilamento
121
+ for palavra in palavras_digitadas:
122
+ print(f'Filtrando as palavras:{palavra}')
123
+ # Para CADA palavra, ele procura na tabela e joga fora o que não bater
124
+ mascara_busca = df_filtrado.astype(str).apply(
125
+ lambda col: col.str.lower().str.contains(palavra, na=False))
126
+ df_filtrado = df_filtrado[mascara_busca.any(axis=1)]
127
+
128
+ # No final do loop, só sobram as linhas que têm TODAS as palavras digitadas!
129
+ preencher_tabela(df_filtrado.head(50))
130
+
131
+ # Campo de busca inteligente
132
+ campo_busca = ft.TextField(
133
+ label="Filtrar qualquer campo...",
134
+ prefix_icon=ft.Icons.SEARCH,
135
+ width=250,
136
+ height=40,
137
+ text_size=14,
138
+ border_color="white24",
139
+ focused_border_color="green",
140
+ label_style=ft.TextStyle(color="white70"),
141
+ on_submit=filtrar_tudo # Chama a função quando clicamos no enter
142
+ )
143
+
144
+ # ==============================================================================
145
+ # --- O CABEÇALHO RESPONSIVO (Mesma lógica da tela principal) ---
146
+ # ==============================================================================
147
+ linha_cabecalho = ft.Row(
148
+ controls=[
149
+ # 1. TEXTO DA ESQUERDA
150
+ ft.Text("App Custo de Produtos - Setor Compras", color="white", weight="bold", size=18),
151
+
152
+ # 2. PRIMEIRA MOLA: Empurra o centro
153
+ ft.Container(expand=True),
154
+
155
+ # 3. O "PACOTE" CENTRAL: Busca, Botões e Linhas Divisórias
156
+ ft.Row(
157
+ controls=[
158
+ campo_busca,
159
+
160
+ # Linha Divisória 1
161
+ ft.Container(width=1, height=30, bgcolor="white54", margin=ft.margin.symmetric(horizontal=10)),
162
+
163
+ ft.TextButton("Lista De Produtos", on_click=tela_principal, style=ft.ButtonStyle(
164
+ color={ft.ControlState.DEFAULT: ft.Colors.WHITE,
165
+ ft.ControlState.HOVERED: ft.Colors.BLACK})),
166
+
167
+ # Linha Divisória 2
168
+ ft.Container(width=1, height=30, bgcolor="white54", margin=ft.margin.symmetric(horizontal=10)),
169
+
170
+ ft.TextButton("Meus Registros", style=ft.ButtonStyle(
171
+ color={ft.ControlState.DEFAULT: ft.Colors.WHITE,
172
+ ft.ControlState.HOVERED: ft.Colors.BLACK})),
173
+ ],
174
+ spacing=5,
175
+ vertical_alignment=ft.CrossAxisAlignment.CENTER,
176
+ ),
177
+
178
+ # 4. SEGUNDA MOLA: Empurra os botões de ação lá pro canto direito
179
+ ft.Container(expand=True),
180
+
181
+ # 5. PACOTE DA DIREITA: Agrupa o botão Atualizar e o Sair lado a lado
182
+ ft.Row(
183
+ controls=[
184
+ # Botão Atualizar (Azul)
185
+ ft.Container(
186
+ content=ft.Icon(icon=ft.Icons.UPDATE_ROUNDED, color="white", size=20),
187
+ on_click=atualizar,
188
+ tooltip="Atualizar a tela!",
189
+ bgcolor="blue", width=35, height=35, border_radius=5, alignment=ft.Alignment.CENTER,
190
+ ink=True
191
+ ),
192
+
193
+ # Botão Sair (Vermelho)
194
+ ft.Container(
195
+ content=ft.Icon(icon=ft.Icons.POWER_SETTINGS_NEW, color="white", size=20),
196
+ on_click=ir_para_login,
197
+ tooltip="Sair do Sistema!",
198
+ bgcolor="red", width=35, height=35, border_radius=5, alignment=ft.Alignment.CENTER, ink=True
199
+ )
200
+ ],
201
+ spacing=10 # Pequeno espaço entre o botão azul e o vermelho
202
+ )
203
+ ],
204
+ alignment=ft.MainAxisAlignment.START,
205
+ vertical_alignment=ft.CrossAxisAlignment.CENTER,
206
+ )
207
+
208
+ # ==============================================================================
209
+ # --- A ÁREA DA TABELA RESPONSIVA ---
210
+ # ==============================================================================
211
+ area_tabela = ft.Container(
212
+ content=ft.Column(
213
+ controls=[
214
+ ft.Text("Histórico de Alterações", size=25, weight="bold", color="black"),
215
+ ft.Divider(),
216
+ conteudo_tabela # Aqui entra a tabela que foi montada lá em cima
217
+ ],
218
+ expand=True # Garante que a coluna ocupe tudo
219
+ ),
220
+ bgcolor="white",
221
+ border_radius=10,
222
+ padding=10,
223
+ expand=True # Faz a caixa branca esticar até o fim da tela
224
+ )
225
+
226
+ # ==============================================================================
227
+ # --- O LAYOUT PRINCIPAL DA TELA ---
228
+ # ==============================================================================
229
+ layout = ft.Container(
230
+ image=ft.DecorationImage(
231
+ src="BP - Analítico-App-Custo-v2 (1).png",
232
+ fit="cover"
233
+ ),
234
+ expand=True,
235
+ padding=ft.Padding(top=20, left=30, right=30, bottom=20),
236
+ content=ft.Column(
237
+ controls=[
238
+ linha_cabecalho,
239
+ area_tabela
240
+ ],
241
+ expand=True,
242
+ spacing=20
243
+ )
244
+ )
245
+ # Preenche a tabela com todos os dados logo que a tela carregar
246
+ if not upd.empty:
247
+ # Passamos False para ele NÃO tentar desenhar antes da hora!
248
+ preencher_tabela(upd, precisa_atualizar=False)
249
+
250
+
251
+ return layout
login fundo.webp ADDED
main.py ADDED
@@ -0,0 +1,559 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #Funções de bibliotecas exportadas
2
+ from datetime import datetime
3
+
4
+ #Bibliotecas interas
5
+ import flet as ft
6
+ import asyncio
7
+
8
+ #Importa modulos e suas funções
9
+ import Registros
10
+ import Login
11
+ import Qlik_sense
12
+ import Google_Sheets
13
+
14
+ #Var global para receber os dados do qlik
15
+ df=None
16
+
17
+ #Var de global para buscar o historico
18
+ df_hist=None
19
+
20
+
21
+
22
+ #Função mae, na qual chamamos as demais e oraganizamos a logica
23
+ def main(page: ft.Page):
24
+
25
+ #Definindo tamanho e parametos da pagina, nome etc
26
+ page.padding = 0
27
+ page.spacing = 0
28
+ page.bgcolor = ft.Colors.BLACK
29
+ page.window_maximized = True
30
+ page.title = "Sistema de Precificação"
31
+
32
+
33
+ # 2. FUNÇÃO QUE RODA ESCONDIDA NO FUNDO
34
+ async def carregar_no_fundo():
35
+ global df, df_hist
36
+
37
+ await asyncio.sleep(2)
38
+ print("Iniciando download silencioso do Qlik... e sheets")
39
+ df, df_hist = Qlik_sense.extrair_dados_engine()
40
+
41
+
42
+ print("Download do Qlik e sheets concluído com sucesso!")
43
+
44
+
45
+
46
+ # 3. TELA DE LOGIN
47
+ def ir_para_login(e=None):
48
+
49
+
50
+ page.clean()
51
+ tela = Login.montar_tela_login(ir_para_principal)
52
+ page.add(tela)
53
+ page.update()
54
+
55
+
56
+ #Elimino toda a configuração atual de espaçamento da tela para eu poder configurar como desejar abaixo com flet
57
+ page.padding=0
58
+ page.spacing=0
59
+ page.expand=True
60
+
61
+
62
+
63
+
64
+
65
+ # --- FUNÇÃO 1: CONSTRÓI A TELA DE REGISTROS ---
66
+ def ir_para_registros(e):
67
+ page.clean() # 1. Limpa a tela principal
68
+ global df_hist
69
+ print('Iniciado outro modulo')
70
+
71
+ # 2. Chama o módulo para desenhar a tela nova
72
+ # Passamos a função 'ir_para_principal' para ele saber como voltar
73
+ tela = Registros.montar_tela_registros(ir_para_login,ir_para_principal,df_hist)
74
+
75
+ page.add(tela) # 3. Adiciona a tela nova
76
+ page.update()
77
+
78
+ # Dic que define tamanho das colunas
79
+ larguras_colunas = {
80
+ "SAFRA":100,
81
+ "CÓDIGO DO PRODUTO": 150,
82
+ "DERIVAÇÃO":80,
83
+ "DESCRIÇÃO PRODUTO": 200,
84
+ "MARCA": 180,
85
+ "CUSTO ATUAL": 120, # Maior porque tem muito texto
86
+ "DATA VENC ATUAL": 180,
87
+ "DATA DE VENC NOVA":180,
88
+ "NOVO CUSTO":100,
89
+ "% BARTER":100,
90
+ "QUANTIDADE":100,
91
+ "Salvar alteração":100
92
+
93
+ }
94
+
95
+ def get_largura(nome_coluna):
96
+ return larguras_colunas.get(nome_coluna, 120)
97
+
98
+ #Essa def cria o esqueleto da pagina com a informações retornadas
99
+ def ir_para_principal(e=None):
100
+ page.clean()
101
+ print('Iniciando tela principal')
102
+
103
+ #Uso a var globar para extrari o retorno do qlik
104
+ global df
105
+
106
+ # Criamos a tabela de dados como um objeto global dentro do main para podermos atualizar depois
107
+ tabela_dados_obj = ft.DataTable(
108
+ columns=[ft.DataColumn(ft.Text("")) for _ in df.columns],
109
+ rows=[], # Começa vazia, vamos preencher na função
110
+ column_spacing=50,
111
+ data_row_max_height=float("inf"),
112
+ data_row_min_height=60,
113
+
114
+
115
+
116
+ )
117
+
118
+ #Empacoto a tabela acima que tem só colunas em uma coluna do flet
119
+ container_colum = ft.Column(
120
+ controls=[tabela_dados_obj], # Apenas a tabela diretamente!
121
+ scroll=ft.ScrollMode.AUTO,
122
+ expand=True,
123
+
124
+ )
125
+
126
+
127
+ # Cabeçalho Fixo (seu código atual) AQUI COLOCAMOS O CABEÇALHO EM NEGRITO COM LETRA PRETA
128
+ colunas_cabecalho = [
129
+ ft.DataColumn(
130
+ ft.Container(
131
+ content=ft.Text(col, weight="bold", color="black"),
132
+ width=get_largura(col),
133
+
134
+
135
+
136
+ )
137
+ ) for col in df.columns
138
+ ]
139
+
140
+ #AQUI LEMOS O CABEÇALHO ACIMA COLOCAMOS UM FUNDO TRAPARENTE E UM TAMANHO E ALTURA
141
+ tabela_cabecalho = ft.DataTable(
142
+
143
+ columns=colunas_cabecalho,
144
+
145
+ heading_row_color=ft.Colors.TRANSPARENT,
146
+ column_spacing=50,
147
+ heading_row_height=50,
148
+
149
+ )
150
+
151
+ # Colunas editavel listadas para usarmos em outras defs
152
+ COLUNA_EDITAVEL = ["DATA DE VENC NOVA","NOVO CUSTO","QUANTIDADE","% BARTER"]
153
+
154
+ #Cria uma def para fechar nosso banner imporovisado
155
+ def fechar_banner_custom(e):
156
+ banner_customizado.visible = False
157
+ page.update()
158
+
159
+ #Cria um campo de texto vazio
160
+ texto_banner = ft.Text("", color="black", size=16, weight="bold")
161
+
162
+ #Cria nosso banner e recebe o texto vazio
163
+ banner_customizado = ft.Container(
164
+ content=ft.Row(
165
+ controls=[
166
+ texto_banner,
167
+ ft.Container(expand=True),
168
+ ft.TextButton("OK, ENTENDI", on_click=fechar_banner_custom, style=ft.ButtonStyle(color="black"))
169
+ ]
170
+ ),
171
+ bgcolor="green",
172
+ top=0, left=0, right=0,
173
+ padding=15,
174
+ visible=False,
175
+ )
176
+
177
+
178
+
179
+ # --- FUNÇÃO QUE SALVA A LINHA TODA ---
180
+ def salvar_edicao(df_index, caixas_da_linha):
181
+
182
+ print("Iniciando verificação da linha completa...")
183
+
184
+ valores_validados = {} # Vamos guardar aqui os valores que passarem no teste
185
+
186
+ # 1. VALIDAÇÃO: Vamos olhar cada caixinha daquela linha
187
+ for nome_coluna, campo_texto in caixas_da_linha.items():
188
+ valor_digitado = campo_texto.value
189
+
190
+ # Se o campo estiver vazio, pula e não altera nada
191
+ if valor_digitado == "" or valor_digitado == "-":
192
+ continue
193
+
194
+ # Testa os números
195
+ if nome_coluna in ["NOVO CUSTO", "% BARTER", "QUANTIDADE"]:
196
+ try:
197
+ valor_limpo = valor_digitado.replace('.', '').replace(',', '.')
198
+ valores_validados[nome_coluna] = float(valor_limpo)
199
+ print('Salvo valor numerico com sucesso')
200
+ except ValueError:
201
+ print('deu erro na hora de validar campos de valor')
202
+
203
+ banner_customizado.bgcolor = "orange"
204
+ texto_banner.value = f" ❌ O campo NOVO CUSTO, % BARTER, QUANTIDADE , precisa ser valor númerico valido "
205
+ banner_customizado.visible = True
206
+ page.update()
207
+ return # Aborta o salvamento da linha inteira!
208
+
209
+ # Testa as datas
210
+ elif nome_coluna in ["DATA DE VENC NOVA"]:
211
+ try:
212
+ datetime.strptime(valor_digitado, "%d/%m/%Y")
213
+ valores_validados[nome_coluna] = valor_digitado
214
+
215
+ except ValueError:
216
+
217
+ print('Deu erro no momento de validar data')
218
+
219
+ banner_customizado.bgcolor = "orange"
220
+ texto_banner.value = f" ❌ O campo DATA DE VENC NOVA , precisa ser uma data valida "
221
+ banner_customizado.visible = True
222
+ page.update()
223
+ return # Aborta o salvamento da linha inteira!
224
+
225
+
226
+ # 2. SE PASSOU EM TUDO SEM ERROS, ATUALIZA O PANDAS
227
+ for nome_coluna, valor_novo in valores_validados.items():
228
+ if df[nome_coluna].dtype == 'string' or df[nome_coluna].dtype.name == 'string':
229
+ df[nome_coluna] = df[nome_coluna].astype('object')
230
+
231
+ df.at[df_index, nome_coluna] = valor_novo
232
+ print(f'Resposta salva no df:{df[nome_coluna]}')
233
+
234
+ # 3. Tira a foto da linha atualizada com TUDO o que foi digitado
235
+ linha_atualizada = df.loc[df_index]
236
+
237
+ # 4. Monta o Dicionário mestre para o Google Sheets
238
+ data_hora = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
239
+ registro = {
240
+ "Data/Hora": data_hora,
241
+ "User": page.session.store.get("user") or "Desconhecido",
242
+ "Safra": linha_atualizada['SAFRA'],
243
+ "Produto": linha_atualizada['CÓDIGO DO PRODUTO'],
244
+ "Derv": linha_atualizada['DERIVAÇÃO'],
245
+ "desc prod": linha_atualizada['DESCRIÇÃO PRODUTO'],
246
+ "Marca": linha_atualizada['MARCA'],
247
+ "Nv custo": linha_atualizada['CUSTO ATUAL'],
248
+ "Data venc atual": linha_atualizada['DATA VENC ATUAL'],
249
+ "Data de venc nv": linha_atualizada['DATA DE VENC NOVA'],
250
+ "Novo custo": linha_atualizada['NOVO CUSTO'],
251
+ "Barter": linha_atualizada['% BARTER'],
252
+ "qtd": linha_atualizada['QUANTIDADE'],
253
+
254
+ }
255
+
256
+
257
+ # 5. Envia para o Google Sheets UMA única vez
258
+ retorno=Google_Sheets.adicionar_linha_sheets(registro)
259
+ print(f'Retorno do mod sheets {retorno}')
260
+ if retorno =='1':
261
+ print('Tentando atualizar tela sucesso')
262
+
263
+ banner_customizado.bgcolor = "white"
264
+ texto_banner.value = "✔️ Registro salvo com sucesso 💾!"
265
+ banner_customizado.visible = True
266
+ page.update()
267
+
268
+ else:
269
+
270
+ print('Tentando atualizar tela com erro (Banner)')
271
+
272
+ banner_customizado.bgcolor = "orange"
273
+ texto_banner.value = f"❌ Ops! Ocorreu um erro: {retorno}"
274
+ banner_customizado.visible = True
275
+ page.update()
276
+
277
+
278
+
279
+ # --- FUNÇÃO DE PREENCHER A TABELA (MODIFICADA) ELA QUE VAI SEMPRE ATUALZIAR A TABELA COM OS DADOS RETORNADOS DO QLIK E FILTROS ---
280
+ def preencher_tabela(dataframe):
281
+ linhas = []
282
+
283
+
284
+ # iterrows() agora nos dá o 'index' real da linha no DataFrame (fundamental para atualizar o dado correto)
285
+ for index, row in dataframe.iterrows():
286
+ celulas = []
287
+
288
+ # um dic para guardar al alterações
289
+ cx_tx_linha = {}
290
+
291
+ for col_name in dataframe.columns:
292
+ valor = row[col_name]
293
+ if col_name == 'CÓDIGO DO PRODUTO':
294
+ texto_formatado=str(valor)
295
+
296
+ else:
297
+ texto_formatado = f"{valor:.2f}" if isinstance(valor, (int, float)) else str(valor)
298
+
299
+
300
+ #Verifico se estou olhando para a coluna botão
301
+ if col_name == "Salvar alteração":
302
+
303
+ botao_salvar= ft.IconButton(
304
+ icon=ft.Icons.SAVE,
305
+ icon_color="green",
306
+ tooltip="Salvar registro",
307
+
308
+ # Aqui damos o poder par ao botão enviar para a def que mnd para o sheets
309
+ on_click=lambda e, idx=index, cx=cx_tx_linha: salvar_edicao(idx, cx)
310
+
311
+
312
+
313
+ )
314
+ celulas.append(ft.DataCell(ft.Container(content=botao_salvar, width=get_largura(col_name))))
315
+ # SE FOR A COLUNA EDITÁVEL, COLOCA UM TEXTFIELD
316
+ elif col_name in COLUNA_EDITAVEL:
317
+ campo_editavel = ft.TextField(
318
+ value=texto_formatado,
319
+ text_size=12,
320
+ height=35,
321
+ content_padding=5,
322
+ border_color="transparent", # Deixa invisível até focar
323
+ focused_border_color="blue",
324
+
325
+ )
326
+
327
+ # Guarda essa caixinha no nosso dicionário para o botão ler depois!
328
+ cx_tx_linha[col_name] = campo_editavel
329
+
330
+ celulas.append(ft.DataCell(ft.Container(content=campo_editavel,width=get_largura(col_name))))
331
+
332
+ # SE FOR OUTRA COLUNA, COLOCA TEXTO NORMAL (Bloqueado)
333
+ else:
334
+ texto_visual = ft.Text(texto_formatado, color="black", size=12)
335
+ # Envolvemos no Container
336
+ celulas.append(ft.DataCell(ft.Container(content=texto_visual, width=get_largura(col_name), padding=ft.Padding.symmetric(vertical=10, horizontal=0))))
337
+
338
+ #Adicionamos os valores na lsita declarada no topo da def adicionams um hover nele
339
+ linhas.append(ft.DataRow(cells=celulas,
340
+
341
+ #Criamos uma configuração de cores para ele ter o visual de houver na tela
342
+ color={
343
+ #Cor quando ele passa o moouse por cima
344
+ ft.ControlState.HOVERED:"#a7d498",
345
+
346
+ # Cor para o exato momento do clique
347
+ ft.ControlState.PRESSED: "#a7d498",
348
+
349
+ # Cor de quando você está com o cursor piscando lá dentro digitando
350
+ ft.ControlState.FOCUSED: "#a7d498",
351
+
352
+ # Outra cor para quando ele clica
353
+ ft.ControlState.SELECTED:"#a7d498",
354
+
355
+ #Outra cor para quando ele não passa o mouse nem clica
356
+ ft.ControlState.DEFAULT:ft.Colors.TRANSPARENT,
357
+
358
+ },
359
+ #Essa função serve apenas para avisar ao flet que essa linha deve ser ativa, assim o hover funciona
360
+ on_select_change=lambda e: print('Acordando a linha')
361
+
362
+
363
+ ))
364
+
365
+ #Add na tabelas essas linhs configuradas
366
+ tabela_dados_obj.rows = linhas
367
+ tabela_dados_obj.update()
368
+
369
+ # 2. NOVO: Força a barra de rolagem a recalcular o tamanho correto!
370
+ container_colum.update()
371
+ page.update()
372
+
373
+ # --- A SUPER BUSCA GLOBAL (ESTILO GOOGLE) ---
374
+ def filtrar_tudo(e):
375
+
376
+ print('iniciando filtro')
377
+ # 1. Pega o texto, deixa minúsculo e tira espaços sobrando nas pontas
378
+ texto = e.control.value.lower().strip()
379
+
380
+ # 2. Se apagou tudo, devolve a tabela original
381
+ if texto == "" or texto== " ":
382
+ preencher_tabela(df.head(50))
383
+ print('sem dados para filtrar')
384
+ return
385
+
386
+ # 3. O SEGREDO: Corta o texto nos espaços!
387
+ # Se ele digitou "AGRO 3000", vira uma lista: ["agro", "3000"]
388
+ palavras_digitadas = texto.split()
389
+
390
+ # Fazemos uma cópia da tabela inteira para ir "espremendo" ela
391
+ df_filtrado = df.copy()
392
+
393
+ # 4. O Loop do Afunilamento
394
+ for palavra in palavras_digitadas:
395
+
396
+ print(f'Filtrando as palavras:{palavra}')
397
+ # Para CADA palavra, ele procura na tabela e joga fora o que não bater
398
+ mascara_busca = df_filtrado.astype(str).apply(
399
+ lambda col: col.str.lower().str.contains(palavra, na=False))
400
+ df_filtrado = df_filtrado[mascara_busca.any(axis=1)]
401
+
402
+ # No final do loop, só sobram as linhas que têm TODAS as palavras digitadas!
403
+ preencher_tabela(df_filtrado.head(50))
404
+
405
+ # Campo de busca inteligente
406
+ campo_busca = ft.TextField(
407
+ label="Filtrar qualquer campo...",
408
+ prefix_icon=ft.Icons.SEARCH,
409
+ width=250,
410
+ height=40,
411
+ text_size=14,
412
+ border_color="white24",
413
+ focused_border_color="green",
414
+ label_style=ft.TextStyle(color="white70"),
415
+ on_submit=filtrar_tudo # Chama a função quando clicamos no enter
416
+ )
417
+
418
+
419
+
420
+
421
+ #Etapa onde definimos valores fora da tabela principal, como botões de atualizar, abrir novas abas etc
422
+
423
+ # --- O CABEÇALHO RESPONSIVO ---
424
+ # --- O CABEÇALHO RESPONSIVO ---
425
+ linha_cabecalho = ft.Row(
426
+ controls=[
427
+
428
+ # 1. O TEXTO DO APLICATIVO (O jeito certo de colocar texto)
429
+ # Remova o texto da sua imagem PNG e deixe o Flet desenhar ele aqui,
430
+ # assim ele NUNCA mais será cortado em telas menores!
431
+ ft.Text("App Custo de Produtos - Setor Compras", color="white", weight="bold", size=18),
432
+
433
+ # 2. PRIMEIRA MOLA: Fica entre o título e o grupo central
434
+ ft.Container(expand=True),
435
+
436
+ # 3. O "PACOTE" CENTRAL: Agrupamos a busca e os botões juntos aqui
437
+ # 3. O "PACOTE" CENTRAL: Busca, Botões e Linhas Divisórias
438
+ ft.Row(
439
+ controls=[
440
+ # O Campo de busca fica de um lado
441
+ campo_busca,
442
+
443
+ # --- LINHA DIVISÓRIA 1 (Feita pelo Flet) ---
444
+ ft.Container(
445
+ width=1, # Espessura da linha
446
+ height=30, # Altura da linha
447
+ bgcolor="white54", # Branco com um pouco de transparência
448
+ margin=ft.margin.symmetric(horizontal=10) # Dá um respiro para os lados
449
+ ),
450
+
451
+ # O primeiro botão fica no meio
452
+ ft.TextButton("Lista De Produtos", style=ft.ButtonStyle(
453
+ color={ft.ControlState.DEFAULT: ft.Colors.WHITE,
454
+ ft.ControlState.HOVERED: ft.Colors.BLACK})),
455
+
456
+ # --- LINHA DIVISÓRIA 2 (Feita pelo Flet) ---
457
+ ft.Container(
458
+ width=1,
459
+ height=30,
460
+ bgcolor="white54",
461
+ margin=ft.margin.symmetric(horizontal=10)
462
+ ),
463
+
464
+ # O segundo botão fica do outro lado
465
+ ft.TextButton("Meus Registros", on_click=ir_para_registros, style=ft.ButtonStyle(
466
+ color={ft.ControlState.DEFAULT: ft.Colors.WHITE,
467
+ ft.ControlState.HOVERED: ft.Colors.BLACK})),
468
+ ],
469
+ spacing=5, # O spacing pode ser menor agora, pois a margem das linhas já afasta os itens
470
+ vertical_alignment=ft.CrossAxisAlignment.CENTER,
471
+ ),
472
+
473
+ # 4. SEGUNDA MOLA: Fica entre o grupo central e o botão de sair
474
+ ft.Container(expand=True),
475
+
476
+ # 5. BOTÃO DE SAIR (A segunda mola empurra ele lá pro canto direito)
477
+ ft.Container(
478
+ content=ft.Icon(icon=ft.Icons.POWER_SETTINGS_NEW, color="white", size=20),
479
+ on_click=ir_para_login,
480
+ tooltip="Sair do Sistema!",
481
+ bgcolor="red",
482
+ width=35,
483
+ height=35,
484
+ border_radius=5,
485
+ alignment=ft.Alignment.CENTER,
486
+ ink=True,
487
+ )
488
+ ],
489
+ alignment=ft.MainAxisAlignment.START,
490
+ vertical_alignment=ft.CrossAxisAlignment.CENTER,
491
+ )
492
+
493
+ # --- A ÁREA DA TABELA RESPONSIVA ---
494
+ area_tabela = ft.Container(
495
+ content=ft.Row(
496
+ controls=[
497
+ ft.Column(
498
+ controls=[
499
+ tabela_cabecalho,
500
+ container_colum
501
+ ],
502
+ spacing=0,
503
+ expand=True # Permite que a coluna da tabela cresça verticalmente
504
+ )
505
+ ],
506
+ scroll=ft.ScrollMode.AUTO, # Mantém o scroll horizontal essencial para tabelas grandes
507
+ expand=True,
508
+ vertical_alignment=ft.CrossAxisAlignment.START
509
+ ),
510
+ bgcolor="white",
511
+ border_radius=10,
512
+ padding=10,
513
+ expand=True # Faz a caixa branca da tabela ocupar todo o espaço restante da tela
514
+ )
515
+
516
+ # --- O LAYOUT PRINCIPAL (Substitui o Stack) ---
517
+ # --- O LAYOUT PRINCIPAL (Atualizado Flet 0.22+) ---
518
+ conteudo = ft.Container(
519
+ # NOVA FORMA DE COLOCAR IMAGEM DE FUNDO:
520
+ image=ft.DecorationImage(
521
+ src="BP - Analítico-App-Custo-v2 (1).png",
522
+ fit="cover"
523
+ ),
524
+ expand=True,
525
+ # NOVA FORMA DE FAZER O PADDING (Resolve o aviso amarelo):
526
+ padding=ft.Padding(top=20, left=30, right=30, bottom=20),
527
+ content=ft.Column(
528
+ controls=[
529
+ linha_cabecalho,
530
+ area_tabela # A tabela vai preencher todo o resto do espaço
531
+ ],
532
+ expand=True,
533
+ spacing=20 # Espaçamento dinâmico entre o cabeçalho e a tabela
534
+ )
535
+ )
536
+
537
+ # Como tiramos o Stack, o banner agora vai na "camada superior" da página (Overlay)
538
+ # Isso garante que ele sempre flutue no topo da tela, independente do tamanho!
539
+ page.overlay.append(banner_customizado)
540
+
541
+ page.add(conteudo)
542
+
543
+
544
+ preencher_tabela(df.head(50))
545
+
546
+ # Assim que abrir a tela de login chamamos a def que irá buscar os dados do QLik
547
+ page.run_task(carregar_no_fundo)
548
+ ir_para_login()
549
+
550
+ #ft.run(main, view=ft.AppView.WEB_BROWSER, assets_dir=".") # Removido o modo browser para rodar como App Desktop nativo
551
+
552
+
553
+ ft.run(
554
+ main,
555
+ view=ft.WEB_BROWSER,
556
+ assets_dir=".", # <--- Mantém as suas imagens funcionando!
557
+ host="0.0.0.0", # <--- Libera o acesso externo
558
+ port=7860 # <--- A porta obrigatória do Hugging Face
559
+ )
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ flet
2
+ pandas
3
+ python-dotenv
4
+ google-auth
5
+ gspread
6
+ qlik-sdk
7
+ requests