155 lines
5.8 KiB
TypeScript
155 lines
5.8 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 { calculatePositions } 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';
|
|
|
|
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 inputPath = args.find((_, i) => args[i - 1] === '--input' || args[i - 1] === '-i');
|
|
const outputPath = args.find((_, i) => args[i - 1] === '--output' || args[i - 1] === '-o');
|
|
|
|
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 totalPrice = calculateTotal(state);
|
|
const monthlyPrice = calculateMonthly(state);
|
|
const totalPagesCount = (state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0);
|
|
|
|
const finalOutputPath = outputPath || 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()
|
|
}) 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 calculateTotal(state: any) {
|
|
// Basic duplication of logic from ContactForm for consistency
|
|
if (state.projectType !== 'website') return 0;
|
|
const totalPagesCount = (state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0);
|
|
let total = PRICING.BASE_WEBSITE;
|
|
total += totalPagesCount * PRICING.PAGE;
|
|
total += ((state.features?.length || 0) + (state.otherFeatures?.length || 0) + (state.otherFeaturesCount || 0)) * PRICING.FEATURE;
|
|
total += ((state.functions?.length || 0) + (state.otherFunctions?.length || 0) + (state.otherFunctionsCount || 0)) * PRICING.FUNCTION;
|
|
total += ((state.apiSystems?.length || 0) + (state.otherTech?.length || 0) + (state.otherTechCount || 0)) * PRICING.API_INTEGRATION;
|
|
|
|
if (state.cmsSetup) {
|
|
total += PRICING.CMS_SETUP;
|
|
total += ((state.features?.length || 0) + (state.otherFeatures?.length || 0) + (state.otherFeaturesCount || 0)) * PRICING.CMS_CONNECTION_PER_FEATURE;
|
|
}
|
|
|
|
const languagesCount = state.languagesList?.length || 1;
|
|
if (languagesCount > 1) {
|
|
total *= (1 + (languagesCount - 1) * 0.2);
|
|
}
|
|
return Math.round(total);
|
|
}
|
|
|
|
function calculateMonthly(state: any) {
|
|
if (state.projectType !== 'website') return 0;
|
|
return PRICING.HOSTING_MONTHLY + ((state.storageExpansion || 0) * PRICING.STORAGE_EXPANSION_MONTHLY);
|
|
}
|
|
|
|
function generateDefaultPath(state: any) {
|
|
const now = new Date();
|
|
const month = now.toISOString().slice(0, 7);
|
|
const day = now.toISOString().slice(0, 10);
|
|
const company = (state.companyName || state.name || 'Unknown').replace(/[^a-z0-9]/gi, '_');
|
|
return path.join(process.cwd(), 'out', 'estimations', month, `${day}_${company}_${state.projectType}.pdf`);
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error('❌ Error:', err);
|
|
process.exit(1);
|
|
});
|