From d6e765ce3c959aa9e1356346e89a99360c12b102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vitor=20Hugo=20Belorio=20Sim=C3=A3o?= Date: Mon, 30 Mar 2026 15:45:22 -0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(admin):=20concluir=20telas=20d?= =?UTF-8?q?a=20fase=204=20no=20painel=20interno?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entrega as telas de configuracoes do sistema, relatorios comerciais, locacao e monitoramento operacional do bot dentro da sessao web do admin, com navegacao integrada ao dashboard e carregamento real pela sessao do painel. Tambem simplifica a linguagem das superficies, remove detalhes tecnicos desnecessarios para o usuario, corrige o ponto quebrado que abria contrato em JSON bruto e ajusta grids, cards e quebra de conteudo para melhorar a leitura nas telas da fase 4. --- admin_app/view/rendering.py | 512 +++++++++++++ admin_app/view/router.py | 327 ++++++++- admin_app/view/static/scripts/panel.js | 686 ++++++++++++++++++ admin_app/view/static/styles/panel.css | 157 +++- admin_app/view/view_models.py | 54 ++ tests/test_admin_panel_bot_monitoring_view.py | 65 ++ tests/test_admin_panel_rental_reports_view.py | 64 ++ tests/test_admin_panel_reports_web.py | 118 +++ ..._admin_panel_sales_revenue_reports_view.py | 65 ++ ...st_admin_panel_system_configuration_web.py | 82 +++ 10 files changed, 2116 insertions(+), 14 deletions(-) create mode 100644 tests/test_admin_panel_bot_monitoring_view.py create mode 100644 tests/test_admin_panel_rental_reports_view.py create mode 100644 tests/test_admin_panel_reports_web.py create mode 100644 tests/test_admin_panel_sales_revenue_reports_view.py create mode 100644 tests/test_admin_panel_system_configuration_web.py 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

Catalogo ainda nao carregado

Atualize a leitura para montar a superficie funcional desta etapa.

+

Governanca do bot

Campos sob governanca administrativa

Os ajustes do atendimento aparecem aqui de forma agrupada e clara.

Parent config keys
Aguardando

Governanca ainda nao carregada

Os campos governados pelo bot aparecem aqui depois da primeira leitura.

+
+
+

Runtime administrativo

Informacoes essenciais do painel

Mostra apenas o contexto util para a operacao.

Runtime aguardando leitura

Esta area e carregada conforme a permissao da sessao atual.

+

Postura de seguranca

Regras visiveis de senha e sessao

Exibe somente o necessario para orientar o uso da sessao.

Seguranca aguardando leitura

A sessao atual precisa de permissao elevada para ver este snapshot.

+
+
+

Separacao de runtime

Modelos do atendimento versus geracao de tools

Aqui fica clara a separacao entre atendimento e geracao de tools.

Perfis de runtime aguardando leitura

A superficie completa aparece quando a sessao pode consultar manage_settings.

+

Fontes do snapshot

De onde cada configuracao vem

Resumo das bases usadas para montar esta tela.

Fontes aguardando leitura

As fontes completas entram quando a sessao pode consultar o overview tecnico.

+
+
+
+
+ + + + +''' + +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.

+
+
+

Vendas aguardando leitura

Clique em atualizar leitura para carregar o snapshot de vendas.

+
+
+
+
+
Proximas melhorias
+
+
+
+
+
+
+
+
+
+

Arrecadacao

+

O que acompanhar em arrecadacao

+

Pagamentos liquidados, valor arrecadado e conciliacao por contrato.

+
+
+

Arrecadacao aguardando leitura

Clique em atualizar leitura para carregar o snapshot de arrecadacao.

+
+
+
+
+
Proximas melhorias
+
+
+
+
+
+
+
+
+
+ + + + +''' + +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.

+
+
+

Locacao aguardando leitura

Clique em atualizar leitura para montar o snapshot desta superficie.

+
+
+
+
Proximas melhorias
+
+
+
+
+
+
+
+
+
+

Relatorios desta etapa

+

Relatorios disponiveis nesta etapa

+

Disponibilidade de frota, lifecycle de contratos, devolucoes em atraso, ocupacao e receita prevista versus final.

+
+
+

Catalogo aguardando leitura

Os cards de relatorio aparecem aqui depois da primeira leitura do painel.

+
+
+
+
+
+
+
+
+ + + + +''' + +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.

+
+
+

Fluxo aguardando leitura

Clique em atualizar leitura para carregar o snapshot operacional do bot.

+
+
+
+
+
Proximas melhorias
+
+
+
+
+
+
+
+
+
+

Telemetria conversacional

+

Saude do atendimento

+

Volume, latencia, distribuicao por dominio e sinais de saude da conversa.

+
+
+

Telemetria aguardando leitura

Clique em atualizar leitura para carregar o snapshot conversacional.

+
+
+
+
+
Proximas melhorias
+
+
+
+
+
+
+
+
+
+ + + + +''' 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("") + : `

Nenhuma configuracao encontrada

O catalogo funcional nao retornou itens nesta leitura.

`; + } + + 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("") + : `

Nenhum campo governado encontrado

A governanca do bot nao retornou itens nesta leitura.

`; + } + + 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("")}
` + : `

Nenhum perfil retornado

A separacao de runtime nao retornou perfis nesta leitura.

`; + } + + 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("") + : `

Nenhuma fonte encontrada

O overview tecnico nao retornou fontes nesta leitura.

`; + } + + 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 = `

${escapeHtml(title)}

${escapeHtml(message)}

`; + } + + 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("")}
` + : `

Metricas nao disponiveis

O overview nao retornou metricas nesta leitura.

`; + + 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("") + : `

Nenhum relatorio previsto

O overview nao retornou relatorios para este dominio.

`; + + nextStepsTarget.innerHTML = nextSteps.length > 0 + ? nextSteps.map((item) => `
${escapeHtml(item)}
`).join("") + : `

Sem proximos passos

Nenhuma orientacao adicional foi retornada para este overview.

`; + } + + function renderLockedState(container, title, message) { + container.innerHTML = `

${escapeHtml(title)}

${escapeHtml(message)}

`; + } + + 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("")}
` + : `

Metricas nao disponiveis

O overview de locacao nao retornou metricas nesta leitura.

`; + + 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("") + : `

Nenhum relatorio previsto

O overview nao retornou relatorios de locacao nesta leitura.

`; + + nextSteps.innerHTML = plannedSteps.length > 0 + ? plannedSteps.map((item) => `
${escapeHtml(item)}
`).join("") + : `

Sem proximos passos

Nenhuma orientacao adicional foi retornada para locacao.

`; + + 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 = `

Locacao indisponivel

${escapeHtml(result.message || "Nao foi possivel carregar o overview de locacao.")}

`; + 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("")}
` + : `

Metricas nao disponiveis

O overview nao retornou metricas nesta leitura.

`; + + 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("") + : `

Nenhum relatorio previsto

O overview nao retornou relatorios para este dominio.

`; + + nextStepsTarget.innerHTML = nextSteps.length > 0 + ? nextSteps.map((item) => `
${escapeHtml(item)}
`).join("") + : `

Sem proximos passos

Nenhuma orientacao adicional foi retornada para este overview.

`; + } + + function renderLockedState(container, title, message) { + container.innerHTML = `

${escapeHtml(title)}

${escapeHtml(message)}

`; + } + + 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()