Files
mintel.me/scripts/generate-quote.ts

168 lines
6.6 KiB
TypeScript

import * as React from 'react';
import { renderToFile } from '@react-pdf/renderer';
import { EstimationPDF } from '../src/components/EstimationPDF';
import {
PRICING,
initialState,
PAGE_SAMPLES,
FEATURE_OPTIONS,
FUNCTION_OPTIONS,
API_OPTIONS,
DESIGN_OPTIONS,
DEADLINE_LABELS,
ASSET_OPTIONS,
EMPLOYEE_OPTIONS,
calculatePositions,
FormState
} from '../src/logic/pricing';
import * as fs from 'fs';
import * as path from 'path';
import { parseArgs } from 'util';
import * as readline from 'readline/promises';
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
async function ask(question: string, defaultValue?: string): Promise<string> {
const answer = await rl.question(`${question}${defaultValue ? ` [${defaultValue}]` : ''}: `);
return answer.trim() || defaultValue || '';
}
async function selectOne(question: string, options: { id: string; label: string }[], defaultValue?: string): Promise<string> {
console.log(`\n${question}`);
options.forEach((opt, i) => console.log(`${i + 1}. ${opt.label}`));
const answer = await ask('Select number');
const index = parseInt(answer) - 1;
return options[index]?.id || defaultValue || options[0].id;
}
async function selectMany(question: string, options: { id: string; label: string }[]): Promise<string[]> {
console.log(`\n${question} (comma separated numbers, e.g. 1,2,4)`);
options.forEach((opt, i) => console.log(`${i + 1}. ${opt.label}`));
const answer = await ask('Selection');
if (!answer) return [];
return answer.split(',')
.map(n => parseInt(n.trim()) - 1)
.filter(i => options[i])
.map(i => options[i].id);
}
async function generate() {
const { values } = parseArgs({
options: {
input: { type: 'string', short: 'i' },
output: { type: 'string', short: 'o' },
interactive: { type: 'boolean', short: 'I', default: false },
name: { type: 'string' },
company: { type: 'string' },
email: { type: 'string' },
'project-type': { type: 'string' },
},
});
let state: FormState = { ...initialState };
// Apply command line flags first
if (values.name) state.name = values.name as string;
if (values.company) state.companyName = values.company as string;
if (values.email) state.email = values.email as string;
if (values['project-type']) state.projectType = values['project-type'] as 'website' | 'web-app';
if (values.input) {
const inputPath = path.resolve(process.cwd(), values.input);
if (fs.existsSync(inputPath)) {
const fileData = JSON.parse(fs.readFileSync(inputPath, 'utf-8'));
state = { ...state, ...fileData };
}
}
if (values.interactive) {
console.log('--- Mintel Quote Generator Wizard ---');
state.name = await ask('Client Name', state.name);
state.companyName = await ask('Company Name', state.companyName);
state.email = await ask('Client Email', state.email);
state.projectType = (await selectOne('Project Type', [
{ id: 'website', label: 'Website' },
{ id: 'web-app', label: 'Web App' }
], state.projectType)) as 'website' | 'web-app';
if (state.projectType === 'website') {
state.websiteTopic = await ask('Website Topic', state.websiteTopic);
state.selectedPages = await selectMany('Pages', PAGE_SAMPLES);
state.features = await selectMany('Features', FEATURE_OPTIONS);
state.functions = await selectMany('Functions', FUNCTION_OPTIONS);
state.apiSystems = await selectMany('APIs', API_OPTIONS);
const cms = await ask('CMS Setup? (y/n)', state.cmsSetup ? 'y' : 'n');
state.cmsSetup = cms.toLowerCase() === 'y';
const langs = await ask('Languages (comma separated)', state.languagesList.join(', '));
state.languagesList = langs.split(',').map(l => l.trim()).filter(Boolean);
} else {
state.targetAudience = await selectOne('Target Audience', [
{ id: 'internal', label: 'Internal Tool' },
{ id: 'customers', label: 'Customer Portal' }
], state.targetAudience);
state.platformType = await selectOne('Platform', [
{ id: 'web-only', label: 'Web Only' },
{ id: 'cross-platform', label: 'Cross Platform' }
], state.platformType);
state.dataSensitivity = await selectOne('Data Sensitivity', [
{ id: 'standard', label: 'Standard' },
{ id: 'high', label: 'High / Sensitive' }
], state.dataSensitivity);
}
state.employeeCount = await selectOne('Company Size', EMPLOYEE_OPTIONS, state.employeeCount);
state.designVibe = await selectOne('Design Vibe', DESIGN_OPTIONS, state.designVibe);
state.deadline = await selectOne('Timeline', Object.entries(DEADLINE_LABELS).map(([id, label]) => ({ id, label })), state.deadline);
state.assets = await selectMany('Available Assets', ASSET_OPTIONS);
state.message = await ask('Additional Message / Remarks', state.message);
}
rl.close();
const totalPagesCount = state.selectedPages.length + state.otherPages.length + (state.otherPagesCount || 0);
const positions = calculatePositions(state, PRICING);
const totalPrice = positions.reduce((sum, p) => sum + p.price, 0);
const monthlyPrice = PRICING.HOSTING_MONTHLY + (state.storageExpansion * PRICING.STORAGE_EXPANSION_MONTHLY);
const outputPath = values.output
? path.resolve(process.cwd(), values.output)
: path.resolve(process.cwd(), `quote_${state.companyName.replace(/\s+/g, '_') || 'client'}_${new Date().toISOString().split('T')[0]}.pdf`);
console.log(`\nGenerating PDF for ${state.companyName || state.name || 'Unknown Client'}...`);
const headerIcon = path.resolve(process.cwd(), 'src/assets/logo/Icon White Transparent.png');
const footerLogo = path.resolve(process.cwd(), 'src/assets/logo/Logo Black Transparent.png');
try {
// @ts-ignore
await renderToFile(
React.createElement(EstimationPDF, {
state,
totalPrice,
monthlyPrice,
totalPagesCount,
pricing: PRICING,
headerIcon,
footerLogo,
}),
outputPath
);
console.log(`Successfully generated: ${outputPath}`);
} catch (error) {
console.error('Error generating PDF:', error);
process.exit(1);
}
}
generate();