168 lines
6.6 KiB
TypeScript
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();
|