| 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/copyrightchain_app/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, Users, Search, Filter, PlusCircle
} 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';
type TowerUser = {
id: string;
email: string;
name: string;
token: string;
wallet: string;
plan: string;
balance_abo: number;
cert_count: number;
created_at: string;
updated_at: string;
};
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 [towerUsers, setTowerUsers] = useState<TowerUser[]>([]);
const [towerUsersLoading, setTowerUsersLoading] = useState(false);
const [userSearch, setUserSearch] = useState('');
const [userPlanFilter, setUserPlanFilter] = useState('');
const [hideDirtyUsers, setHideDirtyUsers] = useState(true);
const [selectedUserForCerts, setSelectedUserForCerts] = useState<any | null>(null);
const [selectedUserCerts, setSelectedUserCerts] = useState<any[]>([]);
const [selectedUserCertsLoading, setSelectedUserCertsLoading] = 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);
}
}
async function loadTowerUsers() {
const tokenToUse = localStorage.getItem(LS_AI_ADMIN_TOKEN) || '';
if (!tokenToUse) {
notify('Imposta prima il token admin in Systems', 'error');
return;
}
setTowerUsersLoading(true);
try {
const res = await fetch('https://app.copyrightchain.it/api/ai/tower/users', {
method: 'GET',
headers: {
'X-CCP-Token': tokenToUse,
},
});
const text = await res.text();
const json = text ? JSON.parse(text) : null;
console.log('TOWER USERS RESPONSE', json);
if (!res.ok || !json?.ok) {
throw new Error(json?.error || 'Errore caricamento utenti Tower');
}
const users = Array.isArray(json.users) ? json.users : [];
setTowerUsers(users);
} catch (e: any) {
console.error('LOAD TOWER USERS ERROR', e);
notify(e?.message || 'Errore caricamento utenti Tower', 'error');
setTowerUsers([]);
} finally {
setTowerUsersLoading(false);
}
}
async function handleCreditUser(userId: string, userName: string) {
const tokenToUse = localStorage.getItem(LS_AI_ADMIN_TOKEN) || '';
if (!tokenToUse) {
notify('Imposta prima il token admin in Systems', 'error');
return;
}
const amountRaw = window.prompt(`Importo ABO da caricare per ${userName || userId}`, '1');
if (amountRaw === null) return;
const amount = Number(String(amountRaw).replace(',', '.'));
if (!Number.isFinite(amount) || amount === 0) {
notify('Importo non valido', 'error');
return;
}
const note = window.prompt('Causale operazione', `Ricarica manuale Tower per ${userName || userId}`) || '';
try {
const res = await fetch('https://app.copyrightchain.it/api/ai/tower/users/credit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CCP-Token': tokenToUse,
},
body: JSON.stringify({
user_id: userId,
amount,
note,
}),
});
const text = await res.text();
const json = text ? JSON.parse(text) : null;
if (!res.ok || !json?.ok) {
throw new Error(json?.error || 'Errore ricarica ABO');
}
notify(`Ricarica completata: ${amount}`, 'success');
await loadTowerUsers();
} catch (e: any) {
notify(e?.message || 'Errore ricarica ABO', 'error');
}
}
async function handleResetPassword(userId: string, userName: string) {
const tokenToUse = localStorage.getItem(LS_AI_ADMIN_TOKEN) || '';
if (!tokenToUse) {
notify('Imposta prima il token admin in Systems', 'error');
return;
}
const newPassword = window.prompt(`Nuova password temporanea per ${userName || userId}`, 'TempPass2026!');
if (newPassword === null) return;
if (!newPassword || newPassword.length < 8) {
notify('Password troppo debole', 'error');
return;
}
try {
const res = await fetch('https://app.copyrightchain.it/api/ai/tower/users/reset-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CCP-Token': tokenToUse,
},
body: JSON.stringify({
user_id: userId,
new_password: newPassword,
}),
});
const text = await res.text();
const json = text ? JSON.parse(text) : null;
if (!res.ok || !json?.ok) {
throw new Error(json?.error || 'Errore reset password');
}
notify('Password reimpostata. Cambio obbligatorio al prossimo login.', 'success');
await loadTowerUsers();
} catch (e: any) {
notify(e?.message || 'Errore reset password', 'error');
}
}
async function handleViewUserCertificates(userObj: any) {
const tokenToUse = localStorage.getItem(LS_AI_ADMIN_TOKEN) || '';
if (!tokenToUse) {
notify('Imposta prima il token admin in Systems', 'error');
return;
}
setSelectedUserForCerts(userObj);
setSelectedUserCerts([]);
setSelectedUserCertsLoading(true);
try {
const res = await fetch(`https://app.copyrightchain.it/api/ai/tower/users/${encodeURIComponent(userObj.id)}/certificates`, {
method: 'GET',
headers: {
'X-CCP-Token': tokenToUse,
},
});
const text = await res.text();
const json = text ? JSON.parse(text) : null;
if (!res.ok || !json?.ok) {
throw new Error(json?.error || 'Errore caricamento certificati utente');
}
setSelectedUserCerts(Array.isArray(json.certificates) ? json.certificates : []);
} catch (e: any) {
notify(e?.message || 'Errore caricamento certificati utente', 'error');
setSelectedUserCerts([]);
} finally {
setSelectedUserCertsLoading(false);
}
}
function openTowerPdf(path: string) {
const tokenToUse = localStorage.getItem(LS_AI_ADMIN_TOKEN) || '';
if (!tokenToUse) {
notify('Imposta prima il token admin in Systems', 'error');
return;
}
const url = `https://app.copyrightchain.it/api/ai/tower/certificates/file?path=${encodeURIComponent(path)}&token=${encodeURIComponent(tokenToUse)}`;
window.open(url, '_blank', 'noopener,noreferrer');
}
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>
);
const visibleTowerUsers = useMemo(() => {
const q = userSearch.trim().toLowerCase();
return towerUsers.filter((u) => {
const dirty =
(!u.email && !u.token) ||
String(u.id || '').trim() === 'TOK_CLIENTE_1' ||
(String(u.id || '').trim() === 'u1' && String(u.token || '').trim() === 'TOK_CLIENTE_1');
if (hideDirtyUsers && dirty) return false;
if (userPlanFilter && String(u.plan || '').toLowerCase() !== userPlanFilter.toLowerCase()) {
return false;
}
if (!q) return true;
const hay = [
u.name || '',
u.email || '',
u.id || '',
u.token || '',
u.plan || '',
].join(' ').toLowerCase();
return hay.includes(q);
});
}, [towerUsers, userSearch, userPlanFilter, hideDirtyUsers]);
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="space-y-8">
<div className="bg-white p-10 rounded-[3rem] border border-slate-100 shadow-xl">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<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>
<button onClick={loadTowerUsers} 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 users
</button>
</div>
<div className="mt-6 grid grid-cols-1 md:grid-cols-4 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">Users</div>
<div className="text-2xl font-black">{visibleTowerUsers.length}</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">Certificates</div>
<div className="text-2xl font-black">{visibleTowerUsers.reduce((a, u) => a + Number(u.cert_count || 0), 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">ABO Balance</div>
<div className="text-2xl font-black">{visibleTowerUsers.reduce((a, u) => a + Number(u.balance_abo || 0), 0).toFixed(2)}</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>
<div className="bg-white rounded-[3rem] border border-slate-100 shadow-xl overflow-hidden">
<div className="px-8 py-6 border-b border-slate-100 space-y-5">
<div className="flex items-center justify-between gap-4">
<div>
<h4 className="text-xl font-black uppercase tracking-tight flex items-center gap-3"><Users size={20} className="text-blue-600" /> Utenti registrati</h4>
<p className="text-slate-500 text-sm mt-2">Utenti reali letti dal gateway Tower.</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="md:col-span-2 relative">
<Search size={16} className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" />
<input
value={userSearch}
onChange={e => setUserSearch(e.target.value)}
placeholder="Cerca per nome, email, ID, token..."
className="w-full pl-12 pr-4 py-4 bg-slate-50 border border-slate-100 rounded-2xl font-bold text-sm outline-none"
/>
</div>
<select
value={userPlanFilter}
onChange={e => setUserPlanFilter(e.target.value)}
className="w-full p-4 bg-slate-50 border border-slate-100 rounded-2xl font-black text-xs uppercase outline-none"
>
<option value="">Tutti i piani</option>
<option value="admin">Admin</option>
<option value="pro">Pro</option>
<option value="free">Free</option>
</select>
<label className="flex items-center gap-3 px-4 py-4 bg-slate-50 border border-slate-100 rounded-2xl text-xs font-black uppercase tracking-widest text-slate-700">
<Filter size={14} />
<input
type="checkbox"
checked={hideDirtyUsers}
onChange={e => setHideDirtyUsers(e.target.checked)}
/>
Nascondi sporchi
</label>
</div>
</div>
{towerUsersLoading ? (
<div className="p-10 flex items-center justify-center gap-3 text-slate-500 font-bold">
<Loader2 size={18} className="animate-spin" /> Caricamento utenti...
</div>
) : visibleTowerUsers.length === 0 ? (
<div className="p-10 text-slate-500 font-medium">Nessun utente disponibile con i filtri correnti.</div>
) : (
<div className="overflow-x-auto">
<table className="w-full min-w-[1100px]">
<thead>
<tr className="bg-slate-50 border-b border-slate-100">
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Utente</th>
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Email</th>
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">ID</th>
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Token</th>
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Plan</th>
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">ABO</th>
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Certificati</th>
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Azioni</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{visibleTowerUsers.map((u) => (
<tr key={u.id} className="hover:bg-slate-50/40">
<td className="px-6 py-5">
<div className="font-black text-slate-900">{u.name || '—'}</div>
</td>
<td className="px-6 py-5">
<div className="text-sm font-bold text-slate-700">{u.email || '—'}</div>
</td>
<td className="px-6 py-5">
<div className="text-xs font-mono font-black text-slate-600">{u.id || '—'}</div>
</td>
<td className="px-6 py-5">
<div className="text-xs font-mono font-black text-slate-600 break-all max-w-[260px]">{u.token || '—'}</div>
</td>
<td className="px-6 py-5">
<span className="inline-flex px-3 py-1 rounded-full bg-slate-100 text-slate-700 text-[10px] font-black uppercase tracking-widest">
{u.plan || 'free'}
</span>
</td>
<td className="px-6 py-5">
<div className="text-sm font-black text-slate-900">{Number(u.balance_abo || 0).toFixed(2)}</div>
</td>
<td className="px-6 py-5">
<div className="text-sm font-black text-slate-900">{Number(u.cert_count || 0)}</div>
</td>
<td className="px-6 py-5">
<div className="flex flex-wrap gap-2">
<button
onClick={() => handleCreditUser(u.id, u.name || u.email || u.id)}
className="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-blue-600 text-white text-[10px] font-black uppercase tracking-widest shadow hover:bg-blue-700"
>
<PlusCircle size={14} />
Ricarica ABO
</button>
<button
onClick={() => handleResetPassword(u.id, u.name || u.email || u.id)}
className="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-slate-900 text-white text-[10px] font-black uppercase tracking-widest shadow hover:bg-slate-800"
>
Reset PW
</button>
<button
onClick={() => handleViewUserCertificates(u)}
className="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-emerald-600 text-white text-[10px] font-black uppercase tracking-widest shadow hover:bg-emerald-700"
>
Certificati
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{selectedUserForCerts && (
<div className="bg-white rounded-[3rem] border border-slate-100 shadow-xl overflow-hidden">
<div className="px-8 py-6 border-b border-slate-100 flex items-center justify-between gap-4">
<div>
<h4 className="text-xl font-black uppercase tracking-tight">Certificati utente</h4>
<p className="text-slate-500 text-sm mt-2">
{selectedUserForCerts.name || '—'} · {selectedUserForCerts.email || selectedUserForCerts.id}
</p>
</div>
<button
onClick={() => {
setSelectedUserForCerts(null);
setSelectedUserCerts([]);
}}
className="px-4 py-3 rounded-2xl bg-slate-50 border border-slate-100 text-slate-600 font-black uppercase text-[10px] tracking-widest"
>
Chiudi
</button>
</div>
{selectedUserCertsLoading ? (
<div className="p-10 flex items-center justify-center gap-3 text-slate-500 font-bold">
<Loader2 size={18} className="animate-spin" /> Caricamento certificati...
</div>
) : selectedUserCerts.length === 0 ? (
<div className="p-10 text-slate-500 font-medium">
Nessun certificato trovato per questo utente.
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full min-w-[1100px]">
<thead>
<tr className="bg-slate-50 border-b border-slate-100">
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">ID</th>
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Titolo/File</th>
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Tipo</th>
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Stato</th>
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Data</th>
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Hash</th>
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Azioni</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{selectedUserCerts.map((c: any) => (
<tr key={String(c?.cert_id || c?.request_id || Math.random())} className="hover:bg-slate-50/40">
<td className="px-6 py-5">
<div className="text-xs font-mono font-black text-slate-700">{c?.cert_id || c?.request_id || '—'}</div>
</td>
<td className="px-6 py-5">
<div className="font-black text-slate-900">{c?.title || '—'}</div>
<div className="text-xs font-bold text-slate-500 break-all">{c?.original_filename || '—'}</div>
</td>
<td className="px-6 py-5">
<span className="inline-flex px-3 py-1 rounded-full bg-slate-100 text-slate-700 text-[10px] font-black uppercase tracking-widest">
{c?.cert_type || 'base'}
</span>
</td>
<td className="px-6 py-5">
<span className={`inline-flex px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest ${
(c?.blockchain_status || '') === 'confirmed'
? 'bg-emerald-50 text-emerald-700'
: (c?.blockchain_status || '') === 'pending'
? 'bg-amber-50 text-amber-700'
: 'bg-slate-100 text-slate-700'
}`}>
{c?.blockchain_status || (c?.cert_type === 'base' ? 'base' : '—')}
</span>
</td>
<td className="px-6 py-5">
<div className="text-sm font-black text-slate-900">{c?.deposit_date || c?.ts || '—'}</div>
</td>
<td className="px-6 py-5">
<div className="text-xs font-mono font-black text-slate-600 break-all max-w-[260px]">{c?.file_hash || '—'}</div>
</td>
<td className="px-6 py-5">
<div className="flex flex-wrap gap-2">
{c?.verify_url ? (
<a
href={c.verify_url}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-blue-600 text-white text-[10px] font-black uppercase tracking-widest shadow hover:bg-blue-700"
>
Verifica
</a>
) : null}
{c?.certificate_path ? (
<button
onClick={() => openTowerPdf(c.certificate_path)}
className="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-slate-900 text-white text-[10px] font-black uppercase tracking-widest shadow hover:bg-slate-800"
>
Apri PDF
</button>
) : null}
</div>
</td>
</tr>
))}
</tbody>
</table>
</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 gateway: <b>{aiStatus?.ok ? 'OK' : '—'}</b><br/>
OpenAI key nel gateway: <b>{aiStatus?.has_openai_key ? 'OK' : 'NO'}</b><br/>
Gemini key nel gateway: <b>{aiStatus?.has_gemini_key ? 'OK' : 'NO'}</b>
</div>
<div className="p-4 bg-blue-50 border border-blue-100 rounded-2xl text-xs text-blue-900 leading-relaxed">
Le chiavi AI di produzione sono già caricate sul VPS nel gateway. Questa sezione va considerata come pannello tecnico di override/test e non come configurazione obbligatoria per il funzionamento quotidiano.
</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 di produzione sono già salvate sul VPS nel gateway.</li>
<li>I campi qui servono solo per override tecnico o test manuali.</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>
);
}