aireturns / src /App.jsx
pramodmisra's picture
Upload 2 files
382f2ce verified
import { useState, useMemo } from "react";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ReferenceLine, ResponsiveContainer, Area, AreaChart, ComposedChart, Bar } from "recharts";
const TOOLS_DEFAULT = [
{
name: "Comulate",
color: "#E85D26",
monthlyCost: 2500,
setupCost: 5000,
hoursPerWeekSaved: 12,
avgHourlyCost: 45,
},
{
name: "Fulcrum",
color: "#2D7DD2",
monthlyCost: 1800,
setupCost: 3500,
hoursPerWeekSaved: 8,
avgHourlyCost: 45,
},
{
name: "Tailwind",
color: "#17B890",
monthlyCost: 1200,
setupCost: 2000,
hoursPerWeekSaved: 6,
avgHourlyCost: 45,
},
];
const MONTHS = 18;
function calcBreakevenMonth(tool) {
const monthlySavings = tool.hoursPerWeekSaved * tool.avgHourlyCost * 4.33;
const netMonthly = monthlySavings - tool.monthlyCost;
if (netMonthly <= 0) return null;
return Math.ceil(tool.setupCost / netMonthly);
}
function generateData(tools, months) {
const data = [];
for (let m = 0; m <= months; m++) {
const point = { month: m };
let totalCost = 0;
let totalSavings = 0;
let totalHrs = 0;
tools.forEach((t) => {
const cost = t.setupCost + t.monthlyCost * m;
const savings = t.hoursPerWeekSaved * t.avgHourlyCost * 4.33 * m;
point[`${t.name}_cost`] = Math.round(cost);
point[`${t.name}_savings`] = Math.round(savings);
point[`${t.name}_net`] = Math.round(savings - cost);
point[`${t.name}_hrs`] = t.hoursPerWeekSaved;
totalCost += cost;
totalSavings += savings;
totalHrs += t.hoursPerWeekSaved;
});
point.totalHrs = totalHrs;
point.totalCost = Math.round(totalCost);
point.totalSavings = Math.round(totalSavings);
point.totalNet = Math.round(totalSavings - totalCost);
data.push(point);
}
return data;
}
const fmt = (v) => `$${Math.abs(v).toLocaleString()}`;
const fmtAxis = (v) => {
if (Math.abs(v) >= 1000) return `$${(v / 1000).toFixed(0)}k`;
return `$${v}`;
};
const CustomTooltip = ({ active, payload, label }) => {
if (!active || !payload) return null;
return (
<div style={{
background: "#1a1a2e",
border: "1px solid rgba(255,255,255,0.1)",
borderRadius: 10,
padding: "12px 16px",
fontFamily: "'DM Sans', sans-serif",
fontSize: 13,
color: "#ccc",
boxShadow: "0 8px 32px rgba(0,0,0,0.4)",
}}>
<div style={{ fontWeight: 700, color: "#fff", marginBottom: 6, fontSize: 14 }}>
Month {label}
</div>
{payload.map((p, i) => (
<div key={i} style={{ display: "flex", justifyContent: "space-between", gap: 20, marginBottom: 2 }}>
<span style={{ color: p.color }}>● {p.name}</span>
<span style={{ fontWeight: 600, color: "#fff" }}>{fmt(p.value)}</span>
</div>
))}
</div>
);
};
function InputField({ label, value, onChange, prefix = "$", suffix = "" }) {
return (
<div style={{ marginBottom: 10 }}>
<label style={{
display: "block",
fontSize: 11,
fontWeight: 500,
color: "rgba(255,255,255,0.45)",
textTransform: "uppercase",
letterSpacing: "0.08em",
marginBottom: 4,
fontFamily: "'DM Sans', sans-serif",
}}>
{label}
</label>
<div style={{
display: "flex",
alignItems: "center",
background: "rgba(255,255,255,0.05)",
borderRadius: 8,
border: "1px solid rgba(255,255,255,0.08)",
padding: "6px 10px",
}}>
{prefix && <span style={{ color: "rgba(255,255,255,0.3)", fontSize: 13, marginRight: 4 }}>{prefix}</span>}
<input
type="number"
value={value}
onChange={(e) => onChange(Number(e.target.value) || 0)}
style={{
background: "transparent",
border: "none",
outline: "none",
color: "#fff",
fontSize: 14,
fontFamily: "'DM Mono', monospace",
width: "100%",
fontWeight: 500,
}}
/>
{suffix && <span style={{ color: "rgba(255,255,255,0.3)", fontSize: 12, marginLeft: 4, whiteSpace: "nowrap" }}>{suffix}</span>}
</div>
</div>
);
}
function ToolCard({ tool, onChange, breakevenMonth }) {
const monthlySavings = tool.hoursPerWeekSaved * tool.avgHourlyCost * 4.33;
const netMonthly = monthlySavings - tool.monthlyCost;
const isPositive = netMonthly > 0;
return (
<div style={{
background: "rgba(255,255,255,0.03)",
borderRadius: 14,
padding: 20,
border: `1px solid ${tool.color}22`,
position: "relative",
overflow: "hidden",
}}>
<div style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: 3,
background: `linear-gradient(90deg, ${tool.color}, ${tool.color}44)`,
}} />
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 16 }}>
<div style={{
width: 10,
height: 10,
borderRadius: "50%",
background: tool.color,
boxShadow: `0 0 12px ${tool.color}66`,
}} />
<h3 style={{
margin: 0,
fontSize: 18,
fontWeight: 700,
color: "#fff",
fontFamily: "'Bricolage Grotesque', sans-serif",
}}>
{tool.name}
</h3>
</div>
<div style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: 8,
marginBottom: 14,
}}>
<InputField label="Setup Cost" value={tool.setupCost} onChange={(v) => onChange({ ...tool, setupCost: v })} />
<InputField label="Monthly Cost" value={tool.monthlyCost} onChange={(v) => onChange({ ...tool, monthlyCost: v })} />
<InputField label="Hrs Saved/Week" value={tool.hoursPerWeekSaved} prefix="" suffix="hrs" onChange={(v) => onChange({ ...tool, hoursPerWeekSaved: v })} />
<InputField label="Avg Hourly Cost" value={tool.avgHourlyCost} onChange={(v) => onChange({ ...tool, avgHourlyCost: v })} />
</div>
<div style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: 8,
}}>
<div style={{
background: "rgba(255,255,255,0.04)",
borderRadius: 10,
padding: "10px 12px",
textAlign: "center",
}}>
<div style={{ fontSize: 11, color: "rgba(255,255,255,0.4)", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 4 }}>
Net Monthly
</div>
<div style={{
fontSize: 18,
fontWeight: 700,
fontFamily: "'DM Mono', monospace",
color: isPositive ? "#17B890" : "#E85D26",
}}>
{isPositive ? "+" : "-"}{fmt(netMonthly)}
</div>
</div>
<div style={{
background: breakevenMonth ? `${tool.color}11` : "rgba(255,255,255,0.04)",
borderRadius: 10,
padding: "10px 12px",
textAlign: "center",
border: breakevenMonth ? `1px solid ${tool.color}33` : "none",
}}>
<div style={{ fontSize: 11, color: "rgba(255,255,255,0.4)", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 4 }}>
Breakeven
</div>
<div style={{
fontSize: 18,
fontWeight: 700,
fontFamily: "'DM Mono', monospace",
color: breakevenMonth ? tool.color : "rgba(255,255,255,0.25)",
}}>
{breakevenMonth ? `Month ${breakevenMonth}` : "N/A"}
</div>
</div>
</div>
</div>
);
}
export default function BreakevenDashboard() {
const [tools, setTools] = useState(TOOLS_DEFAULT);
const [view, setView] = useState("combined");
const data = useMemo(() => generateData(tools, MONTHS), [tools]);
const breakevenMonths = useMemo(() => tools.map(calcBreakevenMonth), [tools]);
const combinedBreakeven = useMemo(() => {
const totalSetup = tools.reduce((s, t) => s + t.setupCost, 0);
const totalMonthlyNet = tools.reduce((s, t) => {
const savings = t.hoursPerWeekSaved * t.avgHourlyCost * 4.33;
return s + (savings - t.monthlyCost);
}, 0);
if (totalMonthlyNet <= 0) return null;
return Math.ceil(totalSetup / totalMonthlyNet);
}, [tools]);
const totalAnnualSavings = useMemo(() => {
return tools.reduce((s, t) => {
const monthly = t.hoursPerWeekSaved * t.avgHourlyCost * 4.33 - t.monthlyCost;
return s + monthly * 12;
}, 0) - tools.reduce((s, t) => s + t.setupCost, 0);
}, [tools]);
const totalHoursSaved = tools.reduce((s, t) => s + t.hoursPerWeekSaved, 0);
const totalFTEEquiv = (totalHoursSaved / 40).toFixed(1);
const updateTool = (idx, newTool) => {
const next = [...tools];
next[idx] = newTool;
setTools(next);
};
return (
<div style={{
minHeight: "100vh",
background: "#0d0d1a",
color: "#fff",
fontFamily: "'DM Sans', sans-serif",
padding: "32px 24px",
}}>
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@400;600;700;800&family=DM+Sans:wght@400;500;600;700&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet" />
{/* Header */}
<div style={{ maxWidth: 1200, margin: "0 auto 32px" }}>
<div style={{
display: "flex",
alignItems: "center",
gap: 12,
marginBottom: 8,
}}>
<div style={{
width: 36,
height: 36,
borderRadius: 10,
background: "linear-gradient(135deg, #E85D26, #2D7DD2, #17B890)",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 18,
}}>
</div>
<h1 style={{
margin: 0,
fontSize: 28,
fontFamily: "'Bricolage Grotesque', sans-serif",
fontWeight: 800,
background: "linear-gradient(135deg, #fff, rgba(255,255,255,0.6))",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}}>
AI Tool ROI — Breakeven Analysis
</h1>
</div>
<p style={{ margin: 0, color: "rgba(255,255,255,0.4)", fontSize: 14 }}>
Adjust the inputs below to model manpower savings across Comulate, Fulcrum & Tailwind
</p>
</div>
{/* Summary Strip */}
<div style={{
maxWidth: 1200,
margin: "0 auto 28px",
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
gap: 12,
}}>
{[
{ label: "Combined Breakeven", value: combinedBreakeven ? `Month ${combinedBreakeven}` : "N/A", accent: "#2D7DD2" },
{ label: "Hours Saved / Week", value: `${totalHoursSaved} hrs`, accent: "#17B890" },
{ label: "FTE Equivalent", value: `${totalFTEEquiv} FTEs`, accent: "#E85D26" },
{ label: "Year 1 Net Savings", value: totalAnnualSavings > 0 ? `+${fmt(totalAnnualSavings)}` : `-${fmt(totalAnnualSavings)}`, accent: totalAnnualSavings > 0 ? "#17B890" : "#E85D26" },
].map((s, i) => (
<div key={i} style={{
background: "rgba(255,255,255,0.03)",
borderRadius: 12,
padding: "16px 18px",
border: "1px solid rgba(255,255,255,0.06)",
}}>
<div style={{ fontSize: 11, color: "rgba(255,255,255,0.4)", textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 6 }}>
{s.label}
</div>
<div style={{
fontSize: 22,
fontWeight: 700,
fontFamily: "'DM Mono', monospace",
color: s.accent,
}}>
{s.value}
</div>
</div>
))}
</div>
{/* Tool Cards */}
<div style={{
maxWidth: 1200,
margin: "0 auto 32px",
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: 16,
}}>
{tools.map((t, i) => (
<ToolCard
key={t.name}
tool={t}
onChange={(newT) => updateTool(i, newT)}
breakevenMonth={breakevenMonths[i]}
/>
))}
</div>
{/* Chart Toggle */}
<div style={{ maxWidth: 1200, margin: "0 auto 16px", display: "flex", gap: 8 }}>
{["combined", "individual"].map((v) => (
<button
key={v}
onClick={() => setView(v)}
style={{
background: view === v ? "rgba(255,255,255,0.12)" : "rgba(255,255,255,0.03)",
border: view === v ? "1px solid rgba(255,255,255,0.2)" : "1px solid rgba(255,255,255,0.06)",
borderRadius: 8,
padding: "8px 18px",
color: view === v ? "#fff" : "rgba(255,255,255,0.4)",
fontSize: 13,
fontWeight: 600,
cursor: "pointer",
fontFamily: "'DM Sans', sans-serif",
transition: "all 0.2s",
}}
>
{v === "combined" ? "Combined View" : "Per Tool View"}
</button>
))}
</div>
{/* Charts */}
<div style={{
maxWidth: 1200,
margin: "0 auto",
background: "rgba(255,255,255,0.02)",
borderRadius: 16,
border: "1px solid rgba(255,255,255,0.06)",
padding: "24px 20px 16px",
}}>
{view === "combined" ? (
<>
<h3 style={{
margin: "0 0 16px 8px",
fontSize: 15,
fontWeight: 600,
color: "rgba(255,255,255,0.6)",
fontFamily: "'Bricolage Grotesque', sans-serif",
}}>
Total Investment vs. Total Savings ({MONTHS}-Month Projection)
</h3>
<ResponsiveContainer width="100%" height={380}>
<ComposedChart data={data} margin={{ top: 5, right: 20, bottom: 5, left: 10 }}>
<defs>
<linearGradient id="savingsGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#17B890" stopOpacity={0.25} />
<stop offset="100%" stopColor="#17B890" stopOpacity={0.02} />
</linearGradient>
<linearGradient id="costGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#E85D26" stopOpacity={0.2} />
<stop offset="100%" stopColor="#E85D26" stopOpacity={0.02} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" />
<XAxis
dataKey="month"
stroke="rgba(255,255,255,0.2)"
tick={{ fill: "rgba(255,255,255,0.35)", fontSize: 12 }}
tickFormatter={(v) => `M${v}`}
/>
<YAxis
stroke="rgba(255,255,255,0.2)"
tick={{ fill: "rgba(255,255,255,0.35)", fontSize: 12 }}
tickFormatter={fmtAxis}
/>
<Tooltip content={<CustomTooltip />} />
<Legend
wrapperStyle={{ paddingTop: 12, fontSize: 13, color: "rgba(255,255,255,0.5)" }}
/>
<Area type="monotone" dataKey="totalSavings" name="Total Savings" fill="url(#savingsGrad)" stroke="#17B890" strokeWidth={2.5} dot={false} />
<Area type="monotone" dataKey="totalCost" name="Total Cost" fill="url(#costGrad)" stroke="#E85D26" strokeWidth={2.5} dot={false} />
{combinedBreakeven && combinedBreakeven <= MONTHS && (
<ReferenceLine
x={combinedBreakeven}
stroke="#FFD166"
strokeDasharray="6 4"
strokeWidth={2}
label={{
value: `BreakevenMonth ${combinedBreakeven}`,
position: "top",
fill: "#FFD166",
fontSize: 12,
fontWeight: 600,
}}
/>
)}
</ComposedChart>
</ResponsiveContainer>
{/* FTE Hours Chart */}
<div style={{
marginTop: 32,
paddingTop: 28,
borderTop: "1px solid rgba(255,255,255,0.06)",
}}>
<h3 style={{
margin: "0 0 6px 8px",
fontSize: 15,
fontWeight: 600,
color: "rgba(255,255,255,0.6)",
fontFamily: "'Bricolage Grotesque', sans-serif",
}}>
Weekly Hours Saved vs. FTE Capacity
</h3>
<p style={{
margin: "0 0 16px 8px",
fontSize: 12,
color: "rgba(255,255,255,0.3)",
}}>
Stacked hours per tool — dashed lines mark each FTE threshold (40 hrs/week)
</p>
<ResponsiveContainer width="100%" height={320}>
<ComposedChart data={data} margin={{ top: 10, right: 20, bottom: 5, left: 10 }}>
<defs>
{tools.map((t) => (
<linearGradient key={`grad-${t.name}`} id={`hrsGrad_${t.name}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={t.color} stopOpacity={0.85} />
<stop offset="100%" stopColor={t.color} stopOpacity={0.55} />
</linearGradient>
))}
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" />
<XAxis
dataKey="month"
stroke="rgba(255,255,255,0.2)"
tick={{ fill: "rgba(255,255,255,0.35)", fontSize: 12 }}
tickFormatter={(v) => `M${v}`}
/>
<YAxis
stroke="rgba(255,255,255,0.2)"
tick={{ fill: "rgba(255,255,255,0.35)", fontSize: 12 }}
tickFormatter={(v) => `${v}h`}
domain={[0, (dataMax) => {
const maxFTE = Math.ceil(dataMax / 40) * 40;
return Math.max(maxFTE, 40) + 10;
}]}
/>
<Tooltip
content={({ active, payload, label }) => {
if (!active || !payload) return null;
const totalH = payload.reduce((s, p) => s + (p.value || 0), 0);
const fteCount = (totalH / 40).toFixed(1);
return (
<div style={{
background: "#1a1a2e",
border: "1px solid rgba(255,255,255,0.1)",
borderRadius: 10,
padding: "12px 16px",
fontFamily: "'DM Sans', sans-serif",
fontSize: 13,
color: "#ccc",
boxShadow: "0 8px 32px rgba(0,0,0,0.4)",
}}>
<div style={{ fontWeight: 700, color: "#fff", marginBottom: 6, fontSize: 14 }}>
Month {label}
</div>
{payload.map((p, i) => (
<div key={i} style={{ display: "flex", justifyContent: "space-between", gap: 20, marginBottom: 2 }}>
<span style={{ color: p.color || p.fill }}>● {p.name}</span>
<span style={{ fontWeight: 600, color: "#fff" }}>{p.value} hrs</span>
</div>
))}
<div style={{
marginTop: 6,
paddingTop: 6,
borderTop: "1px solid rgba(255,255,255,0.1)",
display: "flex",
justifyContent: "space-between",
gap: 20,
}}>
<span style={{ color: "#FFD166" }}>Total</span>
<span style={{ fontWeight: 700, color: "#FFD166" }}>{totalH} hrs ({fteCount} FTEs)</span>
</div>
</div>
);
}}
/>
<Legend wrapperStyle={{ paddingTop: 12, fontSize: 13 }} />
{tools.map((t) => (
<Bar
key={`bar-${t.name}`}
dataKey={`${t.name}_hrs`}
name={t.name}
stackId="hrs"
fill={`url(#hrsGrad_${t.name})`}
/>
))}
{(() => {
const maxFTEs = Math.ceil(tools.reduce((s, t) => s + t.hoursPerWeekSaved, 0) / 40);
const lines = [];
for (let f = 1; f <= Math.max(maxFTEs, 1); f++) {
lines.push(
<ReferenceLine
key={`fte-${f}`}
y={f * 40}
stroke="#FFD166"
strokeDasharray="8 5"
strokeWidth={1.5}
strokeOpacity={0.7}
label={{
value: `${f} FTE (${f * 40}h)`,
position: "right",
fill: "#FFD166",
fontSize: 11,
fontWeight: 600,
}}
/>
);
}
return lines;
})()}
</ComposedChart>
</ResponsiveContainer>
{/* FTE status callout */}
{(() => {
const total = tools.reduce((s, t) => s + t.hoursPerWeekSaved, 0);
const ftes = Math.floor(total / 40);
const remainder = total % 40;
const pct = ((total / 40) * 100).toFixed(0);
const crossed = total >= 40;
return (
<div style={{
marginTop: 16,
padding: "14px 20px",
borderRadius: 12,
background: crossed
? "linear-gradient(135deg, rgba(23,184,144,0.12), rgba(23,184,144,0.04))"
: "linear-gradient(135deg, rgba(255,209,102,0.1), rgba(255,209,102,0.03))",
border: crossed
? "1px solid rgba(23,184,144,0.25)"
: "1px solid rgba(255,209,102,0.2)",
display: "flex",
alignItems: "center",
gap: 14,
}}>
<div style={{ fontSize: 28, lineHeight: 1 }}>
{crossed ? "✅" : "⏳"}
</div>
<div>
<div style={{
fontSize: 15,
fontWeight: 700,
color: crossed ? "#17B890" : "#FFD166",
fontFamily: "'Bricolage Grotesque', sans-serif",
marginBottom: 3,
}}>
{crossed
? `Crossed ${ftes} FTE${ftes > 1 ? "s" : ""} — ${total} hrs/week saved`
: `${total} hrs/week saved — ${pct}% toward 1 FTE`
}
</div>
<div style={{ fontSize: 13, color: "rgba(255,255,255,0.45)" }}>
{crossed
? `That's ${ftes} full-time equivalent${ftes > 1 ? "s" : ""} + ${remainder} hrs/week of capacity reclaimed across the 3 tools.`
: `You need ${40 - total} more hrs/week of automation to reach 1 full FTE equivalent.`
}
</div>
</div>
</div>
);
})()}
</div>
</>
) : (
<>
<h3 style={{
margin: "0 0 16px 8px",
fontSize: 15,
fontWeight: 600,
color: "rgba(255,255,255,0.6)",
fontFamily: "'Bricolage Grotesque', sans-serif",
}}>
Net Cumulative Value Per Tool ({MONTHS}-Month Projection)
</h3>
<ResponsiveContainer width="100%" height={380}>
<ComposedChart data={data} margin={{ top: 5, right: 20, bottom: 5, left: 10 }}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" />
<XAxis
dataKey="month"
stroke="rgba(255,255,255,0.2)"
tick={{ fill: "rgba(255,255,255,0.35)", fontSize: 12 }}
tickFormatter={(v) => `M${v}`}
/>
<YAxis
stroke="rgba(255,255,255,0.2)"
tick={{ fill: "rgba(255,255,255,0.35)", fontSize: 12 }}
tickFormatter={fmtAxis}
/>
<Tooltip content={<CustomTooltip />} />
<Legend wrapperStyle={{ paddingTop: 12, fontSize: 13 }} />
<ReferenceLine y={0} stroke="rgba(255,255,255,0.15)" strokeWidth={1.5} />
{tools.map((t, i) => (
<Line
key={t.name}
type="monotone"
dataKey={`${t.name}_net`}
name={t.name}
stroke={t.color}
strokeWidth={2.5}
dot={false}
activeDot={{ r: 5, stroke: t.color, strokeWidth: 2, fill: "#0d0d1a" }}
/>
))}
{tools.map((t, i) =>
breakevenMonths[i] && breakevenMonths[i] <= MONTHS ? (
<ReferenceLine
key={`ref-${t.name}`}
x={breakevenMonths[i]}
stroke={t.color}
strokeDasharray="4 4"
strokeOpacity={0.5}
/>
) : null
)}
</ComposedChart>
</ResponsiveContainer>
</>
)}
</div>
{/* Footer */}
<div style={{
maxWidth: 1200,
margin: "24px auto 0",
padding: "14px 18px",
background: "rgba(255,255,255,0.02)",
borderRadius: 10,
border: "1px solid rgba(255,255,255,0.05)",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
fontSize: 12,
color: "rgba(255,255,255,0.3)",
}}>
<span>Monthly savings = (Hours/Week × Hourly Cost × 4.33) − Monthly License Cost</span>
<span>Breakeven = Setup Cost ÷ Net Monthly Savings</span>
</div>
</div>
);
}