/* ============================================================ SolarProVision — Painel de Acompanhamento ============================================================ Sistema SEPARADO do Solar Pro System, mas INTEGRADO no backend (mesmo Cloudflare Worker + Baserow). Dois papéis acessam este painel: 1. gestor_usina (dono da usina) → vê produção, créditos enviados, saldos da SUA usina → relatórios e gráficos mensais → lista de beneficiários (rateio) da usina 2. beneficiario (cliente com desconto) → vê SEU consumo mensal, créditos recebidos → saldo acumulado e gráficos pessoais → acompanha trâmites: cadastro aprovado, entrada em rateio, etc. ARQUITETURA: - Single Page React App, JSX transpilado pelo Babel Standalone - Mesmo backend do Solar Pro System (Cloudflare Worker) - Login via POST /api/auth/login - Filtragem automática por escopo do usuário ============================================================ */ const { useState, useEffect, useMemo, useRef, useCallback } = React; const { BarChart, Bar, LineChart, Line, AreaChart, Area, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, } = Recharts; /* ============================================================ CONFIG + API CLIENT — fala com o mesmo Worker do Solar Pro System ============================================================ */ const API_BASE = (window.SPV_CONFIG?.API_BASE || '').replace(/\/+$/, ''); const SYSTEM_URL = window.SPV_CONFIG?.SYSTEM_URL || './index.html'; const APP_VERSION = window.SPV_CONFIG?.VERSION || '1.0.0'; const api = { async _fetch(path, opts = {}) { if (!API_BASE) throw new Error('LOCAL_MODE'); const url = API_BASE + (path.startsWith('/') ? path : '/' + path); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 15000); try { const r = await fetch(url, { ...opts, headers: { 'Content-Type': 'application/json', ...(opts.headers || {}) }, credentials: 'omit', signal: controller.signal, }); clearTimeout(timeoutId); if (!r.ok) { let detail = ''; try { const j = await r.json(); if (j.error) detail = j.error; else if (j.message) detail = j.message; } catch {} throw new Error('HTTP ' + r.status + (detail ? ' — ' + detail : '')); } return r.json(); } catch (e) { clearTimeout(timeoutId); if (e.name === 'AbortError') throw new Error('Timeout: o servidor não respondeu em 15s'); throw e; } }, login : (b) => api._fetch('/api/auth/login', { method: 'POST', body: JSON.stringify(b) }), forgot : (email) => api._fetch('/api/auth/forgot-password', { method: 'POST', body: JSON.stringify({ email }) }), listUsinas : () => api._fetch('/api/usinas'), listClientes : () => api._fetch('/api/clientes'), listRateios : () => api._fetch('/api/rateios'), listMovimentacoes: () => api._fetch('/api/movimentacoes'), listRelatoriosStatus : () => api._fetch('/api/relatorios-status'), visionMe : () => api._fetch('/api/vision/me'), visionDashboard : () => api._fetch('/api/vision/dashboard'), }; /* ============================================================ ROLE LABELS — espelham app.jsx do System ============================================================ */ const ROLE_LABELS = { gestor_usina: 'Gestor da Usina', beneficiario: 'Beneficiário', }; const normalizeRole = (r) => { if (!r) return 'beneficiario'; if (typeof r === 'string') return r; if (typeof r === 'object' && r.value) return r.value; return 'beneficiario'; }; const isVisionRole = (r) => ['gestor_usina', 'beneficiario'].includes(normalizeRole(r)); /* ============================================================ FORMATADORES ============================================================ */ const fmtKwh = (v) => v == null ? '—' : (Number(v) || 0).toLocaleString('pt-BR', { maximumFractionDigits: 1 }) + ' kWh'; const fmtKwp = (v) => v == null ? '—' : (Number(v) || 0).toLocaleString('pt-BR', { maximumFractionDigits: 1 }) + ' kWp'; const fmtBRL = (v) => v == null ? '—' : (Number(v) || 0).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }); const fmtPct = (v) => v == null ? '—' : (Number(v) || 0).toFixed(1).replace('.', ',') + '%'; const fmtMes = (m) => { if (!m || typeof m !== 'string') return '—'; const [a, mn] = m.split('-'); const meses = ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez']; return (meses[parseInt(mn,10) - 1] || mn) + '/' + a; }; const fmtData = (d) => { if (!d) return '—'; try { return new Date(d).toLocaleDateString('pt-BR'); } catch { return d; } }; const initialsOf = (nome) => { if (!nome) return '?'; return nome.split(/\s+/).filter(Boolean).slice(0, 2).map(p => p[0]?.toUpperCase()).join('') || '?'; }; /* ============================================================ STATUS DOS TRÂMITES — etapas do processo do beneficiário ============================================================ */ const TRAMITE_STEPS = [ { id: 'cadastro_enviado', label: 'Cadastro enviado', desc: 'Recebemos seus dados. Estamos validando.', icon: 'send' }, { id: 'cadastro_aprovado', label: 'Cadastro aprovado', desc: 'Documentação OK. Você está apto a receber créditos.', icon: 'shield-check' }, { id: 'aguardando_rateio', label: 'Aguardando rateio', desc: 'Aprovado — aguardando próxima composição do rateio.', icon: 'clock' }, { id: 'rateio_em_andamento', label: 'Entrada em rateio', desc: 'Você está sendo incluído na divisão de créditos.', icon: 'loader' }, { id: 'rateio_concluido', label: 'Recebendo créditos', desc: 'Tudo certo — créditos sendo aplicados na sua conta.', icon: 'circle-check' }, ]; const STATUS_CADASTRO_INFO = { cadastro_enviado: { label: 'Cadastro enviado', color: 'info', icon: 'send' }, cadastro_aprovado: { label: 'Cadastro aprovado', color: 'success', icon: 'shield-check' }, cadastro_reprovado: { label: 'Cadastro reprovado', color: 'danger', icon: 'x-circle' }, aguardando_rateio: { label: 'Aguardando rateio', color: 'warning', icon: 'clock' }, rateio_em_andamento: { label: 'Rateio em andamento', color: 'info', icon: 'loader' }, rateio_concluido: { label: 'Recebendo créditos', color: 'success', icon: 'circle-check' }, suspenso: { label: 'Suspenso', color: 'danger', icon: 'pause' }, }; /* ============================================================ ICONS (SVG inline) ============================================================ */ const SVG_PATHS = { 'plus': 'M12 5v14M5 12h14', 'x': 'M18 6L6 18M6 6l12 12', 'check': 'M5 12l5 5L20 7', 'eye': 'M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z M12 9a3 3 0 1 1 0 6 3 3 0 0 1 0-6z', 'eye-off': 'M9.88 9.88a3 3 0 1 0 4.24 4.24 M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68 M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61 M2 2l20 20', 'download': 'M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2 M7 11l5 5 5-5 M12 4v12', 'sun': 'M12 17a5 5 0 1 0 0-10 5 5 0 0 0 0 10z M12 1v2 M12 21v2 M4.22 4.22l1.42 1.42 M18.36 18.36l1.42 1.42 M1 12h2 M21 12h2 M4.22 19.78l1.42-1.42 M18.36 5.64l1.42-1.42', 'bolt': 'M13 2L3 14h9l-1 8 10-12h-9l1-8z', 'building-factory-2': 'M3 21h18 M5 21V8l4-4h6l4 4v13 M9 21V10 M15 21V10 M9 14h6 M9 18h6', 'users': 'M9 7m-4 0a4 4 0 1 0 8 0 4 4 0 1 0-8 0 M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2 M16 3.13a4 4 0 0 1 0 7.75 M21 21v-2a4 4 0 0 0-3-3.85', 'user': 'M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z M4 21v-1a6 6 0 0 1 6-6h4a6 6 0 0 1 6 6v1', 'circle-check': 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z M9 12l2 2 4-4', 'circle-x': 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z M15 9l-6 6 M9 9l6 6', 'x-circle': 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z M15 9l-6 6 M9 9l6 6', 'shield-check': 'M12 22s-8-4-8-12V5l8-3 8 3v5c0 8-8 12-8 12z M9 12l2 2 4-4', 'clock': 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z M12 6v6l4 2', 'loader': 'M12 6v4 M12 14v4 M6 12h4 M14 12h4 M7.05 7.05l2.83 2.83 M14.12 14.12l2.83 2.83 M7.05 16.95l2.83-2.83 M14.12 9.88l2.83-2.83', 'loader-2': 'M12 3a9 9 0 1 0 9 9', 'pause': 'M6 4h4v16H6z M14 4h4v16h-4z', 'send': 'M22 2L11 13 M22 2l-7 20-4-9-9-4 20-7z', 'arrow-right': 'M5 12h14 M13 6l6 6-6 6', 'home': 'M3 12l9-9 9 9 M5 10v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V10 M9 21V12h6v9', 'layout-dashboard':'M3 3h7v7H3z M14 3h7v7h-7z M14 14h7v7h-7z M3 14h7v7H3z', 'chart-bar': 'M3 21h18 M7 16v4 M11 11v9 M15 7v13 M19 3v17', 'chart-area': 'M3 21V3 M21 21H3 M3 17l6-4 4 3 8-6', 'chart-line': 'M3 3v18h18 M7 14l4-4 4 4 6-7', 'history': 'M3 12a9 9 0 1 0 9-9 8.84 8.84 0 0 0-6.36 2.64L3 8 M3 3v5h5 M12 7v5l3 3', 'wallet': 'M19 7H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-2 M22 12h-4a2 2 0 1 0 0 4h4 M16 7V4a1 1 0 0 0-1.4-.9L4.6 7', 'leaf': 'M5 21c.5-4.5 2.5-8 7-10 M21 6c0 9-5 13-9 14-3 0-6-3-6-6 0-9 7-12 15-8z', 'info-circle': 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z M12 8h.01 M11 12h1v4h1', 'alert-triangle': 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z M12 9v4 M12 17h.01', 'alert-circle': 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z M12 8v4 M12 16h.01', 'logout': 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4 M16 17l5-5-5-5 M21 12H9', 'login': 'M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4 M10 17l5-5-5-5 M15 12H3', 'mail': 'M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z M22 6l-10 7L2 6', 'whatsapp': 'M3 21l1.65-3.8a9 9 0 1 1 3.4 2.9L3 21 M9 10a.5.5 0 1 0 1 0V9a.5.5 0 0 0-1 0v1c0 1 1 3 3 3s2-1 2-1', 'refresh': 'M23 4v6h-6 M1 20v-6h6 M3.51 9a9 9 0 0 1 14.85-3.36L23 10 M1 14l4.64 4.36A9 9 0 0 0 20.49 15', 'file-text': 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z M14 2v6h6 M16 13H8 M16 17H8 M10 9H8', 'chevron-down': 'M6 9l6 6 6-6', 'map-pin': 'M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 1 1 18 0z M12 13a3 3 0 1 0 0-6 3 3 0 0 0 0 6z', 'calendar': 'M19 4H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z M16 2v4 M8 2v4 M3 10h18', 'tag': 'M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z M7 7h.01', 'gauge': 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z M12 12l4-4 M8 12a4 4 0 1 0 4-4', 'trending-up': 'M3 17l6-6 4 4 8-8 M14 7h7v7', 'sparkles': 'M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5z M19 14l.7 2.3L22 17l-2.3.7L19 20l-.7-2.3L16 17l2.3-.7z M5 14l.7 2.3L8 17l-2.3.7L5 20l-.7-2.3L2 17l2.3-.7z', }; const Icon = ({ name, size = 16, color, style, className }) => { const path = SVG_PATHS[name]; if (!path) return ; return ( {path.split(' M').map((d, i) => )} ); }; /* ============================================================ COMPONENTES PRIMITIVOS ============================================================ */ const Button = ({ children, variant = 'primary', size = 'md', icon, onClick, disabled, type = 'button', style, title, full }) => { const variants = { primary: { bg: 'linear-gradient(135deg, #14b8a6, #0e7490)', color: '#04141c', border: 'transparent' }, secondary: { bg: 'var(--bg-card)', color: 'var(--text-primary)', border: 'var(--border-strong)' }, ghost: { bg: 'transparent', color: 'var(--text-secondary)', border: 'var(--border)' }, danger: { bg: 'var(--danger)', color: '#fff', border: 'transparent' }, }; const sizes = { sm: { padding: '7px 12px', fontSize: 12 }, md: { padding: '10px 16px', fontSize: 13 }, lg: { padding: '12px 20px', fontSize: 14 }, }; const v = variants[variant] || variants.primary; return ( ); }; const Card = ({ children, style, padding = 22, hover = false }) => (
{ e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.borderColor = 'var(--border-strong)'; } : undefined} onMouseLeave={hover ? e => { e.currentTarget.style.transform = 'none'; e.currentTarget.style.borderColor = 'var(--border)'; } : undefined} > {children}
); const Input = ({ label, error, style, ...rest }) => (
{label && } { e.currentTarget.style.borderColor = 'var(--vision-teal)'; e.currentTarget.style.boxShadow = '0 0 0 3px rgba(20,184,166,0.15)'; }} onBlur={e => { e.currentTarget.style.borderColor = error ? 'var(--danger)' : 'var(--border)'; e.currentTarget.style.boxShadow = 'none'; }} {...rest} /> {error && {error}}
); const Badge = ({ children, color = 'gray', size = 'md', style }) => { const colors = { success: { bg: 'var(--success-bg)', text: 'var(--success)' }, danger: { bg: 'var(--danger-bg)', text: 'var(--danger)' }, warning: { bg: 'var(--warning-bg)', text: 'var(--warning)' }, info: { bg: 'var(--info-bg)', text: 'var(--info)' }, teal: { bg: 'rgba(20,184,166,0.14)', text: 'var(--vision-teal)' }, orange: { bg: 'rgba(245,158,11,0.14)', text: 'var(--solar-orange)' }, gray: { bg: 'rgba(148,163,184,0.14)', text: 'var(--text-secondary)' }, }; const c = colors[color] || colors.gray; const sizing = size === 'sm' ? { fontSize: 10.5, padding: '2px 8px' } : { fontSize: 11.5, padding: '3px 10px' }; return {children}; }; const StatCard = ({ title, value, sub, icon, color = 'teal' }) => { const palette = { teal: { glow: 'rgba(20,184,166,0.16)', fg: 'var(--vision-teal)' }, cyan: { glow: 'rgba(56,189,248,0.16)', fg: 'var(--vision-cyan)' }, orange: { glow: 'rgba(245,158,11,0.18)', fg: 'var(--solar-orange)' }, success: { glow: 'rgba(16,185,129,0.18)', fg: 'var(--success)' }, danger: { glow: 'rgba(239,68,68,0.18)', fg: 'var(--danger)' }, info: { glow: 'rgba(56,189,248,0.16)', fg: 'var(--info)' }, gray: { glow: 'rgba(148,163,184,0.14)', fg: 'var(--text-secondary)' }, }; const c = palette[color] || palette.teal; return (
{ e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.borderColor = 'var(--border-strong)'; }} onMouseLeave={e => { e.currentTarget.style.transform = 'none'; e.currentTarget.style.borderColor = 'var(--border)'; }} >
{title}
{icon &&
}
{value}
{sub &&
{sub}
}
); }; const VisionLogo = ({ size = 38 }) => ( ); /* ============================================================ SEED DATA — para modo local (sem API_BASE) ============================================================ */ const DEMO_USUARIOS = [ { id: 'demo-gestor', nome: 'Rodrigo Godoy (DEMO)', email: 'gestor@demo.solarpro', senha: 'demo123', role: 'gestor_usina', avatar: 'RG', usina_gerenciada_id: 'U023', }, { id: 'demo-benef', nome: 'Cliente Beneficiário (DEMO)', email: 'cliente@demo.solarpro', senha: 'demo123', role: 'beneficiario', avatar: 'CB', cliente_id: 'C0001', }, ]; const DEMO_USINA = { id: 'U023', nome: 'Usina Rodrigo Godoy', responsavel: 'Rodrigo Godoy', cidade: 'Inhumas', estado: 'GO', capacidade_kwp: 517.0, geracao_kwh: 67216.1, mes_referencia: '2025-02', status: 'ativa', tarifa_kwh: 0.92, data_inicio: '2024-01-15', }; const DEMO_BENEFICIARIO = { id: 'C0001', nome: 'Cliente Beneficiário (DEMO)', uc: '10000000001', cidade: 'Goiânia', estado: 'GO', email: 'cliente@demo.solarpro', status: 'ativo', status_tramite: 'rateio_concluido', data_cadastro: '2024-03-15', data_aprovacao: '2024-04-02', data_entrada_rateio: '2024-05-10', usina_id: 'U023', usina_nome: 'Usina Rodrigo Godoy', percentual_rateio: 8.5, consumo_medio_kwh: 1200, desconto_pct: 18, }; /* Gera 12 meses de dados para gráficos demo */ function gerarHistorico12Meses(base, variabilidade) { if (variabilidade == null) variabilidade = 0.15; const meses = []; const hoje = new Date(); for (let i = 11; i >= 0; i--) { const d = new Date(hoje.getFullYear(), hoje.getMonth() - i, 1); const m = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0'); const factor = 1 + (Math.sin(i * 0.7) * variabilidade); meses.push({ mes: m, label: fmtMes(m), geracao: Math.round(base * factor), consumo: Math.round(base * factor * 0.92), credito: Math.round(base * factor * 0.88), saldo: Math.round(base * factor * 0.05), }); } return meses; } /* ============================================================ LOGIN SCREEN — design dedicado do Vision ============================================================ */ function LoginScreen({ onLogin }) { const [email, setEmail] = useState(''); const [senha, setSenha] = useState(''); const [showSenha, setShowSenha] = useState(false); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); const [showForgot, setShowForgot] = useState(false); const [forgotEmail, setForgotEmail] = useState(''); const [forgotSent, setForgotSent] = useState(false); const handleSubmit = async (e) => { if (e) e.preventDefault(); setError(''); if (!email.trim() || !senha) { setError('Informe e-mail e senha.'); return; } setLoading(true); /* MODO DEMO */ if (!API_BASE) { const u = DEMO_USUARIOS.find(x => x.email === email.trim().toLowerCase() && x.senha === senha); if (u) { const userPub = { ...u }; delete userPub.senha; setLoading(false); onLogin(userPub); return; } setLoading(false); setError('Modo DEMO ativo. Use os logins de demonstração abaixo, ou configure API_BASE no solarprovision.html para autenticar via Baserow.'); return; } try { const res = await api.login({ email: email.trim(), senha }); if (res && res.user) { const u = { ...res.user, role: normalizeRole(res.user.role) }; if (!isVisionRole(u.role)) { setError('Este e-mail pertence ao painel administrativo (Solar Pro System). Acesse pelo endereço index.html.'); setLoading(false); return; } if (u.role === 'gestor_usina' && !u.usina_gerenciada_id) { setError('Seu usuário ainda não está vinculado a nenhuma usina. Solicite ao administrador.'); setLoading(false); return; } if (u.role === 'beneficiario' && !u.cliente_id) { setError('Seu usuário ainda não está vinculado a um cliente. Solicite ao administrador.'); setLoading(false); return; } onLogin(u); } else { setError('Credenciais inválidas. Verifique e tente novamente.'); } } catch (err) { const msg = err.message || ''; if (msg.includes('401')) setError('E-mail ou senha incorretos.'); else if (msg.includes('403')) setError('Usuário inativo. Contate o administrador.'); else if (msg.toLowerCase().includes('failed to fetch')) setError('Não foi possível conectar ao servidor. Verifique sua internet.'); else setError(msg || 'Falha desconhecida.'); } finally { setLoading(false); } }; const handleForgot = async (e) => { if (e) e.preventDefault(); if (!forgotEmail || !forgotEmail.includes('@')) return; if (API_BASE) { try { await api.forgot(forgotEmail.trim()); } catch {} } setForgotSent(true); setTimeout(() => { setShowForgot(false); setForgotSent(false); setForgotEmail(''); }, 3500); }; return (
{/* Coluna esquerda: branding */}
SolarPro Vision
Painel de Acompanhamento

Acompanhe sua energia solar em tempo real

Painel exclusivo para gestores de usinas e beneficiários do rateio. Veja produção mensal, créditos transferidos, saldo acumulado, gráficos e o andamento do seu cadastro — tudo num só lugar.

Gestor da Usina
Geração, créditos enviados, lista de beneficiários e relatórios mensais.
Beneficiário
Consumo, créditos recebidos, saldo, gráficos pessoais e trâmites.
v{APP_VERSION} • Integrado ao Solar Pro System
{/* Coluna direita: formulário */}
{!showForgot ? (

Entrar no Vision

Acesse com o e-mail cadastrado pelo administrador.

setEmail(e.target.value)} placeholder="seu@email.com" autoComplete="email" autoFocus />
setSenha(e.target.value)} placeholder="Sua senha" autoComplete="current-password" />
{error && (
{error}
)} {!API_BASE && (
Modo demonstração
Tente os logins de exemplo:
{DEMO_USUARIOS.map(u => ( ))}
)}
É administrador? Acessar Solar Pro System →
) : (

Recuperar acesso

Informe seu e-mail. Se houver cadastro, enviaremos as instruções.

{!forgotSent ? ( <> setForgotEmail(e.target.value)} placeholder="seu@email.com" autoFocus /> ) : (
Se o e-mail estiver cadastrado, você receberá as instruções em alguns minutos. Confira também a caixa de spam.
)}
)}
); } /* ============================================================ TOPBAR ============================================================ */ function TopBar({ user, onLogout, scope }) { const [menuOpen, setMenuOpen] = useState(false); return (
SolarPro Vision {ROLE_LABELS[user.role]}
{scope && (
{scope}
)}
{menuOpen && ( <>
setMenuOpen(false)} style={{ position: 'fixed', inset: 0, zIndex: 25 }} />
{user.nome}
{user.email}
{ROLE_LABELS[user.role]}
)}
); } /* ============================================================ PAINEL DO GESTOR — quem é dono da usina ============================================================ */ function GestorPainel({ user, dados }) { const [aba, setAba] = useState('geral'); if (!dados || !dados.usina) { return (

Usina não encontrada

O administrador precisa vincular sua conta a uma usina ativa.

); } const { usina, rateios, historico, relatorioStatus, totalBenef, totalCreditos, totalConsumo, totalSaldo } = dados; return (
{/* Cabeçalho com info da usina */}

{usina.nome}

{usina.cidade}/{usina.estado} Desde {fmtData(usina.data_inicio)} {usina.status}
Responsável: {usina.responsavel || '—'} • Tarifa: {fmtBRL(usina.tarifa_kwh)} /kWh
Mês de referência
{fmtMes(usina.mes_referencia)}
{/* Tabs */}
{[ { id: 'geral', label: 'Visão geral', icon: 'layout-dashboard' }, { id: 'beneficiarios', label: 'Beneficiários', icon: 'users' }, { id: 'historico', label: 'Histórico mensal', icon: 'chart-area' }, { id: 'relatorios', label: 'Relatórios', icon: 'file-text' }, ].map(t => ( ))}
{aba === 'geral' && (

Produção dos últimos 12 meses

Geração em kWh

(v/1000).toFixed(0) + 'k'} /> fmtKwh(v)} />

Distribuição de créditos no mês

Por beneficiário

{rateios && rateios.length > 0 ? ( ({ name: (r.cliente_nome || '').split(' ').slice(0, 2).join(' '), valor: r.credito_enviado_kwh || 0 }))} margin={{ top: 5, right: 5, left: 0, bottom: 5 }}> (v/1000).toFixed(0) + 'k'} /> fmtKwh(v)} /> ) : (
Sem rateio registrado para o mês atual.
)}
{relatorioStatus && (

Relatório de {fmtMes(relatorioStatus.mes_referencia)}

{relatorioStatus.observacoes || 'Sem observações.'}

{relatorioStatus.status === 'concluido' ? 'Concluído' : relatorioStatus.status === 'em_andamento' ? 'Em andamento' : relatorioStatus.status === 'pendente' ? 'Pendente' : 'Não iniciado'}
{relatorioStatus.status === 'concluido' && (
{relatorioStatus.data_geracao && Gerado em {fmtData(relatorioStatus.data_geracao)}} {relatorioStatus.enviado_email && E-mail enviado} {relatorioStatus.enviado_whatsapp && WhatsApp enviado}
)}
)}
)} {aba === 'beneficiarios' && (

Beneficiários vinculados

{totalBenef} cliente{totalBenef !== 1 ? 's' : ''} recebendo créditos desta usina

{fmtPct(rateios.reduce((s, v) => s + (v.percentual_rateio || 0), 0))} alocado
{[...rateios].sort((a,b) => (b.credito_enviado_kwh || 0) - (a.credito_enviado_kwh || 0)).map((r, i) => ( ))} {rateios.length === 0 && ( )}
Beneficiário UC % Crédito enviado Consumo Saldo
{r.cliente_nome} {r.uc || '—'} {fmtPct(r.percentual_rateio)} {fmtKwh(r.credito_enviado_kwh)} {fmtKwh(r.consumo_cliente_kwh)} 0 ? 'var(--success)' : 'var(--text-tertiary)' }}>{fmtKwh(r.saldo_cliente_kwh)}
Nenhum beneficiário vinculado a esta usina ainda.
)} {aba === 'historico' && (

Histórico — produção × créditos enviados × saldo

Últimos 12 meses

(v/1000).toFixed(0) + 'k'} /> fmtKwh(v)} />
)} {aba === 'relatorios' && (

Relatórios mensais

Status de geração e envio para cada mês

{historico.slice(-6).reverse().map((h, i) => { const idx = (i + (relatorioStatus && relatorioStatus.status === 'concluido' ? 0 : 1)) % 4; const status = i === 0 && relatorioStatus ? relatorioStatus.status : idx === 0 ? 'concluido' : idx === 1 ? 'em_andamento' : idx === 2 ? 'pendente' : 'nao_iniciado'; const cor = status === 'concluido' ? 'success' : status === 'em_andamento' ? 'info' : status === 'pendente' ? 'warning' : 'gray'; const lbl = status === 'concluido' ? 'Concluído' : status === 'em_andamento' ? 'Em andamento' : status === 'pendente' ? 'Pendente' : 'Não iniciado'; return (
Relatório de {h.label}
Geração: {fmtKwh(h.geracao)} • Crédito: {fmtKwh(h.credito)}
{lbl}
); })}
Os relatórios são gerados pela equipe operacional no Solar Pro System e enviados a você por e-mail/WhatsApp quando concluídos.
)}
); } /* ============================================================ PAINEL DO BENEFICIÁRIO — cliente com desconto na conta ============================================================ */ function BeneficiarioPainel({ user, dados }) { if (!dados || !dados.cliente) { return (

Cadastro não encontrado

O administrador precisa vincular sua conta a um cliente cadastrado.

); } const { cliente, usina, historico, historicoCompleto, totalCredito, totalConsumo, totalSaldo, economiaEst } = dados; const statusAtual = cliente.status_tramite || 'rateio_concluido'; const statusInfo = STATUS_CADASTRO_INFO[statusAtual] || STATUS_CADASTRO_INFO.cadastro_enviado; const stepAtualIdx = TRAMITE_STEPS.findIndex(s => s.id === statusAtual); return (
{/* Saudação + status */}
Olá, {(cliente.nome || '').split(' ')[0]}

Seu painel de energia solar

UC {cliente.uc} {usina && ( Vinculado à {usina.nome} )} {cliente.percentual_rateio != null && ( {fmtPct(cliente.percentual_rateio)} do rateio )}
Status
{statusInfo.label}
{/* Cards principais */}
0 ? 'success' : 'gray'} />
{/* Stepper de trâmites */}

Como anda seu processo

Acompanhe cada etapa do seu cadastro até começar a receber créditos.

{TRAMITE_STEPS.map((step, i) => { const isCurrent = i === stepAtualIdx; const isDone = i < stepAtualIdx; const isFuture = i > stepAtualIdx; return (
{i < TRAMITE_STEPS.length - 1 && (
)}
{step.label} {isCurrent && Atual} {isDone && }
{step.desc}
{isDone && step.id === 'cadastro_enviado' && cliente.data_cadastro && (
{fmtData(cliente.data_cadastro)}
)} {isDone && step.id === 'cadastro_aprovado' && cliente.data_aprovacao && (
{fmtData(cliente.data_aprovacao)}
)} {isDone && step.id === 'rateio_em_andamento' && cliente.data_entrada_rateio && (
{fmtData(cliente.data_entrada_rateio)}
)}
); })}
{/* Gráficos */}

Consumo × Crédito recebido

Últimos 12 meses (kWh)

fmtKwh(v)} />

