| Server IP : 54.37.205.81 / Your IP : 216.73.216.76 Web Server : nginx/1.22.1 System : Linux vps-249481fa 6.1.0-50-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.176-1 (2026-07-02) x86_64 User : debian ( 1000) PHP Version : 8.2.32 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/app.copyrightchain.it/patch/frontend/components/AdminDashboard/ |
Upload File : |
import React, { useEffect, useMemo, useState } from 'react';
import {
Activity, CreditCard, KeyRound, Loader2, Rocket, Save, Settings, Share2, Sparkles, Image as ImageIcon, Copy, RefreshCw
} from 'lucide-react';
import type { Certificate, Language, AppUser } from '../../types';
import { useNotification } from '../../App';
import { usePersistedState } from '../../hooks/usePersistedState';
import { apiFetch } from '../../services/apiClient';
import { aiGenerateImage, aiGenerateText, aiHealth, aiSetKeys, type AiProvider } from '../../services/aiGatewayService';
interface AdminDashboardProps {
certificates: Certificate[];
lang: Language;
users: AppUser[];
onUpdateUser: (user: AppUser) => void;
}
type AdminTab = 'stats' | 'growth' | 'systems';
const LS_AI_ADMIN_TOKEN = 'ccp_ai_admin_token';
export default function AdminDashboard(props: AdminDashboardProps) {
const notify = useNotification();
const [adminTab, setAdminTab] = useState<AdminTab>('stats');
// --- GROWTH ---
const [mktType, setMktType] = useState<'Post'|'Email'|'Blog'|'Ad'>('Post');
const [mktChannel, setMktChannel] = useState('LinkedIn');
const [mktAspectRatio, setMktAspectRatio] = useState('1:1');
const [mktContext, setMktContext] = useState('');
const [textProvider, setTextProvider] = useState<AiProvider>('openai');
const [imageProvider, setImageProvider] = useState<AiProvider>('gemini');
const [isGenText, setIsGenText] = useState(false);
const [isGenVisual, setIsGenVisual] = useState(false);
const [genContent, setGenContent] = useState('');
const [genVisual, setGenVisual] = useState('');
// --- SYSTEMS ---
const [aiAdminToken, setAiAdminToken] = useState(localStorage.getItem(LS_AI_ADMIN_TOKEN) || '');
const [openaiKeyDraft, setOpenaiKeyDraft] = useState('');
const [geminiKeyDraft, setGeminiKeyDraft] = useState('');
const [aiStatus, setAiStatus] = useState<any>(null);
const [isSavingKeys, setIsSavingKeys] = useState(false);
const [isTestingAi, setIsTestingAi] = useState(false);
const onCopy = async (t: string) => {
try { await navigator.clipboard.writeText(t); notify('Copiato', 'success'); } catch { notify('Copia non riuscita', 'error'); }
};
async function refreshAiHealth() {
try {
const j = await aiHealth();
setAiStatus(j);
} catch {
setAiStatus({ ok:false });
}
}
useEffect(() => { refreshAiHealth(); }, []);
const promptBase = useMemo(() => {
const langLabel = props.lang === 'it' ? 'Italiano' : 'English';
return `Tipo: ${mktType}\nCanale: ${mktChannel}\nLingua: ${langLabel}\nContesto: ${mktContext}`.trim();
}, [mktType, mktChannel, props.lang, mktContext]);
async function handleGenerateCampaign() {
if (!aiAdminToken) return notify('Imposta il token AI in Systems', 'error');
setIsGenText(true);
try {
const r = await aiGenerateText(aiAdminToken, textProvider, promptBase + "\n\nGenera copy marketing pronto da pubblicare, con CTA e 3 varianti.");
if (!r.ok) throw new Error(r.error || 'AI error');
setGenContent(r.text || '');
} catch (e:any) {
notify(e?.message || 'Errore generazione testo', 'error');
} finally {
setIsGenText(false);
}
}
async function handleGenerateVisual() {
if (!aiAdminToken) return notify('Imposta il token AI in Systems', 'error');
setIsGenVisual(true);
try {
const r = await aiGenerateImage(aiAdminToken, imageProvider, `Crea un visual marketing professionale. ${promptBase}`, mktAspectRatio);
if (!r.ok) throw new Error(r.error || 'AI error');
setGenVisual(r.data_url || '');
} catch (e:any) {
notify(e?.message || 'Errore generazione immagine', 'error');
} finally {
setIsGenVisual(false);
}
}
async function handleSaveAiKeys() {
if (!aiAdminToken) return notify('Token AI mancante', 'error');
setIsSavingKeys(true);
try {
localStorage.setItem(LS_AI_ADMIN_TOKEN, aiAdminToken);
const j = await aiSetKeys(aiAdminToken, openaiKeyDraft || undefined, geminiKeyDraft || undefined);
if (!j?.ok) throw new Error(j?.error || 'Save keys failed');
setOpenaiKeyDraft('');
setGeminiKeyDraft('');
await refreshAiHealth();
notify('Chiavi salvate nel gateway', 'success');
} catch (e:any) {
notify(e?.message || 'Errore salvataggio', 'error');
} finally {
setIsSavingKeys(false);
}
}
async function handleTestAi() {
if (!aiAdminToken) return notify('Token AI mancante', 'error');
setIsTestingAi(true);
try {
const r = await aiGenerateText(aiAdminToken, 'openai', 'Rispondi solo con OK.');
if (!r.ok) throw new Error(r.error || 'Test failed');
notify('AI OK', 'success');
await refreshAiHealth();
} catch (e:any) {
notify(e?.message || 'Test AI fallito', 'error');
} finally {
setIsTestingAi(false);
}
}
const TabBtn = ({id,label,icon}:{id:AdminTab,label:string,icon:React.ReactNode}) => (
<button onClick={() => setAdminTab(id)} className={`px-5 py-3 rounded-2xl text-[11px] font-black uppercase tracking-widest flex items-center gap-2 transition-all ${adminTab===id?'bg-slate-900 text-white shadow':'text-slate-500 hover:text-slate-900 hover:bg-slate-50'}`}>
{icon}{label}
</button>
);
return (
<div className="p-10 space-y-10">
<div className="flex flex-wrap gap-3">
<TabBtn id="stats" label="Stats" icon={<Activity size={16} />} />
<TabBtn id="growth" label="Growth" icon={<Rocket size={16} />} />
<TabBtn id="systems" label="Systems" icon={<Settings size={16} />} />
</div>
{adminTab === 'stats' && (
<div className="bg-white p-10 rounded-[3rem] border border-slate-100 shadow-xl">
<h3 className="text-2xl font-black uppercase tracking-tight">Tower Overview</h3>
<p className="text-slate-500 text-sm mt-3">Questa sezione è per amministrazione interna (Tower). Il cliente non la vede.</p>
<div className="mt-6 grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-6 bg-slate-50 rounded-3xl border border-slate-100">
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Works</div>
<div className="text-2xl font-black">{props.certificates?.length ?? 0}</div>
</div>
<div className="p-6 bg-slate-50 rounded-3xl border border-slate-100">
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Users</div>
<div className="text-2xl font-black">{props.users?.length ?? 0}</div>
</div>
<div className="p-6 bg-slate-50 rounded-3xl border border-slate-100">
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">AI Gateway</div>
<div className="text-2xl font-black">{aiStatus?.ok ? 'OK' : '—'}</div>
</div>
</div>
</div>
)}
{adminTab === 'growth' && (
<div className="space-y-8">
<div className="bg-white p-12 rounded-[4rem] border border-slate-100 shadow-xl space-y-10">
<div className="flex justify-between items-center">
<h3 className="text-2xl font-black uppercase tracking-tight flex items-center gap-4"><Sparkles size={28} className="text-indigo-600" /> Growth Hub (Marketing)</h3>
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-slate-400">
<Share2 size={14}/>solo admin
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="space-y-3">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Cosa vuoi promuovere?</label>
<textarea value={mktContext} onChange={e => setMktContext(e.target.value)} className="w-full p-8 bg-slate-50 border border-slate-100 rounded-[2.5rem] text-sm font-medium h-48 outline-none" placeholder="Es: Feature OTS + prova gratuita 7 giorni..." />
</div>
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Formato</label>
<select value={mktType} onChange={e => setMktType(e.target.value as any)} className="w-full p-5 bg-slate-50 border border-slate-100 rounded-3xl font-black text-xs uppercase outline-none">
<option>Post</option><option>Email</option><option>Blog</option><option>Ad</option>
</select>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Canale</label>
<select value={mktChannel} onChange={e => setMktChannel(e.target.value)} className="w-full p-5 bg-slate-50 border border-slate-100 rounded-3xl font-black text-xs uppercase outline-none">
<option value="LinkedIn">LinkedIn</option>
<option value="Instagram">Instagram</option>
<option value="X (Twitter)">X (Twitter)</option>
<option value="Email">Newsletter</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Aspect Ratio</label>
<select value={mktAspectRatio} onChange={e => setMktAspectRatio(e.target.value)} className="w-full p-5 bg-slate-50 border border-slate-100 rounded-3xl font-black text-xs uppercase outline-none">
<option value="1:1">1:1</option>
<option value="16:9">16:9</option>
<option value="9:16">9:16</option>
</select>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Provider</label>
<div className="grid grid-cols-2 gap-2">
<select value={textProvider} onChange={e => setTextProvider(e.target.value as AiProvider)} className="w-full p-5 bg-slate-50 border border-slate-100 rounded-3xl font-black text-xs uppercase outline-none">
<option value="openai">Text: OpenAI</option>
<option value="gemini">Text: Gemini</option>
</select>
<select value={imageProvider} onChange={e => setImageProvider(e.target.value as AiProvider)} className="w-full p-5 bg-slate-50 border border-slate-100 rounded-3xl font-black text-xs uppercase outline-none">
<option value="gemini">Image: Gemini</option>
</select>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<button onClick={handleGenerateCampaign} disabled={isGenText || !mktContext} className="py-6 bg-slate-900 text-white rounded-[2rem] font-black uppercase text-[10px] tracking-widest shadow-2xl flex items-center justify-center gap-3">
{isGenText ? <Loader2 size={18} className="animate-spin" /> : <><Rocket size={18} /> Genera Testo</>}
</button>
<button onClick={handleGenerateVisual} disabled={isGenVisual || !mktContext} className="py-6 bg-indigo-600 text-white rounded-[2rem] font-black uppercase text-[10px] tracking-widest shadow-2xl flex items-center justify-center gap-3">
{isGenVisual ? <Loader2 size={18} className="animate-spin" /> : <><ImageIcon size={18} /> Genera Immagine</>}
</button>
</div>
</div>
</div>
</div>
{(genContent || genVisual) && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="bg-slate-900 text-white p-10 rounded-[3rem] shadow-2xl flex flex-col">
<div className="text-[10px] font-black text-indigo-400 uppercase tracking-widest mb-6">Testo generato</div>
<pre className="text-sm whitespace-pre-wrap flex-1">{genContent}</pre>
<button onClick={() => onCopy(genContent)} className="w-full py-4 mt-6 bg-white/5 border border-white/10 rounded-2xl font-black uppercase text-[10px] tracking-widest flex items-center justify-center gap-2 hover:bg-white/10">
<Copy size={14}/>Copia
</button>
</div>
<div className="bg-white p-3 rounded-[3rem] shadow-2xl border border-slate-100 overflow-hidden">
{genVisual ? <img src={genVisual} className="w-full h-full object-cover rounded-[2.6rem]" alt="AI Visual" /> : null}
</div>
</div>
)}
</div>
)}
{adminTab === 'systems' && (
<div className="bg-white p-12 rounded-[4rem] border border-slate-100 shadow-xl space-y-8">
<div className="flex justify-between items-center">
<h3 className="text-2xl font-black uppercase tracking-tight flex items-center gap-4"><Settings size={28} className="text-blue-600" /> Systems (Tecnico)</h3>
<button onClick={refreshAiHealth} className="px-4 py-3 rounded-2xl bg-slate-50 border border-slate-100 text-slate-600 font-black uppercase text-[10px] tracking-widest flex items-center gap-2">
<RefreshCw size={14}/>refresh
</button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="p-8 bg-slate-50 rounded-[3rem] border border-slate-100 space-y-5">
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">AI Gateway</div>
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Admin Token</label>
<input value={aiAdminToken} onChange={e => setAiAdminToken(e.target.value)} className="w-full p-5 bg-white border border-slate-100 rounded-3xl font-black text-xs outline-none" placeholder="CCP_AI_ADMIN_TOKEN" />
<div className="grid grid-cols-1 gap-3">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">OpenAI API Key (opzionale)</label>
<input value={openaiKeyDraft} onChange={e => setOpenaiKeyDraft(e.target.value)} className="w-full p-5 bg-white border border-slate-100 rounded-3xl font-black text-xs outline-none" placeholder="sk-..." />
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Gemini API Key (opzionale)</label>
<input value={geminiKeyDraft} onChange={e => setGeminiKeyDraft(e.target.value)} className="w-full p-5 bg-white border border-slate-100 rounded-3xl font-black text-xs outline-none" placeholder="AIza..." />
</div>
<div className="grid grid-cols-2 gap-3">
<button onClick={handleSaveAiKeys} disabled={isSavingKeys} className="py-5 bg-slate-900 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest shadow-xl flex items-center justify-center gap-2">
{isSavingKeys ? <Loader2 size={16} className="animate-spin" /> : <><Save size={16}/>Salva</>}
</button>
<button onClick={handleTestAi} disabled={isTestingAi} className="py-5 bg-blue-600 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest shadow-xl flex items-center justify-center gap-2">
{isTestingAi ? <Loader2 size={16} className="animate-spin" /> : <><KeyRound size={16}/>Test</>}
</button>
</div>
<div className="text-xs text-slate-600 leading-relaxed">
Stato: <b>{aiStatus?.ok ? 'OK' : '—'}</b><br/>
OpenAI key: <b>{aiStatus?.has_openai_key ? 'OK' : 'NO'}</b><br/>
Gemini key: <b>{aiStatus?.has_gemini_key ? 'OK' : 'NO'}</b>
</div>
</div>
<div className="p-8 bg-slate-50 rounded-[3rem] border border-slate-100 space-y-5">
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Note</div>
<div className="text-sm text-slate-600 leading-relaxed">
<ul className="list-disc pl-5 space-y-2">
<li>Questa sezione è solo per Tower (admin).</li>
<li>Le chiavi vengono salvate sul VPS nel gateway (non nel browser).</li>
<li>La pagina Growth usa OpenAI per i testi e Gemini per le immagini (se configurati).</li>
</ul>
</div>
<div className="p-5 bg-white rounded-2xl border border-slate-100 text-xs text-slate-600">
Se il gateway non risponde: controlla systemd <code>sudo systemctl status ccp-ai-gateway</code> e NGINX <code>sudo nginx -t</code>.
</div>
</div>
</div>
</div>
)}
</div>
);
}