// BinGest dashboard mock — used in hero (compact) and demo tour (full). // Includes data-hotspot anchors so a parent can render highlight overlays. const DM_NAV = [ { id: "dashboard", label: "Dashboard", Icon: IconDashboard, active: true }, { id: "clientes", label: "Clientes", Icon: IconUsers }, { id: "folha", label: "Folha de ponto", Icon: IconClock }, { id: "obras", label: "Obras", Icon: IconBuilding, sub: "Condomínio das Acácias" }, { id: "servicos", label: "Serviços", Icon: IconClipboard }, { id: "equipe", label: "Equipe", Icon: IconTeam }, { id: "financeiro", label: "Financeiro", Icon: IconWallet }, { id: "relatorios", label: "Relatórios", Icon: IconReport }, { id: "config", label: "Configurações", Icon: IconGear }]; const DM_TABS = ["Consolidado", "Somente Obras", "Somente Assistência Técnica", "Por Cliente"]; function brl(n) { return "R$ " + n.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } // Animated currency that ticks up to the target on mount or when value changes. function TickNumber({ value, prefix = "R$ ", suffix = "", duration = 1200, decimals = 2 }) { const [n, setN] = React.useState(0); const startedRef = React.useRef(false); React.useEffect(() => { let raf, t0; const from = 0,to = value; const step = (t) => { if (!t0) t0 = t; const p = Math.min(1, (t - t0) / duration); const eased = 1 - Math.pow(1 - p, 3); setN(from + (to - from) * eased); if (p < 1) raf = requestAnimationFrame(step); }; raf = requestAnimationFrame(step); return () => cancelAnimationFrame(raf); }, [value]); const formatted = decimals ? n.toLocaleString("pt-BR", { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) : Math.round(n).toLocaleString("pt-BR"); return {prefix}{formatted}{suffix}; } // Bar chart — last 6 months: Receita Obras (blue), Custo Direto (amber), Receita AT (purple), Resultado Op (green) function MiniBarChart({ height = 180 }) { const months = ["dez. 25", "jan. 26", "fev. 26", "mar. 26", "abr. 26", "mai. 26"]; const obras = [3200, 0, 5800, 0, 7800, 19500]; const at = [0, 2400, 0, 1800, 0, 0]; const custo = [-1100, -800, -2200, -700, -2800, -8210]; const resOp = [2100, 1600, 3600, 1100, 5000, 11290]; const all = [...obras, ...at, ...resOp]; const max = Math.max(...all); const w = 560,h = height; const gx = 36,gy = 18; const innerW = w - gx - 8,innerH = h - gy - 24; const groupW = innerW / months.length; const barW = 8; const colors = { obras: "#2A5BFF", at: "#7C5BD8", custo: "#F59E0B", res: "#16A34A" }; const scale = (v) => v / max * innerH; const yTicks = [0, 5000, 10000, 15000, 20000]; return ( {yTicks.map((t, i) => { const y = h - gy - t / max * innerH; return ( R${t / 1000}k ); })} {months.map((m, i) => { const cx = gx + groupW * (i + 0.5); const vals = [ { v: custo[i], color: colors.custo, off: -1.5 }, { v: at[i], color: colors.at, off: -0.5 }, { v: obras[i], color: colors.obras, off: 0.5 }, { v: resOp[i], color: colors.res, off: 1.5 }]; return ( {vals.map((b, k) => { const v = Math.abs(b.v); const bh = scale(v); const x = cx + b.off * (barW + 1) - barW / 2; const y = h - gy - bh; return v === 0 ? null : ; })} {m} ); })} ); } function DashboardMock({ mode = "full", highlight = null, animateValues = true, scale = 1 }) { const rootRef = React.useRef(null); const [box, setBox] = React.useState(null); // Compute highlight bbox relative to mock root React.useEffect(() => { if (!highlight || !rootRef.current) {setBox(null);return;} const el = rootRef.current.querySelector(`[data-hotspot="${highlight}"]`); if (!el) {setBox(null);return;} const rect = el.getBoundingClientRect(); const root = rootRef.current.getBoundingClientRect(); setBox({ top: rect.top - root.top - 6, left: rect.left - root.left - 6, width: rect.width + 12, height: rect.height + 12 }); }, [highlight, mode]); // Recompute on resize React.useEffect(() => { if (!highlight || !rootRef.current) return; const ro = new ResizeObserver(() => { const el = rootRef.current.querySelector(`[data-hotspot="${highlight}"]`); if (!el) return; const rect = el.getBoundingClientRect(); const root = rootRef.current.getBoundingClientRect(); setBox({ top: rect.top - root.top - 6, left: rect.left - root.left - 6, width: rect.width + 12, height: rect.height + 12 }); }); ro.observe(rootRef.current); return () => ro.disconnect(); }, [highlight]); return (
{/* Sidebar */} {/* Main */}
BinGest Construções
BG bingestapp@bingest.com

Olá, Fulano

BinGest Construções · quarta-feira, 13 de maio

{DM_TABS.map((t, i) =>
{t}
)}
{/* KPIs */}
Obras Ativas
1
Funcionários Ativos
2
Receita Bruta Total
{animateValues ? : "R$ 19.500,00"}
maio de 2026
Resultado Operacional
{animateValues ? : "R$ 11.290,00"}
57,9%
{/* Soft cards */}
Obras
R$ 19.500,00
Margem bruta 57.9%
Assistência Técnica
R$ 0,00
Margem bruta 0.0%
{/* DRE + Chart */}
DRE Consolidado % Rec. Valor
Receita Bruta Total R$ 19.500,00
Obras R$ 19.500,00
(−) Custos Diretos Totais −42.1% −R$ 8.210,00
Equipe Obras −R$ 8.210,00
= Lucro Bruto 57.9% R$ 11.290,00
= Resultado Operacional 57.9% R$ 11.290,00
Evolução — últimos 6 meses
Custo Dir. Total Rec. AT Rec. Obras Resultado Op.
{/* Per-obra */}
Resultado por Obra — Maio de 2026
Condomínio das Acácias R$ 19.500,00 −R$ 8.210,00 R$ 11.290,00
{/* Highlight overlay */} {box &&
!
}
); } Object.assign(window, { DashboardMock, TickNumber, MiniBarChart, brl });