gestao-risco-inventario-pralog/scripts/inventario.py

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">&times;</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}")