it/web/components/pricing/ROICalculator.tsx
2025-10-26 02:05:16 +02:00

148 lines
5.6 KiB
TypeScript

// components/pricing/ROICalculator.tsx
"use client";
import { useMemo, useState } from "react";
import { Plan } from "./types";
import { formatEUR } from "./money";
function targetUptime(planId: string) {
return planId === "mission" ? 0.9999 : planId === "growth" ? 0.9995 : 0.999;
}
export default function ROICalculator({ plans }: { plans: Plan[] }) {
const [planId, setPlanId] = useState<string>("growth");
const [monthlyLeads, setMonthlyLeads] = useState<number>(40);
const [avgLeadValue, setAvgLeadValue] = useState<number>(150);
const [currentUptime, setCurrentUptime] = useState<number>(0.995); // 99.5%
const [deliverability, setDeliverability] = useState<number>(0.85); // 85%
const plan = plans.find((p) => p.id === planId)!;
const result = useMemo(() => {
const target = targetUptime(planId);
const uptimeGain = Math.max(0, target - currentUptime); // fraction
// assume +10pp absolute deliverability improvement (conservative)
const deliverabilityGain = Math.max(0, 0.1 + (deliverability < 0.9 ? 0 : -0.02));
const recoveredLeads =
monthlyLeads * uptimeGain + monthlyLeads * (deliverabilityGain * (1 - deliverability));
const recoveredRevenue = recoveredLeads * avgLeadValue;
const planCost = plan.monthlyPrice;
const roi = planCost ? (recoveredRevenue - planCost) / planCost : 0;
return {
target,
uptimeGain,
deliverabilityGain,
recoveredLeads,
recoveredRevenue,
planCost,
roi,
};
}, [planId, plan, monthlyLeads, avgLeadValue, currentUptime, deliverability]);
return (
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<h2 className="text-xl font-semibold">Estimate your ROI</h2>
<p className="mt-1 text-sm text-gray-600">
A simple, conservative model based on uptime & deliverability improvements.
</p>
<div className="mt-6 grid gap-6 md:grid-cols-5">
<div className="md:col-span-1">
<label className="block text-sm font-medium text-gray-700">Plan</label>
<select
className="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2"
value={planId}
onChange={(e) => setPlanId(e.target.value)}
>
{plans.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</div>
<div className="md:col-span-1">
<label className="block text-sm font-medium text-gray-700">Monthly leads</label>
<input
type="number"
className="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2"
min={0}
value={monthlyLeads}
onChange={(e) => setMonthlyLeads(Number(e.target.value || 0))}
/>
</div>
<div className="md:col-span-1">
<label className="block text-sm font-medium text-gray-700">Avg. lead value ()</label>
<input
type="number"
className="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2"
min={0}
value={avgLeadValue}
onChange={(e) => setAvgLeadValue(Number(e.target.value || 0))}
/>
</div>
<div className="md:col-span-1">
<label className="block text-sm font-medium text-gray-700">Current uptime</label>
<input
type="number"
step="0.001"
min={0.95}
max={0.9999}
className="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2"
value={currentUptime}
onChange={(e) => setCurrentUptime(Number(e.target.value || 0))}
/>
<p className="mt-1 text-xs text-gray-500">e.g., 0.995 = 99.5%</p>
</div>
<div className="md:col-span-1">
<label className="block text-sm font-medium text-gray-700">Inbox rate</label>
<input
type="number"
step="0.01"
min={0.5}
max={1}
className="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2"
value={deliverability}
onChange={(e) => setDeliverability(Number(e.target.value || 0))}
/>
<p className="mt-1 text-xs text-gray-500">e.g., 0.85 = 85%</p>
</div>
</div>
<div className="mt-6 grid gap-6 md:grid-cols-3">
<div className="rounded-xl border border-gray-200 p-4">
<p className="text-sm text-gray-600">Target uptime (plan)</p>
<p className="mt-1 text-2xl font-semibold">{(result.target * 100).toFixed(3)}%</p>
</div>
<div className="rounded-xl border border-gray-200 p-4">
<p className="text-sm text-gray-600">Recovered leads (est.)</p>
<p className="mt-1 text-2xl font-semibold">
{Math.max(0, Math.round(result.recoveredLeads))}
</p>
</div>
<div className="rounded-xl border border-gray-200 p-4">
<p className="text-sm text-gray-600">Monthly impact</p>
<p className="mt-1 text-2xl font-semibold">{formatEUR(Math.max(0, Math.round(result.recoveredRevenue)))}</p>
</div>
</div>
<div className="mt-6 rounded-xl border border-emerald-200 bg-emerald-50 p-4">
<p className="text-sm font-semibold text-emerald-900">Estimated ROI</p>
<p className="mt-1 text-lg font-semibold text-emerald-900">
{result.planCost
? `${Math.round(result.roi * 100)}%`
: "—"}
</p>
<p className="mt-1 text-sm text-emerald-900">
Impact is an estimate; we provide before/after proof during your first month.
</p>
</div>
</div>
);
}