import pandas as pd
import numpy as np
import plotly.graph_objects as go
import plotly.express as px
from dash import Dash, dcc, html, Input, Output, callback
import dash_bootstrap_components as dbc
import warnings
warnings.filterwarnings('ignore')
# ─────────────────────────────────────────────────────────────────────────────
# 1. CARREGAMENTO E PREPARAÇÃO DOS DADOS
# ─────────────────────────────────────────────────────────────────────────────
df_raw = pd.read_csv(
'tarefasss_datas_corrigidas_final.csv',
sep=';', encoding='latin-1'
)
df_raw = df_raw[df_raw.iloc[:, 0].notna()].copy()
df_raw.columns = [
'SUB_CIP','PROJETO','DESIGNACAO','TIPO','RB_STATUS','COLABORADOR','OBSERVACOES',
'DATA_ADJ_CLIENTE','DATA_LIMITE_SLA','TEMPO_EXECUCAO_DIAS','PCT_SLA',
'DATA_ENTREGA_V0','DATA_1_REENTREGA_V1','DATA_2_REENTREGA_V2','DATA_3_REENTREGA_V3',
'DATA_VALIDACAO','QTD_VERSOES','TAXA_REJEICAO_VERSOES','TAXA_ACEITACAO_VERSOES',
'QTD_REVISOES','INDICE_QUALIDADE','COLABORADOR_PROD_V0','DATA_INICIO_PROD_V0',
'DATA_FIM_PROD_V0','COLABORADOR_QC_V0','DATA_INICIO_QC_V0','DATA_FIM_QC_V0',
'PCT_ERROS_VISUAIS_V0','PCT_ERROS_MACRO_V0','TAXA_APROVACAO_INTERNA_V0',
'COLABORADOR_PROD_V1','DATA_INICIO_PROD_V1','DATA_FIM_PROD_V1','COLABORADOR_QC_V1',
'DATA_INICIO_QC_V1','DATA_FIM_QC_V1','PCT_ERROS_VISUAIS_V1','PCT_ERROS_MACRO_V1',
'TAXA_ERRO_GLOBAL_V1','COLABORADOR_PROD_V2','DATA_INICIO_PROD_V2','DATA_FIM_PROD_V2',
'COLABORADOR_QC_V2','DATA_INICIO_QC_V2','DATA_FIM_QC_V2','PCT_ERROS_VISUAIS_V2',
'PCT_ERROS_MACRO_V2','TAXA_ERRO_GLOBAL_V2','RESPOSTA_CLIENTE','DATA_RESPOSTA'
]
# Conversões numéricas
for col in ['TEMPO_EXECUCAO_DIAS','QTD_VERSOES','QTD_REVISOES']:
df_raw[col] = pd.to_numeric(
df_raw[col].astype(str).str.replace(',', '.').str.strip(),
errors='coerce'
)
for col in ['PCT_SLA','INDICE_QUALIDADE','TAXA_REJEICAO_VERSOES','TAXA_ACEITACAO_VERSOES',
'PCT_ERROS_VISUAIS_V0','PCT_ERROS_MACRO_V0','TAXA_APROVACAO_INTERNA_V0']:
df_raw[col] = pd.to_numeric(
df_raw[col].astype(str).str.replace('%','').str.replace(',','.').str.strip(),
errors='coerce'
)
# Datas
for col in ['DATA_ADJ_CLIENTE','DATA_LIMITE_SLA','DATA_ENTREGA_V0','DATA_VALIDACAO']:
df_raw[col] = pd.to_datetime(df_raw[col], dayfirst=True, errors='coerce')
df_raw['MES_ADJ'] = df_raw['DATA_ADJ_CLIENTE'].dt.to_period('M').astype(str)
df_raw['MES_LABEL'] = df_raw['DATA_ADJ_CLIENTE'].dt.strftime('%b %Y')
# Normalizar colaborador
df_raw['COLABORADOR'] = df_raw['COLABORADOR'].str.strip().str.upper()
df_raw['COLABORADOR'] = df_raw['COLABORADOR'].replace({'STEPHANIE FARIA': 'STEPHANIE FARIA'})
# Classificação SLA
df_raw['SLA_OK'] = df_raw['PCT_SLA'].apply(
lambda x: 'Dentro do SLA' if pd.notna(x) and x <= 100 else ('Fora do SLA' if pd.notna(x) else 'N/D')
)
# Status simplificado para agrupamento
status_map = {
'08 PROJETO VALIDADO': 'Validado',
'13 FATURADO': 'Faturado',
'07 VALIDAÇO ORANGE': 'Validação Orange',
'12 CANCELADO': 'Cancelado',
'09 TRABALHOS EM CURSO': 'Em Curso',
'11.1 AGUARDA RT': 'Aguarda RT',
'03 POR INICIAR CQ': 'Por Iniciar CQ',
'02.1 PROJETO PENDENTE CLIENTE': 'Pendente Cliente',
'03.3 CQ SOGETREL': 'CQ Sogetrel',
'06.4 AGUARDA PMV+DT': 'Aguarda PMV+DT',
'06.2 AGUARDA DEVIS': 'Aguarda Devis',
'02.3 PROJETO EM CURSO': 'Projeto em Curso',
}
df_raw['STATUS_LABEL'] = df_raw['RB_STATUS'].map(status_map).fillna(df_raw['RB_STATUS'])
# ─────────────────────────────────────────────────────────────────────────────
# 2. PALETA E TEMA
# ─────────────────────────────────────────────────────────────────────────────
COLORS = {
'bg': '#0D1117',
'card': '#161B22',
'card2': '#1C2128',
'border': '#30363D',
'primary': '#2563EB',
'accent': '#3B82F6',
'success': '#10B981',
'warning': '#F59E0B',
'danger': '#EF4444',
'text': '#E6EDF3',
'muted': '#8B949E',
'gold': '#F59E0B',
'purple': '#8B5CF6',
'teal': '#14B8A6',
}
STATUS_COLORS = {
'Validado': '#10B981',
'Faturado': '#2563EB',
'Validação Orange': '#F59E0B',
'Cancelado': '#EF4444',
'Em Curso': '#8B5CF6',
'Aguarda RT': '#14B8A6',
'Por Iniciar CQ': '#6366F1',
'Pendente Cliente': '#F97316',
'CQ Sogetrel': '#EC4899',
'Aguarda PMV+DT': '#84CC16',
'Aguarda Devis': '#06B6D4',
'Projeto em Curso': '#A78BFA',
}
PLOTLY_LAYOUT = dict(
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
font=dict(family='Inter, Segoe UI, sans-serif', color=COLORS['text'], size=12),
margin=dict(l=10, r=10, t=30, b=10),
legend=dict(
bgcolor='rgba(22,27,34,0.8)',
bordercolor=COLORS['border'],
borderwidth=1,
font=dict(size=11),
),
xaxis=dict(
gridcolor=COLORS['border'], gridwidth=0.5,
linecolor=COLORS['border'], tickfont=dict(size=11),
),
yaxis=dict(
gridcolor=COLORS['border'], gridwidth=0.5,
linecolor=COLORS['border'], tickfont=dict(size=11),
),
)
# ─────────────────────────────────────────────────────────────────────────────
# 3. HELPERS
# ─────────────────────────────────────────────────────────────────────────────
def kpi_card(title, value, subtitle=None, color=COLORS['primary'], icon='📊'):
return dbc.Col(
html.Div([
html.Div([
html.Span(icon, style={'fontSize': '22px', 'marginRight': '8px'}),
html.Span(title, style={
'fontSize': '11px', 'fontWeight': '600',
'color': COLORS['muted'], 'textTransform': 'uppercase',
'letterSpacing': '0.08em'
}),
], style={'display': 'flex', 'alignItems': 'center', 'marginBottom': '8px'}),
html.Div(value, style={
'fontSize': '32px', 'fontWeight': '700',
'color': color, 'lineHeight': '1',
}),
html.Div(subtitle or '', style={
'fontSize': '11px', 'color': COLORS['muted'], 'marginTop': '6px'
}),
], style={
'background': COLORS['card'],
'border': f'1px solid {COLORS["border"]}',
'borderTop': f'3px solid {color}',
'borderRadius': '8px',
'padding': '18px 20px',
'height': '100%',
}),
xs=12, sm=6, md=3,
)
def section_title(text):
return html.Div(text, style={
'fontSize': '13px', 'fontWeight': '700',
'color': COLORS['muted'], 'textTransform': 'uppercase',
'letterSpacing': '0.1em', 'marginBottom': '12px',
'paddingBottom': '8px', 'borderBottom': f'1px solid {COLORS["border"]}',
})
def chart_card(children, title=None, height=None):
style = {
'background': COLORS['card'],
'border': f'1px solid {COLORS["border"]}',
'borderRadius': '8px',
'padding': '16px',
'marginBottom': '16px',
}
if height:
style['height'] = height
content = []
if title:
content.append(html.Div(title, style={
'fontSize': '13px', 'fontWeight': '600',
'color': COLORS['text'], 'marginBottom': '12px',
}))
content.append(children)
return html.Div(content, style=style)
# ─────────────────────────────────────────────────────────────────────────────
# 4. APP
# ─────────────────────────────────────────────────────────────────────────────
app = Dash(
__name__,
external_stylesheets=[
dbc.themes.BOOTSTRAP,
'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap',
],
title='Dashboard Executivo — Gestão de Projetos',
suppress_callback_exceptions=True,
)
# ── Filtros disponíveis ───────────────────────────────────────────────────────
meses_sorted = sorted(df_raw['MES_ADJ'].dropna().unique())
colaboradores = sorted(df_raw['COLABORADOR'].dropna().unique())
tipos = sorted(df_raw['TIPO'].dropna().unique())
# ─────────────────────────────────────────────────────────────────────────────
# 5. LAYOUT
# ─────────────────────────────────────────────────────────────────────────────
app.layout = html.Div([
# ── HEADER ────────────────────────────────────────────────────────────────
html.Div([
dbc.Container([
dbc.Row([
dbc.Col([
html.Div([
html.Span('⬡', style={'color': COLORS['primary'], 'fontSize': '24px', 'marginRight': '10px'}),
html.Span('GESTÃO DE PROJETOS', style={
'fontSize': '18px', 'fontWeight': '700',
'color': COLORS['text'], 'letterSpacing': '0.05em',
}),
html.Span(' · Dashboard Executivo', style={
'fontSize': '14px', 'color': COLORS['muted'],
'marginLeft': '8px',
}),
], style={'display': 'flex', 'alignItems': 'center'}),
], md=8),
dbc.Col([
html.Div('Set 2025 – Fev 2026', style={
'textAlign': 'right', 'color': COLORS['muted'],
'fontSize': '12px', 'paddingTop': '4px',
}),
], md=4),
], align='center'),
], fluid=True),
], style={
'background': COLORS['card'],
'borderBottom': f'1px solid {COLORS["border"]}',
'padding': '14px 0',
'position': 'sticky', 'top': '0', 'zIndex': '1000',
}),
# ── FILTROS ───────────────────────────────────────────────────────────────
dbc.Container([
html.Div([
dbc.Row([
dbc.Col([
html.Label('Período', style={'fontSize': '11px', 'color': COLORS['muted'], 'fontWeight': '600'}),
dcc.Dropdown(
id='filter-mes',
options=[{'label': m, 'value': m} for m in meses_sorted],
multi=True, placeholder='Todos os meses',
style={'fontSize': '12px'},
className='dark-dropdown',
),
], md=3),
dbc.Col([
html.Label('Colaborador', style={'fontSize': '11px', 'color': COLORS['muted'], 'fontWeight': '600'}),
dcc.Dropdown(
id='filter-colab',
options=[{'label': c, 'value': c} for c in colaboradores],
multi=True, placeholder='Todos',
style={'fontSize': '12px'},
),
], md=3),
dbc.Col([
html.Label('Tipo de Projeto', style={'fontSize': '11px', 'color': COLORS['muted'], 'fontWeight': '600'}),
dcc.Dropdown(
id='filter-tipo',
options=[{'label': t, 'value': t} for t in tipos],
multi=True, placeholder='Todos',
style={'fontSize': '12px'},
),
], md=3),
dbc.Col([
html.Label('Status', style={'fontSize': '11px', 'color': COLORS['muted'], 'fontWeight': '600'}),
dcc.Dropdown(
id='filter-status',
options=[{'label': s, 'value': s} for s in sorted(df_raw['STATUS_LABEL'].unique())],
multi=True, placeholder='Todos',
style={'fontSize': '12px'},
),
], md=3),
]),
], style={
'background': COLORS['card2'],
'border': f'1px solid {COLORS["border"]}',
'borderRadius': '8px',
'padding': '14px 16px',
'marginTop': '16px',
'marginBottom': '16px',
}),
# ── KPIs ──────────────────────────────────────────────────────────────
html.Div(id='kpi-row', style={'marginBottom': '16px'}),
# ── LINHA 1: Evolução mensal + Distribuição por Status ────────────────
dbc.Row([
dbc.Col(
chart_card(
dcc.Graph(id='chart-evolucao', config={'displayModeBar': False},
style={'height': '300px'}),
title='Evolução Mensal de Projetos Adjudicados'
),
md=7
),
dbc.Col(
chart_card(
dcc.Graph(id='chart-status', config={'displayModeBar': False},
style={'height': '300px'}),
title='Distribuição por Status'
),
md=5
),
]),
# ── LINHA 2: Distribuição por Tipo + Resposta Cliente ─────────────────
dbc.Row([
dbc.Col(
chart_card(
dcc.Graph(id='chart-tipo', config={'displayModeBar': False},
style={'height': '300px'}),
title='Projetos por Tipo'
),
md=5
),
dbc.Col(
chart_card(
dcc.Graph(id='chart-resposta', config={'displayModeBar': False},
style={'height': '300px'}),
title='Resposta do Cliente (OK vs NOK)'
),
md=3
),
dbc.Col(
chart_card(
dcc.Graph(id='chart-sla', config={'displayModeBar': False},
style={'height': '300px'}),
title='Cumprimento de SLA'
),
md=4
),
]),
# ── LINHA 3: Performance Colaboradores ────────────────────────────────
dbc.Row([
dbc.Col(
chart_card(
dcc.Graph(id='chart-colab-volume', config={'displayModeBar': False},
style={'height': '320px'}),
title='Volume de Projetos por Colaborador'
),
md=6
),
dbc.Col(
chart_card(
dcc.Graph(id='chart-colab-qualidade', config={'displayModeBar': False},
style={'height': '320px'}),
title='Índice de Qualidade Médio por Colaborador'
),
md=6
),
]),
# ── LINHA 4: Tempo Execução + Versões ─────────────────────────────────
dbc.Row([
dbc.Col(
chart_card(
dcc.Graph(id='chart-tempo', config={'displayModeBar': False},
style={'height': '300px'}),
title='Distribuição do Tempo de Execução (dias)'
),
md=6
),
dbc.Col(
chart_card(
dcc.Graph(id='chart-versoes', config={'displayModeBar': False},
style={'height': '300px'}),
title='Nº de Versões por Projeto'
),
md=3
),
dbc.Col(
chart_card(
dcc.Graph(id='chart-revisoes', config={'displayModeBar': False},
style={'height': '300px'}),
title='Nº de Revisões por Projeto'
),
md=3
),
]),
# ── LINHA 5: Erros QC ─────────────────────────────────────────────────
dbc.Row([
dbc.Col(
chart_card(
dcc.Graph(id='chart-erros', config={'displayModeBar': False},
style={'height': '300px'}),
title='Taxa de Erros QC — Visuais vs Macro (V0)'
),
md=6
),
dbc.Col(
chart_card(
dcc.Graph(id='chart-qualidade-hist', config={'displayModeBar': False},
style={'height': '300px'}),
title='Distribuição do Índice de Qualidade'
),
md=6
),
]),
# ── RODAPÉ ────────────────────────────────────────────────────────────
html.Div([
html.Span('Dashboard Executivo · Gestão de Projetos', style={'color': COLORS['muted'], 'fontSize': '11px'}),
html.Span(' · Dados: Set 2025 – Fev 2026', style={'color': COLORS['border'], 'fontSize': '11px'}),
], style={
'textAlign': 'center', 'padding': '24px 0 16px',
'borderTop': f'1px solid {COLORS["border"]}',
'marginTop': '8px',
}),
], fluid=True, style={'maxWidth': '1400px'}),
], style={
'background': COLORS['bg'],
'minHeight': '100vh',
'fontFamily': 'Inter, Segoe UI, sans-serif',
'color': COLORS['text'],
})
# ─────────────────────────────────────────────────────────────────────────────
# 6. CALLBACKS
# ─────────────────────────────────────────────────────────────────────────────
def filter_df(mes, colab, tipo, status):
df = df_raw.copy()
if mes:
df = df[df['MES_ADJ'].isin(mes)]
if colab:
df = df[df['COLABORADOR'].isin(colab)]
if tipo:
df = df[df['TIPO'].isin(tipo)]
if status:
df = df[df['STATUS_LABEL'].isin(status)]
return df
@callback(
Output('kpi-row', 'children'),
Input('filter-mes', 'value'),
Input('filter-colab', 'value'),
Input('filter-tipo', 'value'),
Input('filter-status', 'value'),
)
def update_kpis(mes, colab, tipo, status):
df = filter_df(mes, colab, tipo, status)
total = len(df)
validados = len(df[df['STATUS_LABEL'].isin(['Validado', 'Faturado', 'Validação Orange'])])
pct_valid = f"{validados/total*100:.0f}%" if total else "—"
sla_ok = len(df[df['SLA_OK'] == 'Dentro do SLA'])
pct_sla = f"{sla_ok/total*100:.0f}%" if total else "—"
iq_mean = df['INDICE_QUALIDADE'].mean()
iq_str = f"{iq_mean:.0f}%" if pd.notna(iq_mean) else "—"
tempo_med = df['TEMPO_EXECUCAO_DIAS'].median()
tempo_str = f"{tempo_med:.0f} dias" if pd.notna(tempo_med) else "—"
cancelados = len(df[df['STATUS_LABEL'] == 'Cancelado'])
resp_ok = len(df[df['RESPOSTA_CLIENTE'] == 'OK'])
resp_total = len(df[df['RESPOSTA_CLIENTE'].isin(['OK', 'NOK'])])
pct_resp = f"{resp_ok/resp_total*100:.0f}%" if resp_total else "—"
return dbc.Row([
kpi_card('Total de Projetos', f"{total:,}", 'no período selecionado', COLORS['primary'], '📁'),
kpi_card('Taxa de Conclusão', pct_valid, f'{validados} projetos concluídos', COLORS['success'], '✅'),
kpi_card('Cumprimento SLA', pct_sla, f'{sla_ok} dentro do prazo', COLORS['teal'], '⏱'),
kpi_card('Índice de Qualidade', iq_str, 'média dos projetos avaliados', COLORS['gold'], '⭐'),
kpi_card('Tempo Mediano', tempo_str, 'tempo de execução (dias)', COLORS['purple'], '📅'),
kpi_card('Aprovação Cliente', pct_resp, f'{resp_ok} OK de {resp_total} avaliados', COLORS['accent'], '👤'),
kpi_card('Cancelamentos', f"{cancelados}", 'projetos cancelados', COLORS['danger'], '❌'),
kpi_card('Colaboradores Ativos', str(df['COLABORADOR'].nunique()), 'no período', COLORS['muted'], '👥'),
], className='g-2 mb-3')
@callback(
Output('chart-evolucao', 'figure'),
Input('filter-mes', 'value'),
Input('filter-colab', 'value'),
Input('filter-tipo', 'value'),
Input('filter-status', 'value'),
)
def chart_evolucao(mes, colab, tipo, status):
df = filter_df(mes, colab, tipo, status)
monthly = df.groupby('MES_ADJ').size().reset_index(name='count')
monthly = monthly.sort_values('MES_ADJ')
fig = go.Figure()
fig.add_trace(go.Bar(
x=monthly['MES_ADJ'], y=monthly['count'],
marker_color=COLORS['primary'],
marker_line_width=0,
opacity=0.85,
name='Projetos',
hovertemplate='%{x}
%{y} projetos',
))
fig.add_trace(go.Scatter(
x=monthly['MES_ADJ'], y=monthly['count'],
mode='lines+markers',
line=dict(color=COLORS['accent'], width=2),
marker=dict(size=6, color=COLORS['accent']),
name='Tendência',
hovertemplate='%{x}
%{y} projetos',
))
fig.update_layout(**PLOTLY_LAYOUT, showlegend=True)
return fig
@callback(
Output('chart-status', 'figure'),
Input('filter-mes', 'value'),
Input('filter-colab', 'value'),
Input('filter-tipo', 'value'),
Input('filter-status', 'value'),
)
def chart_status(mes, colab, tipo, status):
df = filter_df(mes, colab, tipo, status)
counts = df['STATUS_LABEL'].value_counts().reset_index()
counts.columns = ['status', 'count']
colors = [STATUS_COLORS.get(s, COLORS['muted']) for s in counts['status']]
fig = go.Figure(go.Pie(
labels=counts['status'],
values=counts['count'],
hole=0.55,
marker=dict(colors=colors, line=dict(color=COLORS['bg'], width=2)),
textinfo='percent',
hovertemplate='%{label}
%{value} projetos (%{percent})',
))
layout = dict(**PLOTLY_LAYOUT)
layout['legend'] = dict(
orientation='v', x=1.02, y=0.5,
bgcolor='rgba(0,0,0,0)',
font=dict(size=10),
)
layout['annotations'] = [dict(
text=f"{len(df)}
total",
x=0.5, y=0.5, font_size=16, showarrow=False,
font=dict(color=COLORS['text']),
)]
fig.update_layout(**layout)
return fig
@callback(
Output('chart-tipo', 'figure'),
Input('filter-mes', 'value'),
Input('filter-colab', 'value'),
Input('filter-tipo', 'value'),
Input('filter-status', 'value'),
)
def chart_tipo(mes, colab, tipo, status):
df = filter_df(mes, colab, tipo, status)
counts = df['TIPO'].value_counts().reset_index()
counts.columns = ['tipo', 'count']
counts = counts.sort_values('count', ascending=True)
fig = go.Figure(go.Bar(
x=counts['count'], y=counts['tipo'],
orientation='h',
marker=dict(
color=counts['count'],
colorscale=[[0, '#1e3a5f'], [1, COLORS['primary']]],
line=dict(width=0),
),
hovertemplate='%{y}
%{x} projetos',
))
fig.update_layout(**PLOTLY_LAYOUT, showlegend=False)
fig.update_xaxes(title_text='')
fig.update_yaxes(title_text='')
return fig
@callback(
Output('chart-resposta', 'figure'),
Input('filter-mes', 'value'),
Input('filter-colab', 'value'),
Input('filter-tipo', 'value'),
Input('filter-status', 'value'),
)
def chart_resposta(mes, colab, tipo, status):
df = filter_df(mes, colab, tipo, status)
df_r = df[df['RESPOSTA_CLIENTE'].isin(['OK', 'NOK'])]
counts = df_r['RESPOSTA_CLIENTE'].value_counts().reset_index()
counts.columns = ['resp', 'count']
color_map = {'OK': COLORS['success'], 'NOK': COLORS['danger']}
colors = [color_map.get(r, COLORS['muted']) for r in counts['resp']]
fig = go.Figure(go.Pie(
labels=counts['resp'],
values=counts['count'],
hole=0.6,
marker=dict(colors=colors, line=dict(color=COLORS['bg'], width=3)),
textinfo='percent+label',
hovertemplate='%{label}
%{value} projetos',
))
fig.update_layout(**PLOTLY_LAYOUT, showlegend=False)
return fig
@callback(
Output('chart-sla', 'figure'),
Input('filter-mes', 'value'),
Input('filter-colab', 'value'),
Input('filter-tipo', 'value'),
Input('filter-status', 'value'),
)
def chart_sla(mes, colab, tipo, status):
df = filter_df(mes, colab, tipo, status)
df_s = df[df['SLA_OK'] != 'N/D']
counts = df_s['SLA_OK'].value_counts().reset_index()
counts.columns = ['sla', 'count']
color_map = {'Dentro do SLA': COLORS['success'], 'Fora do SLA': COLORS['danger']}
colors = [color_map.get(s, COLORS['muted']) for s in counts['sla']]
fig = go.Figure(go.Pie(
labels=counts['sla'],
values=counts['count'],
hole=0.6,
marker=dict(colors=colors, line=dict(color=COLORS['bg'], width=3)),
textinfo='percent+label',
hovertemplate='%{label}
%{value} projetos',
))
fig.update_layout(**PLOTLY_LAYOUT, showlegend=False)
return fig
@callback(
Output('chart-colab-volume', 'figure'),
Input('filter-mes', 'value'),
Input('filter-colab', 'value'),
Input('filter-tipo', 'value'),
Input('filter-status', 'value'),
)
def chart_colab_volume(mes, colab, tipo, status):
df = filter_df(mes, colab, tipo, status)
df_c = df[df['COLABORADOR'].notna()]
counts = df_c.groupby(['COLABORADOR', 'STATUS_LABEL']).size().reset_index(name='count')
top_colabs = df_c['COLABORADOR'].value_counts().head(8).index.tolist()
counts = counts[counts['COLABORADOR'].isin(top_colabs)]
fig = go.Figure()
for s, color in STATUS_COLORS.items():
sub = counts[counts['STATUS_LABEL'] == s]
if not sub.empty:
fig.add_trace(go.Bar(
x=sub['COLABORADOR'], y=sub['count'],
name=s, marker_color=color,
hovertemplate='%{x}
' + s + ': %{y}',
))
fig.update_layout(**PLOTLY_LAYOUT, barmode='stack', showlegend=True)
fig.update_xaxes(title_text='')
fig.update_yaxes(title_text='Projetos')
return fig
@callback(
Output('chart-colab-qualidade', 'figure'),
Input('filter-mes', 'value'),
Input('filter-colab', 'value'),
Input('filter-tipo', 'value'),
Input('filter-status', 'value'),
)
def chart_colab_qualidade(mes, colab, tipo, status):
df = filter_df(mes, colab, tipo, status)
df_q = df[df['COLABORADOR'].notna() & df['INDICE_QUALIDADE'].notna()]
if df_q.empty:
return go.Figure().update_layout(**PLOTLY_LAYOUT)
stats = df_q.groupby('COLABORADOR')['INDICE_QUALIDADE'].agg(['mean', 'count']).reset_index()
stats = stats[stats['count'] >= 2].sort_values('mean', ascending=True)
fig = go.Figure(go.Bar(
x=stats['mean'], y=stats['COLABORADOR'],
orientation='h',
marker=dict(
color=stats['mean'],
colorscale=[[0, '#7f1d1d'], [0.5, COLORS['warning']], [1, COLORS['success']]],
cmin=0, cmax=100,
line=dict(width=0),
),
text=[f"{v:.0f}%" for v in stats['mean']],
textposition='outside',
textfont=dict(size=11, color=COLORS['text']),
hovertemplate='%{y}
Índice Qualidade: %{x:.1f}%',
))
fig.update_layout(**PLOTLY_LAYOUT, showlegend=False)
fig.update_xaxes(title_text='Índice de Qualidade (%)', range=[0, 115])
fig.update_yaxes(title_text='')
return fig
@callback(
Output('chart-tempo', 'figure'),
Input('filter-mes', 'value'),
Input('filter-colab', 'value'),
Input('filter-tipo', 'value'),
Input('filter-status', 'value'),
)
def chart_tempo(mes, colab, tipo, status):
df = filter_df(mes, colab, tipo, status)
df_t = df['TEMPO_EXECUCAO_DIAS'].dropna()
if df_t.empty:
return go.Figure().update_layout(**PLOTLY_LAYOUT)
fig = go.Figure()
fig.add_trace(go.Histogram(
x=df_t,
nbinsx=20,
marker_color=COLORS['primary'],
marker_line_color=COLORS['bg'],
marker_line_width=1,
opacity=0.85,
name='Projetos',
hovertemplate='%{x} dias: %{y} projetos',
))
# Linha mediana
med = df_t.median()
fig.add_vline(x=med, line_dash='dash', line_color=COLORS['warning'],
annotation_text=f'Mediana: {med:.0f}d',
annotation_font_color=COLORS['warning'],
annotation_position='top right')
fig.update_layout(**PLOTLY_LAYOUT, showlegend=False)
fig.update_xaxes(title_text='Dias')
fig.update_yaxes(title_text='Nº Projetos')
return fig
@callback(
Output('chart-versoes', 'figure'),
Input('filter-mes', 'value'),
Input('filter-colab', 'value'),
Input('filter-tipo', 'value'),
Input('filter-status', 'value'),
)
def chart_versoes(mes, colab, tipo, status):
df = filter_df(mes, colab, tipo, status)
df_v = df['QTD_VERSOES'].dropna().astype(int)
counts = df_v.value_counts().sort_index().reset_index()
counts.columns = ['versoes', 'count']
fig = go.Figure(go.Bar(
x=counts['versoes'].astype(str),
y=counts['count'],
marker_color=[COLORS['success'], COLORS['warning'], COLORS['danger'], COLORS['purple']],
marker_line_width=0,
hovertemplate='%{x} versão(ões)
%{y} projetos',
))
fig.update_layout(**PLOTLY_LAYOUT, showlegend=False)
fig.update_xaxes(title_text='Nº Versões')
fig.update_yaxes(title_text='Projetos')
return fig
@callback(
Output('chart-revisoes', 'figure'),
Input('filter-mes', 'value'),
Input('filter-colab', 'value'),
Input('filter-tipo', 'value'),
Input('filter-status', 'value'),
)
def chart_revisoes(mes, colab, tipo, status):
df = filter_df(mes, colab, tipo, status)
df_r = df['QTD_REVISOES'].dropna().astype(int)
counts = df_r.value_counts().sort_index().reset_index()
counts.columns = ['revisoes', 'count']
palette = [COLORS['success'], COLORS['teal'], COLORS['warning'],
COLORS['danger'], COLORS['purple'], COLORS['accent'],
COLORS['gold'], COLORS['muted']]
colors = [palette[i % len(palette)] for i in range(len(counts))]
fig = go.Figure(go.Bar(
x=counts['revisoes'].astype(str),
y=counts['count'],
marker_color=colors,
marker_line_width=0,
hovertemplate='%{x} revisão(ões)
%{y} projetos',
))
fig.update_layout(**PLOTLY_LAYOUT, showlegend=False)
fig.update_xaxes(title_text='Nº Revisões')
fig.update_yaxes(title_text='Projetos')
return fig
@callback(
Output('chart-erros', 'figure'),
Input('filter-mes', 'value'),
Input('filter-colab', 'value'),
Input('filter-tipo', 'value'),
Input('filter-status', 'value'),
)
def chart_erros(mes, colab, tipo, status):
df = filter_df(mes, colab, tipo, status)
df_c = df[df['COLABORADOR_PROD_V0'].notna()]
df_c = df_c.groupby('COLABORADOR_PROD_V0').agg(
visuais=('PCT_ERROS_VISUAIS_V0', 'mean'),
macro=('PCT_ERROS_MACRO_V0', 'mean'),
).dropna(how='all').reset_index()
df_c = df_c.sort_values('visuais', ascending=False).head(8)
if df_c.empty:
return go.Figure().update_layout(**PLOTLY_LAYOUT)
fig = go.Figure()
fig.add_trace(go.Bar(
name='Erros Visuais (%)',
x=df_c['COLABORADOR_PROD_V0'],
y=df_c['visuais'],
marker_color=COLORS['warning'],
marker_line_width=0,
hovertemplate='%{x}
Erros Visuais: %{y:.1f}%',
))
fig.add_trace(go.Bar(
name='Erros Macro (%)',
x=df_c['COLABORADOR_PROD_V0'],
y=df_c['macro'],
marker_color=COLORS['danger'],
marker_line_width=0,
hovertemplate='%{x}
Erros Macro: %{y:.1f}%',
))
fig.update_layout(**PLOTLY_LAYOUT, barmode='group', showlegend=True)
fig.update_xaxes(title_text='')
fig.update_yaxes(title_text='Taxa de Erros (%)')
return fig
@callback(
Output('chart-qualidade-hist', 'figure'),
Input('filter-mes', 'value'),
Input('filter-colab', 'value'),
Input('filter-tipo', 'value'),
Input('filter-status', 'value'),
)
def chart_qualidade_hist(mes, colab, tipo, status):
df = filter_df(mes, colab, tipo, status)
df_q = df['INDICE_QUALIDADE'].dropna()
if df_q.empty:
return go.Figure().update_layout(**PLOTLY_LAYOUT)
fig = go.Figure()
fig.add_trace(go.Histogram(
x=df_q,
nbinsx=15,
marker=dict(
color=df_q,
colorscale=[[0, '#7f1d1d'], [0.5, COLORS['warning']], [1, COLORS['success']]],
cmin=0, cmax=100,
line=dict(color=COLORS['bg'], width=1),
),
hovertemplate='Índice %{x}%: %{y} projetos',
))
mean_q = df_q.mean()
fig.add_vline(x=mean_q, line_dash='dash', line_color=COLORS['accent'],
annotation_text=f'Média: {mean_q:.0f}%',
annotation_font_color=COLORS['accent'],
annotation_position='top left')
fig.update_layout(**PLOTLY_LAYOUT, showlegend=False)
fig.update_xaxes(title_text='Índice de Qualidade (%)')
fig.update_yaxes(title_text='Nº Projetos')
return fig
# ─────────────────────────────────────────────────────────────────────────────
# 7. ARRANQUE
# ─────────────────────────────────────────────────────────────────────────────
# Expor o servidor Flask subjacente — necessário para o gunicorn
server = app.server
if __name__ == '__main__':
app.run(host='0.0.0.0', port=7860, debug=False)