From 75ef36eac9dc1166f232eca5e3d5e53613cb558f Mon Sep 17 00:00:00 2001 From: root Date: Tue, 17 Mar 2026 16:48:45 -0300 Subject: [PATCH] feat: SOE Rule Engine + Superset Insights (cherry-pick f0c3ce4) - SOE event-bus, handlers contabil/financial - SOE rule-engine: tipos, fiscal, business, index - SupersetDashboard component - server/erp/routes atualizado - shared/schema: tabelas SOE (soe_regras, soe_eventos, soe_lancamentos) - server/routes: remove Metabase, consolida Superset routes - package.json: adiciona @superset-ui/embedded-sdk Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 10 +- client/src/App.tsx | 6 +- client/src/components/SupersetDashboard.tsx | 160 +++++++++++++ docker-compose.yml | 27 ++- package.json | 1 + server/erp/routes.ts | 62 ++++- server/routes.ts | 11 +- server/soe/event-bus.ts | 112 +++++++++ server/soe/handlers/contabil-handler.ts | 182 +++++++++++++++ server/soe/handlers/financial-handler.ts | 128 +++++++++++ server/soe/rule-engine/business-rules.ts | 72 ++++++ server/soe/rule-engine/fiscal-rules.ts | 150 ++++++++++++ server/soe/rule-engine/index.ts | 240 ++++++++++++++++++++ server/soe/rule-engine/types.ts | 47 ++++ shared/schema.ts | 67 ++++++ 15 files changed, 1257 insertions(+), 18 deletions(-) create mode 100644 client/src/components/SupersetDashboard.tsx create mode 100644 server/soe/event-bus.ts create mode 100644 server/soe/handlers/contabil-handler.ts create mode 100644 server/soe/handlers/financial-handler.ts create mode 100644 server/soe/rule-engine/business-rules.ts create mode 100644 server/soe/rule-engine/fiscal-rules.ts create mode 100644 server/soe/rule-engine/index.ts create mode 100644 server/soe/rule-engine/types.ts diff --git a/.env.example b/.env.example index 656b051..bcc19a1 100644 --- a/.env.example +++ b/.env.example @@ -68,9 +68,15 @@ PLUS_URL=http://localhost:8080 PLUS_PORT=8080 PLUS_API_TOKEN= -# ── Superset (BI avançado) ──────────────────────────────────────────────────── -SUPERSET_SECRET_KEY=troque-por-string-aleatoria-segura +# ── Superset (BI avançado — Arcádia Insights) ──────────────────────────────── +SUPERSET_HOST=superset SUPERSET_PORT=8088 +SUPERSET_SECRET_KEY=troque-por-string-aleatoria-segura # openssl rand -hex 32 +SUPERSET_ADMIN_USER=admin +SUPERSET_ADMIN_EMAIL=admin@arcadia.app +SUPERSET_ADMIN_PASSWORD=troque-senha-forte-em-prod +# URL do banco Arcádia para o Superset acessar os dados (idealmente read-only) +ARCADIA_DATABASE_URL=postgresql://arcadia:arcadia123@db:5432/arcadia # ── Redis ───────────────────────────────────────────────────────────────────── REDIS_URL=redis://localhost:6379 diff --git a/client/src/App.tsx b/client/src/App.tsx index dc3176f..57a89c0 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,5 +1,5 @@ -import { Switch, Route } from "wouter"; -import { lazy, Suspense } from "react"; +import { Switch, Route, useLocation } from "wouter"; +import { lazy, Suspense, useEffect } from "react"; import { queryClient } from "./lib/queryClient"; import { QueryClientProvider } from "@tanstack/react-query"; import { Toaster } from "@/components/ui/toaster"; @@ -124,7 +124,7 @@ function Router() { - + { const [, nav] = useLocation(); useEffect(() => nav("/soe"), []); return null; }} /> diff --git a/client/src/components/SupersetDashboard.tsx b/client/src/components/SupersetDashboard.tsx new file mode 100644 index 0000000..c465bce --- /dev/null +++ b/client/src/components/SupersetDashboard.tsx @@ -0,0 +1,160 @@ +import { useEffect, useRef, useState, useCallback } from "react"; +import { Loader2, AlertTriangle, ExternalLink, RefreshCw } from "lucide-react"; + +interface SupersetDashboardProps { + dashboardId: string; + height?: string | number; + className?: string; + showOpenLink?: boolean; +} + +/** + * Componente reutilizável para embedar dashboards do Apache Superset (Arcádia Insights) + * em QUALQUER tela do Arcádia Suite via Guest Token JWT. + * + * Uso: + * + * + */ +export function SupersetDashboard({ + dashboardId, + height = "calc(100vh - 320px)", + className = "", + showOpenLink = true, +}: SupersetDashboardProps) { + const containerRef = useRef(null); + const sdkRef = useRef(null); + const [status, setStatus] = useState<"loading" | "ready" | "error" | "unavailable">("loading"); + const [errorMsg, setErrorMsg] = useState(""); + + const fetchGuestToken = useCallback(async (): Promise => { + const resp = await fetch("/api/superset/guest-token", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ dashboardId }), + credentials: "include", + }); + if (!resp.ok) { + const err = await resp.json().catch(() => ({ error: "Erro desconhecido" })); + throw new Error(err.error || `HTTP ${resp.status}`); + } + const { token } = await resp.json(); + return token; + }, [dashboardId]); + + const loadDashboard = useCallback(async () => { + if (!containerRef.current) return; + setStatus("loading"); + setErrorMsg(""); + + try { + // Verifica se Superset está disponível + const health = await fetch("/api/superset/health", { credentials: "include" }); + const healthData = await health.json(); + if (!healthData.online) { + setStatus("unavailable"); + return; + } + + // Importa SDK dinamicamente (só quando necessário) + const { embedDashboard } = await import("@superset-ui/embedded-sdk"); + + // Limpa embedding anterior se houver + if (sdkRef.current) { + containerRef.current.innerHTML = ""; + } + + sdkRef.current = await embedDashboard({ + id: dashboardId, + supersetDomain: window.location.origin + "/superset", + mountPoint: containerRef.current, + fetchGuestToken, + dashboardUiConfig: { + hideTitle: true, + hideChartControls: false, + filters: { visible: true, expanded: false }, + }, + }); + + setStatus("ready"); + } catch (err: any) { + console.error("[SupersetDashboard] Erro:", err.message); + // Se SDK não estiver instalado, usa iframe como fallback + if (err.message?.includes("Cannot find module") || err.message?.includes("embedDashboard")) { + setStatus("unavailable"); + } else { + setErrorMsg(err.message); + setStatus("error"); + } + } + }, [dashboardId, fetchGuestToken]); + + useEffect(() => { + loadDashboard(); + return () => { + if (containerRef.current) containerRef.current.innerHTML = ""; + }; + }, [loadDashboard]); + + const heightStyle = typeof height === "number" ? `${height}px` : height; + + if (status === "unavailable") { + return ( +
+