| 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 : /tmp/cc-agency-update/copyrightchain_app/ |
Upload File : |
import React, { useState, createContext, useContext, useMemo, useEffect, Suspense } from 'react';
import LandingPage from './components/LandingPage';
import Dashboard from './components/Dashboard';
import FileUploader from './components/FileUploader';
import PostalService from './components/PostalService';
import CertificateView from './components/CertificateView';
import VerificationPortal from './components/VerificationPortal';
const AdminDashboard = React.lazy(() => import('./components/AdminDashboard'));
import AdminLogin from './components/AdminLogin';
const LegalVault = React.lazy(() => import('./components/LegalVault'));
const AIAssistant = React.lazy(() => import('./components/AIAssistant'));
import UserProfile from './components/UserProfile';
const LegalDefense = React.lazy(() => import('./components/LegalDefense'));
import IPMonitor from './components/IPMonitor';
const BillingCenter = React.lazy(() => import('./components/BillingCenter'));
const LicenseGenerator = React.lazy(() => import('./components/LicenseGenerator'));
import { Certificate, Language, AppUser, IdentityData } from './types';
import { translations } from './translations';
import { draftNotificationEmail, simulateEmailSend } from './emailService';
import { setAuthToken } from './services/apiClient';
import { AuthProvider, useAuth } from './context/AuthContext';
import { Shield, Layout, FileText, Archive, MessageSquare, User as UserIcon, Gavel, Monitor, Menu, X, Coins, Lock, Award, Scale, Home, Send } from 'lucide-react';
const GATEWAY_BASE = 'https://app.copyrightchain.it/api/ai';
type ClientUserApi = {
id: string;
email: string;
name: string;
wallet?: string;
plan?: string;
ai_limit_monthly?: number;
updated_at?: string;
created_at?: string;
must_change_password?: boolean;
profile?: any;
postal_profile?: any;
};
type ClientAuthResponse = {
ok: boolean;
token: string;
user: ClientUserApi;
balance_abo: number;
agency?: any;
pricing?: any;
must_change_password?: boolean;
profile?: any;
postal_profile?: any;
};
export interface NotificationContextType {
notify: (message: string, type?: 'success' | 'error' | 'info' | 'warning') => void;
user: AppUser;
deductCredits: (amount: number) => boolean;
addCredits: (amount: number) => void;
setUserRole: (role: 'admin' | 'client') => void;
onUpdateUser: (updatedUser: AppUser) => void;
}
const NotificationContext = createContext<NotificationContextType | undefined>(undefined);
export const useNotification = () => {
const context = useContext(NotificationContext);
if (!context) throw new Error("useNotification must be used within a NotificationProvider");
return context;
};
function buildEmptyUser(role: 'admin' | 'client'): AppUser {
return {
id: '',
username: 'User',
name: '',
email: '',
role,
identity: { verified: false },
registrationDate: new Date().toISOString(),
credits: { balance: 0 },
wallet: '',
plan: 'free',
ai_limit_monthly: 0,
must_change_password: false,
created_at: '',
updated_at: '',
isAuthenticated: false
};
}
function getLocalPostalProfile() {
try {
return JSON.parse(localStorage.getItem('ccp_profile_postal') || '{}') || {};
} catch (_) {
return {};
}
}
function mapClientUserToAppUser(raw: ClientUserApi, balance: number, role: 'admin' | 'client', agency?: any): AppUser {
const backendName = String(raw?.name || '').trim();
const localPostal = getLocalPostalProfile();
const backendPostal = raw?.postal_profile || raw?.profile?.postal || {};
const hasBackendPostal = backendPostal && typeof backendPostal === 'object' && Object.keys(backendPostal).length > 0;
const postal = hasBackendPostal ? backendPostal : localPostal;
try {
if (postal && typeof postal === 'object' && Object.keys(postal).length > 0) {
localStorage.setItem('ccp_profile_postal', JSON.stringify(postal));
}
} catch (_) {}
const postalName = [postal?.nome, postal?.cognome].filter(Boolean).join(' ').trim();
const name = postalName || backendName;
return {
id: String(raw?.id || ''),
username: name || 'User',
name,
email: String(raw?.email || ''),
role,
identity: { verified: false, postal },
registrationDate: String(raw?.created_at || new Date().toISOString()),
credits: { balance: typeof balance === 'number' ? balance : 0 },
wallet: String(raw?.wallet || ''),
plan: String(raw?.plan || 'free'),
ai_limit_monthly: Number(raw?.ai_limit_monthly || 0),
must_change_password: Boolean(raw?.must_change_password),
created_at: String(raw?.created_at || ''),
updated_at: String(raw?.updated_at || ''),
isAuthenticated: true,
agency: agency || (raw as any)?.agency
};
}
const App: React.FC = () => {
const isAdminHost = window.location.hostname.startsWith('admin.');
const forcedRole: 'admin' | 'client' = isAdminHost ? 'admin' : 'client';
const [lang, setLang] = useState<Language>('it');
const [activeTab, setActiveTab] = useState<'landing' | 'dashboard' | 'upload' | 'postal' | 'postal-register' | 'verify' | 'certificates' | 'admin' | 'ai-assistant' | 'profile' | 'defense' | 'monitor' | 'billing' | 'licenses' | 'auth'>(isAdminHost ? 'admin' : 'landing');
const [certificates, setCertificates] = useState<Certificate[]>(() => {
try {
const raw = sessionStorage.getItem('ccp_certificates');
return raw ? JSON.parse(raw) : [];
} catch (_) {
return [];
}
});
const [selectedCert, setSelectedCert] = useState<Certificate | null>(null);
const [notifications, setNotifications] = useState<{ id: number, message: string, type: string }[]>([]);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [logoClickCount, setLogoClickCount] = useState(0);
const [hasConsent, setHasConsent] = useState<boolean>(() => localStorage.getItem('cc_consent') === 'true');
const [clientToken, setClientToken] = useState<string | null>(() => sessionStorage.getItem('ccp_client_token'));
const [isBootstrappingClient, setIsBootstrappingClient] = useState<boolean>(!isAdminHost && !!sessionStorage.getItem('ccp_client_token'));
const [mustChangePassword, setMustChangePassword] = useState(false);
const [currentPasswordForChange, setCurrentPasswordForChange] = useState('');
const [newForcedPassword, setNewForcedPassword] = useState('');
const [confirmForcedPassword, setConfirmForcedPassword] = useState('');
const [forcedPasswordLoading, setForcedPasswordLoading] = useState(false);
const [user, setUser] = useState<AppUser>(() => {
const saved = sessionStorage.getItem('app_user_data');
if (saved) {
try {
const parsed = JSON.parse(saved);
return {
...buildEmptyUser(forcedRole),
...parsed,
role: forcedRole,
isAuthenticated: Boolean(parsed?.isAuthenticated && sessionStorage.getItem('ccp_client_token'))
};
} catch (_) {}
}
return buildEmptyUser(forcedRole);
});
const t = translations[lang];
const hydrateCertificateFromApi = (row: any): Certificate => {
const certType = String(row?.cert_type || 'base');
const blockchainStatus = String(row?.blockchain_status || (certType === 'blockchain' ? 'pending' : ''));
const originalFilename = String(row?.original_filename || row?.title || '');
const title = String(row?.title || originalFilename || row?.cert_id || 'Certificate');
return {
id: String(row?.cert_id || row?.request_id || ''),
title,
fileName: originalFilename,
fileHash: String(row?.file_hash || ''),
timestamp: String(row?.deposit_date || new Date().toISOString()),
author: String(row?.author || ''),
legalAttestation: String(
row?.legal_attestation ||
`Certificato emesso via CopyrightChain AI Gateway. ID: ${String(row?.cert_id || '')}. Verifica: ${String(row?.verify_url || '')}`
),
status: 'certified',
paymentStatus: 'paid',
mimeType: 'application/pdf',
fileData: String(row?.certificate_url || ''),
language: lang,
tier: certType === 'timestamp' ? 'timestamp' : certType === 'blockchain' ? 'pro' : 'basic',
timestampEvidence: certType === 'timestamp' ? {
status: String(row?.timestamp_status || ''),
provider: String(row?.timestamp_provider || ''),
environment: String(row?.timestamp_environment || ''),
type: String(row?.timestamp_type || ''),
hash: String(row?.timestamp_hash || row?.file_hash || ''),
issuedAt: String(row?.timestamp_issued_at || ''),
transaction: String(row?.timestamp_transaction || ''),
bodyUrl: String(row?.timestamp_body_url || ''),
available: typeof row?.timestamp_available === 'number' ? row.timestamp_available : undefined,
used: typeof row?.timestamp_used === 'number' ? row.timestamp_used : undefined
} : undefined,
verifyUrl: String(row?.verify_url || ''),
certificateUrl: String(row?.certificate_url || ''),
otsProofUrl: String(row?.ots_proof_url || ''),
otsManifestUrl: String(row?.ots_manifest_url || ''),
anchoredAt: String(row?.anchored_at || ''),
confirmedAt: String(row?.confirmed_at || ''),
bitcoinEvidence: certType === 'blockchain' ? {
status: (blockchainStatus as any) || 'pending',
txId: String(row?.btc_txid || ''),
blockHeight: typeof row?.btc_block_height === 'number' ? row.btc_block_height : undefined
} : undefined
} as Certificate;
};
const notify = (message: string, type: 'success' | 'error' | 'info' | 'warning' = 'info') => {
const id = Date.now();
setNotifications(prev => [...prev, { id, message, type }]);
setTimeout(() => setNotifications(prev => prev.filter(n => n.id !== id)), 5000);
};
const loadClientCertificates = async (token: string) => {
try {
const res = await fetch(`${GATEWAY_BASE}/client/certificates`, {
headers: {
'X-CCP-Token': token
}
});
if (!res.ok) {
throw new Error(`client_certificates_${res.status}`);
}
const data = await res.json();
const items: Certificate[] = Array.isArray(data?.certificates)
? data.certificates
.filter((row: any) => String(row?.cert_id || row?.request_id || '').trim())
.map((row: any) => hydrateCertificateFromApi(row))
: [];
setCertificates(items);
setSelectedCert(prev => {
if (!prev) return prev;
return items.find(c => c.id === prev.id) || null;
});
try {
sessionStorage.setItem('ccp_certificates', JSON.stringify(items));
} catch (_) {}
} catch (err) {
console.error('loadClientCertificates failed', err);
}
};
useEffect(() => {
if (isAdminHost) return;
const sp = new URLSearchParams(window.location.search);
const auth = (sp.get('auth') || '').toLowerCase().trim();
if (auth === 'login' || auth === 'register') {
setActiveTab('auth');
}
}, [isAdminHost]);
useEffect(() => {
if (isAdminHost) return;
try {
sessionStorage.setItem('ccp_certificates', JSON.stringify(certificates));
} catch (_) {}
}, [certificates, isAdminHost]);
useEffect(() => {
if (isAdminHost || !clientToken) return;
loadClientCertificates(clientToken);
}, [clientToken, isAdminHost, lang]);
useEffect(() => {
sessionStorage.setItem('app_user_data', JSON.stringify(user));
if (clientToken) sessionStorage.setItem('ccp_client_token', clientToken);
else sessionStorage.removeItem('ccp_client_token');
}, [user, clientToken]);
useEffect(() => {
setAuthToken(clientToken);
}, [clientToken]);
useEffect(() => {
if (!isAdminHost && activeTab === 'admin') {
setActiveTab(user.isAuthenticated ? 'dashboard' : 'auth');
}
}, [activeTab, isAdminHost, user.isAuthenticated]);
useEffect(() => {
if (isAdminHost) return;
if (!clientToken) {
setIsBootstrappingClient(false);
setUser(prev => ({ ...buildEmptyUser(forcedRole), role: forcedRole, credits: { balance: prev.credits?.balance || 0 } }));
return;
}
let cancelled = false;
const bootstrapClient = async () => {
setIsBootstrappingClient(true);
try {
const res = await fetch(`${GATEWAY_BASE}/client/me`, {
method: 'GET',
headers: {
'X-CCP-Token': clientToken
}
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.ok || !data?.user) {
throw new Error(data?.error || 'session_invalid');
}
if (cancelled) return;
const nextUser = mapClientUserToAppUser(data.user, Number(data.balance_abo || 0), forcedRole, data.agency);
setUser(nextUser);
setMustChangePassword(Boolean(data?.must_change_password || data?.user?.must_change_password));
await loadClientCertificates(clientToken);
const sp = new URLSearchParams(window.location.search);
const billing = (sp.get('billing') || '').toLowerCase().trim();
if (billing === 'success') {
notify(lang === 'it' ? 'Pagamento confermato. Wallet aggiornato.' : 'Payment confirmed. Wallet updated.', 'success');
sp.delete('billing');
const u = new URL(window.location.href);
u.search = sp.toString() ? `?${sp.toString()}` : '';
window.history.replaceState({}, '', u.toString());
} else if (billing === 'cancel') {
notify(lang === 'it' ? 'Pagamento annullato.' : 'Payment cancelled.', 'warning');
sp.delete('billing');
const u = new URL(window.location.href);
u.search = sp.toString() ? `?${sp.toString()}` : '';
window.history.replaceState({}, '', u.toString());
}
} catch (_) {
if (cancelled) return;
setClientToken(null);
setUser(buildEmptyUser(forcedRole));
setActiveTab('auth');
} finally {
if (!cancelled) setIsBootstrappingClient(false);
}
};
bootstrapClient();
return () => { cancelled = true; };
}, [clientToken, isAdminHost, forcedRole, lang]);
useEffect(() => {
if (isAdminHost || isBootstrappingClient) return;
const isPublic = ['landing', 'verify', 'auth'].includes(activeTab);
if (!user.isAuthenticated && !isPublic) {
notify(lang === 'it'
? 'Per accedere all’area cliente devi effettuare il login.'
: 'You must login to access the client area.',
'info'
);
setActiveTab('auth');
}
}, [activeTab, user.isAuthenticated, isAdminHost, isBootstrappingClient, lang]);
function handleLogoClick() {
if (!isAdminHost || user.role !== 'admin') return;
setLogoClickCount(prev => {
const next = prev + 1;
if (next === 5) {
setActiveTab('admin');
notify("Admin Portal Unlocked", "success");
return 0;
}
return next;
});
setTimeout(() => setLogoClickCount(0), 2000);
}
const deductCredits = (amount: number): boolean => {
if (user.credits.balance < amount) {
notify(translations[lang].insufficientCredits, "error");
setActiveTab('billing');
return false;
}
setUser(prev => ({
...prev,
credits: { balance: prev.credits.balance - amount }
}));
return true;
};
const addCredits = (_amount: number) => {
notify("Ricarica temporaneamente disattivata: accredito solo dopo conferma pagamento Stripe.", "error");
};
const setUserRole = (role: 'admin' | 'client') => {
setUser(prev => ({ ...prev, role }));
notify(`Modalità ${role === 'admin' ? 'Admin' : 'Client'}`, "info");
};
const handleUpdateUser = (updatedUser: AppUser) => {
setUser(updatedUser);
};
const handleClientAuthSuccess = (payload: ClientAuthResponse) => {
const nextUser = mapClientUserToAppUser(payload.user, Number(payload.balance_abo || 0), forcedRole, payload.agency);
const mustChange = Boolean(payload?.must_change_password || payload?.user?.must_change_password);
setClientToken(payload.token);
setUser(nextUser);
setMustChangePassword(mustChange);
setCurrentPasswordForChange('');
setNewForcedPassword('');
setConfirmForcedPassword('');
setActiveTab(mustChange ? 'profile' : 'dashboard');
};
const handleClientLogout = async () => {
try {
if (clientToken) {
await fetch(`${GATEWAY_BASE}/client/logout`, {
method: 'POST',
headers: {
'X-CCP-Token': clientToken
}
}).catch(() => null);
}
} finally {
setClientToken(null);
try { sessionStorage.removeItem('ccp_certificates'); } catch (_) {}
setCertificates([]);
setSelectedCert(null);
setUser(buildEmptyUser(forcedRole));
setActiveTab('auth');
notify(lang === 'it' ? 'Sessione chiusa.' : 'Session closed.', 'info');
}
};
const handleAddCertificate = (cert: Certificate) => {
setCertificates(prev => [cert, ...prev]);
setSelectedCert(cert);
setActiveTab('certificates');
notify(lang === 'it' ? "Certificato emesso con successo. In fase di ancoraggio..." : "Certificate issued successfully. Anchoring in progress...", "success");
};
async function handleForcedPasswordChange() {
if (!clientToken) {
notify('Sessione non valida', 'error');
return;
}
if (!newForcedPassword.trim() || newForcedPassword.trim().length < 8) {
notify('La nuova password deve avere almeno 8 caratteri', 'error');
return;
}
if (newForcedPassword !== confirmForcedPassword) {
notify('Le password non coincidono', 'error');
return;
}
setForcedPasswordLoading(true);
try {
const res = await fetch(`${GATEWAY_BASE}/client/change_password`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CCP-Token': clientToken
},
body: JSON.stringify({
current_password: currentPasswordForChange,
new_password: newForcedPassword
})
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.ok) {
throw new Error(
data?.error === 'weak_password' ? 'Password troppo debole.' :
data?.error === 'wrong_current_password' ? 'Password corrente errata.' :
data?.error || 'change_password_failed'
);
}
setMustChangePassword(false);
setCurrentPasswordForChange(newForcedPassword);
setNewForcedPassword('');
setConfirmForcedPassword('');
setActiveTab('dashboard');
notify('Password aggiornata con successo', 'success');
} catch (e: any) {
notify(e?.message || 'Errore cambio password', 'error');
} finally {
setForcedPasswordLoading(false);
}
}
const AdminArea: React.FC = () => {
const { token, isAdmin } = useAuth();
const [postalDrafts, setPostalDrafts] = useState<any[]>([]);
const [postalLoading, setPostalLoading] = useState(false);
const [postalError, setPostalError] = useState('');
const [postalAdminTokenInput, setPostalAdminTokenInput] = useState(() => localStorage.getItem('ccp_ai_admin_token') || '');
const [adminSection, setAdminSection] = useState<'overview' | 'agency' | 'postal'>('agency');
const [agencyPlans, setAgencyPlans] = useState<any[]>([]);
const [agencyLoading, setAgencyLoading] = useState(false);
const [agencyError, setAgencyError] = useState('');
const [agencyEmail, setAgencyEmail] = useState('');
const [agencyMonthlyCredits, setAgencyMonthlyCredits] = useState(30);
const [agencyRenewalDate, setAgencyRenewalDate] = useState('');
const [agencyNotes, setAgencyNotes] = useState('');
const getGatewayAdminToken = () => (
postalAdminTokenInput ||
localStorage.getItem('ccp_ai_admin_token') ||
localStorage.getItem('ccp_admin_token') ||
''
);
const loadPostalDrafts = async () => {
const gatewayAdminToken =
postalAdminTokenInput ||
localStorage.getItem('ccp_ai_admin_token') ||
localStorage.getItem('ccp_admin_token') ||
'';
if (!gatewayAdminToken) return;
setPostalLoading(true);
setPostalError('');
try {
const res = await fetch(`${GATEWAY_BASE}/admin/postal/smart/drafts`, {
headers: {
'X-CCP-Token': gatewayAdminToken,
Authorization: `Bearer ${token || gatewayAdminToken}`
}
});
const data = await res.json();
if (!res.ok || !data?.ok) throw new Error(data?.error || `HTTP ${res.status}`);
setPostalDrafts(Array.isArray(data?.drafts) ? data.drafts : []);
} catch (e: any) {
setPostalError(e?.message || 'Errore caricamento bozze invio documenti');
} finally {
setPostalLoading(false);
}
};
const confirmPostalDraft = async (draft: any) => {
const postalId = String(draft?.postal_id || '').trim();
if (!postalId) {
setPostalError('ID invio mancante');
return;
}
const gatewayAdminToken =
postalAdminTokenInput ||
localStorage.getItem('ccp_ai_admin_token') ||
localStorage.getItem('ccp_admin_token') ||
'';
if (!gatewayAdminToken) {
setPostalError('Token gateway admin mancante');
return;
}
const aboCost = draft?.service_mode === 'certifica_invia' ? 3 : 2;
const ok = window.confirm(`Confermare invio ${postalId} e scalare ${aboCost} ABO dal cliente?`);
if (!ok) return;
setPostalLoading(true);
setPostalError('');
try {
localStorage.setItem('ccp_ai_admin_token', gatewayAdminToken);
const res = await fetch(`${GATEWAY_BASE}/admin/postal/smart/confirm`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CCP-Token': gatewayAdminToken,
Authorization: `Bearer ${token || gatewayAdminToken}`
},
body: JSON.stringify({ postal_id: postalId, confirm_phrase: 'CONFIRM_REAL_POSTAL_SEND' })
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.ok) throw new Error(data?.error || `HTTP ${res.status}`);
await loadPostalDrafts();
} catch (e: any) {
setPostalError(e?.message || 'Errore conferma invio');
} finally {
setPostalLoading(false);
}
};
const cancelPostalDraft = async (draft: any) => {
const postalId = String(draft?.postal_id || '').trim();
const ts = String(draft?.ts || '').trim();
if (!postalId && !ts) {
setPostalError('ID invio mancante');
return;
}
const gatewayAdminToken =
postalAdminTokenInput ||
localStorage.getItem('ccp_ai_admin_token') ||
localStorage.getItem('ccp_admin_token') ||
'';
if (!gatewayAdminToken) {
setPostalError('Token gateway admin mancante');
return;
}
const ok = window.confirm(`Annullare questa bozza? Non verrà inviata e non verranno scalati ABO.`);
if (!ok) return;
setPostalLoading(true);
setPostalError('');
try {
localStorage.setItem('ccp_ai_admin_token', gatewayAdminToken);
const res = await fetch(`${GATEWAY_BASE}/admin/postal/smart/cancel`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CCP-Token': gatewayAdminToken,
Authorization: `Bearer ${token || gatewayAdminToken}`
},
body: JSON.stringify({ postal_id: postalId, ts })
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.ok) throw new Error(data?.error || `HTTP ${res.status}`);
await loadPostalDrafts();
} catch (e: any) {
setPostalError(e?.message || 'Errore annullamento bozza');
} finally {
setPostalLoading(false);
}
};
const loadAgencyPlans = async () => {
const gatewayAdminToken = getGatewayAdminToken();
if (!gatewayAdminToken) {
setAgencyError('Token gateway admin mancante');
return;
}
setAgencyLoading(true);
setAgencyError('');
try {
localStorage.setItem('ccp_ai_admin_token', gatewayAdminToken);
const res = await fetch(`${GATEWAY_BASE}/admin/agency/plans`, {
headers: {
'X-CCP-Token': gatewayAdminToken,
Authorization: `Bearer ${token || gatewayAdminToken}`
}
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.ok) throw new Error(data?.error || `HTTP ${res.status}`);
setAgencyPlans(Array.isArray(data.plans) ? data.plans : []);
} catch (e: any) {
setAgencyError(e?.message || 'Errore caricamento piani Agenzia');
} finally {
setAgencyLoading(false);
}
};
const saveAgencyPlan = async (active: boolean, resetUsed = false, target?: any) => {
const gatewayAdminToken = getGatewayAdminToken();
const email = String(target?.email || agencyEmail || '').trim();
const userId = String(target?.user_id || '').trim();
if (!gatewayAdminToken) {
setAgencyError('Token gateway admin mancante');
return;
}
if (!email && !userId) {
setAgencyError('Inserisci email utente o seleziona un piano esistente');
return;
}
setAgencyLoading(true);
setAgencyError('');
try {
localStorage.setItem('ccp_ai_admin_token', gatewayAdminToken);
const res = await fetch(`${GATEWAY_BASE}/admin/agency/plan`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CCP-Token': gatewayAdminToken,
Authorization: `Bearer ${token || gatewayAdminToken}`
},
body: JSON.stringify({
email,
user_id: userId,
active,
monthly_credits: Number(target?.monthly_credits || agencyMonthlyCredits || 30),
renewal_date: target?.renewal_date || agencyRenewalDate,
notes: target?.notes || agencyNotes,
reset_used: resetUsed
})
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.ok) throw new Error(data?.error || `HTTP ${res.status}`);
if (!target) {
setAgencyEmail('');
setAgencyNotes('');
}
await loadAgencyPlans();
} catch (e: any) {
setAgencyError(e?.message || 'Errore salvataggio piano Agenzia');
} finally {
setAgencyLoading(false);
}
};
const resetAgencyPlan = async (plan: any) => {
const gatewayAdminToken = getGatewayAdminToken();
if (!gatewayAdminToken) {
setAgencyError('Token gateway admin mancante');
return;
}
const ok = window.confirm(`Resettare i crediti mensili usati per ${plan?.email || plan?.user_id}?`);
if (!ok) return;
setAgencyLoading(true);
setAgencyError('');
try {
const res = await fetch(`${GATEWAY_BASE}/admin/agency/plan/reset`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CCP-Token': gatewayAdminToken,
Authorization: `Bearer ${token || gatewayAdminToken}`
},
body: JSON.stringify({ user_id: plan?.user_id, email: plan?.email })
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.ok) throw new Error(data?.error || `HTTP ${res.status}`);
await loadAgencyPlans();
} catch (e: any) {
setAgencyError(e?.message || 'Errore reset piano Agenzia');
} finally {
setAgencyLoading(false);
}
};
useEffect(() => {
if (token && isAdmin) {
loadPostalDrafts();
loadAgencyPlans();
}
}, [token, isAdmin]);
if (!token || !isAdmin) return <AdminLogin lang={lang} />;
return (
<div className="space-y-8 p-6 md:p-10">
<div className="bg-white border border-slate-100 rounded-[2.5rem] shadow-sm p-6 space-y-5">
<div className="flex flex-col xl:flex-row xl:items-center xl:justify-between gap-5">
<div>
<p className="text-[10px] font-black uppercase tracking-[0.3em] text-blue-500">Admin CopyrightChain</p>
<h2 className="text-3xl font-black text-slate-900 uppercase tracking-tight">Controllo operativo</h2>
<p className="text-sm font-bold text-slate-500 mt-2">Agenzie, crediti certificazione e invii documentali restano separati.</p>
</div>
<div className="flex flex-col md:flex-row gap-3 md:items-center">
<input
type="password"
value={postalAdminTokenInput}
onChange={(e) => setPostalAdminTokenInput(e.target.value)}
placeholder="Token gateway admin"
className="px-5 py-4 rounded-2xl bg-slate-50 border border-slate-100 text-xs font-black outline-none min-w-[260px]"
/>
<button
onClick={() => {
localStorage.setItem('ccp_ai_admin_token', postalAdminTokenInput);
if (adminSection === 'agency') loadAgencyPlans();
if (adminSection === 'postal') loadPostalDrafts();
}}
disabled={postalLoading || agencyLoading}
className="px-6 py-4 rounded-2xl bg-slate-900 text-white text-[10px] font-black uppercase tracking-widest disabled:opacity-60"
>
Salva token / aggiorna
</button>
</div>
</div>
<div className="flex flex-wrap gap-3">
<button onClick={() => setAdminSection('overview')} className={`px-5 py-3 rounded-2xl text-[10px] font-black uppercase tracking-widest ${adminSection === 'overview' ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500'}`}>Dashboard</button>
<button onClick={() => setAdminSection('agency')} className={`px-5 py-3 rounded-2xl text-[10px] font-black uppercase tracking-widest ${adminSection === 'agency' ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-500'}`}>Agenzie</button>
<button onClick={() => setAdminSection('postal')} className={`px-5 py-3 rounded-2xl text-[10px] font-black uppercase tracking-widest ${adminSection === 'postal' ? 'bg-emerald-600 text-white' : 'bg-slate-100 text-slate-500'}`}>Invio documenti</button>
</div>
</div>
{adminSection === 'overview' && (
<AdminDashboard certificates={certificates} lang={lang} users={[user]} onUpdateUser={handleUpdateUser} />
)}
{adminSection === 'agency' && (
<section className="bg-white border border-slate-100 rounded-[2.5rem] shadow-sm p-8 space-y-8">
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-6">
<div>
<p className="text-[10px] font-black text-blue-500 uppercase tracking-[0.3em]">Agenzia Creativa</p>
<h2 className="text-2xl font-black text-slate-900 uppercase tracking-tight">Crediti certificazione separati</h2>
<p className="text-sm font-bold text-slate-500 mt-2 max-w-3xl">
Questi crediti sono mensili e valgono solo per certificazioni digitali. Non possono essere usati per raccomandate, Openapi, invii documentali o wallet ABO.
</p>
</div>
<button onClick={loadAgencyPlans} disabled={agencyLoading} className="px-6 py-4 rounded-2xl bg-slate-900 text-white text-[10px] font-black uppercase tracking-widest disabled:opacity-60">
{agencyLoading ? 'Aggiornamento...' : 'Aggiorna lista'}
</button>
</div>
{agencyError && (
<div className="bg-red-50 border border-red-100 text-red-700 rounded-2xl p-4 text-xs font-black">{agencyError}</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-5 gap-4 bg-slate-50 rounded-[2rem] p-5">
<input value={agencyEmail} onChange={(e) => setAgencyEmail(e.target.value)} placeholder="Email utente" className="lg:col-span-2 px-5 py-4 rounded-2xl bg-white border border-slate-100 text-xs font-black outline-none" />
<input type="number" min={0} value={agencyMonthlyCredits} onChange={(e) => setAgencyMonthlyCredits(Number(e.target.value || 0))} placeholder="Crediti mensili" className="px-5 py-4 rounded-2xl bg-white border border-slate-100 text-xs font-black outline-none" />
<input type="date" value={agencyRenewalDate} onChange={(e) => setAgencyRenewalDate(e.target.value)} className="px-5 py-4 rounded-2xl bg-white border border-slate-100 text-xs font-black outline-none" />
<button onClick={() => saveAgencyPlan(true, true)} disabled={agencyLoading} className="px-5 py-4 rounded-2xl bg-blue-600 text-white text-[10px] font-black uppercase tracking-widest disabled:opacity-60">Attiva / reset</button>
<textarea value={agencyNotes} onChange={(e) => setAgencyNotes(e.target.value)} placeholder="Note admin" className="lg:col-span-5 px-5 py-4 rounded-2xl bg-white border border-slate-100 text-xs font-bold outline-none min-h-[86px]" />
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-5 rounded-3xl bg-blue-50 border border-blue-100">
<p className="text-[10px] font-black uppercase tracking-widest text-blue-500">Base</p>
<p className="text-2xl font-black text-slate-900 mt-2">1 credito</p>
</div>
<div className="p-5 rounded-3xl bg-indigo-50 border border-indigo-100">
<p className="text-[10px] font-black uppercase tracking-widest text-indigo-500">Bitcoin Proof</p>
<p className="text-2xl font-black text-slate-900 mt-2">2 crediti</p>
</div>
<div className="p-5 rounded-3xl bg-purple-50 border border-purple-100">
<p className="text-[10px] font-black uppercase tracking-widest text-purple-500">InfoCert</p>
<p className="text-2xl font-black text-slate-900 mt-2">3 crediti</p>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left border-separate border-spacing-y-3">
<thead>
<tr className="text-[10px] font-black text-slate-400 uppercase tracking-widest">
<th className="px-4">Cliente</th>
<th className="px-4">Stato</th>
<th className="px-4">Mensili</th>
<th className="px-4">Usati</th>
<th className="px-4">Residui</th>
<th className="px-4">Rinnovo</th>
<th className="px-4">Saldo ABO</th>
<th className="px-4">Azioni</th>
</tr>
</thead>
<tbody>
{agencyPlans.length === 0 && (
<tr><td colSpan={8} className="px-4 py-8 text-center text-sm font-bold text-slate-400">Nessun piano Agenzia configurato.</td></tr>
)}
{agencyPlans.map((p: any) => (
<tr key={p.user_id} className="bg-slate-50 text-xs font-black">
<td className="px-4 py-4 rounded-l-2xl text-slate-900">
<div>{p.email || p.user_id}</div>
<div className="font-bold text-slate-400">{p.name || p.user_id}</div>
</td>
<td className="px-4 py-4">
<span className={`px-3 py-2 rounded-xl ${p.active ? 'bg-emerald-100 text-emerald-700' : 'bg-slate-200 text-slate-500'}`}>{p.active ? 'Attivo' : 'Disattivo'}</span>
</td>
<td className="px-4 py-4">{p.monthly_credits}</td>
<td className="px-4 py-4">{p.used_credits}</td>
<td className="px-4 py-4 text-blue-700">{p.remaining_credits}</td>
<td className="px-4 py-4">{p.renewal_date || '—'}</td>
<td className="px-4 py-4">{p.balance_abo ?? '—'} ABO</td>
<td className="px-4 py-4 rounded-r-2xl">
<div className="flex flex-col gap-2">
<button onClick={() => resetAgencyPlan(p)} disabled={agencyLoading} className="px-4 py-3 rounded-xl bg-blue-50 text-blue-700 border border-blue-100 text-[10px] font-black uppercase tracking-widest disabled:opacity-60">Reset mese</button>
<button onClick={() => saveAgencyPlan(!p.active, false, p)} disabled={agencyLoading} className="px-4 py-3 rounded-xl bg-slate-900 text-white text-[10px] font-black uppercase tracking-widest disabled:opacity-60">{p.active ? 'Disattiva' : 'Riattiva'}</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
)}
{adminSection === 'postal' && (
<section className="bg-white border border-slate-100 rounded-[2.5rem] shadow-sm p-8 space-y-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.25em]">Ufficio Postale</p>
<h2 className="text-2xl font-black text-slate-900 uppercase tracking-tight">Bozze invio documenti</h2>
<p className="text-sm font-bold text-slate-500 mt-2">Coda unica per Solo invio e Certifica + invia prova. La conferma resta manuale admin e usa solo ABO/pagamento separato.</p>
</div>
<button onClick={loadPostalDrafts} disabled={postalLoading} className="px-6 py-4 rounded-2xl bg-slate-900 text-white text-[10px] font-black uppercase tracking-widest disabled:opacity-60">
{postalLoading ? 'Aggiornamento...' : 'Aggiorna bozze'}
</button>
</div>
{postalError && <div className="bg-red-50 border border-red-100 text-red-700 rounded-2xl p-4 text-xs font-black">{postalError}</div>}
<div className="overflow-x-auto">
<table className="w-full text-left border-separate border-spacing-y-3">
<thead>
<tr className="text-[10px] font-black text-slate-400 uppercase tracking-widest">
<th className="px-4">Data</th>
<th className="px-4">Cliente</th>
<th className="px-4">Modalità</th>
<th className="px-4">ID invio</th>
<th className="px-4">Stato</th>
<th className="px-4">Prezzo cliente</th>
<th className="px-4">Costo Openapi</th>
<th className="px-4">Azione</th>
</tr>
</thead>
<tbody>
{postalDrafts.length === 0 && (
<tr><td colSpan={8} className="px-4 py-8 text-center text-sm font-bold text-slate-400">Nessuna bozza invio documenti trovata.</td></tr>
)}
{postalDrafts.map((d: any, idx: number) => (
<tr key={`${d.postal_id || 'draft'}-${idx}`} className="bg-slate-50">
<td className="px-4 py-4 rounded-l-2xl text-xs font-bold text-slate-600">{d.ts ? new Date(Number(d.ts) * 1000).toLocaleString('it-IT') : '—'}</td>
<td className="px-4 py-4 text-xs font-black text-slate-900">{d.user_email || '—'}</td>
<td className="px-4 py-4 text-xs font-black"><span className={`px-3 py-2 rounded-xl ${d.service_mode === 'certifica_invia' ? 'bg-blue-100 text-blue-700' : 'bg-slate-200 text-slate-700'}`}>{d.service_mode === 'certifica_invia' ? 'Certifica + invia prova' : 'Solo invio'}</span></td>
<td className="px-4 py-4 text-xs font-mono font-bold text-slate-700">{d.postal_id || '—'}</td>
<td className="px-4 py-4 text-xs font-black text-slate-700">{d.state || '—'} / {String(d.confirmed)}</td>
<td className="px-4 py-4 text-xs font-black text-emerald-700">{d.public_price_eur ? `${Number(d.public_price_eur).toFixed(2).replace('.', ',')} €` : '—'}</td>
<td className="px-4 py-4 text-xs font-black text-orange-700">{d.provider_cost_eur ? `${Number(d.provider_cost_eur).toFixed(2).replace('.', ',')} €` : '—'}</td>
<td className="px-4 py-4 rounded-r-2xl text-xs font-black">
{d.canceled === true || d.admin_canceled ? (
<span className="px-3 py-2 rounded-xl bg-slate-200 text-slate-600">Annullato</span>
) : d.confirmed === true || d.admin_confirmed ? (
<span className="px-3 py-2 rounded-xl bg-emerald-100 text-emerald-700">Confermato</span>
) : (
<div className="flex flex-col gap-2">
<button onClick={() => confirmPostalDraft(d)} disabled={postalLoading} className="px-4 py-3 rounded-xl bg-slate-900 text-white text-[10px] font-black uppercase tracking-widest disabled:opacity-60">Conferma e scala {d.service_mode === 'certifica_invia' ? '3' : '2'} ABO</button>
<button onClick={() => cancelPostalDraft(d)} disabled={postalLoading} className="px-4 py-3 rounded-xl bg-red-50 text-red-700 border border-red-100 text-[10px] font-black uppercase tracking-widest disabled:opacity-60">Annulla</button>
</div>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
)}
</div>
);
};
const PostalRegister: React.FC<{ lang: any; clientToken: string }> = ({ lang, clientToken }) => {
const [items, setItems] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [section, setSection] = useState<'posta' | 'atti'>('posta');
const loadHistory = async () => {
if (!clientToken) return;
setLoading(true);
setError('');
try {
const res = await fetch(`${GATEWAY_BASE}/client/postal/smart/history`, {
headers: { 'X-CCP-Token': clientToken }
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.ok) throw new Error(data?.error || `HTTP ${res.status}`);
setItems(Array.isArray(data.items) ? data.items : []);
} catch (e: any) {
setError(e?.message || 'Errore caricamento registro invii');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadHistory();
}, [clientToken]);
const formatDate = (ts: any) => {
const n = Number(ts || 0);
if (!n) return '—';
return new Date(n * 1000).toLocaleString('it-IT');
};
const modeLabel = (mode: string) => {
return mode === 'certifica_invia'
? (lang === 'it' ? 'Certifica + invia prova' : 'Certify + send proof')
: (lang === 'it' ? 'Solo invio' : 'Send only');
};
return (
<div className="max-w-7xl mx-auto space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
<div className="bg-white rounded-[2.5rem] p-8 md:p-10 border border-slate-100 shadow-sm">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-6 mb-8">
<div>
<p className="text-[10px] font-black uppercase tracking-[0.35em] text-blue-500 mb-2">
{lang === 'it' ? 'Registro invii' : 'Delivery register'}
</p>
<h2 className="text-3xl font-black text-slate-900">
{lang === 'it' ? 'Posta inviata e atti' : 'Sent mail and legal acts'}
</h2>
<p className="text-sm text-slate-500 font-bold mt-2">
{lang === 'it'
? 'Archivio degli invii creati tramite CopyrightChain.'
: 'Archive of deliveries created through CopyrightChain.'}
</p>
</div>
<button
onClick={loadHistory}
disabled={loading}
className="px-6 py-4 rounded-2xl bg-slate-900 text-white text-xs font-black uppercase tracking-widest disabled:opacity-60"
>
{loading ? '...' : (lang === 'it' ? 'Aggiorna' : 'Refresh')}
</button>
</div>
<div className="flex flex-wrap gap-3 mb-8">
<button
onClick={() => setSection('posta')}
className={`px-5 py-3 rounded-2xl text-xs font-black uppercase tracking-widest ${section === 'posta' ? 'bg-blue-600 text-white shadow-lg' : 'bg-slate-100 text-slate-500'}`}
>
{lang === 'it' ? 'Posta inviata' : 'Sent mail'}
</button>
<button
onClick={() => setSection('atti')}
className={`px-5 py-3 rounded-2xl text-xs font-black uppercase tracking-widest ${section === 'atti' ? 'bg-blue-600 text-white shadow-lg' : 'bg-slate-100 text-slate-500'}`}
>
{lang === 'it' ? 'Atti giudiziari' : 'Legal acts'}
</button>
</div>
{error && (
<div className="mb-6 p-4 rounded-2xl bg-red-50 text-red-700 text-sm font-bold border border-red-100">
{error}
</div>
)}
{section === 'atti' ? (
<div className="p-8 rounded-3xl bg-slate-50 border border-slate-100 text-slate-500 font-bold">
{lang === 'it'
? 'Registro atti giudiziari in preparazione. Qui verranno mostrati gli invii attivati con il modulo Atti Giudiziari.'
: 'Legal acts register in preparation.'}
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-left border-separate border-spacing-y-2">
<thead>
<tr className="text-[10px] font-black uppercase tracking-widest text-slate-400">
<th className="px-4">Data</th>
<th className="px-4">Destinatario</th>
<th className="px-4">Modalità</th>
<th className="px-4">ID invio</th>
<th className="px-4">Stato</th>
<th className="px-4">ABO scalati</th>
<th className="px-4">Saldo dopo</th>
<th className="px-4">Certificato</th>
</tr>
</thead>
<tbody>
{items.length === 0 && (
<tr>
<td colSpan={8} className="px-4 py-8 text-center text-slate-400 font-bold">
{loading
? (lang === 'it' ? 'Caricamento...' : 'Loading...')
: (lang === 'it' ? 'Nessun invio presente.' : 'No deliveries found.')}
</td>
</tr>
)}
{items.map((item, idx) => (
<tr key={`${item.postal_id || idx}`} className="bg-slate-50 text-sm font-bold">
<td className="px-4 py-4 rounded-l-2xl text-slate-600">
{formatDate(item.ts || item.created_at)}
</td>
<td className="px-4 py-4 text-slate-700">
{item.recipient_display || item.recipient?.ragione_sociale || item.recipient?.email || '—'}
</td>
<td className="px-4 py-4">
<span className="px-3 py-2 rounded-xl bg-blue-50 text-blue-700 text-[11px]">
{modeLabel(item.service_mode)}
</span>
</td>
<td className="px-4 py-4 font-mono text-xs text-slate-700">
{item.postal_id || '—'}
</td>
<td className="px-4 py-4">
<span className={`px-3 py-2 rounded-xl text-[11px] ${item.confirmed ? 'bg-emerald-100 text-emerald-700' : 'bg-amber-100 text-amber-700'}`}>
{item.confirmed ? 'Confermato' : (item.state || 'Bozza')}
</span>
</td>
<td className="px-4 py-4 text-slate-700">
{item.abo_charged ? `${item.abo_charged} ABO` : '—'}
</td>
<td className="px-4 py-4 text-slate-700">
{item.balance_after !== undefined && item.balance_after !== null ? `${item.balance_after} ABO` : '—'}
</td>
<td className="px-4 py-4 rounded-r-2xl text-slate-700">
{item.certificate_id || item.certificate?.id ? (
<a
href={item.certificate_verify_url || item.certificate?.verify_url || '#'}
target="_blank"
rel="noreferrer"
className="font-black text-blue-600 hover:underline"
>
{item.certificate_id || item.certificate?.id}
</a>
) : (
<span className="text-slate-400">—</span>
)}
{item.legacy && (
<span className="ml-2 inline-flex px-2 py-1 rounded-full text-[10px] font-black bg-amber-100 text-amber-700">
LEGACY
</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
);
};
const renderContent = () => {
if (!isAdminHost && isBootstrappingClient) {
return (
<div className="max-w-3xl mx-auto p-12">
<div className="bg-white border border-slate-100 shadow-2xl rounded-[2rem] p-12 text-center">
<p className="text-sm font-black uppercase tracking-widest text-slate-400">Sessione cliente</p>
<h2 className="text-3xl font-black text-slate-900 mt-4">Caricamento area riservata…</h2>
</div>
</div>
);
}
if (!isAdminHost && mustChangePassword) {
return (
<div className="max-w-3xl mx-auto p-12">
<div className="bg-white border border-slate-100 shadow-2xl rounded-[2rem] p-10 space-y-8">
<div>
<p className="text-sm font-black uppercase tracking-widest text-slate-400">Sicurezza account</p>
<h2 className="text-3xl font-black text-slate-900 mt-4">Cambio password obbligatorio</h2>
<p className="text-sm font-bold text-slate-500 mt-3">
Prima di continuare devi impostare una nuova password personale.
</p>
</div>
<div className="grid grid-cols-1 gap-6">
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Nuova password</label>
<input
type="password"
value={newForcedPassword}
onChange={(e) => setNewForcedPassword(e.target.value)}
className="w-full px-6 py-4 rounded-2xl bg-slate-50 border border-slate-100 font-black text-xs outline-none focus:ring-4 focus:ring-blue-500/10"
placeholder="Almeno 8 caratteri"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Conferma nuova password</label>
<input
type="password"
value={confirmForcedPassword}
onChange={(e) => setConfirmForcedPassword(e.target.value)}
className="w-full px-6 py-4 rounded-2xl bg-slate-50 border border-slate-100 font-black text-xs outline-none focus:ring-4 focus:ring-blue-500/10"
placeholder="Ripeti la nuova password"
/>
</div>
</div>
<button
onClick={handleForcedPasswordChange}
disabled={forcedPasswordLoading}
className="w-full py-5 rounded-[2rem] font-black uppercase tracking-[0.2em] shadow-2xl bg-slate-900 text-white disabled:opacity-60"
>
{forcedPasswordLoading ? 'Aggiornamento...' : 'Aggiorna password'}
</button>
</div>
</div>
);
}
switch (activeTab) {
case 'auth':
return <AuthPage onAuthSuccess={handleClientAuthSuccess} />;
case 'landing':
return <LandingPage lang={lang} onStart={() => setActiveTab('auth')} />;
case 'dashboard':
return <Dashboard certificates={certificates} onNavigate={setActiveTab} lang={lang} user={user} />;
case 'upload':
return <FileUploader onComplete={handleAddCertificate} lang={lang} />;
case 'verify':
return <VerificationPortal certificates={certificates} lang={lang} />;
case 'postal':
return <PostalService lang={lang} clientToken={clientToken} certificates={certificates} />;
case 'postal-register':
return <PostalRegister lang={lang} clientToken={clientToken} />;
case 'certificates':
if (selectedCert) {
return (
<div className="space-y-6">
<button onClick={() => setSelectedCert(null)} className="flex items-center gap-2 text-slate-400 hover:text-blue-600 font-black text-[10px] uppercase tracking-widest">
← {lang === 'it' ? 'Torna al Vault' : 'Back to Vault'}
</button>
<CertificateView certificate={selectedCert} lang={lang} clientToken={clientToken} />
</div>
);
}
return <LegalVault certificates={certificates} onViewCert={setSelectedCert} lang={lang} userIdentity={user.identity} onUserIdentityUpdate={(data) => setUser(prev => ({ ...prev, identity: data }))} />;
case 'admin':
return <AdminArea />;
case 'ai-assistant':
return <AIAssistant lang={lang} certificates={certificates} />;
case 'profile':
return <UserProfile user={user} lang={lang} onUpdateUser={handleUpdateUser} clientToken={clientToken} />;
case 'defense':
return <LegalDefense certificates={certificates} lang={lang} />;
case 'monitor':
return <IPMonitor certificates={certificates} lang={lang} onNavigate={setActiveTab} />;
case 'billing':
return <BillingCenter lang={lang} user={user} onPurchase={addCredits} clientToken={clientToken} />;
case 'licenses':
return <LicenseGenerator certificates={certificates} lang={lang} />;
default:
return <LandingPage lang={lang} onStart={() => setActiveTab('auth')} />;
}
};
const navItems = [
{ id: 'landing', icon: <Home size={20} />, label: lang === 'it' ? "Home" : "Home" },
{ id: 'dashboard', icon: <Layout size={20} />, label: t.dashboard },
{ id: 'upload', icon: <FileText size={20} />, label: t.newDeposit },
{ id: 'postal', icon: <Send size={20} />, label: lang === 'it' ? 'Invio documenti' : 'Send documents' },
{ id: 'postal-register', icon: <Send size={20} />, label: lang === 'it' ? 'Registro invii' : 'Delivery register' },
{ id: 'certificates', icon: <Archive size={20} />, label: t.certificates },
{ id: 'licenses', icon: <Award size={20} />, label: lang === 'it' ? "Licenze" : "Licenses", group: 'legal' },
{ id: 'ai-assistant', icon: <MessageSquare size={20} />, label: t.aiAssistant, group: 'legal' },
{ id: 'defense', icon: <Gavel size={20} />, label: t.legalDefense, group: 'legal' },
{ id: 'monitor', icon: <Monitor size={20} />, label: t.ipMonitor, group: 'legal' },
{ id: 'billing', icon: <Coins size={20} />, label: t.billing, group: 'billing' }
];
const isAdminFullWidth = activeTab === 'admin';
return (
<AuthProvider>
<NotificationContext.Provider value={{ notify, user, deductCredits, addCredits, setUserRole, onUpdateUser: handleUpdateUser }}>
<div className="min-h-screen bg-[#f8fafc] flex font-sans text-slate-900 overflow-x-hidden">
{!isAdminFullWidth && (
<aside className="w-80 bg-white border-r border-slate-100 p-8 flex flex-col hidden lg:flex sticky top-0 h-screen z-40">
<div className="flex items-center gap-3 mb-12 cursor-pointer select-none" onClick={handleLogoClick}>
<div className="bg-blue-600 text-white p-2.5 rounded-2xl shadow-lg shadow-blue-100"><Shield size={28} /></div>
<h1 className="text-2xl font-black tracking-tighter text-slate-900 uppercase">{t.appName}</h1>
</div>
<nav className="flex-1 space-y-1 overflow-y-auto custom-scrollbar pr-2">
{navItems.map(item => (
<React.Fragment key={item.id}>
{item.group === 'legal' && navItems.findIndex(i => i.id === item.id) === 4 && (
<div className="pt-8 pb-4"><p className="text-[10px] font-black text-slate-300 uppercase tracking-[0.2em] ml-4">Legal Tools</p></div>
)}
{item.group === 'billing' && (
<div className="pt-8 pb-4"><p className="text-[10px] font-black text-slate-300 uppercase tracking-[0.2em] ml-4">Account</p></div>
)}
<NavItem active={activeTab === item.id} onClick={() => setActiveTab(item.id as any)} icon={item.icon} label={item.label} />
</React.Fragment>
))}
</nav>
<div className="mt-auto pt-8 border-t border-slate-50 space-y-2">
<button onClick={() => setActiveTab('profile')} className={`w-full flex items-center gap-4 p-4 rounded-2xl hover:bg-slate-50 transition-all group ${activeTab === 'profile' ? 'bg-blue-50 text-blue-600' : ''}`}>
<div className="w-10 h-10 rounded-xl bg-blue-50 flex items-center justify-center text-blue-600">
<UserIcon size={20} />
</div>
<div className="text-left">
<p className="text-xs font-black text-slate-900 uppercase truncate w-40">{user.username || 'User'}</p>
<p className="text-[9px] font-bold text-slate-400 uppercase tracking-widest">{user.role}</p>
</div>
</button>
{!isAdminHost && user.isAuthenticated && (
<button onClick={handleClientLogout} className="w-full px-4 py-3 rounded-2xl bg-slate-50 border border-slate-100 text-slate-700 text-[10px] font-black uppercase tracking-widest">
{lang === 'it' ? 'Esci' : 'Logout'}
</button>
)}
</div>
</aside>
)}
<main className="flex-1 flex flex-col h-screen overflow-hidden relative">
<header className="bg-white/80 backdrop-blur-md border-b border-slate-100 p-4 md:p-6 flex justify-between items-center z-30 sticky top-0">
<div className="flex items-center gap-4">
<button onClick={() => setIsMobileMenuOpen(true)} className="lg:hidden p-3 -ml-2 text-slate-900 hover:bg-slate-100 rounded-2xl transition-all">
<Menu size={28} />
</button>
<div className="flex items-center gap-4 bg-slate-900 px-6 py-2.5 rounded-2xl border border-white/10 shadow-2xl">
<div className="flex items-center gap-3">
<div className="bg-yellow-500/20 p-1 rounded-lg"><Coins size={16} className="text-yellow-400" /></div>
<div>
<p className="text-[8px] font-black text-slate-400 uppercase leading-none">ABO Wallet</p>
<p className="text-sm font-black text-white">{user.credits.balance} <span className="text-[8px] opacity-60">COINS</span></p>
</div>
</div>
</div>
</div>
<div className="flex items-center gap-2 md:gap-4">
{isAdminHost && user.role === 'admin' && (
<button onClick={() => setActiveTab('admin')} className="p-2.5 bg-slate-50 text-slate-400 hover:text-slate-900 rounded-xl border border-slate-100">
<Lock size={18} />
</button>
)}
<select value={lang} onChange={(e) => setLang(e.target.value as Language)} className="bg-transparent text-[10px] font-black uppercase tracking-widest outline-none border-none cursor-pointer">
<option value="it">IT</option>
<option value="en">EN</option>
</select>
<button onClick={() => setActiveTab('profile')} className="w-10 h-10 rounded-xl bg-slate-50 flex items-center justify-center text-slate-400 hover:text-blue-600 transition-all border border-slate-100">
<UserIcon size={20} />
</button>
</div>
</header>
<div className="flex-1 overflow-y-auto p-0 custom-scrollbar">
<div className="max-w-full mx-auto pb-24">
<Suspense fallback={<div className="p-10 text-slate-400 text-sm font-black uppercase tracking-widest">Loading…</div>}>
{renderContent()}
</Suspense>
</div>
</div>
</main>
{isMobileMenuOpen && (
<div className="fixed inset-0 z-[100] lg:hidden">
<div className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm" onClick={() => setIsMobileMenuOpen(false)}></div>
<aside className="absolute inset-y-0 left-0 w-80 bg-white shadow-2xl p-8 flex flex-col">
<div className="flex justify-between items-center mb-10">
<div className="flex items-center gap-3"><Shield size={20} className="text-blue-600" /><h2 className="font-black uppercase tracking-tighter text-xl">{t.appName}</h2></div>
<button onClick={() => setIsMobileMenuOpen(false)} className="p-2.5 bg-slate-50 text-slate-400 rounded-xl"><X size={24} /></button>
</div>
<nav className="space-y-1 flex-1 overflow-y-auto custom-scrollbar">
{navItems.map(item => (
<button key={item.id} onClick={() => { setActiveTab(item.id as any); setIsMobileMenuOpen(false); }} className={`w-full flex items-center gap-4 px-6 py-4 rounded-2xl transition-all ${activeTab === item.id ? 'bg-blue-600 text-white shadow-xl' : 'text-slate-500 hover:bg-slate-50'}`}>
{item.icon} <span className="text-sm font-black uppercase tracking-tight">{item.label}</span>
</button>
))}
</nav>
{!isAdminHost && user.isAuthenticated && (
<button onClick={() => { setIsMobileMenuOpen(false); handleClientLogout(); }} className="mt-6 w-full px-4 py-3 rounded-2xl bg-slate-50 border border-slate-100 text-slate-700 text-[10px] font-black uppercase tracking-widest">
{lang === 'it' ? 'Esci' : 'Logout'}
</button>
)}
</aside>
</div>
)}
<div className="fixed bottom-24 lg:bottom-8 right-4 lg:right-8 z-[110] space-y-4 max-w-sm w-[calc(100%-2rem)] pointer-events-none">
{notifications.map(n => (
<div key={n.id} className={`p-6 rounded-[2rem] shadow-2xl border flex items-center gap-4 animate-in slide-in-from-right duration-300 bg-white pointer-events-auto ${n.type === 'error' ? 'border-red-100' : 'border-blue-100'}`}>
<div className={`p-2 rounded-xl ${n.type === 'error' ? 'bg-red-500' : 'bg-blue-500'} text-white`}><Shield size={20} /></div>
<p className="text-xs font-bold leading-relaxed">{n.message}</p>
</div>
))}
</div>
</div>
{!hasConsent && (
<div className="fixed bottom-0 left-0 right-0 z-[90] p-4">
<div className="max-w-3xl mx-auto bg-white border border-slate-100 shadow-2xl rounded-[2rem] p-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<p className="text-xs font-black uppercase tracking-widest text-slate-400">Privacy</p>
<p className="text-sm font-bold text-slate-700">Per salvare impostazioni e dati di profilo in modo persistente sul dispositivo, serve il consenso (GDPR). Senza consenso i dati restano solo nella sessione corrente.</p>
</div>
<div className="flex gap-3">
<button onClick={() => { localStorage.setItem('cc_consent', 'false'); setHasConsent(false); }} className="px-6 py-3 rounded-2xl bg-slate-50 border border-slate-100 text-slate-700 text-[10px] font-black uppercase tracking-widest">Rifiuta</button>
<button onClick={() => { localStorage.setItem('cc_consent', 'true'); setHasConsent(true); }} className="px-6 py-3 rounded-2xl bg-slate-900 text-white text-[10px] font-black uppercase tracking-widest">Accetta</button>
</div>
</div>
</div>
)}
</NotificationContext.Provider>
</AuthProvider>
);
};
function AuthPage({ onAuthSuccess }: { onAuthSuccess: (payload: ClientAuthResponse) => void }) {
const sp = new URLSearchParams(window.location.search);
const initialMode = (sp.get('auth') || '').toLowerCase() === 'register' ? 'register' : 'login';
const [mode, setMode] = useState<'login' | 'register'>(initialMode);
const [fullName, setFullName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [wallet, setWallet] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
const current = new URLSearchParams(window.location.search);
const auth = (current.get('auth') || '').toLowerCase().trim();
if (auth === 'register') setMode('register');
else if (auth === 'login') setMode('login');
}, []);
const switchMode = (nextMode: 'login' | 'register') => {
setMode(nextMode);
const u = new URL(window.location.href);
u.searchParams.set('auth', nextMode);
window.history.replaceState({}, '', u.toString());
};
const submit = async () => {
try {
setIsSubmitting(true);
if (!email.trim() || !password.trim()) {
throw new Error('Inserisci email e password.');
}
if (mode === 'register') {
if (!fullName.trim()) throw new Error('Inserisci il nome.');
if (password.length < 8) throw new Error('La password deve avere almeno 8 caratteri.');
if (password !== confirmPassword) throw new Error('Le password non coincidono.');
const res = await fetch(`${GATEWAY_BASE}/client/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: fullName.trim(),
email: email.trim(),
password,
wallet: wallet.trim()
})
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.ok || !data?.token) {
throw new Error(
data?.error === 'email_exists' ? 'Email già registrata.' :
data?.error === 'weak_password' ? 'Password troppo debole.' :
data?.error === 'invalid_email' ? 'Email non valida.' :
data?.error || 'register_failed'
);
}
onAuthSuccess(data as ClientAuthResponse);
} else {
const res = await fetch(`${GATEWAY_BASE}/client/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: email.trim(),
password
})
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.ok || !data?.token) {
throw new Error(
data?.error === 'unauthorized' ? 'Credenziali non valide.' :
data?.error || 'login_failed'
);
}
onAuthSuccess(data as ClientAuthResponse);
}
} catch (e: any) {
alert(`Errore autenticazione: ${e?.message || 'unknown_error'}`);
} finally {
setIsSubmitting(false);
}
};
return (
<div className="max-w-4xl mx-auto p-8">
<div className="bg-white border border-slate-100 shadow-2xl rounded-[2rem] p-10 space-y-6">
<div className="grid grid-cols-2 gap-3 bg-slate-50 rounded-[1.5rem] p-2">
<button
onClick={() => switchMode('login')}
className={`py-4 rounded-[1rem] font-black uppercase tracking-widest text-xs transition-all ${mode === 'login' ? 'bg-slate-900 text-white shadow-lg' : 'text-slate-500'}`}
>
Login
</button>
<button
onClick={() => switchMode('register')}
className={`py-4 rounded-[1rem] font-black uppercase tracking-widest text-xs transition-all ${mode === 'register' ? 'bg-blue-600 text-white shadow-lg shadow-blue-100' : 'text-slate-500'}`}
>
Registrati
</button>
</div>
<h2 className="text-3xl font-black uppercase tracking-tight text-slate-900">
{mode === 'login' ? 'Accesso Area Cliente' : 'Crea Account Cliente'}
</h2>
<p className="text-sm font-bold text-slate-500">
{mode === 'login'
? 'Accedi con email e password.'
: 'Compila il form per creare il tuo account cliente.'}
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{mode === 'register' && (
<div className="space-y-2 md:col-span-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Nome</label>
<input
value={fullName}
onChange={(e) => setFullName(e.target.value)}
className="w-full px-6 py-4 rounded-2xl bg-slate-50 border border-slate-100 font-black text-xs outline-none focus:ring-4 focus:ring-blue-500/10"
placeholder="Mario Rossi"
/>
</div>
)}
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Email</label>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-6 py-4 rounded-2xl bg-slate-50 border border-slate-100 font-black text-xs outline-none focus:ring-4 focus:ring-blue-500/10"
placeholder="email@dominio.it"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-6 py-4 rounded-2xl bg-slate-50 border border-slate-100 font-black text-xs outline-none focus:ring-4 focus:ring-blue-500/10"
placeholder="••••••••"
/>
</div>
{mode === 'register' && (
<>
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Conferma Password</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full px-6 py-4 rounded-2xl bg-slate-50 border border-slate-100 font-black text-xs outline-none focus:ring-4 focus:ring-blue-500/10"
placeholder="••••••••"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Wallet (opzionale)</label>
<input
value={wallet}
onChange={(e) => setWallet(e.target.value)}
className="w-full px-6 py-4 rounded-2xl bg-slate-50 border border-slate-100 font-black text-xs outline-none focus:ring-4 focus:ring-blue-500/10"
placeholder="0x..."
/>
</div>
</>
)}
</div>
<button
onClick={submit}
disabled={isSubmitting}
className={`w-full py-5 rounded-[2rem] font-black uppercase tracking-[0.2em] shadow-2xl ${mode === 'register' ? 'bg-blue-600 text-white shadow-blue-100' : 'bg-slate-900 text-white'} disabled:opacity-60`}
>
{isSubmitting ? 'Attendere...' : (mode === 'register' ? 'Crea account' : 'Entra')}
</button>
<div className="text-[10px] font-bold text-slate-400">
{mode === 'register'
? 'Registrazione: nome, email, password, conferma password, wallet opzionale.'
: 'Login: email e password.'}
</div>
</div>
</div>
);
}
const NavItem = ({ active, onClick, icon, label }: any) => (
<button onClick={onClick} className={`w-full flex items-center gap-4 px-6 py-4 rounded-3xl transition-all ${active ? 'bg-blue-600 text-white shadow-xl shadow-blue-100' : 'text-slate-400 hover:text-slate-900 hover:bg-slate-50'}`}>
{icon} <span className="text-sm font-black uppercase tracking-tight">{label}</span>
</button>
);
export default App;