434 lines
32 KiB
Python
434 lines
32 KiB
Python
# inventario_dashboard_final_v22_corrigido.py
|
|
import pandas as pd
|
|
import re
|
|
from pathlib import Path
|
|
import plotly.graph_objects as go
|
|
import json
|
|
|
|
# --- CONFIGURAÇÃO DE COLUNAS ---
|
|
COLUNAS = {
|
|
"nome": "NOME", "email_pessoal": "Email Pessoal Sendo Usado", "contato_colaborador": "Contato Colaborador",
|
|
"contratacao": "Contratação", "status_contrato": "Status do Contrato", "cliente": "CLIENTE",
|
|
"base": "BASE", "cargo": "CARGO", "responsavel_direto": "RESPONSÁVEL DIRETO",
|
|
"contato_responsavel": "Contato do RESPONSÁVEL",
|
|
"contato_1": "Momento do Contato Para Busca de Dados Contato 1",
|
|
"contato_2": "Momento do Contato Para Busca de Dados Contato 2",
|
|
"contato_3": "Momento do Contato Para Busca de Dados Contato 3",
|
|
"subordinado": "Realmente e um Subordinado?", "usa_notebook": "Usa Notebook da Empresa",
|
|
"email_corporativo_status": "Tem Email Corporativo?", "email_corporativo_endereco": "Email Corporativo",
|
|
"tem_ad": "Tem conta no AD?", "maquina_registrada_status": "Maquina Registrada para o Cliente?",
|
|
"maquina_registrada_nome": "Nome da Maquina Registrada", "tag_ativo": "Tag do Ativo",
|
|
"numero_serie": "Rótulo de Serviço / Número de Série"
|
|
}
|
|
|
|
# --- Funções Auxiliares ---
|
|
def normalizar_texto(texto):
|
|
if isinstance(texto, str): return texto.strip().title()
|
|
return texto
|
|
|
|
def normalizar_telefone(numero):
|
|
if not isinstance(numero, (str, int, float)): return None
|
|
numeros_apenas = re.sub(r'\D', '', str(numero))
|
|
if len(numeros_apenas) == 11 and numeros_apenas.startswith('0'):
|
|
numeros_apenas = numeros_apenas[1:]
|
|
if len(numeros_apenas) in [10, 11]: return f"+55{numeros_apenas}"
|
|
if len(numeros_apenas) == 12 and numeros_apenas.startswith('55'): return f"+{numeros_apenas}"
|
|
if len(numeros_apenas) == 13 and numeros_apenas.startswith('55'): return f"+{numeros_apenas}"
|
|
return None
|
|
|
|
def extrair_sim_nao(texto):
|
|
if not isinstance(texto, str): return "não informado"
|
|
texto_limpo = texto.strip().lower()
|
|
if texto_limpo.startswith('sim'): return 'sim'
|
|
if texto_limpo.startswith('não') or texto_limpo.startswith('nao'): return 'não'
|
|
return 'não informado'
|
|
|
|
def formatar_contratacao(texto):
|
|
if not isinstance(texto, str) or texto == "Não informado": return texto
|
|
texto_lower = texto.lower()
|
|
classe_css = "generico"
|
|
if "pessoa juridica" in texto_lower or "pj" in texto_lower: classe_css = "pj"
|
|
elif "clt" in texto_lower: classe_css = "clt"
|
|
elif "estagio" in texto_lower or "estagiario" in texto_lower: classe_css = "estagio"
|
|
return f"<span class='tag-contratacao {classe_css}'>{texto}</span>"
|
|
|
|
def criar_grafico_pizza(valores, legendas, titulo, cores_personalizadas=None):
|
|
valores_filtrados, legendas_filtradas, cores_filtradas = [], [], []
|
|
cores_originais = cores_personalizadas if cores_personalizadas else ['#28a745', '#dc3545', '#ffc107', '#6c757d']
|
|
for i, valor in enumerate(valores):
|
|
if valor > 0:
|
|
valores_filtrados.append(valor)
|
|
legendas_filtradas.append(legendas[i])
|
|
cores_filtradas.append(cores_originais[i % len(cores_originais)])
|
|
if not valores_filtrados:
|
|
return f"<div class='chart-placeholder'><h4>{titulo}</h4><p>Sem dados para exibir.</p></div>"
|
|
fig = go.Figure(data=[go.Pie(labels=legendas_filtradas, values=valores_filtrados, hole=.4, marker_colors=cores_filtradas, textinfo='percent+value', insidetextfont=dict(color='white', size=14), hovertemplate="<b>%{label}</b><br>%{value}<br>%{percent}<extra></extra>")])
|
|
fig.update_layout(title_text=titulo, title_x=0.5, margin=dict(t=60, b=80, l=20, r=20), showlegend=True, legend=dict(orientation="h", yanchor="top", y=-0.1, xanchor="center", x=0.5), font=dict(family="Arial, sans-serif", size=14), paper_bgcolor='rgba(0,0,0,0)')
|
|
return fig.to_html(full_html=False, include_plotlyjs=False, config={'displayModeBar': False, 'responsive': True})
|
|
|
|
def gerar_tabela_colaboradores(df_grupo):
|
|
tabela_html = "<div class='table-container'><table><thead><tr><th>Colaborador</th><th>Contratação</th><th>Status</th><th class='col-contato'>Contato</th><th>Indicadores de Risco</th><th>Detalhes</th></tr></thead><tbody>"
|
|
for index, linha in df_grupo.iterrows():
|
|
filtros_da_linha = [key for key, value in linha.items() if key.startswith('filtro_') and value == True]
|
|
data_filters_attr = ' '.join(filtros_da_linha)
|
|
contato_colab = linha['Telefone Colaborador Normalizado']
|
|
contato_html = f'<a href="https://wa.me/{contato_colab.replace("+", "")}" target="_blank" class="whatsapp-link">{contato_colab} <i class="fab fa-whatsapp"></i></a>' if contato_colab else "Sem contato"
|
|
contratacao_raw = linha.get(COLUNAS['contratacao'])
|
|
contratacao = formatar_contratacao(contratacao_raw if pd.notna(contratacao_raw) else "Não informado")
|
|
status_contrato = linha.get(COLUNAS['status_contrato'], "Não informado")
|
|
status_contrato = status_contrato if pd.notna(status_contrato) else "Não informado"
|
|
indicadores_html = ""
|
|
info_notebook_original = linha.get(COLUNAS['usa_notebook'])
|
|
classe_linha = ""
|
|
if (isinstance(info_notebook_original, str) and 'saiu da empresa' in info_notebook_original.lower()) or \
|
|
(isinstance(status_contrato, str) and 'saiu da empresa' in status_contrato.lower()):
|
|
classe_linha = " class='terminated'"
|
|
if linha['filtro_em_conformidade']:
|
|
indicadores_html = "<span class='no-risk'><i class='fas fa-check-circle'></i> Em conformidade</span>"
|
|
else:
|
|
if linha.get('filtro_ativo_sem_ad') == True: indicadores_html += "<span class='risk-tag risk-inconsistencia'><i class='fas fa-user-times'></i> Ativo sem Conta AD</span>"
|
|
if linha.get('filtro_ativo_nao_registrado') == True: indicadores_html += "<span class='risk-tag risk-inconsistencia'><i class='fas fa-desktop'></i> Ativo Não Registrado</span>"
|
|
if linha['Usa Notebook (Valor)'] in ['não', 'não informado']: indicadores_html += "<span class='risk-tag risk-ativo-proprio'><i class='fas fa-exclamation-triangle'></i> Ativo Próprio / Não Declarado</span>"
|
|
if pd.notna(linha['Email Pessoal']): indicadores_html += "<span class='risk-tag risk-drive-externo'><i class='fas fa-cloud'></i> Drive Externo</span>"
|
|
if linha['Tem Email Corp (Valor)'] == 'não': indicadores_html += "<span class='risk-tag risk-sem-email'><i class='fas fa-envelope-open-text'></i> Sem E-mail Corp.</span>"
|
|
if str(linha.get(COLUNAS["subordinado"])).lower() == 'não': indicadores_html += "<span class='risk-tag risk-nao-subordinado'><i class='fas fa-user-slash'></i> Não Subordinado</span>"
|
|
is_cadastro_incompleto = linha['filtro_sem_gestor'] or linha['filtro_sem_contrato'] or linha['filtro_sem_status_contrato'] or linha['filtro_sem_cargo']
|
|
if is_cadastro_incompleto: indicadores_html += "<span class='risk-tag risk-cadastro'><i class='fas fa-user-edit'></i> Cadastro Incompleto</span>"
|
|
if not indicadores_html: indicadores_html = "<span class='risk-tag risk-nao-subordinado'><i class='fas fa-info-circle'></i> Fora de Conformidade</span>"
|
|
detalhes_html = ""
|
|
if 'terminated' in classe_linha:
|
|
detalhes_html += "<div><i class='fas fa-user-minus'></i> Colaborador desligado da empresa.</div>"
|
|
if pd.notna(linha.get(COLUNAS['cargo'])):
|
|
detalhes_html += f"<div><i class='fas fa-briefcase'></i> {linha[COLUNAS['cargo']]}</div>"
|
|
if linha['Usa Notebook (Valor)'] == 'sim':
|
|
detalhes_html += "<div><i class='fas fa-laptop'></i> Utiliza equipamento fornecido pela empresa.</div>"
|
|
info_maquina = linha.get(COLUNAS['maquina_registrada_nome'])
|
|
if pd.notna(info_maquina):
|
|
detalhes_html += f"<div><i class='fas fa-desktop'></i> Máquina: {info_maquina}</div>"
|
|
elif isinstance(info_notebook_original, str) and 'usa pessoal' in info_notebook_original.lower():
|
|
detalhes_html += "<div><i class='fas fa-user-shield'></i> Colaborador utiliza equipamento pessoal.</div>"
|
|
elif isinstance(info_notebook_original, str) and 'aguardando equipamento' in info_notebook_original.lower():
|
|
detalhes_html += "<div><i class='fas fa-clock'></i> Aguardando equipamento da empresa.</div>"
|
|
elif isinstance(info_notebook_original, str) and info_notebook_original.strip().lower() == 'não':
|
|
detalhes_html += f"<div><i class='fas fa-question-circle'></i> Informado que não utiliza notebook, sem detalhes adicionais.</div>"
|
|
elif pd.notna(info_notebook_original):
|
|
detalhes_html += f"<div>{info_notebook_original}</div>"
|
|
if pd.notna(linha.get(COLUNAS['tag_ativo'])):
|
|
detalhes_html += f"<div><i class='fas fa-tag'></i> Tag do Ativo: {linha[COLUNAS['tag_ativo']]}</div>"
|
|
if pd.notna(linha.get(COLUNAS['numero_serie'])):
|
|
detalhes_html += f"<div><i class='fas fa-barcode'></i> N/S: {linha[COLUNAS['numero_serie']]}</div>"
|
|
if pd.notna(linha.get(COLUNAS['email_corporativo_endereco'])):
|
|
detalhes_html += f"<div><i class='fas fa-envelope'></i> {linha[COLUNAS['email_corporativo_endereco']]}</div>"
|
|
if pd.notna(linha['Email Pessoal']):
|
|
detalhes_html += f"<div><i class='fas fa-at'></i> {linha['Email Pessoal']}</div>"
|
|
detalhes_final_html = detalhes_html if detalhes_html else "Sem informações adicionais."
|
|
tabela_html += f"<tr id='colaborador-{index}' data-filters='{data_filters_attr}'{classe_linha}><td{classe_linha}>{linha[COLUNAS['nome']]}</td><td{classe_linha}>{contratacao}</td><td{classe_linha}>{status_contrato}</td><td class='col-contato'{classe_linha}>{contato_html}</td><td{classe_linha}>{indicadores_html}</td><td{classe_linha}>{detalhes_final_html}</td></tr>"
|
|
tabela_html += "</tbody></table></div>"
|
|
return tabela_html
|
|
|
|
try:
|
|
caminho_do_script = Path(__file__).parent
|
|
nome_arquivo_excel = caminho_do_script / 'dados.xlsx'
|
|
df = pd.read_excel(nome_arquivo_excel)
|
|
|
|
df.replace(['N/A', 'Não informado', 'Sem Informação', 'Sem Informações'], pd.NA, inplace=True)
|
|
|
|
for col_key in ["nome", "responsavel_direto"]:
|
|
if COLUNAS[col_key] in df.columns:
|
|
df[COLUNAS[col_key]] = df[COLUNAS[col_key]].apply(normalizar_texto)
|
|
|
|
# --- Pré-Cálculos de Status e Riscos ---
|
|
df['Telefone Colaborador Normalizado'] = df.get(COLUNAS["contato_colaborador"], pd.Series(dtype='str')).apply(normalizar_telefone)
|
|
df['Usa Notebook (Valor)'] = df.get(COLUNAS["usa_notebook"], pd.Series(dtype='str')).apply(extrair_sim_nao)
|
|
df['Tem Email Corp (Valor)'] = df.get(COLUNAS["email_corporativo_status"], pd.Series(dtype='str')).apply(extrair_sim_nao)
|
|
df['Tem AD (Valor)'] = df.get(COLUNAS["tem_ad"], pd.Series(dtype='str')).apply(extrair_sim_nao)
|
|
df['Maquina Reg (Valor)'] = df.get(COLUNAS["maquina_registrada_status"], pd.Series(dtype='str')).apply(extrair_sim_nao)
|
|
|
|
if COLUNAS["email_pessoal"] in df.columns:
|
|
df['Email Pessoal'] = df[COLUNAS["email_pessoal"]].apply(lambda x: x if isinstance(x, str) and '@' in x else None)
|
|
else:
|
|
df['Email Pessoal'] = None
|
|
|
|
# --- DEFINIÇÃO DAS CONDIÇÕES DE FILTRO ---
|
|
df['filtro_em_conformidade'] = (df['Usa Notebook (Valor)'] == 'sim') & \
|
|
(df.get(COLUNAS['maquina_registrada_nome'], pd.Series(dtype='str')).notna()) & \
|
|
(df['Tem Email Corp (Valor)'] == 'sim') & \
|
|
(df['Telefone Colaborador Normalizado'].notna()) & \
|
|
(df.get(COLUNAS['contratacao'], pd.Series(dtype='str')).notna()) & \
|
|
(df.get(COLUNAS['status_contrato'], pd.Series(dtype='str')).notna()) & \
|
|
(df['Email Pessoal'].isnull()) & \
|
|
(df.get(COLUNAS['responsavel_direto'], pd.Series(dtype='str')).notna())
|
|
|
|
df['filtro_fora_conformidade'] = ~df['filtro_em_conformidade']
|
|
df['filtro_incontactaveis'] = df['Telefone Colaborador Normalizado'].isnull()
|
|
df['filtro_equip_proprio'] = df['Usa Notebook (Valor)'].isin(['não', 'não informado'])
|
|
df['filtro_drives_externos'] = df['Email Pessoal'].notna()
|
|
df['filtro_sem_email_corp'] = df['Tem Email Corp (Valor)'] == 'não'
|
|
df['filtro_sem_gestor'] = df.get(COLUNAS['responsavel_direto'], pd.Series(dtype='str')).isnull()
|
|
df['filtro_sem_contrato'] = df.get(COLUNAS['contratacao'], pd.Series(dtype='str')).isnull()
|
|
df['filtro_sem_status_contrato'] = df.get(COLUNAS['status_contrato'], pd.Series(dtype='str')).isnull()
|
|
df['filtro_sem_cargo'] = df.get(COLUNAS['cargo'], pd.Series(dtype='str')).isnull()
|
|
df['filtro_ativo_sem_ad'] = (df['Usa Notebook (Valor)'] == 'sim') & (df['Tem AD (Valor)'] == 'não')
|
|
df['filtro_ativo_nao_registrado'] = (df['Usa Notebook (Valor)'] == 'sim') & (df['Maquina Reg (Valor)'] == 'não')
|
|
|
|
# --- Cálculos para KPIs Globais ---
|
|
total_colaboradores = len(df)
|
|
total_conformes = df['filtro_em_conformidade'].sum()
|
|
total_nao_conformes = total_colaboradores - total_conformes
|
|
perc_conformes = (total_conformes / total_colaboradores) * 100 if total_colaboradores > 0 else 0
|
|
perc_nao_conformes = (total_nao_conformes / total_colaboradores) * 100 if total_colaboradores > 0 else 0
|
|
|
|
total_sem_contato_valido = df['filtro_incontactaveis'].sum()
|
|
risco_ativos_nao_declarados = df['filtro_equip_proprio'].sum()
|
|
risco_drives_externos = df['filtro_drives_externos'].sum()
|
|
risco_sem_email_corp = df['filtro_sem_email_corp'].sum()
|
|
risco_sem_gestor = df['filtro_sem_gestor'].sum()
|
|
risco_sem_contratacao = df['filtro_sem_contrato'].sum()
|
|
risco_sem_status_contrato = df['filtro_sem_status_contrato'].sum()
|
|
risco_sem_cargo = df['filtro_sem_cargo'].sum()
|
|
risco_sem_ad_count = df['filtro_ativo_sem_ad'].sum()
|
|
risco_maquina_nao_reg_count = df['filtro_ativo_nao_registrado'].sum()
|
|
|
|
cond_risco_alto = df['filtro_incontactaveis'] | df['filtro_equip_proprio'] | df['filtro_drives_externos'] | df['filtro_sem_email_corp']
|
|
total_risco_alto = cond_risco_alto.sum()
|
|
cond_risco_medio = df['filtro_sem_gestor'] | df['filtro_sem_contrato'] | df['filtro_sem_status_contrato'] | df['filtro_sem_cargo']
|
|
total_risco_medio = cond_risco_medio.sum()
|
|
cond_risco_baixo = df['filtro_ativo_sem_ad'] | df['filtro_ativo_nao_registrado']
|
|
total_risco_baixo = cond_risco_baixo.sum()
|
|
|
|
risco_inconsistencia_total = (df['filtro_ativo_sem_ad'] | df['filtro_ativo_nao_registrado']).sum()
|
|
if risco_inconsistencia_total > 0:
|
|
perc_risco_sem_ad = (risco_sem_ad_count / risco_inconsistencia_total) * 100
|
|
perc_risco_maquina_nao_reg = (risco_maquina_nao_reg_count / risco_inconsistencia_total) * 100
|
|
else:
|
|
perc_risco_sem_ad, perc_risco_maquina_nao_reg = 0, 0
|
|
|
|
# --- Geração das Seções por Gestor ---
|
|
html_secoes_gestores = ""
|
|
if COLUNAS["responsavel_direto"] in df.columns:
|
|
lista_gestores = sorted([g for g in df[COLUNAS["responsavel_direto"]].dropna().unique() if g])
|
|
for gestor in lista_gestores:
|
|
df_equipe = df[df[COLUNAS["responsavel_direto"]] == gestor].copy()
|
|
contato_gestor_raw = df_equipe.iloc[0].get(COLUNAS["contato_responsavel"])
|
|
contato_gestor_norm = normalizar_telefone(contato_gestor_raw)
|
|
contato_gestor_html = f'<a href="https://wa.me/{contato_gestor_norm.replace("+", "")}" target="_blank" class="whatsapp-link">{contato_gestor_norm} <i class="fab fa-whatsapp"></i></a>' if contato_gestor_norm else "Sem contato"
|
|
tentativas_contato = []
|
|
for col_key in ["contato_1", "contato_2", "contato_3"]:
|
|
if COLUNAS[col_key] in df_equipe.columns and pd.notna(df_equipe.iloc[0].get(COLUNAS[col_key])):
|
|
tentativas_contato.append(str(df_equipe.iloc[0][COLUNAS[col_key]]))
|
|
contatos_realizados_html = f"<div class='contact-attempts'><strong>Tentativas de Contato ({len(tentativas_contato)}):</strong> {' | '.join(tentativas_contato) if tentativas_contato else 'Nenhuma registrada'}</div>"
|
|
html_secoes_gestores += f"<div class='details-card manager-section'><h2><i class='fas fa-user-tie'></i> Gestor: {gestor} <small>({contato_gestor_html})</small></h2>{contatos_realizados_html}"
|
|
total_equipe = len(df_equipe)
|
|
notebook_counts_equipe = df_equipe['Usa Notebook (Valor)'].value_counts()
|
|
sim_equipe, nao_equipe, ni_equipe = notebook_counts_equipe.get('sim', 0), notebook_counts_equipe.get('não', 0), notebook_counts_equipe.get('não informado', 0)
|
|
grafico_equipe = criar_grafico_pizza([sim_equipe, nao_equipe, ni_equipe], ['Usa Notebook', 'Não Usa', 'Não Informado'], f"Uso de Notebook na Equipe ({total_equipe} Colaboradores)")
|
|
|
|
tabela_equipe_html = gerar_tabela_colaboradores(df_equipe)
|
|
html_secoes_gestores += f"<div class='manager-content-grid'>{grafico_equipe}{tabela_equipe_html}</div></div>"
|
|
|
|
# FIX: Seção para colaboradores sem gestor
|
|
df_sem_gestor = df[df[COLUNAS["responsavel_direto"]].isnull()].copy()
|
|
if not df_sem_gestor.empty:
|
|
html_secoes_gestores += "<div class='details-card manager-section'><h2><i class='fas fa-user-friends'></i> Colaboradores Sem Gestor Definido</h2>"
|
|
tabela_sem_gestor_html = gerar_tabela_colaboradores(df_sem_gestor)
|
|
html_secoes_gestores += f"<div class='manager-content-grid'>{tabela_sem_gestor_html}</div></div>"
|
|
|
|
|
|
html_content = f"""
|
|
<!DOCTYPE html><html lang="pt-BR"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Dashboard Consolidado de Gestão e Riscos</title>
|
|
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
|
<style>
|
|
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background-color: #f8f9fa; color: #212529; margin: 0; padding: 20px; }}
|
|
h1, h2, h3 {{ text-align: center; color: #1e3a5e; }}
|
|
.kpi-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 15px; }}
|
|
.kpi-card {{ background-color: #fff; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); text-align: center; border-left: 5px solid #007bff; }}
|
|
.kpi-card.kpi-filterable:hover {{ cursor: pointer; transform: translateY(-3px); box-shadow: 0 6px 12px rgba(0,0,0,0.1); transition: all 0.2s ease-in-out; }}
|
|
.kpi-card .kpi-title {{ font-size: 0.9em; color: #6c757d; margin-bottom: 8px; min-height: 2.5em; display: flex; align-items: center; justify-content: center;}}
|
|
.kpi-card .kpi-value {{ font-size: 2em; font-weight: bold; color: #1e3a5e; }}
|
|
.kpi-percentage {{ font-size: 0.6em; font-weight: normal; color: #6c757d; }}
|
|
.kpi-card.ok-green {{ border-left-color: #28a745; }}
|
|
.kpi-card.risk-red {{ border-left-color: #dc3545; }}
|
|
.kpi-card.risk-orange {{ border-left-color: #fd7e14; }}
|
|
.kpi-card.risk-yellow {{ border-left-color: #ffc107; }}
|
|
.kpi-card.risk-blue {{ border-left-color: #007bff; }}
|
|
.main-summary-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 25px; }}
|
|
.risk-group-title {{ text-align: left; font-size: 1.2em; margin-top: 25px; margin-bottom: 15px; color: #343a40; border-bottom: 1px solid #ccc; padding-bottom: 8px;}}
|
|
.risk-group-title .risk-total-count {{ font-size: 0.8em; color: #6c757d; font-weight: normal; float: right; line-height: 1.5;}}
|
|
.risk-icon-high {{ color: #dc3545; }}
|
|
.risk-icon-medium {{ color: #ffc107; }}
|
|
.risk-icon-low {{ color: #007bff; }}
|
|
.main-section-title {{ font-size: 2em; color: #1e3a5e; margin-top: 40px; border-bottom: 2px solid #1e3a5e; padding-bottom: 10px; text-align: left; }}
|
|
.manager-section {{ margin-bottom: 25px; background-color: #fff; border-radius: 8px; padding: 20px; box-shadow: 0 4px 8px rgba(0,0,0,0.05); }}
|
|
.manager-section > h2 {{ text-align: left; border-bottom: 2px solid #0056b3; padding-bottom: 10px; font-size: 1.5em; }}
|
|
.manager-section > h2 small {{ font-weight: normal; font-size: 0.7em; color: #6c757d; }}
|
|
.contact-attempts {{ font-size: 0.9em; color: #6c757d; text-align: left; padding: 5px 0 15px 0; }}
|
|
.manager-content-grid {{ display: flex; flex-direction: column; gap: 20px; }}
|
|
.chart-container-small, .chart-placeholder {{ width: 100%; max-width: 600px; height: 400px; margin: 0 auto; display: flex; flex-direction: column; justify-content: center; align-items: center; }}
|
|
.table-container {{ overflow-x: auto; }}
|
|
table {{ width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 0.9em; }}
|
|
th, td {{ padding: 12px 15px; border: 1px solid #dee2e6; text-align: left; vertical-align: middle; }}
|
|
th {{ background-color: #e9ecef; font-weight: 600; }}
|
|
tbody tr:nth-of-type(odd) {{ background-color: #f8f9fa; }}
|
|
td div {{ margin-bottom: 5px; }} td div:last-child {{ margin-bottom: 0; }}
|
|
.col-contato {{ white-space: nowrap; }}
|
|
tr.terminated, td.terminated {{ text-decoration: line-through; color: #999 !important; background-color: #f2f2f2 !important; }}
|
|
.risk-tag {{ display: inline-block; padding: 4px 8px; border-radius: 4px; color: #fff; font-size: 0.8em; margin: 2px; white-space: nowrap; }}
|
|
.risk-tag.risk-ativo-proprio {{ background-color: #dc3545; }}
|
|
.risk-tag.risk-drive-externo {{ background-color: #fd7e14; }}
|
|
.risk-tag.risk-sem-email {{ background-color: #ffc107; color: #212529; }}
|
|
.risk-tag.risk-inconsistencia {{ background-color: #007bff; }}
|
|
.risk-tag.risk-nao-subordinado {{ background-color: #6c757d; }}
|
|
.risk-tag.risk-cadastro {{ background-color: #6f42c1; }}
|
|
.risk-tag i {{ margin-right: 5px; }}
|
|
.no-risk {{ color: #28a745; font-weight: bold; }}
|
|
.no-risk i {{ margin-right: 5px; }}
|
|
.whatsapp-link {{ color: #25D366; text-decoration: none; font-weight: bold; }}
|
|
.whatsapp-link:hover {{ text-decoration: underline; }}
|
|
.tag-contratacao {{ display: inline-block; padding: 4px 8px; border-radius: 12px; color: #fff; font-size: 0.85em; font-weight: 500; }}
|
|
.tag-contratacao.clt {{ background-color: #007bff; }}
|
|
.tag-contratacao.pj {{ background-color: #17a2b8; }}
|
|
.tag-contratacao.estagio {{ background-color: #fd7e14; }}
|
|
.tag-contratacao.generico {{ background-color: #6c757d; }}
|
|
h3 i, h4 i {{ margin-right: 8px; }}
|
|
/* Estilos para o Modal de Filtro */
|
|
.modal-overlay {{ position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.6); display: none; justify-content: center; align-items: center; z-index: 1000; }}
|
|
.modal-content {{ background-color: #fff; padding: 25px; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.3); width: 90%; max-width: 1200px; max-height: 90vh; display: flex; flex-direction: column; }}
|
|
.modal-header {{ display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #dee2e6; padding-bottom: 15px; margin-bottom: 15px; }}
|
|
.modal-header h2 {{ margin: 0; text-align: left; font-size: 1.5em; }}
|
|
.modal-close-btn {{ font-size: 1.8em; color: #6c757d; background: none; border: none; cursor: pointer; }}
|
|
.modal-body {{ overflow-y: auto; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Dashboard Consolidado de Gestão e Riscos</h1>
|
|
<div class="main-summary-grid">
|
|
<div class="kpi-card">
|
|
<div class="kpi-title"><i class="fas fa-users"></i> Total de Colaboradores</div>
|
|
<div class="kpi-value">{total_colaboradores}</div>
|
|
</div>
|
|
<div class="kpi-card ok-green kpi-filterable" data-filter-key="filtro_em_conformidade" data-filter-title="Colaboradores em Conformidade">
|
|
<div class="kpi-title"><i class="fas fa-check-circle"></i> Em Conformidade</div>
|
|
<div class="kpi-value">{total_conformes} <span class="kpi-percentage">({perc_conformes:.1f}%)</span></div>
|
|
</div>
|
|
<div class="kpi-card risk-red kpi-filterable" data-filter-key="filtro_fora_conformidade" data-filter-title="Colaboradores Fora de Conformidade">
|
|
<div class="kpi-title"><i class="fas fa-exclamation-circle"></i> Fora de Conformidade</div>
|
|
<div class="kpi-value">{total_nao_conformes} <span class="kpi-percentage">({perc_nao_conformes:.1f}%)</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="risk-kpi-section">
|
|
<h3 class="risk-group-title"><i class="fas fa-exclamation-circle risk-icon-high"></i> Indicadores de Risco Alto <span class="risk-total-count">({total_risco_alto} Colaboradores)</span></h3>
|
|
<div class="kpi-grid">
|
|
<div class="kpi-card risk-red kpi-filterable" data-filter-key="filtro_incontactaveis" data-filter-title="Colaboradores Incontactáveis (TI)"><div class="kpi-title">Incontactáveis (TI)</div><div class="kpi-value">{total_sem_contato_valido}</div></div>
|
|
<div class="kpi-card risk-red kpi-filterable" data-filter-key="filtro_equip_proprio" data-filter-title="Colaboradores com Equipamento Próprio / Não Declarado"><div class="kpi-title">Equip. Próprio / Não Declarado</div><div class="kpi-value">{risco_ativos_nao_declarados}</div></div>
|
|
<div class="kpi-card risk-orange kpi-filterable" data-filter-key="filtro_drives_externos" data-filter-title="Colaboradores com Drives Externos (Email Pessoal)"><div class="kpi-title">Drives Externos</div><div class="kpi-value">{risco_drives_externos}</div></div>
|
|
<div class="kpi-card risk-yellow kpi-filterable" data-filter-key="filtro_sem_email_corp" data-filter-title="Colaboradores Sem E-mail Corporativo"><div class="kpi-title">Sem E-mail Corporativo</div><div class="kpi-value">{risco_sem_email_corp}</div></div>
|
|
</div>
|
|
|
|
<h3 class="risk-group-title"><i class="fas fa-exclamation-triangle risk-icon-medium"></i> Indicadores de Risco Médio <span class="risk-total-count">({total_risco_medio} Colaboradores)</span></h3>
|
|
<div class="kpi-grid">
|
|
<div class="kpi-card risk-orange kpi-filterable" data-filter-key="filtro_sem_gestor" data-filter-title="Colaboradores Sem Gestor Definido"><div class="kpi-title">Sem Gestor Definido</div><div class="kpi-value">{risco_sem_gestor}</div></div>
|
|
<div class="kpi-card risk-yellow kpi-filterable" data-filter-key="filtro_sem_contrato" data-filter-title="Colaboradores Sem Tipo de Contrato"><div class="kpi-title">Sem Tipo de Contrato</div><div class="kpi-value">{risco_sem_contratacao}</div></div>
|
|
<div class="kpi-card risk-yellow kpi-filterable" data-filter-key="filtro_sem_status_contrato" data-filter-title="Colaboradores Sem Status de Contrato"><div class="kpi-title">Sem Status de Contrato</div><div class="kpi-value">{risco_sem_status_contrato}</div></div>
|
|
<div class="kpi-card risk-yellow kpi-filterable" data-filter-key="filtro_sem_cargo" data-filter-title="Colaboradores Sem Cargo Definido"><div class="kpi-title">Sem Cargo Definido</div><div class="kpi-value">{risco_sem_cargo}</div></div>
|
|
</div>
|
|
|
|
<h3 class="risk-group-title"><i class="fas fa-info-circle risk-icon-low"></i> Indicadores de Risco Baixo <span class="risk-total-count">({total_risco_baixo} Colaboradores)</span></h3>
|
|
<div class="kpi-grid">
|
|
<div class="kpi-card risk-blue kpi-filterable" data-filter-key="filtro_ativo_sem_ad" data-filter-title="Colaboradores com Ativo sem Conta AD"><div class="kpi-title">Ativo sem Conta AD</div><div class="kpi-value">{risco_sem_ad_count} <span class="kpi-percentage" title="do total de ativos com inconsistência">({perc_risco_sem_ad:.1f}%)</span></div></div>
|
|
<div class="kpi-card risk-blue kpi-filterable" data-filter-key="filtro_ativo_nao_registrado" data-filter-title="Colaboradores com Ativo Não Registrado"><div class="kpi-title">Ativo Não Registrado</div><div class="kpi-value">{risco_maquina_nao_reg_count} <span class="kpi-percentage" title="do total de ativos com inconsistência">({perc_risco_maquina_nao_reg:.1f}%)</span></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="main-content">
|
|
<div class="details-section">
|
|
<h2 class="main-section-title"><i class="fas fa-user-friends"></i> Análise por Equipe</h2>
|
|
{html_secoes_gestores}
|
|
</div>
|
|
</div>
|
|
|
|
<div id="filter-modal" class="modal-overlay">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 id="modal-title">Resultados Filtrados</h2>
|
|
<button id="modal-close" class="modal-close-btn">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="table-container">
|
|
<table id="modal-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Colaborador</th>
|
|
<th>Contratação</th>
|
|
<th>Status</th>
|
|
<th class='col-contato'>Contato</th>
|
|
<th>Indicadores de Risco</th>
|
|
<th>Detalhes</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="modal-table-body"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="text/javascript">
|
|
window.addEventListener('load', function() {{
|
|
var allPlotlyGraphs = document.querySelectorAll('.plotly-graph-div');
|
|
for(var i=0; i < allPlotlyGraphs.length; i++) {{
|
|
Plotly.Plots.resize(allPlotlyGraphs[i]);
|
|
}}
|
|
|
|
const mainContent = document.getElementById('main-content');
|
|
const modal = document.getElementById('filter-modal');
|
|
const modalTitle = document.getElementById('modal-title');
|
|
const modalTableBody = document.getElementById('modal-table-body');
|
|
const closeModalBtn = document.getElementById('modal-close');
|
|
|
|
document.querySelectorAll('.kpi-filterable').forEach(kpiCard => {{
|
|
kpiCard.addEventListener('click', () => {{
|
|
const filterKey = kpiCard.getAttribute('data-filter-key');
|
|
const filterTitle = kpiCard.getAttribute('data-filter-title');
|
|
|
|
// CORREÇÃO 1: Busca apenas dentro do conteúdo principal
|
|
const employeeRows = mainContent.querySelectorAll(`tr[data-filters*='${{filterKey}}']`);
|
|
|
|
modalTitle.textContent = `${{filterTitle}} (${{employeeRows.length}})`;
|
|
modalTableBody.innerHTML = '';
|
|
|
|
employeeRows.forEach(row => {{
|
|
const clonedRow = row.cloneNode(true);
|
|
modalTableBody.appendChild(clonedRow);
|
|
}});
|
|
|
|
modal.style.display = 'flex';
|
|
}});
|
|
}});
|
|
|
|
function closeModal() {{
|
|
modal.style.display = 'none';
|
|
}}
|
|
|
|
closeModalBtn.addEventListener('click', closeModal);
|
|
|
|
modal.addEventListener('click', (event) => {{
|
|
if (event.target === modal) {{
|
|
closeModal();
|
|
}}
|
|
}});
|
|
}});
|
|
</script>
|
|
</body></html>
|
|
"""
|
|
|
|
with open('dashboard_interativo_final.html', 'w', encoding='utf-8') as f:
|
|
f.write(html_content)
|
|
print("Dashboard 'dashboard_interativo_final.html' com KPIs interativos e contagens corrigidas foi gerado com sucesso!")
|
|
|
|
except (FileNotFoundError, ValueError) as e:
|
|
print(f"ERRO: {e}")
|
|
except Exception as e:
|
|
print(f"Ocorreu um erro inesperado: {e}") |