Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions data/calculator-presets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
[
{
"id": "solo-developer",
"name": "Solo Developer",
"description": "One builder automating repetitive coding and support tasks.",
"inputs": {
"agents": 1,
"hoursSavedPerAgentPerDay": 1.5,
"hourlyRate": 90,
"monthlyToolCosts": 149
}
},
{
"id": "small-team",
"name": "Small Team",
"description": "A startup squad using agents across product and operations.",
"inputs": {
"agents": 5,
"hoursSavedPerAgentPerDay": 2,
"hourlyRate": 75,
"monthlyToolCosts": 699
}
},
{
"id": "enterprise",
"name": "Enterprise",
"description": "Cross-functional enterprise deployment with strict SLAs.",
"inputs": {
"agents": 40,
"hoursSavedPerAgentPerDay": 1.75,
"hourlyRate": 95,
"monthlyToolCosts": 9500
}
},
{
"id": "agency",
"name": "Agency",
"description": "Client services team using agents to speed delivery.",
"inputs": {
"agents": 12,
"hoursSavedPerAgentPerDay": 2.5,
"hourlyRate": 85,
"monthlyToolCosts": 2200
}
}
]
53 changes: 53 additions & 0 deletions data/calculator-results.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
[
{
"id": "calc_seed_001",
"name": "Pilot - Internal Ops",
"inputs": {
"agents": 3,
"hoursSavedPerAgentPerDay": 1.8,
"hourlyRate": 70,
"monthlyToolCosts": 399
},
"results": {
"monthlySavings": 7917,
"annualRoi": 1984.21,
"paybackPeriodMonths": 0.05,
"efficiencyGainPercent": 22.5
},
"createdAt": "2026-01-03T14:23:00.000Z"
},
{
"id": "calc_seed_002",
"name": "Sales Enablement Rollout",
"inputs": {
"agents": 8,
"hoursSavedPerAgentPerDay": 1.25,
"hourlyRate": 65,
"monthlyToolCosts": 1200
},
"results": {
"monthlySavings": 13100,
"annualRoi": 1091.67,
"paybackPeriodMonths": 0.09,
"efficiencyGainPercent": 15.63
},
"createdAt": "2026-01-10T09:10:00.000Z"
},
{
"id": "calc_seed_003",
"name": "Agency Client Delivery",
"inputs": {
"agents": 10,
"hoursSavedPerAgentPerDay": 2.4,
"hourlyRate": 80,
"monthlyToolCosts": 2100
},
"results": {
"monthlySavings": 40140,
"annualRoi": 1911.43,
"paybackPeriodMonths": 0.05,
"efficiencyGainPercent": 30
},
"createdAt": "2026-01-22T18:47:00.000Z"
}
]
151 changes: 151 additions & 0 deletions src/app/api/calculator/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { promises as fs } from "fs";
import path from "path";
import { NextRequest, NextResponse } from "next/server";

type CalculatorInputs = {
agents: number;
hoursSavedPerAgentPerDay: number;
hourlyRate: number;
monthlyToolCosts: number;
};

type CalculatorResults = {
monthlySavings: number;
annualRoi: number;
paybackPeriodMonths: number | null;
efficiencyGainPercent: number;
};

type SavedCalculation = {
id: string;
name: string;
inputs: CalculatorInputs;
results: CalculatorResults;
createdAt: string;
};

type SaveCalculationBody = {
name?: unknown;
inputs?: unknown;
results?: unknown;
};

const PRESETS_PATH = path.join(process.cwd(), "data", "calculator-presets.json");
const RESULTS_PATH = path.join(process.cwd(), "data", "calculator-results.json");

function isFiniteNumber(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value);
}

function isInputs(value: unknown): value is CalculatorInputs {
if (!value || typeof value !== "object") {
return false;
}

const inputs = value as Partial<CalculatorInputs>;

return (
isFiniteNumber(inputs.agents) &&
isFiniteNumber(inputs.hoursSavedPerAgentPerDay) &&
isFiniteNumber(inputs.hourlyRate) &&
isFiniteNumber(inputs.monthlyToolCosts)
);
}

function isResults(value: unknown): value is CalculatorResults {
if (!value || typeof value !== "object") {
return false;
}

const results = value as Partial<CalculatorResults>;

const paybackValid =
results.paybackPeriodMonths === null || isFiniteNumber(results.paybackPeriodMonths);

return (
isFiniteNumber(results.monthlySavings) &&
isFiniteNumber(results.annualRoi) &&
paybackValid &&
isFiniteNumber(results.efficiencyGainPercent)
);
}

async function readJsonFile<T>(filePath: string, fallback: T): Promise<T> {
try {
const raw = await fs.readFile(filePath, "utf8");
return JSON.parse(raw) as T;
} catch {
return fallback;
}
}

async function writeResults(results: SavedCalculation[]): Promise<void> {
const dir = path.dirname(RESULTS_PATH);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(RESULTS_PATH, JSON.stringify(results, null, 2), "utf8");
}

export async function GET() {
try {
const presets = await readJsonFile(PRESETS_PATH, []);

if (!Array.isArray(presets)) {
return NextResponse.json({ error: "Invalid presets format" }, { status: 500 });
}

return NextResponse.json({ presets });
} catch (error) {
console.error("Failed to load calculator presets", error);
return NextResponse.json({ error: "Failed to load calculator presets" }, { status: 500 });
}
}

export async function POST(request: NextRequest) {
try {
const body = (await request.json()) as SaveCalculationBody;

const name = typeof body.name === "string" ? body.name.trim() : "";
const inputs = body.inputs;
const results = body.results;

if (!name) {
return NextResponse.json({ error: "name is required" }, { status: 400 });
}

if (!isInputs(inputs)) {
return NextResponse.json(
{ error: "inputs must include agents, hoursSavedPerAgentPerDay, hourlyRate, and monthlyToolCosts" },
{ status: 400 }
);
}

if (!isResults(results)) {
return NextResponse.json(
{
error:
"results must include monthlySavings, annualRoi, paybackPeriodMonths, and efficiencyGainPercent",
},
{ status: 400 }
);
}

const existingResults = await readJsonFile<SavedCalculation[]>(RESULTS_PATH, []);
const safeResults = Array.isArray(existingResults) ? existingResults : [];

const record: SavedCalculation = {
id: `calc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
name,
inputs,
results,
createdAt: new Date().toISOString(),
};

safeResults.unshift(record);
await writeResults(safeResults);

return NextResponse.json({ calculation: record }, { status: 201 });
} catch (error) {
console.error("Failed to save calculator result", error);
return NextResponse.json({ error: "Failed to save calculator result" }, { status: 500 });
}
}
Loading
Loading