Files
mintel.me/apps/web/scripts/generate-estimate.ts
Marc Mintel ecea90dc91
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Failing after 1m27s
Build & Deploy / 🏗️ Build (push) Failing after 1m31s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
chore: stabilize apps/web (lint, build, typecheck fixes)
2026-02-11 11:56:13 +01:00

162 lines
4.6 KiB
TypeScript

import * as fs from "node:fs";
import * as path from "node:path";
import * as readline from "node:readline/promises";
import { fileURLToPath } from "node:url";
import { createElement } from "react";
import { renderToFile } from "@react-pdf/renderer";
import { calculateTotals } from "../src/logic/pricing/calculator.js";
import { CombinedQuotePDF } from "../src/components/CombinedQuotePDF.js";
import { initialState, PRICING } from "../src/logic/pricing/constants.js";
import {
getTechDetails,
getPrinciples,
getMaintenanceDetails,
getStandardsDetails,
} from "../src/logic/content-provider.js";
const __filename = fileURLToPath(import.meta.url);
async function main() {
const args = process.argv.slice(2);
const isInteractive = args.includes("--interactive") || args.includes("-I");
const isEstimationOnly = args.includes("--estimation") || args.includes("-E");
const inputPath = args.find(
(_, i) => args[i - 1] === "--input" || args[i - 1] === "-i",
);
let state = { ...initialState };
if (inputPath) {
const rawData = fs.readFileSync(
path.resolve(process.cwd(), inputPath),
"utf8",
);
const diskState = JSON.parse(rawData);
state = { ...state, ...diskState };
}
if (isInteractive) {
state = await runWizard(state);
}
// Final confirmation of data needed for PDF
if (!state.name || !state.email) {
console.warn(
"⚠️ Missing recipient name or email. Document might look incomplete.",
);
}
const totals = calculateTotals(state, PRICING);
const { totalPrice, monthlyPrice, totalPagesCount } = totals;
const finalOutputPath = generateDefaultPath(state);
const outputDir = path.dirname(finalOutputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Resolve assets for the PDF
const assetsDir = path.resolve(process.cwd(), "src/assets");
const headerIcon = path.join(assetsDir, "logo/Icon White Transparent.png");
const footerLogo = path.join(assetsDir, "logo/Logo Black Transparent.png");
console.log(`🚀 Generating PDF: ${finalOutputPath}`);
const estimationProps = {
state,
totalPrice,
monthlyPrice,
totalPagesCount,
pricing: PRICING,
headerIcon,
footerLogo,
};
await renderToFile(
createElement(CombinedQuotePDF as any, {
estimationProps,
techDetails: getTechDetails(),
principles: getPrinciples(),
maintenanceDetails: getMaintenanceDetails(),
standardsDetails: getStandardsDetails(),
mode: isEstimationOnly ? "estimation" : "full",
showAgbs: !isEstimationOnly, // AGBS only for full quotes
}) as any,
finalOutputPath,
);
console.log("✅ Done!");
}
async function runWizard(state: any) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
console.log("\n--- Mintel Quote Generator Wizard ---\n");
const ask = async (q: string, def?: string) => {
const answer = await rl.question(`${q}${def ? ` [${def}]` : ""}: `);
return answer || def || "";
};
const selectOne = async (
q: string,
options: { id: string; label: string }[],
) => {
console.log(`\n${q}:`);
options.forEach((opt, i) => console.log(`${i + 1}) ${opt.label}`));
const answer = await rl.question("Selection (number): ");
const idx = parseInt(answer) - 1;
return options[idx]?.id || options[0].id;
};
state.name = await ask("Recipient Name", state.name);
state.email = await ask("Recipient Email", state.email);
state.companyName = await ask("Company Name", state.companyName);
state.projectType = await selectOne("Project Type", [
{ id: "website", label: "Website" },
{ id: "web-app", label: "Web App" },
]);
if (state.projectType === "website") {
state.websiteTopic = await ask("Website Topic", state.websiteTopic);
// Simplified for now, in a real tool we'd loop through all options
}
rl.close();
return state;
}
function generateDefaultPath(state: any) {
const now = new Date();
const month = now.toISOString().slice(0, 7);
const day = now.toISOString().slice(0, 10);
// Add seconds and minutes for 100% unique names without collision
const time = now
.toLocaleTimeString("de-DE", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})
.replace(/:/g, "-");
const company = (state.companyName || state.name || "Unknown").replace(
/[^a-z0-9]/gi,
"_",
);
return path.join(
process.cwd(),
"out",
"estimations",
month,
`${day}_${time}_${company}_${state.projectType}.pdf`,
);
}
main().catch((err) => {
console.error("❌ Error:", err);
process.exit(1);
});