import React, { useState, useMemo, useRef, useEffect } from 'react';
import { Briefcase, AlertTriangle, Target, Zap, Building2, Landmark, PiggyBank, MoreHorizontal, ArrowRight, TrendingUp, Calculator, ShieldCheck, ChevronRight, ArrowLeft, PlusCircle, Scale, Mail, X, CheckCircle2 } from 'lucide-react';
// Firebase Imports
import { initializeApp } from 'firebase/app';
import { getAuth, signInWithCustomToken, signInAnonymously, onAuthStateChanged } from 'firebase/auth';
import { getFirestore, collection, addDoc, serverTimestamp } from 'firebase/firestore';
// --- FIREBASE INITIALIZATION ---
const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
const appId = typeof __app_id !== 'undefined' ? __app_id : 'eis-risk-visualiser';
// --- STABLE SUB-COMPONENTS ---
const CurrencyInput = ({ value, onChange, placeholder, className = "" }) => {
const inputRef = useRef(null);
const handleChange = (e) => {
const input = e.target;
const rawValue = input.value;
const selectionStart = input.selectionStart;
const digitsBefore = rawValue.substring(0, selectionStart).replace(/\D/g, "").length;
const numericValue = rawValue.replace(/\D/g, "");
onChange(numericValue);
window.requestAnimationFrame(() => {
if (!inputRef.current) return;
const newValue = numericValue ? parseInt(numericValue).toLocaleString() : "";
let newPos = 0;
let digitsFound = 0;
while (digitsFound < digitsBefore && newPos < newValue.length) {
if (/\d/.test(newValue[newPos])) digitsFound++;
newPos++;
}
inputRef.current.setSelectionRange(newPos, newPos);
});
};
const displayValue = value ? parseInt(value).toLocaleString() : "";
return (
);
};
const AssetRow = ({ id, label, Icon, assetData, onUpdate }) => (
{label}
onUpdate(id, 'value', val)} placeholder="0" />
onUpdate(id, 'risk', e.target.value)}
className="block w-full px-2 py-2 bg-indigo-50/50 border border-indigo-100 rounded-lg text-xs font-black text-indigo-700 focus:ring-2 focus:ring-indigo-500 outline-none text-center transition-all"
/>
);
// --- MAIN APPLICATION ---
const App = () => {
const [currentPage, setCurrentPage] = useState('inventory');
const [showPopup, setShowPopup] = useState(false);
const [email, setEmail] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const [user, setUser] = useState(null);
const [riskAppetite, setRiskAppetite] = useState(5);
const [eisInvestment, setEisInvestment] = useState("");
const [assets, setAssets] = useState({
pension: { value: "", risk: "3" },
isa: { value: "", risk: "5" },
property: { value: "", risk: "4" },
other: { value: "", risk: "7" }
});
// Handle Authentication
useEffect(() => {
const initAuth = async () => {
if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
await signInWithCustomToken(auth, __initial_auth_token);
} else {
await signInAnonymously(auth);
}
};
initAuth();
const unsubscribe = onAuthStateChanged(auth, setUser);
return () => unsubscribe();
}, []);
// Popup Trigger Logic
useEffect(() => {
let timer;
if (currentPage === 'impact' && !isSubmitted && !showPopup) {
timer = setTimeout(() => setShowPopup(true), 6000);
}
return () => clearTimeout(timer);
}, [currentPage, isSubmitted, showPopup]);
const parseNum = (str) => {
if (!str) return 0;
const numeric = str.toString().replace(/\D/g, "");
return parseInt(numeric) || 0;
};
const portfolioData = useMemo(() => {
let totalValue = 0;
let weightedRiskSum = 0;
Object.values(assets).forEach(asset => {
const val = parseNum(asset.value);
const r = parseInt(asset.risk) || 0;
totalValue += val;
weightedRiskSum += (val * r);
});
const avgRisk = totalValue === 0 ? 0 : (weightedRiskSum / totalValue);
const eisAmount = parseNum(eisInvestment);
const simulatedTotal = totalValue + eisAmount;
const simulatedWeightedSum = weightedRiskSum + (eisAmount * 9);
const simulatedAvgRisk = simulatedTotal === 0 ? 0 : (simulatedWeightedSum / simulatedTotal);
return {
totalValue,
avgRisk: parseFloat(avgRisk.toFixed(1)),
roundedRisk: Math.round(avgRisk),
simulatedTotal,
simulatedAvgRisk: parseFloat(simulatedAvgRisk.toFixed(1)),
riskShift: parseFloat((simulatedAvgRisk - avgRisk).toFixed(1))
};
}, [assets, eisInvestment]);
// --- SAVE TO CLOUD LOGIC ---
const handleEmailSubmit = async (e) => {
e.preventDefault();
if (!user) return;
setIsSubmitting(true);
try {
// Save data to Firestore (Public Collection for Submissions)
const submissionsCol = collection(db, 'artifacts', appId, 'public', 'data', 'submissions');
await addDoc(submissionsCol, {
email,
riskAppetite,
eisInvestment: parseNum(eisInvestment),
portfolioAssets: assets,
summary: {
currentTotal: portfolioData.totalValue,
currentRisk: portfolioData.avgRisk,
newTotal: portfolioData.simulatedTotal,
newRisk: portfolioData.simulatedAvgRisk
},
timestamp: serverTimestamp()
});
setIsSubmitting(false);
setIsSubmitted(true);
setTimeout(() => setShowPopup(false), 2500);
} catch (error) {
console.error("Error saving submission:", error);
setIsSubmitting(false);
}
};
const handleUpdate = (key, type, val) => {
setAssets(prev => ({ ...prev, [key]: { ...prev[key], [type]: val } }));
};
const getRiskProfile = (val) => {
if (val <= 2) return { label: "Very Conservative", color: "text-emerald-700", desc: "Prioritises capital preservation with minimal exposure to market fluctuations." };
if (val <= 4) return { label: "Conservative", color: "text-emerald-600", desc: "Willing to accept small fluctuations for modest long-term growth potential." };
if (val <= 6) return { label: "Balanced", color: "text-indigo-600", desc: "Seeking a balance between capital growth and income with moderate volatility." };
if (val <= 8) return { label: "Growth / Aggressive", color: "text-indigo-800", desc: "Higher tolerance for volatility to achieve significant capital appreciation." };
return { label: "Very Aggressive", color: "text-rose-600", desc: "Maximum risk for high growth, including high-risk or illiquid alternative assets." };
};
const profile = getRiskProfile(riskAppetite);
if (currentPage === 'inventory') {
return (
EIS Portfolio Risk Visualiser
Assessment of the effect of a EIS fund investment on client portfolios
Step 1: Client risk profile
Target Risk Appetite
{riskAppetite}/10
setRiskAppetite(parseInt(e.target.value))}
className="w-full h-3 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"
/>
Conservative (1)
Aggressive (10)
{profile.label}
"{profile.desc}"
Step 2: Current Asset Inventory & Risk Scoring
Asset Class
Market Value
Risk (1-10)
Total Assets
£{portfolioData.totalValue.toLocaleString()}
Avg Risk Score
{portfolioData.avgRisk} / 10
setCurrentPage('impact')} className="relative z-10 mt-8 w-full bg-white hover:bg-slate-50 text-indigo-900 py-4 px-4 rounded-2xl text-[12px] font-black uppercase tracking-wider shadow-lg transform hover:-translate-y-1 transition-all">
Impact of an EIS investment
Current portfolio weighted risk
{profile.label}
{profile.desc}
Risk Alignment
Appetite vs Reality
2 ? 'text-amber-400' : 'text-emerald-400'}`}>
{riskAppetite > portfolioData.roundedRisk ? 'Under-Exposed' : riskAppetite < portfolioData.roundedRisk ? 'Over-Exposed' : 'Balanced'}
Risk Variance
{Math.abs(riskAppetite - portfolioData.roundedRisk)} pts
);
}
return (
setCurrentPage('inventory')} className="flex items-center text-sm font-bold text-slate-400 hover:text-indigo-600 transition-colors group">
Back to Inventory
setShowPopup(true)} className="flex items-center gap-2 bg-indigo-100 hover:bg-indigo-200 text-indigo-700 py-3 px-6 rounded-2xl font-black text-xs uppercase tracking-wider transition-all shadow-sm">
Send me a copy of this
Impact of EIS on the overall clients portfolio risk
Assessment of the effect of a EIS fund investment on client portfolios
Proposed Simulation
Proposed EIS Investment
EIS Fund risk score
EIS are high risk and often illiquid investments
9 / 10
Fixed Rating
Client Goal {riskAppetite} / 10 Risk Appetite
Current Assets
£{portfolioData.totalValue.toLocaleString()}
Current Risk
{portfolioData.avgRisk}
Post-Investment Impact
New Total Assets
£{portfolioData.simulatedTotal.toLocaleString()}
New overall portfolio risk score
{portfolioData.simulatedAvgRisk}
{portfolioData.riskShift > 0 && +{portfolioData.riskShift} }
Current Risk {portfolioData.avgRisk}
Post-EIS Risk {portfolioData.simulatedAvgRisk}
Alignment Assessment
{Math.abs(riskAppetite - portfolioData.simulatedAvgRisk) <= 0.5 ? 'Perfect Strategic Fit' :
portfolioData.simulatedAvgRisk > riskAppetite ? 'Aggressive Growth Position' : 'Remaining Risk Capacity'}
Portfolio variance: {Math.abs(riskAppetite - portfolioData.simulatedAvgRisk).toFixed(1)} pts
{showPopup && (
setShowPopup(false)} className="absolute top-6 right-6 p-2 text-slate-400 hover:text-slate-600 transition-all">
{!isSubmitted ? (
<>
Send me an illustration of this
Enter your email to receive a professional PDF breakdown and save your simulation results.
>
) : (
Saved Successfully!
The illustration has been logged. You can now connect an automated email service like Zapier to this database.
)}
)}
Illustrative Simulation. EIS investments carry high risk and loss of capital. Weighted risk calculations based on manual asset entries.
);
};
export default App;