Import ClientFlow production v4928.1.5.132.4
This commit is contained in:
@@ -10,7 +10,7 @@ from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from app.operations_service import get_operations_summary
|
||||
from app.admin_ui.labels import primary_action_label
|
||||
from app.admin_ui.labels import primary_action_label, queue_label
|
||||
from app.admin_ui.guidance import item_requires_fiscal_customer, work_item_blockers
|
||||
|
||||
# Legacy label kept for regression context: Ambíguas.
|
||||
@@ -56,11 +56,20 @@ def operation_card_title(item: dict[str, Any]) -> str:
|
||||
if str(item.get("source") or "") == "outbox":
|
||||
return str(item.get("customer_name") or item.get("title") or "Integração")
|
||||
fiscal = str(item.get("fiscal_customer_name") or "").strip()
|
||||
contact = str(item.get("contact_display_name") or item.get("customer_name") or "").strip()
|
||||
contact = str(item.get("contact_display_name") or "").strip()
|
||||
customer = str(item.get("customer_name") or "").strip()
|
||||
process_hint = str(item.get("process_customer_hint") or "").strip()
|
||||
identity_status = str(item.get("contact_identity_status") or "").strip().lower()
|
||||
if fiscal and not _is_numeric_reference(fiscal) and not _looks_like_generic_label(fiscal):
|
||||
return fiscal
|
||||
if identity_status == "unsafe" and process_hint and not _is_numeric_reference(process_hint):
|
||||
return process_hint
|
||||
if contact and not _is_numeric_reference(contact) and not _looks_like_generic_label(contact):
|
||||
return contact
|
||||
if customer and not _is_numeric_reference(customer) and not _looks_like_generic_label(customer):
|
||||
return customer
|
||||
if process_hint and not _is_numeric_reference(process_hint):
|
||||
return process_hint
|
||||
return "Contacto sem identificação"
|
||||
|
||||
|
||||
@@ -111,6 +120,10 @@ def operation_card_detail(item: dict[str, Any]) -> str:
|
||||
|
||||
|
||||
def operation_status_chip(item: dict[str, Any]) -> str:
|
||||
if is_association_review(item):
|
||||
return "associação por confirmar"
|
||||
if str(item.get("action_code") or "").upper() == "REVIEW_RECONSTRUCTED_PROCESS":
|
||||
return "validação obrigatória"
|
||||
blockers = work_item_blockers(item)
|
||||
if blockers:
|
||||
return "bloqueada"
|
||||
@@ -121,27 +134,106 @@ def operation_status_chip(item: dict[str, Any]) -> str:
|
||||
return "normal"
|
||||
|
||||
|
||||
def is_association_review(item: dict[str, Any]) -> bool:
|
||||
linking = str(item.get("opportunity_linking_status") or "").lower()
|
||||
code = str(item.get("action_code") or "").upper()
|
||||
return linking in {"ambiguous", "review_required"} or code == "ASSOCIATE_OPPORTUNITY"
|
||||
|
||||
|
||||
def is_ambiguous(item: dict[str, Any]) -> bool:
|
||||
return str(item.get("opportunity_linking_status") or "").lower() == "ambiguous"
|
||||
return is_association_review(item)
|
||||
|
||||
|
||||
def _parse_dt(value: Any) -> datetime | None:
|
||||
if not value:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return None
|
||||
else:
|
||||
dt = value
|
||||
if getattr(dt, "tzinfo", None) is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt
|
||||
|
||||
|
||||
def is_scheduled_for_future(item: dict[str, Any]) -> bool:
|
||||
action_code = str(item.get("action_code") or "").upper()
|
||||
if not action_code.startswith("FOLLOW_UP_"):
|
||||
return False
|
||||
due_dt = _parse_dt(item.get("due_at"))
|
||||
return bool(due_dt and due_dt > datetime.now(timezone.utc))
|
||||
|
||||
|
||||
def is_overdue(item: dict[str, Any]) -> bool:
|
||||
created = item.get("created_at")
|
||||
if not created:
|
||||
due_dt = _parse_dt(item.get("due_at"))
|
||||
if due_dt is not None:
|
||||
return due_dt < datetime.now(timezone.utc)
|
||||
created_dt = _parse_dt(item.get("created_at"))
|
||||
if not created_dt:
|
||||
return False
|
||||
if isinstance(created, str):
|
||||
try:
|
||||
created_dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return False
|
||||
else:
|
||||
created_dt = created
|
||||
if created_dt.tzinfo is None:
|
||||
created_dt = created_dt.replace(tzinfo=timezone.utc)
|
||||
queue = str(item.get("queue") or "").lower()
|
||||
hours = {"suporte": 2, "vendas": 4, "financeiro": 8, "operacoes": 24, "logistica": 24, "rever": 24}.get(queue, 24)
|
||||
return (datetime.now(timezone.utc) - created_dt).total_seconds() > hours * 3600
|
||||
|
||||
def operation_due_label(item: dict[str, Any], *, now: datetime | None = None) -> str:
|
||||
now = now or datetime.now(timezone.utc)
|
||||
due_dt = _parse_dt(item.get("due_at"))
|
||||
if due_dt is not None:
|
||||
seconds = (due_dt - now).total_seconds()
|
||||
days = int(abs(seconds) // 86400)
|
||||
hours = max(1, int(abs(seconds) // 3600))
|
||||
if seconds < 0:
|
||||
if days >= 1:
|
||||
return f"Vencida há {days} dia(s)"
|
||||
return f"Vencida há {hours} h"
|
||||
if days >= 1:
|
||||
return f"Vence em {days} dia(s)"
|
||||
return f"Vence em {hours} h"
|
||||
created_dt = _parse_dt(item.get("created_at"))
|
||||
if created_dt is None:
|
||||
return "Sem prazo"
|
||||
age_seconds = max((now - created_dt).total_seconds(), 0)
|
||||
age_days = int(age_seconds // 86400)
|
||||
if age_days >= 1:
|
||||
return f"Criada há {age_days} dia(s)"
|
||||
return f"Criada há {max(1, int(age_seconds // 3600))} h"
|
||||
|
||||
|
||||
def operation_value(item: dict[str, Any]) -> float:
|
||||
for key in ("opportunity_value_amount", "amount", "total_amount"):
|
||||
try:
|
||||
value = float(item.get(key) or 0)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if value > 0:
|
||||
return value
|
||||
return 0.0
|
||||
|
||||
|
||||
def operation_queue_label(item: dict[str, Any]) -> str:
|
||||
return queue_label(item.get("queue"))
|
||||
|
||||
|
||||
def operation_priority_reason(item: dict[str, Any]) -> str:
|
||||
if is_association_review(item):
|
||||
return "Bloqueia a ligação segura do processo"
|
||||
if str(item.get("action_code") or "").upper() == "REVIEW_RECONSTRUCTED_PROCESS":
|
||||
return "Validação necessária antes de ação sensível"
|
||||
if is_overdue(item):
|
||||
return "Prazo ultrapassado"
|
||||
if str(item.get("source") or "").lower() == "outbox":
|
||||
return "Integração exige intervenção"
|
||||
if str(item.get("queue") or "").lower() == "suporte":
|
||||
return "Cliente aguarda resposta de suporte"
|
||||
value = operation_value(item)
|
||||
if value > 0 and str(item.get("queue") or "").lower() in {"financeiro", "vendas"}:
|
||||
return f"Impacto comercial/financeiro de {value:.2f} EUR"
|
||||
return "Prioridade definida pela fila operacional"
|
||||
|
||||
|
||||
def is_high_priority(item: dict[str, Any]) -> bool:
|
||||
status = str(item.get("status") or "").lower()
|
||||
source = str(item.get("source") or "").lower()
|
||||
@@ -159,7 +251,7 @@ def is_high_priority(item: dict[str, Any]) -> bool:
|
||||
return False
|
||||
if queue == "marketing" or action_code in {"REMOVE_FROM_LIST", "IGNORE_SPAM", "NO_ACTION", "REVIEW_MANUALLY"}:
|
||||
return False
|
||||
if action_code in {"SEND_PROFORMA", "SEND_INVOICE", "CONFIRM_PAYMENT", "PREPARE_ORDER", "CREATE_SHIPMENT"}:
|
||||
if action_code in {"SEND_PROFORMA", "SEND_INVOICE", "CONFIRM_PAYMENT", "PREPARE_ORDER", "VALIDATE_PHYSICAL_ORDER", "CREATE_SHIPMENT"}:
|
||||
return priority in {"alta", "high", "urgente", "critical"} or status in {"failed", "blocked"}
|
||||
return priority in {"alta", "high", "urgente"} or status in {"failed", "blocked"}
|
||||
|
||||
@@ -195,6 +287,13 @@ def operation_primary_label(item: dict[str, Any]) -> str:
|
||||
if str(item.get("source") or "") == "outbox":
|
||||
status = str(item.get("status") or "").lower()
|
||||
return "Reprocessar" if status == "failed" else "Ver bloqueio"
|
||||
if is_association_review(item):
|
||||
source = str(item.get("source_system") or "").strip().lower()
|
||||
if source == "jasmin":
|
||||
return "Validar associação do documento"
|
||||
if source == "odoo":
|
||||
return "Validar associação da venda"
|
||||
return "Validar associação"
|
||||
return primary_action_label(item.get("action_code") or item.get("action_label"), fallback="Abrir")
|
||||
|
||||
|
||||
@@ -218,7 +317,7 @@ def build_operations_view_model(scope: str = "all", *, limit: int = 30) -> dict[
|
||||
normal_items = [item for item in visible_items if not is_high_priority(item)]
|
||||
review_total = int(counts.get("review_tasks", 0) or 0) + int(counts.get("communications_needs_review", 0) or 0)
|
||||
blocked_total = int(counts.get("blocked_outbox", 0) or 0) + sum(1 for item in work_items if is_blocked(item))
|
||||
ambiguous_total = sum(1 for item in work_items if is_ambiguous(item))
|
||||
ambiguous_total = sum(1 for item in work_items if is_association_review(item))
|
||||
overdue_total = int(counts.get("overdue_tasks", 0) or 0) + sum(1 for item in work_items if item.get("source") == "outbox" and str(item.get("status") or "").lower() in {"failed", "stale"})
|
||||
return {
|
||||
"scope": scope,
|
||||
|
||||
Reference in New Issue
Block a user