diff --git a/admin_app/view/rendering.py b/admin_app/view/rendering.py
index 35f0a24..32f2927 100644
--- a/admin_app/view/rendering.py
+++ b/admin_app/view/rendering.py
@@ -1,6 +1,7 @@
from html import escape
from admin_app.view.view_models import (
+ AdminBotMonitoringPageView,
AdminCollaboratorManagementPageView,
AdminLoginPageView,
AdminPanelHomeView,
@@ -10,6 +11,9 @@ from admin_app.view.view_models import (
AdminPanelQuickAction,
AdminPanelRoadmapItem,
AdminPanelSurfaceLink,
+ AdminRentalReportsPageView,
+ AdminSalesRevenueReportsPageView,
+ AdminSystemConfigurationPageView,
AdminToolIntakePageView,
AdminToolIntakeParameterTypeOption,
AdminToolReviewPageView,
@@ -1237,3 +1241,511 @@ def _render_collaborator_cards(items: list[dict]) -> str:
for item in items
)
+
+
+def render_system_configuration_page(
+ view: AdminSystemConfigurationPageView,
+ *,
+ css_href: str,
+ js_href: str,
+) -> str:
+ access_notes_markup = _render_text_list(view.access_notes)
+ governance_notes_markup = _render_text_list(view.governance_notes)
+
+ return f'''
+
+
+
+
+ {escape(view.title)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Somente leitura
+ Sessao protegida
+
+
Configuracao funcional e regras do painel em uma unica tela
+
Aqui voce acompanha o que esta disponivel para consulta agora, sem expor detalhes internos demais do ambiente.
+
+
+
+
+
+
+
+
Configuracoes funcionais
0
Itens que o time pode acompanhar nesta etapa.
+
Campos governados pelo bot
0
Ajustes do atendimento sob controle do admin.
+
Perfis de runtime
0
Separacao entre atendimento e geracao de tools quando liberada.
+
Fontes de configuracao
0
Base usada para montar os dados desta tela.
+
+
+
Catalogo funcional
Configuracoes funcionais do sistema
Cada card resume o que a configuracao cobre e como ela impacta a operacao.
Contrato compartilhado
+
Governanca do bot
Campos sob governanca administrativa
Os ajustes do atendimento aparecem aqui de forma agrupada e clara.
Parent config keys
Aguardando
+
+
+
Runtime administrativo
Informacoes essenciais do painel
Mostra apenas o contexto util para a operacao.
+
Postura de seguranca
Regras visiveis de senha e sessao
Exibe somente o necessario para orientar o uso da sessao.
+
+
+
Separacao de runtime
Modelos do atendimento versus geracao de tools
Aqui fica clara a separacao entre atendimento e geracao de tools.
+
Fontes do snapshot
De onde cada configuracao vem
Resumo das bases usadas para montar esta tela.
+
+
+
+
+
+
+
+
+'''
+
+def render_sales_revenue_reports_page(
+ view: AdminSalesRevenueReportsPageView,
+ *,
+ css_href: str,
+ js_href: str,
+) -> str:
+ access_notes_markup = _render_text_list(view.access_notes)
+ reading_notes_markup = _render_text_list(view.reading_notes)
+
+ return f'''
+
+
+
+
+ {escape(view.title)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Leitura operacional
+ Fase 4
+
+
Vendas e arrecadacao na mesma visao do painel
+
Aqui o time acompanha os principais blocos comerciais de forma simples e organizada.
+
+
+
+
+
+
+
+
Relatorios de vendas
0
Estrutura inicial do dominio comercial.
+
Relatorios de arrecadacao
0
Leitura inicial dos recebimentos de locacao.
+
Bases de leitura
0
Bases consolidadas usadas para montar esta tela.
+
Atualizacao
--
Ritmo atual da carga exibida no painel.
+
+
+
+
+
+
+
Vendas
+
O que acompanhar em vendas
+
Volume de pedidos, ticket medio, cancelamentos e comparativos principais.
+
+
+
+
+
+
+
+
+
+
+
+
+
Arrecadacao
+
O que acompanhar em arrecadacao
+
Pagamentos liquidados, valor arrecadado e conciliacao por contrato.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+'''
+
+def render_rental_reports_page(
+ view: AdminRentalReportsPageView,
+ *,
+ css_href: str,
+ js_href: str,
+) -> str:
+ access_notes_markup = _render_text_list(view.access_notes)
+ reading_notes_markup = _render_text_list(view.reading_notes)
+
+ return f'''
+
+
+
+
+ {escape(view.title)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Leitura operacional
+ Fase 4
+
+
Visao inicial de locacao para frota e contratos
+
Acompanhe os principais blocos do dominio em uma leitura organizada do painel.
+
+
+
+
+
+
+
+
Relatorios
0
Temas iniciais que o time ja consegue acompanhar.
+
Bases de leitura
0
Bases consolidadas de frota e contratos.
+
Atualizacao
--
Ritmo atual da carga exibida no painel.
+
Area acompanhada
--
Dominio principal desta leitura.
+
+
+
+
+
+
+
Overview de locacao
+
Resumo inicial da operacao
+
Os indicadores mostram rapidamente a situacao atual de frota e contratos.
+
+
+
+
+
+
+
+
+
+
+
+
Relatorios desta etapa
+
Relatorios disponiveis nesta etapa
+
Disponibilidade de frota, lifecycle de contratos, devolucoes em atraso, ocupacao e receita prevista versus final.
+
+
+
+
+
+
+
+
+
+
+
+
+
+'''
+
+def render_bot_monitoring_page(
+ view: AdminBotMonitoringPageView,
+ *,
+ css_href: str,
+ js_href: str,
+) -> str:
+ access_notes_markup = _render_text_list(view.access_notes)
+ reading_notes_markup = _render_text_list(view.reading_notes)
+
+ return f'''
+
+
+
+
+ {escape(view.title)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Observabilidade operacional
+ Fase 4
+
+
Fluxo do bot e saude do atendimento na mesma tela
+
Acompanhe o basico da operacao e da telemetria sem entrar em detalhes de infraestrutura.
+
+
+
+
+
+
+
+
Relatorios de fluxo
0
Status, roteamento, tools, fallback e falhas do turno.
+
Relatorios de saude
0
Volume, latencia, distribuicao por dominio e saude do atendimento.
+
Bases de leitura
0
Base consolidada usada pelas duas visoes.
+
Atualizacao
--
Ritmo atual da carga exibida no painel.
+
+
+
+
+
+
+
Fluxo do bot
+
Triagem da operacao
+
Veja status, roteamento, uso de tools, fallback, handoff e falhas do turno.
+
+
+
+
+
+
+
+
+
+
+
+
+
Telemetria conversacional
+
Saude do atendimento
+
Volume, latencia, distribuicao por dominio e sinais de saude da conversa.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+'''
diff --git a/admin_app/view/router.py b/admin_app/view/router.py
index 78ab08a..7c1669b 100644
--- a/admin_app/view/router.py
+++ b/admin_app/view/router.py
@@ -6,13 +6,18 @@ from admin_app.core import AdminSettings, AuthenticatedStaffContext, get_admin_s
from admin_app.services import ToolManagementService
from admin_app.view.assets import PANEL_STATIC_MOUNT_NAME
from admin_app.view.rendering import (
+ render_bot_monitoring_page,
render_collaborator_management_page,
render_login_page,
render_panel_home,
+ render_rental_reports_page,
+ render_sales_revenue_reports_page,
+ render_system_configuration_page,
render_tool_intake_page,
render_tool_review_page,
)
from admin_app.view.view_models import (
+ AdminBotMonitoringPageView,
AdminCollaboratorManagementPageView,
AdminLoginPageView,
AdminPanelHomeView,
@@ -22,6 +27,9 @@ from admin_app.view.view_models import (
AdminPanelQuickAction,
AdminPanelRoadmapItem,
AdminPanelSurfaceLink,
+ AdminRentalReportsPageView,
+ AdminSalesRevenueReportsPageView,
+ AdminSystemConfigurationPageView,
AdminToolIntakeDomainOption,
AdminToolIntakePageView,
AdminToolIntakeParameterTypeOption,
@@ -119,6 +127,74 @@ def collaborator_management_page(
return HTMLResponse(render_collaborator_management_page(view, css_href=css_href, js_href=js_href))
+@panel_router.get("/panel/sistema/configuracoes", response_class=HTMLResponse, name="admin_system_configuration_view")
+def system_configuration_page(
+ request: Request,
+ current_context: AuthenticatedStaffContext | None = Depends(get_optional_panel_staff_context),
+) -> Response:
+ if current_context is None:
+ return _redirect_to_route(request, "admin_login_view")
+ if not role_has_permission(current_context.principal.role, AdminPermission.VIEW_SYSTEM):
+ return _redirect_to_route(request, "panel_home")
+
+ settings = _resolve_settings(request)
+ view = _build_system_configuration_view(request, settings)
+ css_href = str(request.url_for(PANEL_STATIC_MOUNT_NAME, path="styles/panel.css"))
+ js_href = str(request.url_for(PANEL_STATIC_MOUNT_NAME, path="scripts/panel.js"))
+ return HTMLResponse(render_system_configuration_page(view, css_href=css_href, js_href=js_href))
+
+
+@panel_router.get("/panel/relatorios/vendas-arrecadacao", response_class=HTMLResponse, name="admin_sales_revenue_reports_view")
+def sales_revenue_reports_page(
+ request: Request,
+ current_context: AuthenticatedStaffContext | None = Depends(get_optional_panel_staff_context),
+) -> Response:
+ if current_context is None:
+ return _redirect_to_route(request, "admin_login_view")
+ if not role_has_permission(current_context.principal.role, AdminPermission.VIEW_REPORTS):
+ return _redirect_to_route(request, "panel_home")
+
+ settings = _resolve_settings(request)
+ view = _build_sales_revenue_reports_view(request, settings)
+ css_href = str(request.url_for(PANEL_STATIC_MOUNT_NAME, path="styles/panel.css"))
+ js_href = str(request.url_for(PANEL_STATIC_MOUNT_NAME, path="scripts/panel.js"))
+ return HTMLResponse(render_sales_revenue_reports_page(view, css_href=css_href, js_href=js_href))
+
+
+@panel_router.get("/panel/relatorios/locacao", response_class=HTMLResponse, name="admin_rental_reports_view")
+def rental_reports_page(
+ request: Request,
+ current_context: AuthenticatedStaffContext | None = Depends(get_optional_panel_staff_context),
+) -> Response:
+ if current_context is None:
+ return _redirect_to_route(request, "admin_login_view")
+ if not role_has_permission(current_context.principal.role, AdminPermission.VIEW_REPORTS):
+ return _redirect_to_route(request, "panel_home")
+
+ settings = _resolve_settings(request)
+ view = _build_rental_reports_view(request, settings)
+ css_href = str(request.url_for(PANEL_STATIC_MOUNT_NAME, path="styles/panel.css"))
+ js_href = str(request.url_for(PANEL_STATIC_MOUNT_NAME, path="scripts/panel.js"))
+ return HTMLResponse(render_rental_reports_page(view, css_href=css_href, js_href=js_href))
+
+
+@panel_router.get("/panel/monitoramento/bot", response_class=HTMLResponse, name="admin_bot_monitoring_view")
+def bot_monitoring_page(
+ request: Request,
+ current_context: AuthenticatedStaffContext | None = Depends(get_optional_panel_staff_context),
+) -> Response:
+ if current_context is None:
+ return _redirect_to_route(request, "admin_login_view")
+ if not role_has_permission(current_context.principal.role, AdminPermission.VIEW_REPORTS):
+ return _redirect_to_route(request, "panel_home")
+
+ settings = _resolve_settings(request)
+ view = _build_bot_monitoring_view(request, settings)
+ css_href = str(request.url_for(PANEL_STATIC_MOUNT_NAME, path="styles/panel.css"))
+ js_href = str(request.url_for(PANEL_STATIC_MOUNT_NAME, path="scripts/panel.js"))
+ return HTMLResponse(render_bot_monitoring_page(view, css_href=css_href, js_href=js_href))
+
+
def _build_home_view(
request: Request,
settings: AdminSettings,
@@ -128,7 +204,10 @@ def _build_home_view(
tool_intake_view_href = str(request.url_for("admin_tool_intake_view"))
tool_review_view_href = str(request.url_for("admin_tool_review_view"))
collaborator_management_view_href = str(request.url_for("admin_collaborator_management_view"))
- system_configuration_href = _build_prefixed_path(settings.admin_api_prefix, "/system/configuration")
+ system_configuration_view_href = str(request.url_for("admin_system_configuration_view"))
+ sales_revenue_reports_view_href = str(request.url_for("admin_sales_revenue_reports_view"))
+ rental_reports_view_href = str(request.url_for("admin_rental_reports_view"))
+ bot_monitoring_view_href = str(request.url_for("admin_bot_monitoring_view"))
audit_href = _build_prefixed_path(settings.admin_api_prefix, "/audit/events")
can_manage_collaborators = role_has_permission(
current_context.principal.role,
@@ -155,6 +234,30 @@ def _build_home_view(
description="Fluxo humano de revisao, aprovacao e ativacao.",
badge="Operacao",
),
+ AdminPanelNavigationItem(
+ label="Configuracoes do sistema",
+ href=system_configuration_view_href,
+ description="Leitura funcional, runtime e governanca do admin em uma tela dedicada.",
+ badge="Fase 4",
+ ),
+ AdminPanelNavigationItem(
+ label="Relatorios comerciais",
+ href=sales_revenue_reports_view_href,
+ description="Tela combinada para vendas e arrecadacao dentro da sessao do painel.",
+ badge="Relatorios",
+ ),
+ AdminPanelNavigationItem(
+ label="Relatorios de locacao",
+ href=rental_reports_view_href,
+ description="Tela dedicada para frota, contratos e ocupacao na sessao do painel.",
+ badge="Locacao",
+ ),
+ AdminPanelNavigationItem(
+ label="Monitoramento do bot",
+ href=bot_monitoring_view_href,
+ description="Fluxo operacional e telemetria conversacional em uma superficie dedicada.",
+ badge="Bot",
+ ),
AdminPanelNavigationItem(
label="Areas do sistema",
href="#modules",
@@ -180,10 +283,25 @@ def _build_home_view(
button_class="btn-outline-dark",
),
AdminPanelQuickAction(
- label="Ver areas",
- href="#modules",
+ label="Config. sistema",
+ href=system_configuration_view_href,
button_class="btn-outline-secondary",
),
+ AdminPanelQuickAction(
+ label="Relatorios",
+ href=sales_revenue_reports_view_href,
+ button_class="btn-outline-dark",
+ ),
+ AdminPanelQuickAction(
+ label="Locacao",
+ href=rental_reports_view_href,
+ button_class="btn-outline-dark",
+ ),
+ AdminPanelQuickAction(
+ label="Monitorar bot",
+ href=bot_monitoring_view_href,
+ button_class="btn-outline-dark",
+ ),
]
modules = [
AdminPanelModuleCard(
@@ -219,14 +337,62 @@ def _build_home_view(
AdminPanelModuleCard(
eyebrow="Acompanhamento",
title="Configuracao do sistema",
- description="Snapshot do runtime administrativo, politicas de seguranca e dados de sessao do painel.",
- status_label="API pronta",
- status_variant="secondary",
+ description="Tela dedicada para consultar configuracao funcional, runtime administrativo e governanca do sistema sem sair do painel.",
+ status_label="Tela ativa",
+ status_variant="primary",
highlights=(
- "Runtime e banco monitorados",
- "Politicas de credencial centralizadas",
- "Base pronta para futura tela dedicada",
+ "Catalogo funcional e governanca do bot conectados",
+ "Runtime e seguranca carregados por permissao",
+ "Separacao de modelos visivel na mesma superficie",
),
+ cta_label="Abrir configuracoes",
+ href=system_configuration_view_href,
+ is_available=True,
+ ),
+ AdminPanelModuleCard(
+ eyebrow="Relatorios operacionais",
+ title="Relatorios de vendas e arrecadacao",
+ description="Tela dedicada para acompanhar o bootstrap de vendas e arrecadacao com leitura pronta para sessao web do painel.",
+ status_label="Tela ativa",
+ status_variant="info",
+ highlights=(
+ "Vendas e arrecadacao na mesma superficie",
+ "Leitura protegida por sessao web",
+ "Materializacao e proximos passos visiveis na UI",
+ ),
+ cta_label="Abrir relatorios",
+ href=sales_revenue_reports_view_href,
+ is_available=True,
+ ),
+ AdminPanelModuleCard(
+ eyebrow="Relatorios operacionais",
+ title="Relatorios de locacao",
+ description="Tela dedicada para acompanhar a estrutura inicial de frota e contratos de locacao pela sessao web do painel.",
+ status_label="Tela ativa",
+ status_variant="info",
+ highlights=(
+ "Frota e contratos lidos na mesma superficie",
+ "Leitura protegida por sessao web",
+ "Catalogo inicial e materializacao visiveis na UI",
+ ),
+ cta_label="Abrir locacao",
+ href=rental_reports_view_href,
+ is_available=True,
+ ),
+ AdminPanelModuleCard(
+ eyebrow="Monitoramento operacional",
+ title="Monitoramento do bot",
+ description="Tela dedicada para acompanhar fluxo operacional e telemetria conversacional pela sessao web do painel.",
+ status_label="Tela ativa",
+ status_variant="info",
+ highlights=(
+ "Fluxo do bot e telemetria na mesma superficie",
+ "Leitura protegida por sessao web",
+ "Materializacao e proximos passos visiveis na UI",
+ ),
+ cta_label="Abrir monitoramento",
+ href=bot_monitoring_view_href,
+ is_available=True,
),
AdminPanelModuleCard(
eyebrow="Governanca",
@@ -261,10 +427,28 @@ def _build_home_view(
description="Area com fila, contrato e catalogo ativo para tomada de decisao.",
),
AdminPanelSurfaceLink(
- method="Runtime",
- label="Configuracao do sistema",
- href=system_configuration_href,
- description="Snapshot tecnico do ambiente, mantido como superficie protegida enquanto a tela visual nao chega.",
+ method="Sistema",
+ label="Configuracoes do sistema",
+ href=system_configuration_view_href,
+ description="Tela dedicada para leitura funcional, runtime e governanca administrativa.",
+ ),
+ AdminPanelSurfaceLink(
+ method="Relatorios",
+ label="Vendas e arrecadacao",
+ href=sales_revenue_reports_view_href,
+ description="Tela combinada para a primeira camada visual dos relatorios comerciais da fase 4.",
+ ),
+ AdminPanelSurfaceLink(
+ method="Relatorios",
+ label="Locacao",
+ href=rental_reports_view_href,
+ description="Tela dedicada para a estrutura inicial de frota, contratos e ocupacao da fase 4.",
+ ),
+ AdminPanelSurfaceLink(
+ method="Monitoramento",
+ label="Bot operacional",
+ href=bot_monitoring_view_href,
+ description="Tela dedicada para fluxo operacional e telemetria conversacional do bot na fase 4.",
),
AdminPanelSurfaceLink(
method="Auditoria",
@@ -575,6 +759,123 @@ def _build_tool_review_view(request: Request, settings: AdminSettings) -> AdminT
)
+
+def _build_sales_revenue_reports_view(
+ request: Request,
+ settings: AdminSettings,
+) -> AdminSalesRevenueReportsPageView:
+ return AdminSalesRevenueReportsPageView(
+ app_name=settings.admin_app_name,
+ title="Relatorios de vendas e arrecadacao",
+ subtitle=(
+ "Visao unica para acompanhar vendas e arrecadacao no painel administrativo."
+ ),
+ environment=settings.admin_environment,
+ version=settings.admin_version,
+ dashboard_href=str(request.url_for("panel_home")),
+ sales_overview_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/panel/reports/sales/overview"),
+ revenue_overview_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/panel/reports/arrecadacao/overview"),
+ access_notes=(
+ "A equipe interna pode consultar esta tela com a permissao de relatorios.",
+ "Os dados exibidos aparecem de forma consolidada para leitura segura no admin.",
+ "Vendas e arrecadacao ficam juntas para facilitar a rotina do time.",
+ ),
+ reading_notes=(
+ "Comece pelos indicadores de cada bloco para ter uma leitura rapida.",
+ "Use os cards para entender rapidamente o foco de cada relatorio.",
+ "A area de proximas melhorias mostra o que entra nas etapas seguintes.",
+ ),
+ )
+
+
+def _build_rental_reports_view(
+ request: Request,
+ settings: AdminSettings,
+) -> AdminRentalReportsPageView:
+ return AdminRentalReportsPageView(
+ app_name=settings.admin_app_name,
+ title="Relatorios de locacao",
+ subtitle=(
+ "Visao dedicada da locacao para acompanhar frota, contratos e receita operacional."
+ ),
+ environment=settings.admin_environment,
+ version=settings.admin_version,
+ dashboard_href=str(request.url_for("panel_home")),
+ overview_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/panel/reports/locacao/overview"),
+ access_notes=(
+ "A equipe interna pode consultar esta tela com a permissao de relatorios.",
+ "Os dados exibidos aparecem de forma consolidada para leitura segura no admin.",
+ "Locacao fica em uma tela propria para manter a leitura mais organizada.",
+ ),
+ reading_notes=(
+ "Comece pelos indicadores principais para entender o momento da operacao.",
+ "Use os cards para ver rapidamente os temas cobertos nesta etapa.",
+ "A area de proximas melhorias indica o que ainda entra nas proximas entregas.",
+ ),
+ )
+
+
+def _build_bot_monitoring_view(
+ request: Request,
+ settings: AdminSettings,
+) -> AdminBotMonitoringPageView:
+ return AdminBotMonitoringPageView(
+ app_name=settings.admin_app_name,
+ title="Monitoramento operacional do bot",
+ subtitle=(
+ "Painel unico para acompanhar o fluxo do bot e a telemetria do atendimento."
+ ),
+ environment=settings.admin_environment,
+ version=settings.admin_version,
+ dashboard_href=str(request.url_for("panel_home")),
+ bot_flow_overview_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/panel/reports/fluxo-bot/overview"),
+ telemetry_overview_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/panel/reports/telemetria-conversacional/overview"),
+ access_notes=(
+ "A equipe interna pode consultar esta tela com a permissao de relatorios.",
+ "Os dados exibidos aparecem de forma consolidada para leitura segura no painel.",
+ "Fluxo e telemetria ficam juntos para agilizar a analise da operacao.",
+ ),
+ reading_notes=(
+ "Comece pelo fluxo do bot para localizar status, desvios e pontos de atencao.",
+ "Use a telemetria para acompanhar volume, latencia e saude do atendimento.",
+ "A area de proximas melhorias resume o que ainda entra antes dos dashboards completos.",
+ ),
+ )
+
+
+def _build_system_configuration_view(
+ request: Request,
+ settings: AdminSettings,
+) -> AdminSystemConfigurationPageView:
+ return AdminSystemConfigurationPageView(
+ app_name=settings.admin_app_name,
+ title="Configuracoes do sistema",
+ subtitle=(
+ "Visao unica do catalogo funcional, das regras do atendimento e das protecoes do painel."
+ ),
+ environment=settings.admin_environment,
+ version=settings.admin_version,
+ dashboard_href=str(request.url_for("panel_home")),
+ overview_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/system/configuration"),
+ runtime_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/system/configuration/runtime"),
+ security_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/system/configuration/security"),
+ model_runtimes_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/system/configuration/model-runtimes"),
+ functional_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/system/configuration/functional"),
+ functional_detail_base=_build_prefixed_path(settings.admin_api_prefix, "/system/configuration/functional"),
+ bot_governance_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/system/configuration/functional/bot-governance"),
+ access_notes=(
+ "A equipe interna ja consegue consultar o catalogo funcional e os ajustes do atendimento nesta tela.",
+ "Detalhes mais sensiveis do ambiente continuam reservados para perfis com permissao elevada.",
+ "Toda a superficie segue somente leitura nesta etapa.",
+ ),
+ governance_notes=(
+ "Configuracoes do atendimento e da geracao de tools aparecem separadas para evitar confusao.",
+ "Os ajustes do bot continuam bloqueados para escrita direta nas tabelas operacionais.",
+ "O foco aqui e leitura clara do estado atual antes da futura tela de edicao.",
+ ),
+ )
+
+
def _build_collaborator_management_view(
request: Request,
settings: AdminSettings,
diff --git a/admin_app/view/static/scripts/panel.js b/admin_app/view/static/scripts/panel.js
index 21a7dd6..334c5e4 100644
--- a/admin_app/view/static/scripts/panel.js
+++ b/admin_app/view/static/scripts/panel.js
@@ -4,6 +4,10 @@ const loginForm = document.querySelector('[data-admin-login-form="true"]');
const reviewBoard = document.querySelector('[data-admin-tool-review-board="true"]');
const toolIntakePage = document.querySelector('[data-admin-tool-intake="true"]');
const collaboratorBoard = document.querySelector('[data-admin-collaborator-board="true"]');
+const systemConfigurationPage = document.querySelector('[data-admin-system-configuration="true"]');
+const salesRevenueReportsPage = document.querySelector('[data-admin-sales-revenue-reports="true"]');
+const rentalReportsPage = document.querySelector('[data-admin-rental-reports="true"]');
+const botMonitoringPage = document.querySelector('[data-admin-bot-monitoring="true"]');
if (loginForm) {
mountLoginForm(loginForm);
@@ -21,6 +25,22 @@ if (collaboratorBoard) {
mountCollaboratorBoard(collaboratorBoard);
}
+if (systemConfigurationPage) {
+ mountSystemConfigurationPage(systemConfigurationPage);
+}
+
+if (salesRevenueReportsPage) {
+ mountSalesRevenueReportsPage(salesRevenueReportsPage);
+}
+
+if (rentalReportsPage) {
+ mountRentalReportsPage(rentalReportsPage);
+}
+
+if (botMonitoringPage) {
+ mountBotMonitoringPage(botMonitoringPage);
+}
+
function mountLoginForm(form) {
const feedback = document.getElementById("admin-login-feedback");
const submitButton = form.querySelector('button[type="submit"]');
@@ -554,6 +574,672 @@ function mountCollaboratorBoard(board) {
}
}
+
+function mountSystemConfigurationPage(page) {
+ const refreshButton = page.querySelector("[data-admin-system-refresh]");
+ const refreshLabel = page.querySelector("[data-system-refresh-label]");
+ const refreshSpinner = page.querySelector("[data-system-refresh-spinner]");
+ const feedback = document.getElementById("admin-system-configuration-feedback");
+ const functionalList = page.querySelector("[data-system-functional-list]");
+ const parentKeys = page.querySelector("[data-system-parent-keys]");
+ const botSettingsList = page.querySelector("[data-system-bot-settings-list]");
+ const runtimeSummary = page.querySelector("[data-system-runtime-summary]");
+ const securitySummary = page.querySelector("[data-system-security-summary]");
+ const modelRuntimeSummary = page.querySelector("[data-system-model-runtime-summary]");
+ const sourceList = page.querySelector("[data-system-source-list]");
+
+ if (!refreshButton || !refreshLabel || !refreshSpinner || !feedback || !functionalList || !parentKeys || !botSettingsList || !runtimeSummary || !securitySummary || !modelRuntimeSummary || !sourceList) {
+ return;
+ }
+
+ refreshButton.addEventListener("click", () => {
+ void loadConfiguration();
+ });
+
+ void loadConfiguration();
+
+ async function loadConfiguration() {
+ toggleRefreshing(true);
+ clearFeedback();
+
+ const [overviewResult, runtimeResult, securityResult, modelRuntimesResult, functionalResult, botGovernanceResult] = await Promise.all([
+ fetchPanelJson(page.dataset.overviewEndpoint),
+ fetchPanelJson(page.dataset.runtimeEndpoint),
+ fetchPanelJson(page.dataset.securityEndpoint),
+ fetchPanelJson(page.dataset.modelRuntimesEndpoint),
+ fetchPanelJson(page.dataset.functionalEndpoint),
+ fetchPanelJson(page.dataset.botGovernanceEndpoint),
+ ]);
+
+ if (functionalResult.ok) {
+ renderFunctionalCatalog(functionalResult.body);
+ } else {
+ renderLockedState(functionalList, "Catalogo funcional indisponivel", functionalResult.message || "Nao foi possivel carregar o catalogo funcional.");
+ setText("[data-system-config-count]", "0");
+ setText("[data-system-functional-mode]", "Bloqueado");
+ }
+
+ if (botGovernanceResult.ok) {
+ renderBotGovernance(botGovernanceResult.body);
+ } else {
+ parentKeys.innerHTML = 'Bloqueado';
+ renderLockedState(botSettingsList, "Governanca do bot indisponivel", botGovernanceResult.message || "Nao foi possivel carregar os campos governados pelo bot.");
+ setText("[data-system-bot-setting-count]", "0");
+ }
+
+ if (runtimeResult.ok) {
+ renderRuntime(runtimeResult.body);
+ } else {
+ renderLockedState(runtimeSummary, "Runtime protegido", runtimeResult.message || "A sessao atual nao pode ler o runtime administrativo.");
+ }
+
+ if (securityResult.ok) {
+ renderSecurity(securityResult.body);
+ } else {
+ renderLockedState(securitySummary, "Seguranca protegida", securityResult.message || "A sessao atual nao pode ler o snapshot de seguranca.");
+ }
+
+ if (modelRuntimesResult.ok) {
+ renderModelRuntimes(modelRuntimesResult.body);
+ } else {
+ renderLockedState(modelRuntimeSummary, "Separacao tecnica protegida", modelRuntimesResult.message || "A sessao atual nao pode ler os perfis de runtime.");
+ setText("[data-system-runtime-profile-count]", "0");
+ }
+
+ if (overviewResult.ok) {
+ renderSources(overviewResult.body);
+ } else {
+ renderLockedState(sourceList, "Overview tecnico protegido", overviewResult.message || "A sessao atual nao pode ler as fontes do snapshot.");
+ setText("[data-system-source-count]", "0");
+ }
+
+ const directorOnlyLocked = [overviewResult, runtimeResult, securityResult, modelRuntimesResult].some((result) => !result.ok);
+ if (functionalResult.ok && botGovernanceResult.ok && directorOnlyLocked) {
+ showFeedback("info", "A sessao atual consegue consultar a configuracao funcional do sistema. Blocos de runtime, seguranca e separacao tecnica exigem manage_settings.");
+ } else if (functionalResult.ok && botGovernanceResult.ok) {
+ showFeedback("success", "Snapshot de configuracoes do sistema carregado com sucesso.");
+ } else {
+ showFeedback("warning", "A tela nao conseguiu carregar todas as superficies de configuracao com a sessao atual.");
+ }
+
+ setText("[data-system-last-sync]", formatNow());
+ toggleRefreshing(false);
+ }
+
+ function renderFunctionalCatalog(payload) {
+ const configurations = Array.isArray(payload?.configurations) ? payload.configurations : [];
+ setText("[data-system-config-count]", String(configurations.length));
+ setText("[data-system-functional-mode]", formatModeLabel(payload?.mode));
+
+ functionalList.innerHTML = configurations.length > 0
+ ? configurations.map((item) => {
+ const writableCount = Array.isArray(item?.fields)
+ ? item.fields.filter((field) => field?.writable).length
+ : 0;
+ const editingLabel = writableCount > 0 ? `${writableCount} campo(s) ajustavel(is)` : "Somente leitura";
+ const impactLabel = item?.affects_product_runtime ? "Impacta o atendimento" : "Uso interno do admin";
+ return `${escapeHtml(formatDomainLabel(item?.domain || "sistema"))}
${escapeHtml(formatConfigTitle(item?.config_key || "configuracao"))}
${escapeHtml(item?.description || "")}
${escapeHtml(formatMutabilityLabel(item?.mutability || "readonly"))}Campos visiveis: ${escapeHtml(String(Array.isArray(item?.fields) ? item.fields.length : 0))}
Ajustes nesta fase: ${escapeHtml(editingLabel)}
Impacto: ${escapeHtml(impactLabel)}
`;
+ }).join("")
+ : ``;
+ }
+
+ function renderBotGovernance(payload) {
+ const settings = Array.isArray(payload?.settings) ? payload.settings : [];
+ const parentConfigKeys = Array.isArray(payload?.parent_config_keys) ? payload.parent_config_keys : [];
+ setText("[data-system-bot-setting-count]", String(settings.length));
+
+ parentKeys.innerHTML = parentConfigKeys.length > 0
+ ? parentConfigKeys.map((item) => `${escapeHtml(item)}`).join("")
+ : 'Sem parent keys';
+
+ botSettingsList.innerHTML = settings.length > 0
+ ? settings.slice(0, 12).map((item) => `${escapeHtml(formatDomainLabel(item?.area || "bot"))}
${escapeHtml(humanizeKey(item?.setting_key || "setting"))}
${escapeHtml(item?.description || "")}
${escapeHtml(formatMutabilityLabel(item?.mutability || "versioned"))}Grupo: ${escapeHtml(formatConfigTitle(item?.parent_config_key || "-"))}
Escrita direta: ${item?.direct_product_write_allowed ? 'Permitida' : 'Bloqueada'}
`).join("")
+ : ``;
+ }
+
+ function renderRuntime(payload) {
+ const runtime = payload?.runtime;
+ if (!runtime) {
+ renderLockedState(runtimeSummary, "Runtime indisponivel", "Nao foi possivel interpretar a resposta do runtime.");
+ return;
+ }
+
+ runtimeSummary.innerHTML = `Aplicacao
Nome: ${escapeHtml(runtime?.application?.app_name || "-")}
Ambiente: ${escapeHtml(runtime?.application?.environment || "-")}
Versao: ${escapeHtml(runtime?.application?.version || "-")}
Modo debug: ${runtime?.application?.debug ? 'Ativo' : 'Desligado'}
Detalhes internos de infraestrutura e cookies nao aparecem aqui para manter a tela mais limpa.
`;
+ }
+
+ function renderSecurity(payload) {
+ const security = payload?.security;
+ if (!security) {
+ renderLockedState(securitySummary, "Seguranca indisponivel", "Nao foi possivel interpretar a resposta de seguranca.");
+ return;
+ }
+
+ securitySummary.innerHTML = `Senha e sessao
Tamanho minimo: ${escapeHtml(String(security?.password?.min_length || 0))} caracteres
Requisitos: ${renderPasswordRequirements(security?.password)}
Acesso expira em: ${escapeHtml(String(security?.tokens?.access_token_ttl_minutes || 0))} min
Renovacao disponivel por: ${escapeHtml(String(security?.tokens?.refresh_token_ttl_days || 0))} dias
Informacoes internas de assinatura e bootstrap ficam fora desta tela para reduzir ruido.
`;
+ }
+
+ function renderModelRuntimes(payload) {
+ const modelRuntimes = payload?.model_runtimes;
+ const profiles = Array.isArray(modelRuntimes?.runtime_profiles) ? modelRuntimes.runtime_profiles : [];
+ const separationRules = Array.isArray(modelRuntimes?.separation_rules) ? modelRuntimes.separation_rules : [];
+ setText("[data-system-runtime-profile-count]", String(profiles.length));
+
+ modelRuntimeSummary.innerHTML = profiles.length > 0
+ ? `Regras principais
${separationRules.map((rule) => `- ${escapeHtml(rule)}
`).join("")}
${profiles.map((profile) => `
${escapeHtml(formatRuntimeTargetLabel(profile?.runtime_target || "runtime"))}
${escapeHtml(formatConfigTitle(profile?.config_key || "perfil"))}
${escapeHtml(profile?.description || "")}
${profile?.affects_customer_response ? 'Atendimento' : 'Interno'}Servico: ${escapeHtml(humanizeKey(profile?.consumed_by_service || "-"))}
Uso principal: ${escapeHtml(formatPurposeLabel(profile?.purpose || "-"))}
Gera tools: ${profile?.can_generate_code ? 'Sim' : 'Nao'}
Rollback separado: ${profile?.rollback_independently ? 'Sim' : 'Nao'}
`).join("")}
`
+ : ``;
+ }
+
+ function renderSources(payload) {
+ const sources = Array.isArray(payload?.sources) ? payload.sources : [];
+ setText("[data-system-source-count]", String(sources.length));
+
+ sourceList.innerHTML = sources.length > 0
+ ? sources.map((item) => `${escapeHtml(formatSourceLabel(item?.source || "origem"))}
${escapeHtml(formatConfigTitle(item?.key || "configuracao"))}
${escapeHtml(item?.description || "")}
${item?.mutable ? 'Pode mudar' : 'Base fixa'} `).join("")
+ : ``;
+ }
+
+ function renderPasswordRequirements(passwordPolicy) {
+ const requirements = [];
+ if (passwordPolicy?.require_uppercase) requirements.push("letra maiuscula");
+ if (passwordPolicy?.require_lowercase) requirements.push("letra minuscula");
+ if (passwordPolicy?.require_digit) requirements.push("numero");
+ if (passwordPolicy?.require_symbol) requirements.push("simbolo");
+ return requirements.length > 0 ? escapeHtml(requirements.join(", ")) : "nenhum requisito adicional";
+ }
+
+ function renderLockedState(container, title, message) {
+ container.innerHTML = ``;
+ }
+
+ function toggleRefreshing(isLoading) {
+ refreshButton.disabled = isLoading;
+ refreshSpinner.classList.toggle("d-none", !isLoading);
+ refreshLabel.textContent = isLoading ? "Atualizando..." : "Atualizar leitura";
+ }
+
+ function clearFeedback() {
+ feedback.className = "alert d-none rounded-4 mb-4";
+ feedback.textContent = "";
+ }
+
+ function showFeedback(variant, message) {
+ feedback.className = `alert alert-${variant} rounded-4 mb-4`;
+ feedback.textContent = message;
+ }
+}
+
+function mountSalesRevenueReportsPage(page) {
+ const refreshButton = page.querySelector("[data-admin-commercial-refresh]");
+ const refreshLabel = page.querySelector("[data-commercial-refresh-label]");
+ const refreshSpinner = page.querySelector("[data-commercial-refresh-spinner]");
+ const feedback = document.getElementById("admin-commercial-feedback");
+ const salesMetrics = page.querySelector("[data-sales-overview-metrics]");
+ const salesMaterialization = page.querySelector("[data-sales-materialization]");
+ const salesReportList = page.querySelector("[data-sales-report-list]");
+ const salesNextSteps = page.querySelector("[data-sales-next-steps]");
+ const revenueMetrics = page.querySelector("[data-revenue-overview-metrics]");
+ const revenueMaterialization = page.querySelector("[data-revenue-materialization]");
+ const revenueReportList = page.querySelector("[data-revenue-report-list]");
+ const revenueNextSteps = page.querySelector("[data-revenue-next-steps]");
+
+ if (!refreshButton || !refreshLabel || !refreshSpinner || !feedback || !salesMetrics || !salesMaterialization || !salesReportList || !salesNextSteps || !revenueMetrics || !revenueMaterialization || !revenueReportList || !revenueNextSteps) {
+ return;
+ }
+
+ refreshButton.addEventListener("click", () => {
+ void loadReports();
+ });
+
+ void loadReports();
+
+ async function loadReports() {
+ toggleRefreshing(true);
+ clearFeedback();
+
+ const [salesResult, revenueResult] = await Promise.all([
+ fetchPanelJson(page.dataset.salesOverviewEndpoint),
+ fetchPanelJson(page.dataset.revenueOverviewEndpoint),
+ ]);
+
+ if (salesResult.ok) {
+ renderDomainOverview({ kind: "sales", payload: salesResult.body, metricsTarget: salesMetrics, materializationTarget: salesMaterialization, reportsTarget: salesReportList, nextStepsTarget: salesNextSteps });
+ } else {
+ renderLockedState(salesMetrics, "Vendas indisponivel", salesResult.message || "Nao foi possivel carregar o overview de vendas.");
+ salesMaterialization.innerHTML = "";
+ salesReportList.innerHTML = "";
+ salesNextSteps.innerHTML = "";
+ setText("[data-sales-report-count]", "0");
+ }
+
+ if (revenueResult.ok) {
+ renderDomainOverview({ kind: "revenue", payload: revenueResult.body, metricsTarget: revenueMetrics, materializationTarget: revenueMaterialization, reportsTarget: revenueReportList, nextStepsTarget: revenueNextSteps });
+ } else {
+ renderLockedState(revenueMetrics, "Arrecadacao indisponivel", revenueResult.message || "Nao foi possivel carregar o overview de arrecadacao.");
+ revenueMaterialization.innerHTML = "";
+ revenueReportList.innerHTML = "";
+ revenueNextSteps.innerHTML = "";
+ setText("[data-revenue-report-count]", "0");
+ }
+
+ if (salesResult.ok && revenueResult.ok) {
+ const salesPayload = salesResult.body;
+ const revenuePayload = revenueResult.body;
+ const datasetCount = uniqueCount(salesPayload?.source_dataset_keys, revenuePayload?.source_dataset_keys);
+ const syncStrategy = salesPayload?.materialization?.sync_strategy === revenuePayload?.materialization?.sync_strategy
+ ? salesPayload?.materialization?.sync_strategy
+ : "mixed";
+ setText("[data-commercial-dataset-count]", String(datasetCount));
+ setText("[data-commercial-sync-strategy]", formatSyncStrategyLabel(syncStrategy || "--"));
+ showFeedback("success", "Relatorios de vendas e arrecadacao carregados com sucesso na sessao do painel.");
+ } else if (salesResult.ok || revenueResult.ok) {
+ const onlyLoaded = salesResult.ok ? "vendas" : "arrecadacao";
+ const datasetCount = salesResult.ok
+ ? (Array.isArray(salesResult.body?.source_dataset_keys) ? salesResult.body.source_dataset_keys.length : 0)
+ : (Array.isArray(revenueResult.body?.source_dataset_keys) ? revenueResult.body.source_dataset_keys.length : 0);
+ const syncStrategy = salesResult.ok ? salesResult.body?.materialization?.sync_strategy : revenueResult.body?.materialization?.sync_strategy;
+ setText("[data-commercial-dataset-count]", String(datasetCount));
+ setText("[data-commercial-sync-strategy]", formatSyncStrategyLabel(syncStrategy || "--"));
+ showFeedback("warning", `A tela carregou apenas o overview de ${onlyLoaded} com a sessao atual.`);
+ } else {
+ setText("[data-commercial-dataset-count]", "0");
+ setText("[data-commercial-sync-strategy]", "--");
+ showFeedback("warning", "Nao foi possivel carregar os relatorios comerciais na sessao atual.");
+ }
+
+ setText("[data-commercial-last-sync]", formatNow());
+ toggleRefreshing(false);
+ }
+
+ function renderDomainOverview({ kind, payload, metricsTarget, materializationTarget, reportsTarget, nextStepsTarget }) {
+ const reports = Array.isArray(payload?.reports) ? payload.reports : [];
+ const metrics = Array.isArray(payload?.metrics) ? payload.metrics : [];
+ const nextSteps = Array.isArray(payload?.next_steps) ? payload.next_steps : [];
+ const reportCountSelector = kind === "sales" ? "[data-sales-report-count]" : "[data-revenue-report-count]";
+ setText(reportCountSelector, String(reports.length));
+
+ metricsTarget.innerHTML = metrics.length > 0
+ ? `${metrics.map((item) => `
${escapeHtml(item?.label || item?.key || "metrica")}
${escapeHtml(item?.value || "0")}
${escapeHtml(item?.description || "")}
`).join("")}
`
+ : ``;
+
+ materializationTarget.innerHTML = payload?.materialization
+ ? `Atualizacao da tela
Ritmo: ${escapeHtml(formatSyncStrategyLabel(payload?.materialization?.sync_strategy || "-"))}
Camada: ${escapeHtml(formatStorageLabel(payload?.materialization?.storage_shape || "-"))}
Consulta: ${escapeHtml(formatQuerySurfaceLabel(payload?.materialization?.query_surface || "-"))}
`
+ : "";
+
+ reportsTarget.innerHTML = reports.length > 0
+ ? reports.map((item) => `${escapeHtml(item?.label || humanizeKey(item?.report_key || "relatorio"))}
${escapeHtml(item?.description || "")}
${escapeHtml(formatGranularityLabel(item?.default_granularity || "aggregate"))}Indicadores: ${escapeHtml(String((item?.supported_metric_keys || []).length))}Recortes: ${escapeHtml(String((item?.supported_dimension_fields || []).length))}
`).join("")
+ : ``;
+
+ nextStepsTarget.innerHTML = nextSteps.length > 0
+ ? nextSteps.map((item) => `${escapeHtml(item)}
`).join("")
+ : ``;
+ }
+
+ function renderLockedState(container, title, message) {
+ container.innerHTML = ``;
+ }
+
+ function toggleRefreshing(isLoading) {
+ refreshButton.disabled = isLoading;
+ refreshSpinner.classList.toggle("d-none", !isLoading);
+ refreshLabel.textContent = isLoading ? "Atualizando..." : "Atualizar leitura";
+ }
+
+ function clearFeedback() {
+ feedback.className = "alert d-none rounded-4 mb-4";
+ feedback.textContent = "";
+ }
+
+ function showFeedback(variant, message) {
+ feedback.className = `alert alert-${variant} rounded-4 mb-4`;
+ feedback.textContent = message;
+ }
+}
+function mountRentalReportsPage(page) {
+ const refreshButton = page.querySelector("[data-admin-rental-refresh]");
+ const refreshLabel = page.querySelector("[data-rental-refresh-label]");
+ const refreshSpinner = page.querySelector("[data-rental-refresh-spinner]");
+ const feedback = document.getElementById("admin-rental-feedback");
+ const overviewMetrics = page.querySelector("[data-rental-overview-metrics]");
+ const materialization = page.querySelector("[data-rental-materialization]");
+ const reportList = page.querySelector("[data-rental-report-list]");
+ const nextSteps = page.querySelector("[data-rental-next-steps]");
+
+ if (!refreshButton || !refreshLabel || !refreshSpinner || !feedback || !overviewMetrics || !materialization || !reportList || !nextSteps) {
+ return;
+ }
+
+ refreshButton.addEventListener("click", () => {
+ void loadOverview();
+ });
+
+ void loadOverview();
+
+ async function loadOverview() {
+ toggleRefreshing(true);
+ clearFeedback();
+
+ const result = await fetchPanelJson(page.dataset.rentalOverviewEndpoint);
+ if (result.ok) {
+ const payload = result.body;
+ const metrics = Array.isArray(payload?.metrics) ? payload.metrics : [];
+ const reports = Array.isArray(payload?.reports) ? payload.reports : [];
+ const datasets = Array.isArray(payload?.source_dataset_keys) ? payload.source_dataset_keys : [];
+ const plannedSteps = Array.isArray(payload?.next_steps) ? payload.next_steps : [];
+
+ setText("[data-rental-report-count]", String(reports.length));
+ setText("[data-rental-dataset-count]", String(datasets.length));
+ setText("[data-rental-sync-strategy]", formatSyncStrategyLabel(payload?.materialization?.sync_strategy || "--"));
+ setText("[data-rental-source-domain]", formatDomainLabel(payload?.source_domain || "--"));
+
+ overviewMetrics.innerHTML = metrics.length > 0
+ ? `${metrics.map((item) => `
${escapeHtml(item?.label || item?.key || "metrica")}
${escapeHtml(item?.value || "0")}
${escapeHtml(item?.description || "")}
`).join("")}
`
+ : ``;
+
+ materialization.innerHTML = payload?.materialization
+ ? `Atualizacao da tela
Ritmo: ${escapeHtml(formatSyncStrategyLabel(payload?.materialization?.sync_strategy || "-"))}
Camada: ${escapeHtml(formatStorageLabel(payload?.materialization?.storage_shape || "-"))}
Consulta: ${escapeHtml(formatQuerySurfaceLabel(payload?.materialization?.query_surface || "-"))}
`
+ : "";
+
+ reportList.innerHTML = reports.length > 0
+ ? reports.map((item) => `${escapeHtml(item?.label || humanizeKey(item?.report_key || "relatorio"))}
${escapeHtml(item?.description || "")}
${escapeHtml(formatGranularityLabel(item?.default_granularity || "aggregate"))}Indicadores: ${escapeHtml(String((item?.supported_metric_keys || []).length))}Recortes: ${escapeHtml(String((item?.supported_dimension_fields || []).length))}Filtros: ${escapeHtml(String((item?.supported_filter_fields || []).length))}
`).join("")
+ : ``;
+
+ nextSteps.innerHTML = plannedSteps.length > 0
+ ? plannedSteps.map((item) => `${escapeHtml(item)}
`).join("")
+ : ``;
+
+ showFeedback("success", "Relatorios de locacao carregados com sucesso na sessao do painel.");
+ } else {
+ setText("[data-rental-report-count]", "0");
+ setText("[data-rental-dataset-count]", "0");
+ setText("[data-rental-sync-strategy]", "--");
+ setText("[data-rental-source-domain]", "--");
+ overviewMetrics.innerHTML = ``;
+ materialization.innerHTML = "";
+ reportList.innerHTML = "";
+ nextSteps.innerHTML = "";
+ showFeedback("warning", result.message || "Nao foi possivel carregar os relatorios de locacao na sessao atual.");
+ }
+
+ setText("[data-rental-last-sync]", formatNow());
+ toggleRefreshing(false);
+ }
+
+ function toggleRefreshing(isLoading) {
+ refreshButton.disabled = isLoading;
+ refreshSpinner.classList.toggle("d-none", !isLoading);
+ refreshLabel.textContent = isLoading ? "Atualizando..." : "Atualizar leitura";
+ }
+
+ function clearFeedback() {
+ feedback.className = "alert d-none rounded-4 mb-4";
+ feedback.textContent = "";
+ }
+
+ function showFeedback(variant, message) {
+ feedback.className = `alert alert-${variant} rounded-4 mb-4`;
+ feedback.textContent = message;
+ }
+}
+
+
+
+function mountBotMonitoringPage(page) {
+ const refreshButton = page.querySelector("[data-admin-bot-monitoring-refresh]");
+ const refreshLabel = page.querySelector("[data-bot-monitoring-refresh-label]");
+ const refreshSpinner = page.querySelector("[data-bot-monitoring-refresh-spinner]");
+ const feedback = document.getElementById("admin-bot-monitoring-feedback");
+ const botFlowMetrics = page.querySelector("[data-bot-flow-overview-metrics]");
+ const botFlowMaterialization = page.querySelector("[data-bot-flow-materialization]");
+ const botFlowReportList = page.querySelector("[data-bot-flow-report-list]");
+ const botFlowNextSteps = page.querySelector("[data-bot-flow-next-steps]");
+ const telemetryMetrics = page.querySelector("[data-bot-telemetry-overview-metrics]");
+ const telemetryMaterialization = page.querySelector("[data-bot-telemetry-materialization]");
+ const telemetryReportList = page.querySelector("[data-bot-telemetry-report-list]");
+ const telemetryNextSteps = page.querySelector("[data-bot-telemetry-next-steps]");
+
+ if (!refreshButton || !refreshLabel || !refreshSpinner || !feedback || !botFlowMetrics || !botFlowMaterialization || !botFlowReportList || !botFlowNextSteps || !telemetryMetrics || !telemetryMaterialization || !telemetryReportList || !telemetryNextSteps) {
+ return;
+ }
+
+ refreshButton.addEventListener("click", () => {
+ void loadMonitoring();
+ });
+
+ void loadMonitoring();
+
+ async function loadMonitoring() {
+ toggleRefreshing(true);
+ clearFeedback();
+
+ const [botFlowResult, telemetryResult] = await Promise.all([
+ fetchPanelJson(page.dataset.botFlowOverviewEndpoint),
+ fetchPanelJson(page.dataset.telemetryOverviewEndpoint),
+ ]);
+
+ if (botFlowResult.ok) {
+ renderDomainOverview({ kind: "flow", payload: botFlowResult.body, metricsTarget: botFlowMetrics, materializationTarget: botFlowMaterialization, reportsTarget: botFlowReportList, nextStepsTarget: botFlowNextSteps });
+ } else {
+ renderLockedState(botFlowMetrics, "Fluxo do bot indisponivel", botFlowResult.message || "Nao foi possivel carregar o overview operacional do bot.");
+ botFlowMaterialization.innerHTML = "";
+ botFlowReportList.innerHTML = "";
+ botFlowNextSteps.innerHTML = "";
+ setText("[data-bot-flow-report-count]", "0");
+ }
+
+ if (telemetryResult.ok) {
+ renderDomainOverview({ kind: "telemetry", payload: telemetryResult.body, metricsTarget: telemetryMetrics, materializationTarget: telemetryMaterialization, reportsTarget: telemetryReportList, nextStepsTarget: telemetryNextSteps });
+ } else {
+ renderLockedState(telemetryMetrics, "Telemetria indisponivel", telemetryResult.message || "Nao foi possivel carregar o overview de telemetria conversacional.");
+ telemetryMaterialization.innerHTML = "";
+ telemetryReportList.innerHTML = "";
+ telemetryNextSteps.innerHTML = "";
+ setText("[data-bot-telemetry-report-count]", "0");
+ }
+
+ if (botFlowResult.ok && telemetryResult.ok) {
+ const botFlowPayload = botFlowResult.body;
+ const telemetryPayload = telemetryResult.body;
+ const datasetCount = uniqueCount(botFlowPayload?.source_dataset_keys, telemetryPayload?.source_dataset_keys);
+ const syncStrategy = botFlowPayload?.materialization?.sync_strategy === telemetryPayload?.materialization?.sync_strategy
+ ? botFlowPayload?.materialization?.sync_strategy
+ : "mixed";
+ setText("[data-bot-monitoring-dataset-count]", String(datasetCount));
+ setText("[data-bot-monitoring-sync-strategy]", formatSyncStrategyLabel(syncStrategy || "--"));
+ showFeedback("success", "Fluxo operacional do bot e telemetria conversacional carregados com sucesso na sessao do painel.");
+ } else if (botFlowResult.ok || telemetryResult.ok) {
+ const onlyLoaded = botFlowResult.ok ? "fluxo do bot" : "telemetria conversacional";
+ const datasetCount = botFlowResult.ok
+ ? (Array.isArray(botFlowResult.body?.source_dataset_keys) ? botFlowResult.body.source_dataset_keys.length : 0)
+ : (Array.isArray(telemetryResult.body?.source_dataset_keys) ? telemetryResult.body.source_dataset_keys.length : 0);
+ const syncStrategy = botFlowResult.ok ? botFlowResult.body?.materialization?.sync_strategy : telemetryResult.body?.materialization?.sync_strategy;
+ setText("[data-bot-monitoring-dataset-count]", String(datasetCount));
+ setText("[data-bot-monitoring-sync-strategy]", formatSyncStrategyLabel(syncStrategy || "--"));
+ showFeedback("warning", `A tela carregou apenas ${onlyLoaded} com a sessao atual.`);
+ } else {
+ setText("[data-bot-monitoring-dataset-count]", "0");
+ setText("[data-bot-monitoring-sync-strategy]", "--");
+ showFeedback("warning", "Nao foi possivel carregar o monitoramento operacional do bot na sessao atual.");
+ }
+
+ setText("[data-bot-monitoring-last-sync]", formatNow());
+ toggleRefreshing(false);
+ }
+
+ function renderDomainOverview({ kind, payload, metricsTarget, materializationTarget, reportsTarget, nextStepsTarget }) {
+ const reports = Array.isArray(payload?.reports) ? payload.reports : [];
+ const metrics = Array.isArray(payload?.metrics) ? payload.metrics : [];
+ const nextSteps = Array.isArray(payload?.next_steps) ? payload.next_steps : [];
+ const reportCountSelector = kind === "flow" ? "[data-bot-flow-report-count]" : "[data-bot-telemetry-report-count]";
+ setText(reportCountSelector, String(reports.length));
+
+ metricsTarget.innerHTML = metrics.length > 0
+ ? `${metrics.map((item) => `
${escapeHtml(item?.label || item?.key || "metrica")}
${escapeHtml(item?.value || "0")}
${escapeHtml(item?.description || "")}
`).join("")}
`
+ : ``;
+
+ materializationTarget.innerHTML = payload?.materialization
+ ? `Atualizacao da tela
Ritmo: ${escapeHtml(formatSyncStrategyLabel(payload?.materialization?.sync_strategy || "-"))}
Camada: ${escapeHtml(formatStorageLabel(payload?.materialization?.storage_shape || "-"))}
Consulta: ${escapeHtml(formatQuerySurfaceLabel(payload?.materialization?.query_surface || "-"))}
`
+ : "";
+
+ reportsTarget.innerHTML = reports.length > 0
+ ? reports.map((item) => `${escapeHtml(item?.label || humanizeKey(item?.report_key || "relatorio"))}
${escapeHtml(item?.description || "")}
${escapeHtml(formatGranularityLabel(item?.default_granularity || "aggregate"))}Indicadores: ${escapeHtml(String((item?.supported_metric_keys || []).length))}Recortes: ${escapeHtml(String((item?.supported_dimension_fields || []).length))}
`).join("")
+ : ``;
+
+ nextStepsTarget.innerHTML = nextSteps.length > 0
+ ? nextSteps.map((item) => `${escapeHtml(item)}
`).join("")
+ : ``;
+ }
+
+ function renderLockedState(container, title, message) {
+ container.innerHTML = ``;
+ }
+
+ function toggleRefreshing(isLoading) {
+ refreshButton.disabled = isLoading;
+ refreshSpinner.classList.toggle("d-none", !isLoading);
+ refreshLabel.textContent = isLoading ? "Atualizando..." : "Atualizar leitura";
+ }
+
+ function clearFeedback() {
+ feedback.className = "alert d-none rounded-4 mb-4";
+ feedback.textContent = "";
+ }
+
+ function showFeedback(variant, message) {
+ feedback.className = `alert alert-${variant} rounded-4 mb-4`;
+ feedback.textContent = message;
+ }
+}
+
+
+function humanizeKey(value) {
+ const raw = String(value || "").trim();
+ if (!raw) {
+ return "-";
+ }
+ return raw
+ .replace(/[_-]+/g, " ")
+ .replace(/\b\w/g, (char) => char.toUpperCase());
+}
+
+function formatFriendlyLabel(value, mapping, fallback = "-") {
+ const normalized = String(value || "").trim().toLowerCase();
+ if (!normalized) {
+ return fallback;
+ }
+ return mapping[normalized] || humanizeKey(normalized);
+}
+
+function formatConfigTitle(value) {
+ return formatFriendlyLabel(value, {
+ application: "Aplicacao",
+ database: "Banco administrativo",
+ security: "Politicas de acesso",
+ panel_session: "Sessao do painel",
+ functional_configuration_contracts: "Catalogo funcional",
+ bot_governed_configuration_contracts: "Ajustes do atendimento",
+ model_runtime_separation: "Separacao de modelos",
+ write_governance: "Protecao de escrita",
+ atendimento_runtime_profile: "Modelo do atendimento",
+ tool_generation_runtime_profile: "Geracao de tools",
+ published_runtime_state: "Estado publicado"
+ }, "Configuracao");
+}
+
+function formatModeLabel(value) {
+ return formatFriendlyLabel(value, {
+ shared_contract_bootstrap: "Contrato base",
+ sales_contract_bootstrap: "Estrutura inicial",
+ revenue_contract_bootstrap: "Estrutura inicial",
+ rental_contract_bootstrap: "Estrutura inicial",
+ bot_flow_contract_bootstrap: "Estrutura inicial",
+ conversation_telemetry_contract_bootstrap: "Estrutura inicial",
+ mixed: "Leituras combinadas"
+ }, "Leitura base");
+}
+
+function formatMutabilityLabel(value) {
+ return formatFriendlyLabel(value, {
+ readonly: "Somente leitura",
+ read_only: "Somente leitura",
+ versioned: "Versionado",
+ mutable: "Editavel",
+ governed: "Governado"
+ }, "Somente leitura");
+}
+
+function formatGranularityLabel(value) {
+ return formatFriendlyLabel(value, {
+ aggregate: "Visao consolidada",
+ daily: "Por dia",
+ weekly: "Por semana",
+ monthly: "Por mes"
+ }, "Visao consolidada");
+}
+
+function formatSyncStrategyLabel(value) {
+ return formatFriendlyLabel(value, {
+ etl_incremental: "Atualizacao em lote",
+ snapshot_refresh: "Atualizacao por snapshot",
+ mixed: "Leituras combinadas"
+ }, "Nao informado");
+}
+
+function formatStorageLabel(value) {
+ return formatFriendlyLabel(value, {
+ snapshot_table: "Snapshot consolidado",
+ dedicated_view: "Visao preparada"
+ }, "Nao informado");
+}
+
+function formatQuerySurfaceLabel(value) {
+ return formatFriendlyLabel(value, {
+ dedicated_view: "Consulta preparada",
+ analytical_view: "Consulta analitica",
+ report_endpoint: "Consulta do painel"
+ }, "Nao informado");
+}
+
+function formatDomainLabel(value) {
+ return formatFriendlyLabel(value, {
+ sistema: "Sistema",
+ sales: "Vendas",
+ arrecadacao: "Arrecadacao",
+ rental: "Locacao",
+ fluxo_bot: "Fluxo do bot",
+ telemetria_conversacional: "Telemetria conversacional",
+ bot: "Atendimento"
+ }, "-");
+}
+
+function formatSourceLabel(value) {
+ return formatFriendlyLabel(value, {
+ env: "Ambiente",
+ runtime: "Aplicacao",
+ shared_contract: "Contrato compartilhado",
+ runtime_guard: "Protecao ativa"
+ }, "Origem");
+}
+
+function formatRuntimeTargetLabel(value) {
+ return formatFriendlyLabel(value, {
+ atendimento: "Atendimento",
+ tool_generation: "Geracao de tools"
+ }, "Runtime");
+}
+
+function formatPurposeLabel(value) {
+ return formatFriendlyLabel(value, {
+ customer_response: "Resposta ao cliente",
+ tool_generation: "Geracao de tools",
+ decision_support: "Apoio a decisao"
+ }, "Uso interno");
+}
+
+function uniqueCount(...collections) {
+ return new Set(
+ collections.flatMap((items) => Array.isArray(items) ? items : []).filter(Boolean)
+ ).size;
+}
+
async function fetchPanelJson(url) {
const response = await fetch(url, {
credentials: "same-origin",
diff --git a/admin_app/view/static/styles/panel.css b/admin_app/view/static/styles/panel.css
index c2c4093..74ef3cc 100644
--- a/admin_app/view/static/styles/panel.css
+++ b/admin_app/view/static/styles/panel.css
@@ -1,4 +1,4 @@
-:root {
+:root {
--admin-bg: #f6f1e8;
--admin-surface: rgba(255, 255, 255, 0.84);
--admin-surface-strong: rgba(255, 255, 255, 0.92);
@@ -321,3 +321,158 @@ body.admin-view-body {
display: grid;
gap: 0.45rem;
}
+
+.admin-system-page .admin-hero-card::after {
+ background: radial-gradient(circle, rgba(20, 77, 71, 0.22), transparent 72%);
+}
+
+.admin-system-grid {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+}
+
+.admin-system-stack {
+ display: grid;
+ gap: 1rem;
+}
+
+.admin-system-item {
+ background: var(--admin-surface-strong);
+ border: 1px solid var(--admin-line);
+ border-radius: 1.35rem;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
+}
+
+.admin-system-meta {
+ display: grid;
+ gap: 0.45rem;
+}
+
+.admin-system-chip-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+.admin-commercial-reports-page .admin-hero-card::after {
+ background: radial-gradient(circle, rgba(193, 106, 51, 0.2), transparent 72%);
+}
+
+.admin-commercial-grid {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+}
+
+.admin-commercial-stack,
+.admin-commercial-list {
+ display: grid;
+ gap: 1rem;
+}
+
+.admin-commercial-item {
+ background: var(--admin-surface-strong);
+ border: 1px solid var(--admin-line);
+ border-radius: 1.35rem;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
+}
+
+.admin-commercial-meta {
+ display: grid;
+ gap: 0.45rem;
+}
+
+.admin-commercial-chip-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+.admin-rental-reports-page .admin-hero-card::after {
+ background: radial-gradient(circle, rgba(38, 88, 132, 0.2), transparent 72%);
+}
+
+.admin-rental-grid {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+}
+
+.admin-rental-stack,
+.admin-rental-list {
+ display: grid;
+ gap: 1rem;
+}
+
+.admin-rental-item {
+ background: var(--admin-surface-strong);
+ border: 1px solid var(--admin-line);
+ border-radius: 1.35rem;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
+}
+
+.admin-rental-meta {
+ display: grid;
+ gap: 0.45rem;
+}
+
+.admin-rental-chip-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+.admin-bot-monitoring-page .admin-hero-card::after {
+ background: radial-gradient(circle, rgba(22, 63, 58, 0.22), transparent 72%);
+}
+
+.admin-bot-monitoring-grid {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+}
+
+.admin-bot-monitoring-stack,
+.admin-bot-monitoring-list {
+ display: grid;
+ gap: 1rem;
+}
+
+.admin-bot-monitoring-item {
+ background: var(--admin-surface-strong);
+ border: 1px solid var(--admin-line);
+ border-radius: 1.35rem;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
+}
+
+.admin-bot-monitoring-meta {
+ display: grid;
+ gap: 0.45rem;
+}
+
+.admin-bot-monitoring-chip-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+.admin-system-item,
+.admin-commercial-item,
+.admin-rental-item,
+.admin-bot-monitoring-item {
+ overflow-wrap: anywhere;
+}
+
+.admin-system-item h4,
+.admin-commercial-item h4,
+.admin-rental-item h4,
+.admin-bot-monitoring-item h4 {
+ overflow-wrap: anywhere;
+}
+
+.admin-system-chip-group .badge,
+.admin-commercial-chip-group .badge,
+.admin-rental-chip-group .badge,
+.admin-bot-monitoring-chip-group .badge {
+ white-space: normal;
+ text-align: left;
+}
diff --git a/admin_app/view/view_models.py b/admin_app/view/view_models.py
index 76cc422..1c57ff4 100644
--- a/admin_app/view/view_models.py
+++ b/admin_app/view/view_models.py
@@ -145,3 +145,57 @@ class AdminCollaboratorManagementPageView(BaseModel):
password_policy_label: str
onboarding_notes: tuple[str, ...]
governance_notes: tuple[str, ...]
+
+
+class AdminSystemConfigurationPageView(BaseModel):
+ app_name: str
+ title: str
+ subtitle: str
+ environment: str
+ version: str
+ dashboard_href: str
+ overview_endpoint: str
+ runtime_endpoint: str
+ security_endpoint: str
+ model_runtimes_endpoint: str
+ functional_endpoint: str
+ functional_detail_base: str
+ bot_governance_endpoint: str
+ access_notes: tuple[str, ...]
+ governance_notes: tuple[str, ...]
+
+
+class AdminSalesRevenueReportsPageView(BaseModel):
+ app_name: str
+ title: str
+ subtitle: str
+ environment: str
+ version: str
+ dashboard_href: str
+ sales_overview_endpoint: str
+ revenue_overview_endpoint: str
+ access_notes: tuple[str, ...]
+ reading_notes: tuple[str, ...]
+
+class AdminRentalReportsPageView(BaseModel):
+ app_name: str
+ title: str
+ subtitle: str
+ environment: str
+ version: str
+ dashboard_href: str
+ overview_endpoint: str
+ access_notes: tuple[str, ...]
+ reading_notes: tuple[str, ...]
+
+class AdminBotMonitoringPageView(BaseModel):
+ app_name: str
+ title: str
+ subtitle: str
+ environment: str
+ version: str
+ dashboard_href: str
+ bot_flow_overview_endpoint: str
+ telemetry_overview_endpoint: str
+ access_notes: tuple[str, ...]
+ reading_notes: tuple[str, ...]
diff --git a/tests/test_admin_panel_bot_monitoring_view.py b/tests/test_admin_panel_bot_monitoring_view.py
new file mode 100644
index 0000000..f4b5f38
--- /dev/null
+++ b/tests/test_admin_panel_bot_monitoring_view.py
@@ -0,0 +1,65 @@
+import unittest
+
+from fastapi.testclient import TestClient
+
+from admin_app.api.dependencies import get_optional_panel_staff_context
+from admin_app.app_factory import create_app
+from admin_app.core import AdminSettings, AuthenticatedStaffContext, AuthenticatedStaffPrincipal
+from shared.contracts import StaffRole
+
+
+def _build_panel_context(role: StaffRole = StaffRole.COLABORADOR) -> AuthenticatedStaffContext:
+ return AuthenticatedStaffContext(
+ principal=AuthenticatedStaffPrincipal(
+ id=41 if role == StaffRole.COLABORADOR else 42,
+ email="colaborador@empresa.com" if role == StaffRole.COLABORADOR else "diretor@empresa.com",
+ display_name="Equipe de Monitoramento do Bot",
+ role=role,
+ is_active=True,
+ ),
+ session_id=101,
+ )
+
+
+class AdminPanelBotMonitoringViewTests(unittest.TestCase):
+ def test_bot_monitoring_page_redirects_to_login_without_session(self):
+ app = create_app(AdminSettings(admin_api_prefix="/admin"))
+ client = TestClient(app)
+
+ response = client.get("/admin/panel/monitoramento/bot", follow_redirects=False)
+
+ self.assertEqual(response.status_code, 302)
+ self.assertTrue(response.headers["location"].endswith("/admin/login"))
+
+ def test_bot_monitoring_page_renders_for_colaborador_session(self):
+ app = create_app(AdminSettings(admin_app_name="Admin Interno", admin_version="1.4.0", admin_api_prefix="/admin"))
+ app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context(StaffRole.COLABORADOR)
+ client = TestClient(app)
+ try:
+ response = client.get("/admin/panel/monitoramento/bot")
+ finally:
+ app.dependency_overrides.clear()
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("Monitoramento operacional do bot", response.text)
+ self.assertIn('data-admin-bot-monitoring="true"', response.text)
+ self.assertIn('data-bot-flow-overview-endpoint="/admin/panel/reports/fluxo-bot/overview"', response.text)
+ self.assertIn('data-telemetry-overview-endpoint="/admin/panel/reports/telemetria-conversacional/overview"', response.text)
+ self.assertIn("Atualizar leitura", response.text)
+
+ def test_dashboard_exposes_bot_monitoring_screen_link(self):
+ app = create_app(AdminSettings(admin_api_prefix="/admin"))
+ app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context(StaffRole.DIRETOR)
+ client = TestClient(app)
+ try:
+ response = client.get("/admin/panel/admin")
+ finally:
+ app.dependency_overrides.clear()
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("/admin/panel/monitoramento/bot", response.text)
+ self.assertIn("Abrir monitoramento", response.text)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_admin_panel_rental_reports_view.py b/tests/test_admin_panel_rental_reports_view.py
new file mode 100644
index 0000000..2be14b6
--- /dev/null
+++ b/tests/test_admin_panel_rental_reports_view.py
@@ -0,0 +1,64 @@
+import unittest
+
+from fastapi.testclient import TestClient
+
+from admin_app.api.dependencies import get_optional_panel_staff_context
+from admin_app.app_factory import create_app
+from admin_app.core import AdminSettings, AuthenticatedStaffContext, AuthenticatedStaffPrincipal
+from shared.contracts import StaffRole
+
+
+def _build_panel_context(role: StaffRole = StaffRole.COLABORADOR) -> AuthenticatedStaffContext:
+ return AuthenticatedStaffContext(
+ principal=AuthenticatedStaffPrincipal(
+ id=31 if role == StaffRole.COLABORADOR else 32,
+ email="colaborador@empresa.com" if role == StaffRole.COLABORADOR else "diretor@empresa.com",
+ display_name="Equipe de Locacao Interna",
+ role=role,
+ is_active=True,
+ ),
+ session_id=97,
+ )
+
+
+class AdminPanelRentalReportsViewTests(unittest.TestCase):
+ def test_rental_reports_page_redirects_to_login_without_session(self):
+ app = create_app(AdminSettings(admin_api_prefix="/admin"))
+ client = TestClient(app)
+
+ response = client.get("/admin/panel/relatorios/locacao", follow_redirects=False)
+
+ self.assertEqual(response.status_code, 302)
+ self.assertTrue(response.headers["location"].endswith("/admin/login"))
+
+ def test_rental_reports_page_renders_for_colaborador_session(self):
+ app = create_app(AdminSettings(admin_app_name="Admin Interno", admin_version="1.4.0", admin_api_prefix="/admin"))
+ app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context(StaffRole.COLABORADOR)
+ client = TestClient(app)
+ try:
+ response = client.get("/admin/panel/relatorios/locacao")
+ finally:
+ app.dependency_overrides.clear()
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("Relatorios de locacao", response.text)
+ self.assertIn('data-admin-rental-reports="true"', response.text)
+ self.assertIn('data-rental-overview-endpoint="/admin/panel/reports/locacao/overview"', response.text)
+ self.assertIn("Atualizar leitura", response.text)
+
+ def test_dashboard_exposes_rental_reports_screen_link(self):
+ app = create_app(AdminSettings(admin_api_prefix="/admin"))
+ app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context(StaffRole.DIRETOR)
+ client = TestClient(app)
+ try:
+ response = client.get("/admin/panel/admin")
+ finally:
+ app.dependency_overrides.clear()
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("/admin/panel/relatorios/locacao", response.text)
+ self.assertIn("Abrir locacao", response.text)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_admin_panel_reports_web.py b/tests/test_admin_panel_reports_web.py
new file mode 100644
index 0000000..f5dbb83
--- /dev/null
+++ b/tests/test_admin_panel_reports_web.py
@@ -0,0 +1,118 @@
+import unittest
+
+from fastapi.testclient import TestClient
+
+from admin_app.api.dependencies import get_current_panel_staff_principal
+from admin_app.app_factory import create_app
+from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
+from shared.contracts import StaffRole
+
+
+class AdminPanelReportsWebTests(unittest.TestCase):
+ def _build_client_with_role(
+ self,
+ role: StaffRole,
+ settings: AdminSettings | None = None,
+ ) -> tuple[TestClient, object]:
+ app = create_app(
+ settings
+ or AdminSettings(
+ admin_auth_token_secret="test-secret",
+ admin_api_prefix="/admin",
+ )
+ )
+ app.dependency_overrides[get_current_panel_staff_principal] = lambda: AuthenticatedStaffPrincipal(
+ id=61,
+ email="colaborador@empresa.com" if role == StaffRole.COLABORADOR else "diretor@empresa.com",
+ display_name="Equipe de Relatorios Web",
+ role=role,
+ is_active=True,
+ )
+ return TestClient(app), app
+
+ def test_panel_reports_require_panel_session(self):
+ app = create_app(AdminSettings(admin_auth_token_secret="test-secret", admin_api_prefix="/admin"))
+ client = TestClient(app)
+
+ response = client.get("/admin/panel/reports/sales/overview")
+
+ self.assertEqual(response.status_code, 401)
+ self.assertEqual(response.json()["detail"], "Sessao administrativa web obrigatoria.")
+
+ def test_panel_sales_overview_is_available_for_colaborador(self):
+ client, app = self._build_client_with_role(StaffRole.COLABORADOR)
+ try:
+ response = client.get("/admin/panel/reports/sales/overview")
+ finally:
+ app.dependency_overrides.clear()
+
+ self.assertEqual(response.status_code, 200)
+ payload = response.json()
+ self.assertEqual(payload["mode"], "sales_contract_bootstrap")
+ self.assertEqual(payload["source_dataset_keys"], ["sales_orders"])
+ self.assertEqual(len(payload["reports"]), 4)
+ self.assertEqual(payload["materialization"]["sync_strategy"], "etl_incremental")
+
+ def test_panel_revenue_overview_is_available_for_colaborador(self):
+ client, app = self._build_client_with_role(StaffRole.COLABORADOR)
+ try:
+ response = client.get("/admin/panel/reports/arrecadacao/overview")
+ finally:
+ app.dependency_overrides.clear()
+
+ self.assertEqual(response.status_code, 200)
+ payload = response.json()
+ self.assertEqual(payload["area"], "arrecadacao")
+ self.assertEqual(payload["source_dataset_keys"], ["rental_payments"])
+ self.assertEqual(len(payload["reports"]), 3)
+ self.assertEqual(payload["materialization"]["storage_shape"], "snapshot_table")
+
+ def test_panel_rental_overview_is_available_for_colaborador(self):
+ client, app = self._build_client_with_role(StaffRole.COLABORADOR)
+ try:
+ response = client.get("/admin/panel/reports/locacao/overview")
+ finally:
+ app.dependency_overrides.clear()
+
+ self.assertEqual(response.status_code, 200)
+ payload = response.json()
+ self.assertEqual(payload["area"], "locacao")
+ self.assertEqual(payload["source_domain"], "rental")
+ self.assertEqual(payload["source_dataset_keys"], ["rental_fleet", "rental_contracts"])
+ self.assertEqual(len(payload["reports"]), 5)
+ self.assertEqual(payload["materialization"]["sync_strategy"], "etl_incremental")
+
+
+ def test_panel_bot_flow_overview_is_available_for_colaborador(self):
+ client, app = self._build_client_with_role(StaffRole.COLABORADOR)
+ try:
+ response = client.get("/admin/panel/reports/fluxo-bot/overview")
+ finally:
+ app.dependency_overrides.clear()
+
+ self.assertEqual(response.status_code, 200)
+ payload = response.json()
+ self.assertEqual(payload["area"], "fluxo_bot")
+ self.assertEqual(payload["source_domain"], "conversation")
+ self.assertEqual(payload["source_dataset_keys"], ["conversation_turns"])
+ self.assertEqual(len(payload["reports"]), 5)
+ self.assertEqual(payload["materialization"]["sync_strategy"], "etl_incremental")
+
+ def test_panel_conversation_telemetry_overview_is_available_for_colaborador(self):
+ client, app = self._build_client_with_role(StaffRole.COLABORADOR)
+ try:
+ response = client.get("/admin/panel/reports/telemetria-conversacional/overview")
+ finally:
+ app.dependency_overrides.clear()
+
+ self.assertEqual(response.status_code, 200)
+ payload = response.json()
+ self.assertEqual(payload["area"], "telemetria_conversacional")
+ self.assertEqual(payload["source_domain"], "conversation")
+ self.assertEqual(payload["source_dataset_keys"], ["conversation_turns"])
+ self.assertEqual(len(payload["reports"]), 5)
+ self.assertEqual(payload["materialization"]["sync_strategy"], "etl_incremental")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_admin_panel_sales_revenue_reports_view.py b/tests/test_admin_panel_sales_revenue_reports_view.py
new file mode 100644
index 0000000..3c7c2e9
--- /dev/null
+++ b/tests/test_admin_panel_sales_revenue_reports_view.py
@@ -0,0 +1,65 @@
+import unittest
+
+from fastapi.testclient import TestClient
+
+from admin_app.api.dependencies import get_optional_panel_staff_context
+from admin_app.app_factory import create_app
+from admin_app.core import AdminSettings, AuthenticatedStaffContext, AuthenticatedStaffPrincipal
+from shared.contracts import StaffRole
+
+
+def _build_panel_context(role: StaffRole = StaffRole.COLABORADOR) -> AuthenticatedStaffContext:
+ return AuthenticatedStaffContext(
+ principal=AuthenticatedStaffPrincipal(
+ id=27 if role == StaffRole.COLABORADOR else 28,
+ email="colaborador@empresa.com" if role == StaffRole.COLABORADOR else "diretor@empresa.com",
+ display_name="Equipe Comercial Interna",
+ role=role,
+ is_active=True,
+ ),
+ session_id=92,
+ )
+
+
+class AdminPanelSalesRevenueReportsViewTests(unittest.TestCase):
+ def test_sales_revenue_reports_page_redirects_to_login_without_session(self):
+ app = create_app(AdminSettings(admin_api_prefix="/admin"))
+ client = TestClient(app)
+
+ response = client.get("/admin/panel/relatorios/vendas-arrecadacao", follow_redirects=False)
+
+ self.assertEqual(response.status_code, 302)
+ self.assertTrue(response.headers["location"].endswith("/admin/login"))
+
+ def test_sales_revenue_reports_page_renders_for_colaborador_session(self):
+ app = create_app(AdminSettings(admin_app_name="Admin Interno", admin_version="1.4.0", admin_api_prefix="/admin"))
+ app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context(StaffRole.COLABORADOR)
+ client = TestClient(app)
+ try:
+ response = client.get("/admin/panel/relatorios/vendas-arrecadacao")
+ finally:
+ app.dependency_overrides.clear()
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("Relatorios de vendas e arrecadacao", response.text)
+ self.assertIn('data-admin-sales-revenue-reports="true"', response.text)
+ self.assertIn('data-sales-overview-endpoint="/admin/panel/reports/sales/overview"', response.text)
+ self.assertIn('data-revenue-overview-endpoint="/admin/panel/reports/arrecadacao/overview"', response.text)
+ self.assertIn("Atualizar leitura", response.text)
+
+ def test_dashboard_exposes_sales_revenue_reports_screen_link(self):
+ app = create_app(AdminSettings(admin_api_prefix="/admin"))
+ app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context(StaffRole.DIRETOR)
+ client = TestClient(app)
+ try:
+ response = client.get("/admin/panel/admin")
+ finally:
+ app.dependency_overrides.clear()
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("/admin/panel/relatorios/vendas-arrecadacao", response.text)
+ self.assertIn("Abrir relatorios", response.text)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_admin_panel_system_configuration_web.py b/tests/test_admin_panel_system_configuration_web.py
new file mode 100644
index 0000000..68403f1
--- /dev/null
+++ b/tests/test_admin_panel_system_configuration_web.py
@@ -0,0 +1,82 @@
+import unittest
+
+from fastapi.testclient import TestClient
+
+from admin_app.api.dependencies import get_optional_panel_staff_context
+from admin_app.app_factory import create_app
+from admin_app.core import AdminSettings, AuthenticatedStaffContext, AuthenticatedStaffPrincipal
+from shared.contracts import StaffRole
+
+
+def _build_panel_context(role: StaffRole = StaffRole.COLABORADOR) -> AuthenticatedStaffContext:
+ return AuthenticatedStaffContext(
+ principal=AuthenticatedStaffPrincipal(
+ id=17 if role == StaffRole.COLABORADOR else 18,
+ email="colaborador@empresa.com" if role == StaffRole.COLABORADOR else "diretor@empresa.com",
+ display_name="Equipe de Configuracao",
+ role=role,
+ is_active=True,
+ ),
+ session_id=91,
+ )
+
+
+class AdminPanelSystemConfigurationWebTests(unittest.TestCase):
+ def test_system_configuration_page_redirects_to_login_without_session(self):
+ app = create_app(AdminSettings(admin_api_prefix="/admin"))
+ client = TestClient(app)
+
+ response = client.get("/admin/panel/sistema/configuracoes", follow_redirects=False)
+
+ self.assertEqual(response.status_code, 302)
+ self.assertTrue(response.headers["location"].endswith("/admin/login"))
+
+ def test_system_configuration_page_renders_for_colaborador_session(self):
+ app = create_app(AdminSettings(admin_app_name="Admin Interno", admin_version="1.4.0", admin_api_prefix="/admin"))
+ app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context(StaffRole.COLABORADOR)
+ client = TestClient(app)
+ try:
+ response = client.get("/admin/panel/sistema/configuracoes")
+ finally:
+ app.dependency_overrides.clear()
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("Configuracoes do sistema", response.text)
+ self.assertIn('data-admin-system-configuration="true"', response.text)
+ self.assertIn('data-functional-endpoint="/admin/system/configuration/functional"', response.text)
+ self.assertIn('data-bot-governance-endpoint="/admin/system/configuration/functional/bot-governance"', response.text)
+ self.assertIn('data-runtime-endpoint="/admin/system/configuration/runtime"', response.text)
+ self.assertIn('data-security-endpoint="/admin/system/configuration/security"', response.text)
+ self.assertIn('data-model-runtimes-endpoint="/admin/system/configuration/model-runtimes"', response.text)
+ self.assertIn("Atualizar leitura", response.text)
+
+ def test_dashboard_exposes_system_configuration_screen_link(self):
+ app = create_app(AdminSettings(admin_api_prefix="/admin"))
+ app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context(StaffRole.DIRETOR)
+ client = TestClient(app)
+ try:
+ response = client.get("/admin/panel/admin")
+ finally:
+ app.dependency_overrides.clear()
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("/admin/panel/sistema/configuracoes", response.text)
+ self.assertIn("Abrir configuracoes", response.text)
+
+ def test_system_configuration_page_renders_for_director_session(self):
+ app = create_app(AdminSettings(admin_api_prefix="/admin"))
+ app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context(StaffRole.DIRETOR)
+ client = TestClient(app)
+ try:
+ response = client.get("/admin/panel/sistema/configuracoes")
+ finally:
+ app.dependency_overrides.clear()
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIn('data-overview-endpoint="/admin/system/configuration"', response.text)
+ self.assertIn('data-functional-detail-base="/admin/system/configuration/functional"', response.text)
+ self.assertIn("Somente leitura", response.text)
+
+
+if __name__ == "__main__":
+ unittest.main()