refactor: estimation generation
All checks were successful
Build & Deploy / 🔍 Prepare Environment (push) Successful in 32s
Build & Deploy / 🏗️ Build (push) Successful in 4m41s
Build & Deploy / 🧪 QA (push) Successful in 5m56s
Build & Deploy / 🚀 Deploy (push) Successful in 12s
Build & Deploy / 🔔 Notifications (push) Successful in 1s
Build & Deploy / ⚡ PageSpeed (push) Successful in 55s
All checks were successful
Build & Deploy / 🔍 Prepare Environment (push) Successful in 32s
Build & Deploy / 🏗️ Build (push) Successful in 4m41s
Build & Deploy / 🧪 QA (push) Successful in 5m56s
Build & Deploy / 🚀 Deploy (push) Successful in 12s
Build & Deploy / 🔔 Notifications (push) Successful in 1s
Build & Deploy / ⚡ PageSpeed (push) Successful in 55s
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,131 +1,165 @@
|
||||
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 { calculatePositions, 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 } from '../src/logic/content-provider.js';
|
||||
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 {
|
||||
calculatePositions,
|
||||
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);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
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');
|
||||
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 };
|
||||
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(),
|
||||
mode: isEstimationOnly ? 'estimation' : 'full',
|
||||
showAgbs: !isEstimationOnly // AGBS only for full quotes
|
||||
}) as any,
|
||||
finalOutputPath
|
||||
if (inputPath) {
|
||||
const rawData = fs.readFileSync(
|
||||
path.resolve(process.cwd(), inputPath),
|
||||
"utf8",
|
||||
);
|
||||
const diskState = JSON.parse(rawData);
|
||||
state = { ...state, ...diskState };
|
||||
}
|
||||
|
||||
console.log('✅ Done!');
|
||||
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
|
||||
});
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
console.log('\n--- Mintel Quote Generator Wizard ---\n');
|
||||
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 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;
|
||||
};
|
||||
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.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' }
|
||||
]);
|
||||
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
|
||||
}
|
||||
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;
|
||||
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`);
|
||||
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);
|
||||
main().catch((err) => {
|
||||
console.error("❌ Error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user