| 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') |
|
|
| |
| |
| |
| 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' |
| ] |
|
|
| |
| 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' |
| ) |
|
|
| |
| 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') |
|
|
| |
| df_raw['COLABORADOR'] = df_raw['COLABORADOR'].str.strip().str.upper() |
| df_raw['COLABORADOR'] = df_raw['COLABORADOR'].replace({'STEPHANIE FARIA': 'STEPHANIE FARIA'}) |
|
|
| |
| 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_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']) |
|
|
| |
| |
| |
| 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), |
| ), |
| ) |
|
|
| |
| |
| |
| 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) |
|
|
| |
| |
| |
| 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, |
| ) |
|
|
| |
| meses_sorted = sorted(df_raw['MES_ADJ'].dropna().unique()) |
| colaboradores = sorted(df_raw['COLABORADOR'].dropna().unique()) |
| tipos = sorted(df_raw['TIPO'].dropna().unique()) |
|
|
| |
| |
| |
| app.layout = html.Div([ |
|
|
| |
| 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', |
| }), |
|
|
| |
| 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', |
| }), |
|
|
| |
| html.Div(id='kpi-row', style={'marginBottom': '16px'}), |
|
|
| |
| 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 |
| ), |
| ]), |
|
|
| |
| 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 |
| ), |
| ]), |
|
|
| |
| 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 |
| ), |
| ]), |
|
|
| |
| 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 |
| ), |
| ]), |
|
|
| |
| 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 |
| ), |
| ]), |
|
|
| |
| 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'], |
| }) |
|
|
| |
| |
| |
| 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='<b>%{x}</b><br>%{y} projetos<extra></extra>', |
| )) |
| 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='<b>%{x}</b><br>%{y} projetos<extra></extra>', |
| )) |
| 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='<b>%{label}</b><br>%{value} projetos (%{percent})<extra></extra>', |
| )) |
| 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"<b>{len(df)}</b><br><span style='font-size:10px'>total</span>", |
| 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='<b>%{y}</b><br>%{x} projetos<extra></extra>', |
| )) |
| 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='<b>%{label}</b><br>%{value} projetos<extra></extra>', |
| )) |
| 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='<b>%{label}</b><br>%{value} projetos<extra></extra>', |
| )) |
| 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='<b>%{x}</b><br>' + s + ': %{y}<extra></extra>', |
| )) |
| 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='<b>%{y}</b><br>Γndice Qualidade: %{x:.1f}%<extra></extra>', |
| )) |
| 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<extra></extra>', |
| )) |
| |
| 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='<b>%{x} versΓ£o(Γ΅es)</b><br>%{y} projetos<extra></extra>', |
| )) |
| 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='<b>%{x} revisΓ£o(Γ΅es)</b><br>%{y} projetos<extra></extra>', |
| )) |
| 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='<b>%{x}</b><br>Erros Visuais: %{y:.1f}%<extra></extra>', |
| )) |
| 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='<b>%{x}</b><br>Erros Macro: %{y:.1f}%<extra></extra>', |
| )) |
| 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<extra></extra>', |
| )) |
| 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 |
|
|
|
|
| |
| |
| |
|
|
| |
| server = app.server |
|
|
| if __name__ == '__main__': |
| app.run(host='0.0.0.0', port=7860, debug=False) |
|
|