Saldo de créditos acumulado

Energia ainda disponível

fmtKwh(v)} />
{/* Tabela de histórico */}

Histórico de créditos recebidos

Detalhamento mês a mês

{historicoCompleto.slice().reverse().map((h, i) => ( ))}
Mês Usina % Crédito Consumo Saldo
{h.label} {(usina && usina.nome) || '—'} {fmtPct(cliente.percentual_rateio)} {fmtKwh(h.credito)} {fmtKwh(h.consumo)} 0 ? 'var(--success)' : 'var(--text-tertiary)' }}>{fmtKwh(h.saldo)}
{/* Info da usina */} {usina && (
Sua energia vem desta usina

{usina.nome}

Localização
{usina.cidade}/{usina.estado}
Potência
{fmtKwp(usina.capacidade_kwp)}
Geração mensal
{fmtKwh(usina.geracao_kwh)}
Em operação desde
{fmtData(usina.data_inicio)}
)}
); } /* ============================================================ ROOT — SolarProVision ============================================================ */ function SolarProVision() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(false); const [dados, setDados] = useState(null); const [erro, setErro] = useState(''); /* Carrega dados após login */ useEffect(() => { if (!user) return; setLoading(true); setErro(''); const carregar = async () => { try { if (!API_BASE) { /* === MODO DEMO === */ if (user.role === 'gestor_usina') { const usina = DEMO_USINA; const historico = gerarHistorico12Meses(usina.geracao_kwh, 0.18); const rateios = [ { id: 'R1', cliente_id: 'C0001', cliente_nome: 'SUPER RIO SUPERMERCADOS LTDA.', uc: '10000825849', percentual_rateio: 4.7, credito_enviado_kwh: 3186, consumo_cliente_kwh: 3186, saldo_cliente_kwh: 0 }, { id: 'R2', cliente_id: 'C0002', cliente_nome: 'ELAINE MARIA GUTEMBERG CAMPOS', uc: '10036971020', percentual_rateio: 5.6, credito_enviado_kwh: 3784.27, consumo_cliente_kwh: 1805, saldo_cliente_kwh: 1979.3 }, { id: 'R3', cliente_id: 'C0003', cliente_nome: 'Thiago Vinicius Silva leite', uc: '16551588', percentual_rateio: 6.5, credito_enviado_kwh: 4382.49, consumo_cliente_kwh: 4382.49, saldo_cliente_kwh: 0 }, { id: 'R4', cliente_id: 'C0004', cliente_nome: 'ALDEMIR ALVES CARNEIRO-ME', uc: '10005131942', percentual_rateio: 6.5, credito_enviado_kwh: 4382.49, consumo_cliente_kwh: 4382.49, saldo_cliente_kwh: 0 }, { id: 'R5', cliente_id: 'C0005', cliente_nome: 'Carpal Tratores', uc: '10039399743', percentual_rateio: 3.9, credito_enviado_kwh: 2587.82, consumo_cliente_kwh: 2587.82, saldo_cliente_kwh: 0 }, { id: 'R6', cliente_id: 'C0006', cliente_nome: 'Omar Felipe Machado Júnior', uc: '16686809', percentual_rateio: 3.2, credito_enviado_kwh: 2184.52, consumo_cliente_kwh: 2184.52, saldo_cliente_kwh: 0 }, { id: 'R7', cliente_id: 'C0007', cliente_nome: 'Jhonatan Barbosa Fernandes', uc: '10773502', percentual_rateio: 2.5, credito_enviado_kwh: 1666.96, consumo_cliente_kwh: 1666.96, saldo_cliente_kwh: 0 }, { id: 'R8', cliente_id: 'C0008', cliente_nome: 'Damiane Evangelista dos Santos',uc: '10035980069', percentual_rateio: 2.4, credito_enviado_kwh: 1586.30, consumo_cliente_kwh: 1586.30, saldo_cliente_kwh: 0 }, ]; const totalBenef = rateios.length; const totalCreditos = rateios.reduce((s, r) => s + (r.credito_enviado_kwh || 0), 0); const totalConsumo = rateios.reduce((s, r) => s + (r.consumo_cliente_kwh || 0), 0); const totalSaldo = rateios.reduce((s, r) => s + (r.saldo_cliente_kwh || 0), 0); setDados({ usina, vinculos: rateios, rateios, historico, relatorioStatus: { mes_referencia: usina.mes_referencia, status: 'concluido', data_geracao: '2025-03-01T10:00:00Z', enviado_email: true, enviado_whatsapp: true, observacoes: 'Relatório enviado com sucesso.' }, totalBenef, totalCreditos, totalConsumo, totalSaldo, }); } else { const cliente = DEMO_BENEFICIARIO; const usina = DEMO_USINA; const historicoCompleto = gerarHistorico12Meses(cliente.consumo_medio_kwh, 0.20); const totalCredito = historicoCompleto[historicoCompleto.length - 1].credito; const totalConsumo = historicoCompleto[historicoCompleto.length - 1].consumo; const totalSaldo = historicoCompleto.reduce((s, h) => s + h.saldo, 0); const economiaEst = totalCredito * (usina.tarifa_kwh || 0.92) * ((cliente.desconto_pct || 18) / 100); setDados({ cliente, usina, rateios: [], historico: historicoCompleto, historicoCompleto, totalCredito, totalConsumo, totalSaldo, economiaEst, }); } setLoading(false); return; } /* === MODO ONLINE === */ /* Tenta endpoint dedicado primeiro */ let dashboard = null; try { dashboard = await api.visionDashboard(); } catch {} if (dashboard) { setDados(dashboard); setLoading(false); return; } /* Fallback: lista entidades gerais e filtra pelo escopo do usuário */ const [usinas, clientes, rateios] = await Promise.all([ api.listUsinas().catch(() => []), api.listClientes().catch(() => []), api.listRateios().catch(() => []), ]); if (user.role === 'gestor_usina') { const usina = usinas.find(u => u.id === user.usina_gerenciada_id); if (!usina) { setErro('Usina vinculada não encontrada no servidor. Solicite ao administrador.'); setLoading(false); return; } const rateiosDaUsina = rateios.filter(r => r.usina_id === usina.id); const rateiosEnriq = rateiosDaUsina.map(r => ({ ...r, uc: (clientes.find(c => c.id === r.cliente_id) || {}).uc || '', })); const totalBenef = new Set(rateiosEnriq.map(r => r.cliente_id)).size; const totalCreditos = rateiosEnriq.reduce((s, r) => s + (r.credito_enviado_kwh || 0), 0); const totalConsumo = rateiosEnriq.reduce((s, r) => s + (r.consumo_cliente_kwh || 0), 0); const totalSaldo = rateiosEnriq.reduce((s, r) => s + (r.saldo_cliente_kwh || 0), 0); const historico = gerarHistorico12Meses(usina.geracao_kwh || 1000, 0.18); let relatorioStatus = null; try { const list = await api.listRelatoriosStatus(); relatorioStatus = list.find(r => r.usina_id === usina.id && r.mes_referencia === usina.mes_referencia) || null; } catch {} setDados({ usina, vinculos: rateiosEnriq, rateios: rateiosEnriq, historico, relatorioStatus, totalBenef, totalCreditos, totalConsumo, totalSaldo }); } else { /* beneficiario */ const cliente = clientes.find(c => c.id === user.cliente_id); if (!cliente) { setErro('Cliente vinculado não encontrado no servidor. Solicite ao administrador.'); setLoading(false); return; } const rateiosDoCliente = rateios.filter(r => r.cliente_id === cliente.id); const usina = usinas.find(u => u.id === cliente.usina_id) || (rateiosDoCliente[0] ? usinas.find(u => u.id === rateiosDoCliente[0].usina_id) : null); const ultRateio = rateiosDoCliente[rateiosDoCliente.length - 1] || {}; const historicoCompleto = gerarHistorico12Meses(cliente.consumo_medio_kwh || ultRateio.consumo_cliente_kwh || 500, 0.20); const totalCredito = ultRateio.credito_enviado_kwh || historicoCompleto[historicoCompleto.length - 1].credito; const totalConsumo = ultRateio.consumo_cliente_kwh || historicoCompleto[historicoCompleto.length - 1].consumo; const totalSaldo = rateiosDoCliente.reduce((s, r) => s + (r.saldo_cliente_kwh || 0), 0); const tarifa = (usina && usina.tarifa_kwh) || 0.92; const economiaEst = totalCredito * tarifa * ((cliente.desconto_pct || 18) / 100); setDados({ cliente, usina, rateios: rateiosDoCliente, historico: historicoCompleto, historicoCompleto, totalCredito, totalConsumo, totalSaldo, economiaEst }); } } catch (e) { setErro('Falha ao carregar dados: ' + (e.message || 'erro desconhecido')); } finally { setLoading(false); } }; carregar(); }, [user]); const handleLogout = () => { setUser(null); setDados(null); }; if (!user) { return ( <>
); } const scope = user.role === 'gestor_usina' ? ((dados && dados.usina && dados.usina.nome) || 'Usina vinculada') : (dados && dados.cliente ? 'UC ' + dados.cliente.uc : 'Cliente vinculado'); return ( <>
{loading && (
Carregando seus dados...
)} {erro && !loading && (

Erro ao carregar painel

{erro}

)} {!loading && !erro && dados && ( user.role === 'gestor_usina' ? : )}
SolarProVision v{APP_VERSION} • Sistema integrado ao Solar Pro System • {API_BASE ? ' modo online' : ' modo demonstração'}
); } /* ============================================================ MOUNT ============================================================ */ const splash = document.getElementById('splash'); if (splash) splash.style.display = 'none'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render();