import React, { useEffect, useState, useRef, useCallback } from 'react'
import { useRouter } from 'next/router'
import { motion, AnimatePresence } from 'framer-motion'
import {
Settings, Paintbrush, Globe, Bell, Mail, Users, Wrench,
Upload, Save, Check, AlertTriangle, Eye, EyeOff, Send,
Image as ImageIcon, Smile, RefreshCw, Webhook,
} from 'lucide-react'
import { useAuth } from '@/context/AuthContext'
import { api } from '@/services/chatApiService'
import { PageHeader, Card, Badge } from '@/components/ui'
// ─── Types ────────────────────────────────────────────────────────────────────
interface SystemSettings {
systemName: string
timezone: string
logoUrl: string | null
faviconUrl: string | null
accentColor: string
sounds: { newMessage: boolean; notification: boolean; connectionStatus: boolean }
smtp: { host: string; port: number; user: string; password: string; from: string; secure: boolean }
emailTemplates: { welcome: string; passwordReset: string; trialExpiring: string }
registration: { mode: 'open' | 'closed' | 'invite'; defaultPlanId: string | null }
maintenance: { enabled: boolean; message: string }
webhook: { url: string; secret: string }
}
const TABS = [
{ id: 'branding', label: 'Branding', icon: Paintbrush },
{ id: 'regional', label: 'Regional', icon: Globe },
{ id: 'sounds', label: 'Sons', icon: Bell },
{ id: 'email', label: 'E-mail', icon: Mail },
{ id: 'registration', label: 'Registro', icon: Users },
{ id: 'system', label: 'Sistema', icon: Wrench },
{ id: 'deploys', label: 'Versões', icon: RefreshCw },
] as const
type Tab = typeof TABS[number]['id']
const TIMEZONES = [
'America/Sao_Paulo','America/Manaus','America/Belem','America/Fortaleza',
'America/Recife','America/Maceio','America/Bahia','America/Cuiaba',
'America/Porto_Velho','America/Boa_Vista','America/Rio_Branco','America/Noronha',
'America/New_York','America/Chicago','America/Denver','America/Los_Angeles',
'Europe/London','Europe/Paris','Europe/Berlin','Europe/Lisbon',
'Asia/Tokyo','Asia/Shanghai','Asia/Kolkata','Australia/Sydney','UTC',
]
// ─── Sub-components ───────────────────────────────────────────────────────────
function SectionTitle({ children }: { children: React.ReactNode }) {
return
{children}
}
function Field({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) {
return (
{label}
{children}
{hint &&
{hint}
}
)
}
function TextInput({ value, onChange, placeholder, type = 'text', disabled }: {
value: string; onChange: (v: string) => void; placeholder?: string; type?: string; disabled?: boolean
}) {
return (
onChange(e.target.value)}
placeholder={placeholder}
disabled={disabled}
className="w-full bg-white/[0.03] border border-white/5 rounded-xl px-4 py-2.5 text-sm text-slate-200 placeholder-slate-600 focus:outline-none focus:border-brand-500/40 focus:bg-white/[0.05] transition-all disabled:opacity-40"
/>
)
}
function Toggle({ checked, onChange, label, hint }: { checked: boolean; onChange: (v: boolean) => void; label: string; hint?: string }) {
return (
onChange(!checked)}
className={`relative w-11 h-6 rounded-full transition-colors duration-200 ${checked ? 'bg-brand-500' : 'bg-white/10'}`}
>
)
}
function AssetUpload({ label, current, onUpload, accept = 'image/*' }: {
label: string; current: string | null; onUpload: (f: File) => Promise; accept?: string
}) {
const [dragging, setDragging] = useState(false)
const [loading, setLoading] = useState(false)
const inputRef = useRef(null)
const API = process.env.NEXT_PUBLIC_API_URL ?? ''
const handle = async (file: File) => {
setLoading(true)
try { await onUpload(file) } finally { setLoading(false) }
}
return (
{label}
{ e.preventDefault(); setDragging(true) }}
onDragLeave={() => setDragging(false)}
onDrop={e => { e.preventDefault(); setDragging(false); const f = e.dataTransfer.files[0]; if (f) handle(f) }}
onClick={() => inputRef.current?.click()}
className={`relative h-28 rounded-2xl border-2 border-dashed flex flex-col items-center justify-center gap-2 cursor-pointer transition-all
${dragging ? 'border-brand-500 bg-brand-500/5' : 'border-white/10 hover:border-white/20 bg-white/[0.02]'}`}
>
{current ? (
) : (
<>
Arraste ou clique para enviar
>
)}
{loading && (
)}
{ const f = e.target.files?.[0]; if (f) handle(f) }} />
)
}
function SaveBar({ saving, saved, onSave }: { saving: boolean; saved: boolean; onSave: () => void }) {
return (
{saving ? : saved ? : }
{saving ? 'Salvando...' : saved ? 'Salvo!' : 'Salvar Alterações'}
)
}
// ─── Main Page ────────────────────────────────────────────────────────────────
export default function AdminSettings() {
const { user, loading } = useAuth()
const router = useRouter()
const [tab, setTab] = useState('branding')
const [settings, setSettings] = useState(null)
const [saving, setSaving] = useState(false)
const [saved, setSaved] = useState(false)
const [smtpPassVisible, setSmtpPassVisible] = useState(false)
const [sendingTest, setSendingTest] = useState(false)
const [plans, setPlans] = useState<{ id: string; name: string }[]>([])
const [engine, setEngine] = useState<'infinite' | 'official'>('infinite')
const [switchingEngine, setSwitchingEngine] = useState(false)
// ── Estados de Controle de Versões & Deploys ──
const [versionData, setVersionData] = useState<{
version: string;
buildCount: number;
lastDeploy: string;
lastCommit?: { hash: string; message: string; author: string };
} | null>(null)
const [deploysHistory, setDeploysHistory] = useState([])
const [triggeringDeploy, setTriggeringDeploy] = useState(false)
const fetchDeployData = useCallback(async () => {
try {
const vRes = await fetch('/version.json?t=' + Date.now())
if (vRes.ok) {
const vData = await vRes.json()
setVersionData(vData)
}
} catch (e) {}
try {
const dRes = await fetch('/deploys.json?t=' + Date.now())
if (dRes.ok) {
const dData = await dRes.json()
setDeploysHistory(dData)
}
} catch (e) {}
}, [])
const triggerDeploy = async () => {
if (!window.confirm('Deseja realmente iniciar uma nova compilação e deploy em produção agora?\n\nIsso irá atualizar o repositório, reinstalar dependências, compilar o frontend e backend, e reiniciar os serviços.')) return
setTriggeringDeploy(true)
try {
await api.post('/api/admin/deploys/trigger')
alert('Deploy automático iniciado em segundo plano! A VPS 4 está compilando os arquivos. O painel monitorará o progresso automaticamente.')
let attempts = 0
const interval = setInterval(async () => {
attempts++
try {
const vRes = await fetch('/version.json?t=' + Date.now())
if (vRes.ok) {
const vData = await vRes.json()
if (!versionData || vData.buildCount !== versionData.buildCount) {
setVersionData(vData)
const dRes = await fetch('/deploys.json?t=' + Date.now())
if (dRes.ok) {
setDeploysHistory(await dRes.json())
}
setTriggeringDeploy(false)
clearInterval(interval)
alert(`Sucesso! Deploy finalizado. Versão atualizada para ${vData.version}.`)
return
}
}
} catch (e) {}
if (attempts >= 36) { // 3 minutos
clearInterval(interval)
setTriggeringDeploy(false)
alert('O tempo limite de monitoramento esgotou, mas o deploy pode ainda estar rodando. Por favor, recarregue a página em instantes.')
}
}, 5000)
} catch (err) {
alert('Erro ao tentar acionar o deploy automático.')
setTriggeringDeploy(false)
}
}
useEffect(() => {
if (user?.role === 'admin') {
fetchDeployData()
}
}, [user, fetchDeployData])
useEffect(() => {
if (!loading && user?.role !== 'admin') router.replace('/inbox')
}, [loading, user, router])
useEffect(() => {
if (user?.role !== 'admin') return
api.get('/api/admin/settings').then(r => setSettings(r.data)).catch(() => {})
api.get('/api/admin/plans').then(r => setPlans(r.data ?? [])).catch(() => {})
api.get('/api/admin/engine').then(r => setEngine(r.data.engine)).catch(() => {})
}, [user])
const handleEngineSwitch = async (newEngine: 'infinite' | 'official') => {
if (newEngine === engine) return
const confirmSwitch = window.confirm(
`Deseja realmente alternar a Engine para "${newEngine === 'infinite' ? 'InfiniteAPI' : 'Baileys Oficial'}"?\n\nEsta alteração irá reiniciar o backend (~10s) para aplicar as novas configurações.`
)
if (!confirmSwitch) return
setSwitchingEngine(true)
try {
await api.post('/api/admin/engine', { engine: newEngine })
setEngine(newEngine)
alert('Engine configurada com sucesso! O servidor está reiniciando. Por favor, aguarde cerca de 10 segundos antes de recarregar a página.')
} catch {
alert('Erro ao tentar alternar a engine.')
} finally {
setSwitchingEngine(false)
}
}
const set = useCallback((key: K, value: SystemSettings[K]) => {
setSettings(prev => prev ? { ...prev, [key]: value } : prev)
}, [])
const setNested = useCallback((key: K, patch: Partial) => {
setSettings(prev => prev ? { ...prev, [key]: { ...(prev[key] as object), ...patch } } : prev)
}, [])
const save = async () => {
if (!settings) return
setSaving(true)
try {
await api.patch('/api/admin/settings', settings)
setSaved(true)
setTimeout(() => setSaved(false), 3000)
} catch {
} finally {
setSaving(false)
}
}
const uploadAsset = async (type: 'logo' | 'favicon', file: File) => {
const form = new FormData()
form.append('file', file)
const { data } = await api.post(`/api/admin/settings/${type}`, form, { headers: { 'Content-Type': 'multipart/form-data' } })
setSettings(prev => prev ? { ...prev, [`${type}Url`]: data.url } : prev)
}
const sendTestEmail = async () => {
setSendingTest(true)
try {
await api.post('/api/admin/settings/smtp-test')
alert('E-mail de teste enviado!')
} catch {
alert('Falha ao enviar e-mail de teste. Verifique as configurações SMTP.')
} finally {
setSendingTest(false)
}
}
if (loading || user?.role !== 'admin' || !settings) return (
)
return (
{/* Tabs */}
{TABS.map(t => {
const Icon = t.icon
const active = tab === t.id
return (
setTab(t.id)}
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-semibold transition-all ${
active ? 'bg-brand-600 text-white shadow-lg shadow-brand-900/40' : 'text-slate-400 hover:text-white hover:bg-white/5'
}`}
>
{t.label}
)
})}
{/* ── BRANDING ── */}
{tab === 'branding' && (
)}
{/* ── REGIONAL ── */}
{tab === 'regional' && (
Configurações Regionais
set('timezone', e.target.value)}
className="w-full bg-white/[0.03] border border-white/5 rounded-xl px-4 py-2.5 text-sm text-slate-200 focus:outline-none focus:border-brand-500/40 transition-all"
>
{TIMEZONES.map(tz => (
{tz}
))}
Hora atual no fuso selecionado:{' '}
{new Date().toLocaleString('pt-BR', { timeZone: settings.timezone })}
)}
{/* ── SONS ── */}
{tab === 'sounds' && (
Sons do Sistema
setNested('sounds', { newMessage: v })}
label="Nova Mensagem"
hint="Som ao receber uma nova mensagem no inbox"
/>
setNested('sounds', { notification: v })}
label="Notificações"
hint="Som para alertas e notificações do sistema"
/>
setNested('sounds', { connectionStatus: v })}
label="Status de Conexão"
hint="Som ao conectar ou desconectar uma instância WhatsApp"
/>
)}
{/* ── EMAIL ── */}
{tab === 'email' && (
Servidor SMTP
setNested('smtp', { host: v })} placeholder="smtp.gmail.com" />
setNested('smtp', { port: Number(v) || 587 })} placeholder="587" />
setNested('smtp', { user: v })} placeholder="noreply@empresa.com" />
setNested('smtp', { password: v })}
placeholder="••••••••"
/>
setSmtpPassVisible(p => !p)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-500 hover:text-slate-300"
>
{smtpPassVisible ? : }
setNested('smtp', { from: v })} placeholder="NewWhats " />
setNested('smtp', { secure: v })}
label="TLS/SSL"
hint="Porta 465"
/>
{sendingTest ? : }
Enviar E-mail de Teste
Modelos de E-mail
{[
{ key: 'welcome' as const, label: 'Boas-vindas', hint: 'Enviado ao novo usuário após cadastro' },
{ key: 'passwordReset' as const, label: 'Redefinição de Senha', hint: 'Link de reset enviado ao usuário' },
{ key: 'trialExpiring' as const, label: 'Trial Expirando', hint: 'Aviso enviado 3 dias antes do fim do trial' },
].map(({ key, label, hint }) => (
))}
)}
{/* ── REGISTRO ── */}
{tab === 'registration' && (
Controle de Acesso
{([
{ val: 'open', label: 'Aberto', hint: 'Qualquer pessoa pode se cadastrar' },
{ val: 'invite', label: 'Por Convite', hint: 'Somente via link de convite' },
{ val: 'closed', label: 'Fechado', hint: 'Apenas o admin cria usuários' },
] as const).map(opt => (
setNested('registration', { mode: opt.val })}
className={`p-3 rounded-xl border text-left transition-all ${
settings.registration.mode === opt.val
? 'border-brand-500/40 bg-brand-500/10 text-white'
: 'border-white/5 bg-white/[0.02] text-slate-400 hover:border-white/10'
}`}
>
{opt.label}
{opt.hint}
))}
setNested('registration', { defaultPlanId: e.target.value || null })}
className="w-full bg-white/[0.03] border border-white/5 rounded-xl px-4 py-2.5 text-sm text-slate-200 focus:outline-none focus:border-brand-500/40 transition-all"
>
Nenhum (sem plano)
{plans.map(p => (
{p.name}
))}
)}
{/* ── SISTEMA ── */}
{tab === 'system' && (
Manutenção
{settings.maintenance.enabled && (
Modo manutenção ATIVO — usuários não-admin não conseguem fazer login
)}
setNested('maintenance', { enabled: v })}
label="Modo Manutenção"
hint="Bloqueia o login de todos os usuários não-admin"
/>
setNested('maintenance', { message: v })}
placeholder="Sistema em manutenção. Voltamos em breve."
/>
Webhook Global
setNested('webhook', { url: v })} placeholder="https://n8n.empresa.com/webhook/newwhats" />
setNested('webhook', { secret: v })} placeholder="um-segredo-aleatorio-forte" type="password" />
Engine de WhatsApp
Selecione o motor de conexão do WhatsApp. O InfiniteAPI permite o envio nativo de botões, listas e carrosséis que funcionam perfeitamente no celular e no WhatsApp Web. O Baileys Oficial é o motor padrão de mercado (estável, mas sem suporte nativo a botões no Web).
{([
{ val: 'infinite', label: 'InfiniteAPI', hint: 'Suporta botões, listas e carrosséis ricos (Celular & Web)' },
{ val: 'official', label: 'Baileys Oficial', hint: 'Estável padrão de mercado (sem botões no Web)' },
] as const).map(opt => (
handleEngineSwitch(opt.val)}
type="button"
className={`p-4 rounded-xl border text-left transition-all relative ${
engine === opt.val
? 'border-brand-500/40 bg-brand-500/10 text-white'
: 'border-white/5 bg-white/[0.02] text-slate-400 hover:border-white/10'
} disabled:opacity-40`}
>
{opt.label}
{engine === opt.val && (
)}
{opt.hint}
))}
Nota: Alterar a engine causará a reinicialização automática do backend (~10 segundos).
)}
{/* ── DEPLOYS ── */}
{tab === 'deploys' && (
Ciclo de Deploy (GitOps) e Versões
Visualize o histórico de compilação da plataforma e gerencie os deploys automáticos em produção diretamente do painel de administração.
{/* Grid das Versões */}
{/* Card Versão Atual */}
Versão Ativa
{versionData?.version || 'v1.0.0'}
build #{versionData?.buildCount || 0}
Online em Produção (VPS 1)
Último deploy:{' '}
{versionData?.lastDeploy
? new Date(versionData.lastDeploy).toLocaleString('pt-BR')
: 'Pendente ou Manual'}
{/* Card Último Commit */}
Última Alteração Integrada
{versionData?.lastCommit?.message || 'Sem dados de commit'}
Por: {versionData?.lastCommit?.author || 'N/A'}
Hash do commit:
{versionData?.lastCommit?.hash || 'N/A'}
{/* Bloco de Ação Manual */}
Compilação e Deploy Forçado
Deseja puxar a última versão do repositório Git, reinstalar pacotes, rodar o build do Next.js e reiniciar os containers de produção agora?
{triggeringDeploy ? (
<>
Compilando & Sincronizando...
>
) : (
<>
Compilar e Despachar
>
)}
{/* Histórico Completo de Deploys */}
Histórico de Push & Compilações
{deploysHistory.length === 0 ? (
Nenhum histórico de deploy registrado ainda.
) : (
Versão
Autor
Mensagem do Commit
Data & Hora
Status
{deploysHistory.map((deploy, index) => (
{deploy.version}
{deploy.author}
{deploy.commitMessage}
{new Date(deploy.timestamp).toLocaleString('pt-BR')}
{deploy.status || 'Sucesso'}
))}
)}
)}
)
}