8331e87d26
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
269 lines
14 KiB
TypeScript
269 lines
14 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { User, Building2, ShieldCheck, LogOut, KeyRound, CheckCircle2, AlertCircle, Eye, EyeOff, Loader2 } from 'lucide-react';
|
|
import { HybridBackend } from '../services/backend.ts';
|
|
import { GoogleConnectButton } from '../components/GoogleConnectButton.tsx';
|
|
import { useToast } from '../contexts/ToastContext.tsx';
|
|
import { APP_VERSION } from '../constants.ts';
|
|
|
|
export const ConfiguracoesView: React.FC = () => {
|
|
const [connectedAccounts, setConnectedAccounts] = useState<any[]>([]);
|
|
const [credsStatus, setCredsStatus] = useState<{ configured: boolean; clientIdHint: string | null; fromEnv: boolean } | null>(null);
|
|
const [clientId, setClientId] = useState('');
|
|
const [clientSecret, setClientSecret] = useState('');
|
|
const [showSecret, setShowSecret] = useState(false);
|
|
const [savingCreds, setSavingCreds] = useState(false);
|
|
|
|
const currentRole = HybridBackend.getCurrentRole();
|
|
const userName = HybridBackend.getCurrentUserName();
|
|
const userEmail = HybridBackend.getCurrentUser();
|
|
const activeWorkspace = HybridBackend.getActiveWorkspace();
|
|
const clinColor = activeWorkspace?.cor || '#2563eb';
|
|
const toast = useToast();
|
|
|
|
const authHeader = { 'Authorization': `Bearer ${localStorage.getItem('SCOREODONTO_AUTH_TOKEN')}`, 'Content-Type': 'application/json' };
|
|
|
|
const fetchGoogleStatus = async () => {
|
|
try {
|
|
const res = await fetch('/api/auth/google/status');
|
|
setConnectedAccounts(await res.json());
|
|
} catch {}
|
|
};
|
|
|
|
const fetchCredsStatus = async () => {
|
|
try {
|
|
const res = await fetch('/api/settings/google-credentials', { headers: authHeader });
|
|
if (res.ok) setCredsStatus(await res.json());
|
|
} catch {}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchGoogleStatus();
|
|
if (currentRole === 'admin') fetchCredsStatus();
|
|
}, []);
|
|
|
|
const handleSaveCreds = async () => {
|
|
if (!clientId.trim() || !clientSecret.trim()) {
|
|
toast.error('PREENCHA OS DOIS CAMPOS.');
|
|
return;
|
|
}
|
|
setSavingCreds(true);
|
|
try {
|
|
const res = await fetch('/api/settings/google-credentials', {
|
|
method: 'POST',
|
|
headers: authHeader,
|
|
body: JSON.stringify({ clientId: clientId.trim(), clientSecret: clientSecret.trim() })
|
|
});
|
|
if (res.ok) {
|
|
toast.success('CREDENCIAIS SALVAS!');
|
|
setClientId('');
|
|
setClientSecret('');
|
|
await fetchCredsStatus();
|
|
} else {
|
|
const d = await res.json();
|
|
toast.error(d.error || 'ERRO AO SALVAR.');
|
|
}
|
|
} catch {
|
|
toast.error('FALHA NA CONEXÃO.');
|
|
} finally {
|
|
setSavingCreds(false);
|
|
}
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
if (window.confirm('TEM CERTEZA QUE DESEJA SAIR DO SISTEMA?')) {
|
|
HybridBackend.logout();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-8 max-w-2xl">
|
|
<div className="flex items-end justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-black text-gray-900 uppercase tracking-tight">Configurações</h1>
|
|
<p className="text-sm text-gray-400 font-medium mt-1">Gerencie seu perfil e integrações</p>
|
|
</div>
|
|
<span className="bg-gray-100 text-gray-400 text-[11px] font-mono font-bold px-3 py-1 rounded-full border border-gray-200 mb-1">
|
|
{APP_VERSION}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Perfil */}
|
|
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
|
|
<div className="px-6 py-4 border-b border-gray-50">
|
|
<h2 className="text-xs font-black text-gray-500 uppercase tracking-widest">Perfil</h2>
|
|
</div>
|
|
<div className="p-6">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-14 h-14 bg-gray-100 rounded-full flex items-center justify-center text-gray-500 border border-gray-200 flex-shrink-0">
|
|
<User size={24} />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<p className="text-base font-black text-gray-800 uppercase truncate leading-none mb-1">{userName || '—'}</p>
|
|
<p className="text-sm font-medium text-gray-400 truncate">{userEmail || '—'}</p>
|
|
<span
|
|
className="inline-block mt-2 text-[10px] font-black uppercase tracking-widest px-2 py-0.5 rounded-full"
|
|
style={{ backgroundColor: `${clinColor}18`, color: clinColor }}
|
|
>
|
|
{currentRole}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Unidade ativa */}
|
|
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
|
|
<div className="px-6 py-4 border-b border-gray-50">
|
|
<h2 className="text-xs font-black text-gray-500 uppercase tracking-widest">Unidade Ativa</h2>
|
|
</div>
|
|
<div className="p-6">
|
|
<div className="flex items-center gap-4">
|
|
<div
|
|
className="w-12 h-12 rounded-xl flex items-center justify-center text-white flex-shrink-0"
|
|
style={{ backgroundColor: clinColor }}
|
|
>
|
|
<Building2 size={20} />
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-black text-gray-800 uppercase">{activeWorkspace?.nome || 'Sem unidade vinculada'}</p>
|
|
{activeWorkspace?.id && (
|
|
<p className="text-[11px] text-gray-400 font-mono mt-0.5">ID: {activeWorkspace.id}</p>
|
|
)}
|
|
</div>
|
|
<div className="ml-auto w-3 h-3 rounded-full flex-shrink-0" style={{ backgroundColor: clinColor }} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Integrações — só para admin */}
|
|
{currentRole === 'admin' && (
|
|
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
|
|
<div className="px-6 py-4 border-b border-gray-50">
|
|
<h2 className="text-xs font-black text-gray-500 uppercase tracking-widest">Integrações Google</h2>
|
|
</div>
|
|
|
|
{/* Credenciais OAuth */}
|
|
<div className="p-6 space-y-4 border-b border-gray-50">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<KeyRound size={15} className="text-gray-400" />
|
|
<span className="text-xs font-black text-gray-700 uppercase tracking-wide">Credenciais OAuth 2.0</span>
|
|
</div>
|
|
{credsStatus?.configured ? (
|
|
<div className="flex items-center gap-1.5 text-green-600 bg-green-50 px-3 py-1 rounded-full border border-green-100">
|
|
<CheckCircle2 size={12} />
|
|
<span className="text-[10px] font-black uppercase">
|
|
{credsStatus.fromEnv ? 'Via ENV' : 'Configurado'}
|
|
</span>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center gap-1.5 text-amber-600 bg-amber-50 px-3 py-1 rounded-full border border-amber-100">
|
|
<AlertCircle size={12} />
|
|
<span className="text-[10px] font-black uppercase">Não configurado</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{credsStatus?.configured && credsStatus.clientIdHint && !credsStatus.fromEnv && (
|
|
<div className="bg-gray-50 rounded-xl px-4 py-3 border border-gray-100">
|
|
<p className="text-[10px] font-bold text-gray-400 uppercase mb-1">Client ID atual</p>
|
|
<p className="text-xs font-mono text-gray-600">{credsStatus.clientIdHint}</p>
|
|
</div>
|
|
)}
|
|
|
|
{credsStatus?.fromEnv && (
|
|
<p className="text-[11px] text-gray-400 font-medium">
|
|
Credenciais carregadas via variável de ambiente. Para substituir, preencha os campos abaixo.
|
|
</p>
|
|
)}
|
|
|
|
<div className="space-y-3">
|
|
<div>
|
|
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1.5">
|
|
Client ID
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={clientId}
|
|
onChange={e => setClientId(e.target.value)}
|
|
placeholder={credsStatus?.configured ? 'Deixe vazio para manter atual' : 'xxxxxxxxxxxx.apps.googleusercontent.com'}
|
|
className="w-full border border-gray-200 rounded-xl px-4 py-2.5 text-sm font-mono focus:ring-2 focus:ring-blue-100 focus:border-blue-300 outline-none transition-all"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1.5">
|
|
Client Secret
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
type={showSecret ? 'text' : 'password'}
|
|
value={clientSecret}
|
|
onChange={e => setClientSecret(e.target.value)}
|
|
placeholder={credsStatus?.configured ? 'Deixe vazio para manter atual' : 'GOCSPX-...'}
|
|
className="w-full border border-gray-200 rounded-xl px-4 py-2.5 pr-10 text-sm font-mono focus:ring-2 focus:ring-blue-100 focus:border-blue-300 outline-none transition-all"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowSecret(s => !s)}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
>
|
|
{showSecret ? <EyeOff size={15} /> : <Eye size={15} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleSaveCreds}
|
|
disabled={savingCreds || (!clientId.trim() && !clientSecret.trim())}
|
|
className="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl text-xs font-black uppercase tracking-widest transition-all disabled:opacity-40 disabled:cursor-not-allowed bg-gray-900 text-white hover:bg-gray-700"
|
|
>
|
|
{savingCreds ? <Loader2 size={14} className="animate-spin" /> : <KeyRound size={14} />}
|
|
{savingCreds ? 'Salvando...' : 'Salvar Credenciais'}
|
|
</button>
|
|
</div>
|
|
|
|
<p className="text-[10px] text-gray-400 leading-relaxed">
|
|
Obtenha as credenciais em{' '}
|
|
<span className="font-mono text-blue-500">console.cloud.google.com</span>
|
|
{' '}→ APIs & Services → Credentials → OAuth 2.0 Client ID.
|
|
Adicione{' '}
|
|
<span className="font-mono text-gray-600">https://scoreodonto.com/api/oauth2callback</span>
|
|
{' '}como URI de redirecionamento autorizado.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Conectar conta Google */}
|
|
<div className="p-6">
|
|
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-3">Conta conectada</p>
|
|
<GoogleConnectButton
|
|
ownerId="admin"
|
|
isConnected={connectedAccounts.some(a => a.owner_id === 'admin' || a.owner_id === userEmail)}
|
|
onStatusChange={fetchGoogleStatus}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Segurança */}
|
|
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
|
|
<div className="px-6 py-4 border-b border-gray-50">
|
|
<h2 className="text-xs font-black text-gray-500 uppercase tracking-widest">Segurança</h2>
|
|
</div>
|
|
<div className="p-6 space-y-3">
|
|
<div className="flex items-center gap-3 text-green-600 bg-green-50 px-4 py-3 rounded-xl border border-green-100">
|
|
<ShieldCheck size={16} />
|
|
<span className="text-xs font-bold uppercase tracking-wide">Sessão ativa e autenticada</span>
|
|
</div>
|
|
<button
|
|
onClick={handleLogout}
|
|
className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-red-50 text-red-600 hover:bg-red-100 rounded-xl text-xs font-black uppercase tracking-widest transition-all border border-red-100"
|
|
>
|
|
<LogOut size={16} />
|
|
Sair do Sistema
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|