Spaces:
Running
Running
| """Cálculo de métricas de Tarefas (Task). | |
| Todos os indicadores são calculados em Python a partir de DataFrames pandas. | |
| A função principal é :func:`calculate_task_metrics`. | |
| """ | |
| from __future__ import annotations | |
| from datetime import date, datetime | |
| from typing import Any | |
| import pandas as pd | |
| from .comparison_metrics import aplicar_comparacoes | |
| from ..utils.date_utils import agora_tz | |
| from ..utils.validators import arredondar, coluna_existe, is_dataframe_vazio | |
| # Prefixos de Id do Salesforce usados para classificar o vínculo da tarefa. | |
| PREFIXO_ID_LEAD = "00Q" # WhoId de Lead | |
| PREFIXO_ID_OPPORTUNITY = "006" # WhatId de Opportunity | |
| # Chaves numéricas comparáveis contra histórico. | |
| _CHAVES_COMPARAVEIS = [ | |
| "tasks_created", | |
| "tasks_completed", | |
| "tasks_overdue", | |
| "tasks_future", | |
| "overdue_tasks_linked_to_leads", | |
| "overdue_tasks_linked_to_opportunities", | |
| "avg_overdue_delay_days", | |
| ] | |
| def _eh_concluida(df: pd.DataFrame) -> pd.Series: | |
| """Série booleana indicando tarefas concluídas (IsClosed ou Status).""" | |
| if coluna_existe(df, "IsClosed"): | |
| return df["IsClosed"].fillna(False).astype(bool) | |
| if coluna_existe(df, "Status"): | |
| return df["Status"].astype(str).str.lower().eq("completed") | |
| return pd.Series([False] * len(df), index=df.index) | |
| def _contagem_por_responsavel(df: pd.DataFrame) -> dict[str, int]: | |
| """Conta tarefas por ``OwnerId`` (responsável).""" | |
| if not coluna_existe(df, "OwnerId"): | |
| return {} | |
| contagem = df["OwnerId"].fillna("Sem responsável").value_counts() | |
| return {str(k): int(v) for k, v in contagem.items()} | |
| def _atraso_medio_dias(df: pd.DataFrame, referencia: datetime) -> float | None: | |
| """Atraso médio (em dias) das tarefas vencidas, com base em ActivityDate. | |
| As datas são interpretadas em UTC para evitar erro de soma entre datas | |
| com e sem timezone (o extrator normaliza datas para tz-aware). | |
| """ | |
| if is_dataframe_vazio(df) or not coluna_existe(df, "ActivityDate"): | |
| return None | |
| datas = pd.to_datetime(df["ActivityDate"], errors="coerce", utc=True) | |
| ref = pd.Timestamp(referencia.date(), tz="UTC") | |
| atraso = (ref - datas).dt.total_seconds() / 86400.0 | |
| validos = atraso[atraso.notna() & (atraso >= 0)] | |
| if validos.empty: | |
| return None | |
| return arredondar(float(validos.mean()), 2) | |
| def _vinculadas_por_prefixo(df: pd.DataFrame, coluna: str, prefixo: str) -> int: | |
| """Conta registros cujo Id na coluna começa com o prefixo informado.""" | |
| if not coluna_existe(df, coluna): | |
| return 0 | |
| serie = df[coluna].dropna().astype(str) | |
| return int(serie.str.startswith(prefixo).sum()) | |
| def calculate_task_metrics( | |
| tasks_created_df: pd.DataFrame, | |
| tasks_overdue_df: pd.DataFrame, | |
| tasks_future_df: pd.DataFrame, | |
| previous_metrics: dict[str, Any] | None = None, | |
| seven_day_average: dict[str, float] | None = None, | |
| reference_date: date | None = None, | |
| timezone: str = "America/Sao_Paulo", | |
| ) -> dict[str, Any]: | |
| """Calcula as métricas diárias de Tarefas. | |
| Args: | |
| tasks_created_df: Tarefas criadas no dia. | |
| tasks_overdue_df: Tarefas vencidas e ainda abertas. | |
| tasks_future_df: Tarefas abertas com data futura (próximas atividades). | |
| previous_metrics: Métricas do dia anterior (para variação). | |
| seven_day_average: Média de 7 dias por métrica (para variação). | |
| reference_date: Data de referência (apenas informativa). | |
| timezone: Fuso para cálculo de atraso. | |
| Returns: | |
| Dicionário de métricas de Tarefas, incluindo comparações. | |
| """ | |
| agora = agora_tz(timezone) | |
| tasks_created = 0 if is_dataframe_vazio(tasks_created_df) else int(len(tasks_created_df)) | |
| tasks_overdue = 0 if is_dataframe_vazio(tasks_overdue_df) else int(len(tasks_overdue_df)) | |
| tasks_future = 0 if is_dataframe_vazio(tasks_future_df) else int(len(tasks_future_df)) | |
| # --- Concluídas entre as criadas --- | |
| tasks_completed = 0 | |
| if not is_dataframe_vazio(tasks_created_df): | |
| tasks_completed = int(_eh_concluida(tasks_created_df).sum()) | |
| # --- Por responsável (criadas e vencidas) --- | |
| tasks_by_owner = _contagem_por_responsavel(tasks_created_df) if not is_dataframe_vazio( | |
| tasks_created_df | |
| ) else {} | |
| overdue_by_owner = _contagem_por_responsavel(tasks_overdue_df) if not is_dataframe_vazio( | |
| tasks_overdue_df | |
| ) else {} | |
| # --- Vencidas ligadas a Lead / Opportunity --- | |
| overdue_linked_leads = _vinculadas_por_prefixo( | |
| tasks_overdue_df, "WhoId", PREFIXO_ID_LEAD | |
| ) | |
| overdue_linked_opps = _vinculadas_por_prefixo( | |
| tasks_overdue_df, "WhatId", PREFIXO_ID_OPPORTUNITY | |
| ) | |
| # --- Atraso médio das vencidas --- | |
| avg_overdue_delay = _atraso_medio_dias(tasks_overdue_df, agora) | |
| # --- Responsável com mais tarefas vencidas (auxílio ao motor de risco) --- | |
| top_overdue_owner = None | |
| top_overdue_owner_count = 0 | |
| if overdue_by_owner: | |
| top_overdue_owner = max(overdue_by_owner, key=overdue_by_owner.get) | |
| top_overdue_owner_count = int(overdue_by_owner[top_overdue_owner]) | |
| metricas: dict[str, Any] = { | |
| "tasks_created": tasks_created, | |
| "tasks_completed": tasks_completed, | |
| "tasks_overdue": tasks_overdue, | |
| "tasks_future": tasks_future, | |
| "completion_rate": 0.0, | |
| "overdue_tasks_linked_to_leads": overdue_linked_leads, | |
| "overdue_tasks_linked_to_opportunities": overdue_linked_opps, | |
| "avg_overdue_delay_days": avg_overdue_delay, | |
| "tasks_by_owner": tasks_by_owner, | |
| "overdue_by_owner": overdue_by_owner, | |
| "top_overdue_owner": top_overdue_owner, | |
| "top_overdue_owner_count": top_overdue_owner_count, | |
| } | |
| # Taxa de conclusão sobre o que foi criado no dia. | |
| if tasks_created: | |
| metricas["completion_rate"] = round(tasks_completed / tasks_created * 100.0, 2) | |
| # --- Comparações históricas --- | |
| comparacoes = aplicar_comparacoes( | |
| metricas, previous_metrics, seven_day_average, _CHAVES_COMPARAVEIS | |
| ) | |
| metricas["variation_vs_previous_day"] = { | |
| k: v["variation_vs_previous"] for k, v in comparacoes.items() | |
| } | |
| metricas["variation_vs_7day_average"] = { | |
| k: v["variation_vs_7day_avg"] for k, v in comparacoes.items() | |
| } | |
| metricas["comparisons"] = comparacoes | |
| return metricas | |