diff --git a/client/src/pages/XosCentral.tsx b/client/src/pages/XosCentral.tsx index c2db7f4..5f34f78 100644 --- a/client/src/pages/XosCentral.tsx +++ b/client/src/pages/XosCentral.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Link } from "wouter"; import { BrowserFrame } from "@/components/Browser/BrowserFrame"; import { @@ -13,6 +13,8 @@ import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; interface XosStats { total_contacts: number; @@ -66,6 +68,68 @@ interface Activity { export default function XosCentral() { const [activeTab, setActiveTab] = useState("dashboard"); + const [isNewContactOpen, setIsNewContactOpen] = useState(false); + const [newContact, setNewContact] = useState({ name: "", email: "", phone: "", company: "", position: "" }); + const [isNewActivityOpen, setIsNewActivityOpen] = useState(false); + const [newActivity, setNewActivity] = useState({ type: "task", title: "", description: "", due_at: "", priority: "normal" }); + const [isNewSelectorOpen, setIsNewSelectorOpen] = useState(false); + const [isNewDealOpen, setIsNewDealOpen] = useState(false); + const [newDeal, setNewDeal] = useState({ title: "", pipeline_id: "1", stage_id: "1", value: "" }); + const queryClient = useQueryClient(); + + const createContactMutation = useMutation({ + mutationFn: async (data: typeof newContact) => { + const res = await fetch("/api/xos/contacts", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (!res.ok) throw new Error("Erro ao criar contato"); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/xos/contacts"] }); + queryClient.invalidateQueries({ queryKey: ["/api/xos/stats"] }); + setIsNewContactOpen(false); + setNewContact({ name: "", email: "", phone: "", company: "", position: "" }); + }, + }); + + const createDealMutation = useMutation({ + mutationFn: async (data: typeof newDeal) => { + const res = await fetch("/api/xos/deals", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (!res.ok) throw new Error("Erro ao criar negócio"); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/xos/deals"] }); + queryClient.invalidateQueries({ queryKey: ["/api/xos/stats"] }); + setIsNewDealOpen(false); + setNewDeal({ title: "", pipeline_id: "1", stage_id: "1", value: "" }); + }, + }); + + const createActivityMutation = useMutation({ + mutationFn: async (data: typeof newActivity) => { + const res = await fetch("/api/xos/activities", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (!res.ok) throw new Error("Erro ao criar atividade"); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/xos/activities"] }); + queryClient.invalidateQueries({ queryKey: ["/api/xos/stats"] }); + setIsNewActivityOpen(false); + setNewActivity({ type: "task", title: "", description: "", due_at: "", priority: "normal" }); + }, + }); const { data: stats } = useQuery({ queryKey: ["/api/xos/stats"], @@ -177,7 +241,7 @@ export default function XosCentral() { )} - @@ -406,7 +470,7 @@ export default function XosCentral() { Filtros - @@ -478,7 +542,7 @@ export default function XosCentral() {
Atividades - @@ -520,6 +584,175 @@ export default function XosCentral() {
+ + {/* Seletor: o que deseja criar? */} + + + + O que deseja criar? + +
+ + + +
+
+
+ + {/* Dialog Novo Negócio */} + + + + Novo Negócio + +
+
+ + setNewDeal({ ...newDeal, title: e.target.value })} /> +
+
+ + +
+
+ + setNewDeal({ ...newDeal, value: e.target.value })} /> +
+
+ + +
+
+
+
+ + + + + Novo Contato + +
+
+ + setNewContact({ ...newContact, name: e.target.value })} /> +
+
+ + setNewContact({ ...newContact, email: e.target.value })} /> +
+
+ + setNewContact({ ...newContact, phone: e.target.value })} /> +
+
+ + setNewContact({ ...newContact, company: e.target.value })} /> +
+
+ + setNewContact({ ...newContact, position: e.target.value })} /> +
+
+ + +
+
+
+
+ + + + + Nova Atividade + +
+
+ + +
+
+ + setNewActivity({ ...newActivity, title: e.target.value })} /> +
+
+ + setNewActivity({ ...newActivity, description: e.target.value })} /> +
+
+ + setNewActivity({ ...newActivity, due_at: e.target.value })} /> +
+
+ + +
+
+ + +
+
+
+
); } diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 8249874..17af50f 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -78,7 +78,7 @@ services: ERPNEXT_API_KEY: ${ERPNEXT_API_KEY:-} ERPNEXT_API_SECRET: ${ERPNEXT_API_SECRET:-} healthcheck: - test: ["CMD-SHELL", "wget -qO- http://localhost:5000/api/health || exit 1"] + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5000/api/health || exit 1"] interval: 30s timeout: 10s retries: 5 @@ -372,6 +372,11 @@ services: volumes: - ./docker/superset/superset_config.py:/app/pythonpath/superset_config.py:ro - ./docker/superset/init.sh:/app/docker/init.sh:ro + - ./docker/superset/images/arcadia_logo.png:/app/superset/static/assets/images/arcadia_logo.png:ro + - ./docker/superset/images/arcadia_logo_dark.png:/app/superset/static/assets/images/arcadia_logo_dark.png:ro + - ./docker/superset/images/superset-logo-horiz.png:/app/superset/static/assets/images/superset-logo-horiz.png:ro + - ./docker/superset/images/favicon.png:/app/superset/static/assets/images/favicon.png:ro + - ./docker/superset/images/arcadia_favicon.png:/app/superset/static/assets/images/arcadia_favicon.png:ro - superset_home:/app/superset_home depends_on: db: diff --git a/docker/superset/images/arcadia_favicon.png b/docker/superset/images/arcadia_favicon.png new file mode 100644 index 0000000..bf2d785 Binary files /dev/null and b/docker/superset/images/arcadia_favicon.png differ diff --git a/docker/superset/images/arcadia_logo.png b/docker/superset/images/arcadia_logo.png new file mode 100644 index 0000000..82ea02d Binary files /dev/null and b/docker/superset/images/arcadia_logo.png differ diff --git a/docker/superset/images/arcadia_logo_dark.png b/docker/superset/images/arcadia_logo_dark.png new file mode 100644 index 0000000..bba0459 Binary files /dev/null and b/docker/superset/images/arcadia_logo_dark.png differ diff --git a/docker/superset/images/favicon.png b/docker/superset/images/favicon.png new file mode 100644 index 0000000..bf2d785 Binary files /dev/null and b/docker/superset/images/favicon.png differ diff --git a/docker/superset/images/superset-logo-horiz.png b/docker/superset/images/superset-logo-horiz.png new file mode 100644 index 0000000..2e2c469 Binary files /dev/null and b/docker/superset/images/superset-logo-horiz.png differ diff --git a/docker/superset/superset_config.py b/docker/superset/superset_config.py index 61bacbf..b26d314 100644 --- a/docker/superset/superset_config.py +++ b/docker/superset/superset_config.py @@ -46,6 +46,8 @@ SQLLAB_ASYNC_TIME_LIMIT_SEC = 300 # ── Branding Arcádia ────────────────────────────────────────────────────────── APP_NAME = "Arcádia Insights" +APP_ICON = "/static/assets/images/arcadia_logo.png" +APP_ICON_WIDTH = 150 LOGO_TARGET_PATH = "/" FAVICONS = [{"href": "/static/assets/images/favicon.png"}] diff --git a/server/superset/routes.ts b/server/superset/routes.ts index be2e9ec..75d4446 100644 --- a/server/superset/routes.ts +++ b/server/superset/routes.ts @@ -69,10 +69,19 @@ export function registerSupersetRoutes(app: Express): void { // Resolve slug → dashboard ID → embedded UUID // Enviar session cookie junto com o Bearer JWT para que Flask-Login identifique // o admin via user_loader (necessário quando o papel Public tem can_read on Dashboard) - const dashResp = await fetch(`${SUPERSET_URL}/api/v1/dashboard/${dashboardId}`, { + let dashResp = await fetch(`${SUPERSET_URL}/api/v1/dashboard/${dashboardId}`, { headers: { "Authorization": `Bearer ${token}`, "Cookie": session }, signal: AbortSignal.timeout(FETCH_TIMEOUT), }); + // Token pode ter expirado — limpa cache e tenta uma vez com credenciais frescas + if (!dashResp.ok) { + clearTokenCache(); + const fresh = await getServiceToken(); + dashResp = await fetch(`${SUPERSET_URL}/api/v1/dashboard/${dashboardId}`, { + headers: { "Authorization": `Bearer ${fresh.token}`, "Cookie": fresh.session }, + signal: AbortSignal.timeout(FETCH_TIMEOUT), + }); + } if (!dashResp.ok) { return res.status(404).json({ error: `Dashboard '${dashboardId}' não encontrado no Superset` }); }