RateCraft Pro — Freelance Project Rate, Profit & PDF Proposal Studio
https://cdn.tailwindcss.com
https://unpkg.com/lucide@latest
https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js
tailwind.config = {
darkMode: ‘class’,
theme: {
extend: {
fontFamily: {
sans: [‘Inter’, ‘system-ui’, ‘sans-serif’],
mono: [‘JetBrains Mono’, ‘monospace’],
},
colors: {
brand: {
50: ‘#eef2ff’,
100: ‘#e0e7ff’,
500: ‘#6366f1’,
600: ‘#4f46e5’,
700: ‘#4338ca’,
}
}
}
}
}
/* Custom Range Slider Styling */
input[type=”range”] {
-webkit-appearance: none;
appearance: none;
background: transparent;
cursor: pointer;
}
input[type=”range”]::-webkit-slider-runnable-track {
height: 8px;
border-radius: 9999px;
background: #e2e8f0;
}
.dark input[type=”range”]::-webkit-slider-runnable-track {
background: #334155;
}
input[type=”range”]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
margin-top: -5px;
background-color: #4f46e5;
height: 18px;
width: 18px;
border-radius: 9999px;
border: 2px solid #ffffff;
box-shadow: 0 2px 6px rgba(79, 70, 229, 0.4);
transition: transform 0.15s ease, background-color 0.15s ease;
}
input[type=”range”]::-webkit-slider-thumb:hover {
transform: scale(1.15);
background-color: #4338ca;
}
input[type=”range”]::-moz-range-track {
height: 8px;
border-radius: 9999px;
background: #e2e8f0;
}
.dark input[type=”range”]::-moz-range-track {
background: #334155;
}
input[type=”range”]::-moz-range-thumb {
background-color: #4f46e5;
height: 18px;
width: 18px;
border-radius: 9999px;
border: 2px solid #ffffff;
box-shadow: 0 2px 6px rgba(79, 70, 229, 0.4);
transition: transform 0.15s ease;
}
/* Smooth transitions for revenue bar segments */
.bar-segment {
transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Executive A4 Proposal Sheet Styling */
.proposal-a4-sheet {
background: #ffffff;
color: #0f172a;
width: 100%;
max-width: 820px;
margin: 0 auto;
box-shadow: 0 20px 50px rgba(15, 23, 42, 0.12);
border-radius: 16px;
}
/* Dedicated @media print rules: hides calculator UI and prints ONLY the crisp client proposal */
@media print {
@page {
size: A4 portrait;
margin: 12mm;
}
html, body {
background: #ffffff !important;
color: #0f172a !important;
margin: 0 !important;
padding: 0 !important;
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
.no-print {
display: none !important;
}
#proposalStudioSection {
display: block !important;
margin: 0 !important;
padding: 0 !important;
}
#proposalSheetWrapper {
padding: 0 !important;
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
.proposal-a4-sheet {
max-width: 100% !important;
width: 100% !important;
box-shadow: none !important;
border: none !important;
border-radius: 0 !important;
padding: 0 !important;
margin: 0 !important;
}
}
RateCraft Pro
Pricing & PDF Proposal Studio
Reverse-engineer your take-home goal & export branded client proposals
Currency
$ USD
S$ SGD
€ EUR
£ GBP
A$ AUD
C$ CAD
Download PDF Proposal
PDF
Reset
Quick Load Profile:
🌱 Side Hustler (12h/wk)
⚡ Full-Time Solo (25h/wk)
🚀 Agency / Senior Consultant (30h/wk)
1. Financial & Capacity Inputs
Two-way synced sliders & exact number inputs
Target Monthly Take-Home
$
Net personal pay after tax & overhead
$60,000 / yr net
Billable Hours Per Week
hrs
Billable Reality: Most freelancers bill 55%–65% of their workweek. Keep 10–15 hrs/wk for client calls, proposals & admin.
Monthly Software & Business Expenses
$
SaaS tools, AI subscriptions, hosting, insurance
$3,000 / yr
Paid Vacation / Time Off Weeks
wks
Public holidays, leave & sick days
48 active weeks / yr
Estimated Tax Buffer (%)
%
Income tax, CPF/self-employment contributions
+$1,250 / mo
Target Profit Margin (%)
%
Business growth buffer above break-even rate
+$1,625 / mo profit buffer
2. Project Scope for Quote Generation
Powers the 3-tier pricing & PDF client proposal
Project / Engagement Title
Estimated Base Project Hours
hrs
≈ 2.5 workdays (at 8h/day)
0.8 billable weeks
Break-Even Day (8h):
$520/day
+25% Margin
Target Day Rate (8h):
$650/day
Annual Target:
$97,500/yr
Monthly Revenue & Profit Breakdown
Allocation of your $8,125/mo target gross revenue (Break-even floor: $6,500/mo )
100 billable hrs/mo
3-Tier Value-Anchored Pricing
20 hr base scope
Customize the deliverables in each box below—changes sync directly to your printable PDF proposal
Tier 1 • Essential
1.0x Base
Editable Deliverables (1 per line)
Copy Starter Quote
★ Recommended
Tier 2 • Growth
1.35x Value
Editable Deliverables (1 per line)
Copy Growth Quote
Tier 3 • Turnkey
1.8x VIP
Editable Deliverables (1 per line)
Copy VIP Quote
Ready for Client Send-Off
Customize your studio branding, client details, and package display below, then download a crisp A4 PDF quote.
Print / Save Vector PDF
Download Printable PDF Proposal
Customize Client Proposal
Live Preview →
Pricing Display Mode on Proposal
Show All 3 Pricing Tiers (Recommended)
Highlight Single Tier: Tier 1 (Starter)
Highlight Single Tier: Tier 2 (Growth)
Highlight Single Tier: Tier 3 (Turnkey VIP)
Your Studio / Freelancer Info
Your Name / Studio Brand
Email / Website / Portfolio
Prepared For (Client Info)
Client Contact Name
Client Company
Metadata & Validity
Proposal Ref #
Valid Until
Payment Terms & Schedule
Executive Summary / Custom Scope Notes
This proposal outlines the recommended scope, timeline, and investment options designed to achieve your project goals with predictable delivery and measurable ROI.
Download Printable PDF Proposal
Project Proposal & Investment Quote
hello@alexrivera.studio • alexrivera.studio
Proposal Reference
PROP-2026-084
Issued: Oct 27, 2026
Valid Until: Nov 10, 2026
Prepared Exclusively For
Sarah Chen
Lumina Retail Pte Ltd
Project Engagement
Website Redesign & Conversion Optimization
Base Scope: ~20 hrs
Est. Turnaround: ~1–2 weeks
Currency: USD ($)
1. Executive Summary & Scope Overview
This proposal outlines the recommended scope, timeline, and investment options designed to achieve your project goals with predictable delivery and measurable ROI.
2. Investment Options & Deliverables
Out-of-scope additions billed at $81/hr
3. Payment Terms & Schedule
50% deposit upon project kickoff, 50% upon final handoff & approval.
4. Acceptance & Authorization
To approve this proposal and reserve your project kickoff slot, please sign or reply with your selected package tier.
Authorized Signature
Date
// Helper to compute default dates
function getFormattedToday() {
const d = new Date();
return d.toLocaleDateString(‘en-US’, { month: ‘short’, day: ‘numeric’, year: ‘numeric’ });
}
function getDefaultValidUntilISO() {
const d = new Date();
d.setDate(d.getDate() + 14);
return d.toISOString().split(‘T’)[0];
}
function formatISODateToReadable(isoStr) {
if (!isoStr) return ’14 Days from Issue’;
const parts = isoStr.split(‘-‘);
if (parts.length !== 3) return isoStr;
const dateObj = new Date(Number(parts[0]), Number(parts[1]) – 1, Number(parts[2]));
return dateObj.toLocaleDateString(‘en-US’, { month: ‘short’, day: ‘numeric’, year: ‘numeric’ });
}
// Default state values
const DEFAULT_STATE = {
currencySymbol: ‘$’,
currencyCode: ‘USD’,
monthlyIncome: 5000,
billableHoursPerWeek: 25,
monthlyExpenses: 250,
vacationWeeks: 4,
taxPercent: 20,
profitMarginPercent: 25,
projectName: ‘Website Redesign & Conversion Optimization’,
projectHours: 20,
// Editable Tier Names & Deliverables
tier1Name: ‘Starter Scope’,
tier1Deliverables: ‘Core agreed project deliverables\n1 round of consolidated revisions\nStandard delivery timeline\nAsync email status updates’,
tier2Name: ‘Growth & Optimization’,
tier2Deliverables: ‘Everything in Starter Scope, plus:\nUp to 3 rounds of revisions\nPriority Slack/Teams communication\nConversion & QA optimization pass’,
tier3Name: ‘Turnkey VIP Partner’,
tier3Deliverables: ‘Everything in Growth Package, plus:\nFast-track priority delivery\n1-on-1 Kickoff Strategy Workshop\nFull source files + 14-day post-launch support’,
// Proposal Customization State
proposalMode: ‘all’,
studioName: ‘Alex Rivera Studio’,
studioContact: ‘hello@alexrivera.studio • alexrivera.studio’,
clientName: ‘Sarah Chen’,
clientCompany: ‘Lumina Retail Pte Ltd’,
proposalRef: ‘PROP-2026-084’,
proposalValidUntil: getDefaultValidUntilISO(),
paymentTerms: ‘50% deposit upon project kickoff, 50% upon final handoff & approval.’,
proposalNotes: ‘This proposal outlines the recommended scope, timeline, and investment options designed to achieve your project goals with predictable delivery and measurable ROI.’
};
// Preset Profiles
const PRESETS = {
parttime: {
monthlyIncome: 2000,
billableHoursPerWeek: 12,
monthlyExpenses: 80,
vacationWeeks: 4,
taxPercent: 15,
profitMarginPercent: 20,
projectHours: 12
},
fulltime: {
monthlyIncome: 5000,
billableHoursPerWeek: 25,
monthlyExpenses: 250,
vacationWeeks: 4,
taxPercent: 20,
profitMarginPercent: 25,
projectHours: 20
},
senior: {
monthlyIncome: 9500,
billableHoursPerWeek: 30,
monthlyExpenses: 450,
vacationWeeks: 5,
taxPercent: 25,
profitMarginPercent: 35,
projectHours: 35
}
};
// Current State
let state = { …DEFAULT_STATE };
// DOM Elements Reference
const els = {
currencySelect: document.getElementById(‘currencySelect’),
currencySymbols: document.querySelectorAll(‘.currency-symbol’),
resetBtn: document.getElementById(‘resetBtn’),
themeToggleBtn: document.getElementById(‘themeToggleBtn’),
// Inputs & Sliders
incomeInput: document.getElementById(‘incomeInput’),
incomeSlider: document.getElementById(‘incomeSlider’),
hoursInput: document.getElementById(‘hoursInput’),
hoursSlider: document.getElementById(‘hoursSlider’),
expensesInput: document.getElementById(‘expensesInput’),
expensesSlider: document.getElementById(‘expensesSlider’),
vacationInput: document.getElementById(‘vacationInput’),
vacationSlider: document.getElementById(‘vacationSlider’),
taxInput: document.getElementById(‘taxInput’),
taxSlider: document.getElementById(‘taxSlider’),
profitMarginInput: document.getElementById(‘profitMarginInput’),
profitMarginSlider: document.getElementById(‘profitMarginSlider’),
projectNameInput: document.getElementById(‘projectNameInput’),
projectHoursInput: document.getElementById(‘projectHoursInput’),
projectHoursSlider: document.getElementById(‘projectHoursSlider’),
// Input Hints
annualTakeHomeHint: document.getElementById(‘annualTakeHomeHint’),
annualExpensesHint: document.getElementById(‘annualExpensesHint’),
workingWeeksHint: document.getElementById(‘workingWeeksHint’),
taxReserveHint: document.getElementById(‘taxReserveHint’),
profitBufferHint: document.getElementById(‘profitBufferHint’),
projectDaysEquivalent: document.getElementById(‘projectDaysEquivalent’),
projectWeeksEquivalent: document.getElementById(‘projectWeeksEquivalent’),
// Hero Metrics
minHourlyRateDisplay: document.getElementById(‘minHourlyRateDisplay’),
minDayRateDisplay: document.getElementById(‘minDayRateDisplay’),
targetRateBadge: document.getElementById(‘targetRateBadge’),
targetHourlyRateDisplay: document.getElementById(‘targetHourlyRateDisplay’),
targetDayRateDisplay: document.getElementById(‘targetDayRateDisplay’),
grossMonthlyDisplay: document.getElementById(‘grossMonthlyDisplay’),
grossAnnualDisplay: document.getElementById(‘grossAnnualDisplay’),
// Revenue Breakdown
breakdownSubtitleGross: document.getElementById(‘breakdownSubtitleGross’),
breakdownFloorGross: document.getElementById(‘breakdownFloorGross’),
billableCapacityBadge: document.getElementById(‘billableCapacityBadge’),
barTakeHome: document.getElementById(‘barTakeHome’),
barTax: document.getElementById(‘barTax’),
barOverhead: document.getElementById(‘barOverhead’),
barProfit: document.getElementById(‘barProfit’),
pctTakeHome: document.getElementById(‘pctTakeHome’),
pctTax: document.getElementById(‘pctTax’),
pctOverhead: document.getElementById(‘pctOverhead’),
pctProfit: document.getElementById(‘pctProfit’),
valTakeHome: document.getElementById(‘valTakeHome’),
valTax: document.getElementById(‘valTax’),
valOverhead: document.getElementById(‘valOverhead’),
valProfit: document.getElementById(‘valProfit’),
// Pricing Tiers & Editable Deliverables
scopeHoursBadge: document.getElementById(‘scopeHoursBadge’),
tier1NameInput: document.getElementById(‘tier1NameInput’),
tier1Price: document.getElementById(‘tier1Price’),
tier1Meta: document.getElementById(‘tier1Meta’),
tier1DeliverablesInput: document.getElementById(‘tier1DeliverablesInput’),
tier2NameInput: document.getElementById(‘tier2NameInput’),
tier2Price: document.getElementById(‘tier2Price’),
tier2Meta: document.getElementById(‘tier2Meta’),
tier2DeliverablesInput: document.getElementById(‘tier2DeliverablesInput’),
tier3NameInput: document.getElementById(‘tier3NameInput’),
tier3Price: document.getElementById(‘tier3Price’),
tier3Meta: document.getElementById(‘tier3Meta’),
tier3DeliverablesInput: document.getElementById(‘tier3DeliverablesInput’),
copyAllTiersBtn: document.getElementById(‘copyAllTiersBtn’),
// Proposal Studio Form Inputs
proposalModeSelect: document.getElementById(‘proposalModeSelect’),
studioNameInput: document.getElementById(‘studioNameInput’),
studioContactInput: document.getElementById(‘studioContactInput’),
clientNameInput: document.getElementById(‘clientNameInput’),
clientCompanyInput: document.getElementById(‘clientCompanyInput’),
proposalRefInput: document.getElementById(‘proposalRefInput’),
proposalValidInput: document.getElementById(‘proposalValidInput’),
paymentTermsInput: document.getElementById(‘paymentTermsInput’),
proposalNotesInput: document.getElementById(‘proposalNotesInput’),
// Printable Proposal Preview Sheet Elements
printableProposalSheet: document.getElementById(‘printableProposalSheet’),
pdfStudioName: document.getElementById(‘pdfStudioName’),
pdfStudioContact: document.getElementById(‘pdfStudioContact’),
pdfRefNumber: document.getElementById(‘pdfRefNumber’),
pdfIssueDate: document.getElementById(‘pdfIssueDate’),
pdfValidDate: document.getElementById(‘pdfValidDate’),
pdfClientName: document.getElementById(‘pdfClientName’),
pdfClientCompany: document.getElementById(‘pdfClientCompany’),
pdfProjectTitle: document.getElementById(‘pdfProjectTitle’),
pdfEstimatedHoursBadge: document.getElementById(‘pdfEstimatedHoursBadge’),
pdfEstimatedTimelineBadge: document.getElementById(‘pdfEstimatedTimelineBadge’),
pdfCurrencyBadge: document.getElementById(‘pdfCurrencyBadge’),
pdfExecutiveNotes: document.getElementById(‘pdfExecutiveNotes’),
pdfAdditionalRateNote: document.getElementById(‘pdfAdditionalRateNote’),
pdfTiersContainer: document.getElementById(‘pdfTiersContainer’),
pdfPaymentTerms: document.getElementById(‘pdfPaymentTerms’),
// PDF Export Buttons
headerDownloadPdfBtn: document.getElementById(‘headerDownloadPdfBtn’),
downloadPdfBtn: document.getElementById(‘downloadPdfBtn’),
downloadPdfBtnText: document.getElementById(‘downloadPdfBtnText’),
sidebarDownloadPdfBtn: document.getElementById(‘sidebarDownloadPdfBtn’),
printVectorPdfBtn: document.getElementById(‘printVectorPdfBtn’),
toastContainer: document.getElementById(‘toastContainer’)
};
/**
* Formats a number into currency string (e.g., S$5,000)
*/
function formatMoney(amount, decimals = 0) {
const safeAmount = Number.isFinite(amount) ? amount : 0;
const formatted = safeAmount.toLocaleString(‘en-US’, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
});
return `${state.currencySymbol}${formatted}`;
}
/**
* Escapes HTML special characters for safe rendering inside the printable sheet
*/
function escapeHtml(str) {
return String(str || ”)
.replace(/&/g, ‘&’)
.replace(//g, ‘>’)
.replace(/”/g, ‘"’)
.replace(/’/g, ‘'’);
}
/**
* Core Calculation Engine
*/
function calculateMetrics() {
const monthlyTakeHome = Math.max(0, Number(state.monthlyIncome) || 0);
const billableHoursPerWeek = Math.max(1, Number(state.billableHoursPerWeek) || 1);
const monthlyExpenses = Math.max(0, Number(state.monthlyExpenses) || 0);
const vacationWeeks = Math.min(48, Math.max(0, Number(state.vacationWeeks) || 0));
const taxPercent = Math.min(80, Math.max(0, Number(state.taxPercent) || 0));
const profitMarginPercent = Math.min(100, Math.max(0, Number(state.profitMarginPercent) || 0));
const projectHours = Math.max(1, Number(state.projectHours) || 1);
// Working time math
const workingWeeksPerYear = Math.max(1, 52 – vacationWeeks);
const annualBillableHours = billableHoursPerWeek * workingWeeksPerYear;
const monthlyBillableHours = annualBillableHours / 12;
// Tax & Break-Even Floor Math
const taxRateDecimal = taxPercent / 100;
const preTaxIncomeNeeded = taxRateDecimal line.trim())
.filter(Boolean);
}
/**
* Renders the dynamic Pricing Tiers inside the Printable A4 Proposal Preview
*/
function renderProposalSheetTiers(m) {
const tierConfigs = {
tier1: {
key: ‘tier1’,
badge: ‘Option 1 • Essential’,
name: state.tier1Name || ‘Starter Scope’,
price: formatMoney(m.tier1Price),
meta: `${m.projectHours} hrs base scope`,
deliverables: parseDeliverables(state.tier1Deliverables),
featured: false
},
tier2: {
key: ‘tier2’,
badge: ‘Option 2 • Recommended’,
name: state.tier2Name || ‘Growth & Optimization’,
price: formatMoney(m.tier2Price),
meta: `${m.projectHours} hrs priority scope`,
deliverables: parseDeliverables(state.tier2Deliverables),
featured: true
},
tier3: {
key: ‘tier3’,
badge: ‘Option 3 • Turnkey VIP’,
name: state.tier3Name || ‘Turnkey VIP Partner’,
price: formatMoney(m.tier3Price),
meta: `${m.projectHours} hrs VIP turnkey scope`,
deliverables: parseDeliverables(state.tier3Deliverables),
featured: false
}
};
const mode = state.proposalMode || ‘all’;
const keysToRender = mode === ‘all’ ? [‘tier1’, ‘tier2’, ‘tier3’] : [mode];
els.pdfTiersContainer.className = mode === ‘all’
? ‘grid grid-cols-1 md:grid-cols-3 gap-3.5’
: ‘grid grid-cols-1 gap-4’;
els.pdfTiersContainer.innerHTML = keysToRender.map(k => {
const t = tierConfigs[k] || tierConfigs.tier2;
const isSingle = mode !== ‘all’;
const borderClass = t.featured || isSingle
? ‘border-2 border-indigo-600 bg-indigo-50/20’
: ‘border border-slate-200 bg-white’;
const deliverablesHtml = t.deliverables.map(item => `
✓
${escapeHtml(item)}
`).join(”);
return `
${escapeHtml(t.badge)}
${t.featured && !isSingle ? ‘Best Value ‘ : ”}
${escapeHtml(t.name)}
${escapeHtml(t.meta)}
`;
}).join(”);
}
/**
* Updates the entire UI and Live Proposal Preview
*/
function updateUI() {
const m = calculateMetrics();
// Update currency symbols on input fields
els.currencySymbols.forEach(span => {
span.textContent = state.currencySymbol;
});
// Update Input Hints
els.annualTakeHomeHint.textContent = `${formatMoney(m.monthlyTakeHome * 12)} / yr net`;
els.annualExpensesHint.textContent = `${formatMoney(m.monthlyExpenses * 12)} / yr`;
els.workingWeeksHint.textContent = `${m.workingWeeksPerYear} active weeks / yr (${Math.round(m.annualBillableHours)} billable hrs/yr)`;
els.taxReserveHint.textContent = `+${formatMoney(Math.round(m.monthlyTaxReserve))} / mo tax reserve`;
els.profitBufferHint.textContent = `+${formatMoney(Math.round(m.monthlyProfitBuffer))} / mo profit buffer`;
const workdays = (m.projectHours / 8).toFixed(1);
const billableWeeks = (m.projectHours / m.billableHoursPerWeek).toFixed(1);
els.projectDaysEquivalent.textContent = `≈ ${workdays} workdays (at 8h/day)`;
els.projectWeeksEquivalent.textContent = `${billableWeeks} billable wks`;
// Update Hero Cards
els.minHourlyRateDisplay.textContent = formatMoney(Math.round(m.minHourlyRate));
els.minDayRateDisplay.textContent = `${formatMoney(Math.round(m.minDayRate))}/day`;
els.targetRateBadge.textContent = `+${m.profitMarginPercent}% Margin`;
els.targetHourlyRateDisplay.textContent = formatMoney(Math.round(m.targetHourlyRate));
els.targetDayRateDisplay.textContent = `${formatMoney(Math.round(m.targetDayRate))}/day`;
els.grossMonthlyDisplay.textContent = formatMoney(Math.round(m.targetGrossMonthly));
els.grossAnnualDisplay.textContent = `${formatMoney(Math.round(m.targetGrossAnnual))}/yr`;
// Update 4-Part Revenue & Profit Breakdown Bar
els.breakdownSubtitleGross.textContent = `${formatMoney(Math.round(m.targetGrossMonthly))}/mo`;
els.breakdownFloorGross.textContent = `${formatMoney(Math.round(m.breakEvenGrossMonthly))}/mo`;
els.billableCapacityBadge.innerHTML = `
${Math.round(m.monthlyBillableHours)} billable hrs/mo`;
const totalGross = Math.max(1, m.targetGrossMonthly);
const takeHomePct = (m.monthlyTakeHome / totalGross) * 100;
const taxPct = (m.monthlyTaxReserve / totalGross) * 100;
const overheadPct = (m.monthlyExpenses / totalGross) * 100;
const profitPct = (m.monthlyProfitBuffer / totalGross) * 100;
els.barTakeHome.style.width = `${takeHomePct.toFixed(1)}%`;
els.barTax.style.width = `${taxPct.toFixed(1)}%`;
els.barOverhead.style.width = `${overheadPct.toFixed(1)}%`;
els.barProfit.style.width = `${profitPct.toFixed(1)}%`;
els.pctTakeHome.textContent = `${takeHomePct.toFixed(1)}% of rev`;
els.pctTax.textContent = `${taxPct.toFixed(1)}% of rev`;
els.pctOverhead.textContent = `${overheadPct.toFixed(1)}% of rev`;
els.pctProfit.textContent = `${profitPct.toFixed(1)}% of rev`;
els.valTakeHome.textContent = formatMoney(Math.round(m.monthlyTakeHome));
els.valTax.textContent = formatMoney(Math.round(m.monthlyTaxReserve));
els.valOverhead.textContent = formatMoney(Math.round(m.monthlyExpenses));
els.valProfit.textContent = formatMoney(Math.round(m.monthlyProfitBuffer));
// Update Project Pricing Tiers
els.scopeHoursBadge.textContent = `${m.projectHours} hr base scope`;
els.tier1Price.textContent = formatMoney(m.tier1Price);
els.tier1Meta.textContent = `${m.projectHours} hrs • ${formatMoney(Math.round(m.tier1Price / m.projectHours))}/hr eff.`;
els.tier2Price.textContent = formatMoney(m.tier2Price);
els.tier2Meta.textContent = `${m.projectHours} hrs • ${formatMoney(Math.round(m.tier2Price / m.projectHours))}/hr eff.`;
els.tier3Price.textContent = formatMoney(m.tier3Price);
els.tier3Meta.textContent = `${m.projectHours} hrs • ${formatMoney(Math.round(m.tier3Price / m.projectHours))}/hr eff.`;
// Update Live Printable PDF Proposal Sheet
els.pdfStudioName.textContent = state.studioName || ‘Freelance Studio’;
els.pdfStudioContact.textContent = state.studioContact || ”;
els.pdfRefNumber.textContent = state.proposalRef || ‘PROP-001’;
els.pdfIssueDate.textContent = getFormattedToday();
els.pdfValidDate.textContent = formatISODateToReadable(state.proposalValidUntil);
els.pdfClientName.textContent = state.clientName || ‘Valued Client’;
els.pdfClientCompany.textContent = state.clientCompany || ”;
els.pdfProjectTitle.textContent = state.projectName || ‘Custom Project Engagement’;
const estWeeks = Math.max(1, Math.ceil(m.projectHours / m.billableHoursPerWeek));
els.pdfEstimatedHoursBadge.textContent = `Base Scope: ~${m.projectHours} hrs`;
els.pdfEstimatedTimelineBadge.textContent = `Est. Turnaround: ~${estWeeks} ${estWeeks === 1 ? ‘week’ : ‘weeks’}`;
els.pdfCurrencyBadge.textContent = `Currency: ${state.currencyCode} (${state.currencySymbol})`;
els.pdfExecutiveNotes.textContent = state.proposalNotes || ”;
els.pdfAdditionalRateNote.textContent = `Out-of-scope additions billed at ${formatMoney(Math.round(m.targetHourlyRate))}/hr`;
els.pdfPaymentTerms.textContent = state.paymentTerms || ”;
renderProposalSheetTiers(m);
if (window.lucide) {
lucide.createIcons();
}
}
/**
* Generates clean text quote for clipboard copying
*/
function generateFullProposalText(m) {
const projTitle = (state.projectName || ‘Custom Project’).trim();
const estWeeks = Math.max(1, Math.ceil(m.projectHours / m.billableHoursPerWeek));
const t1Bullets = parseDeliverables(state.tier1Deliverables).map(b => `• ${b}`).join(‘\n’);
const t2Bullets = parseDeliverables(state.tier2Deliverables).map(b => `• ${b}`).join(‘\n’);
const t3Bullets = parseDeliverables(state.tier3Deliverables).map(b => `• ${b}`).join(‘\n’);
return `PROPOSAL ESTIMATE: ${projTitle.toUpperCase()}
Prepared For: ${state.clientName} (${state.clientCompany})
Prepared By: ${state.studioName} (${state.studioContact})
Reference: ${state.proposalRef} | Valid Until: ${formatISODateToReadable(state.proposalValidUntil)}
Estimated Scope: ~${m.projectHours} hours (Approx. ${estWeeks} ${estWeeks === 1 ? ‘week’ : ‘weeks’})
————————————————–
OPTION 1: ${state.tier1Name} — ${formatMoney(m.tier1Price)} (${state.currencyCode})
${t1Bullets}
OPTION 2: ${state.tier2Name} (Recommended) — ${formatMoney(m.tier2Price)} (${state.currencyCode})
${t2Bullets}
OPTION 3: ${state.tier3Name} — ${formatMoney(m.tier3Price)} (${state.currencyCode})
${t3Bullets}
————————————————–
Payment Terms: ${state.paymentTerms}
Standard Hourly Rate for out-of-scope additions: ${formatMoney(Math.round(m.targetHourlyRate))}/hr`;
}
/**
* Generates single tier quote text
*/
function generateSingleTierText(tierNumber) {
const m = calculateMetrics();
const projTitle = (state.projectName || ‘Custom Project’).trim();
const tiers = {
1: {
name: state.tier1Name,
price: formatMoney(m.tier1Price),
bullets: parseDeliverables(state.tier1Deliverables)
},
2: {
name: state.tier2Name,
price: formatMoney(m.tier2Price),
bullets: parseDeliverables(state.tier2Deliverables)
},
3: {
name: state.tier3Name,
price: formatMoney(m.tier3Price),
bullets: parseDeliverables(state.tier3Deliverables)
}
};
const selected = tiers[tierNumber];
return `PROJECT QUOTE: ${projTitle}
Prepared For: ${state.clientName} (${state.clientCompany})
Package: ${selected.name}
Investment: ${selected.price} (${state.currencyCode})
Included Deliverables:
• ${selected.bullets.join(‘\n• ‘)}
Payment Terms: ${state.paymentTerms}
Additional out-of-scope work billed at ${formatMoney(Math.round(m.targetHourlyRate))}/hr.`;
}
/**
* Exports the #printableProposalSheet directly as a downloadable .pdf file using html2pdf.js
*/
async function downloadPrintablePdfProposal() {
const sheetEl = els.printableProposalSheet;
if (!sheetEl) return;
const safeClient = (state.clientCompany || state.clientName || ‘Client’)
.replace(/[^a-zA-Z0-9_-]/g, ‘-‘)
.replace(/-+/g, ‘-‘)
.replace(/^-|-$/g, ”);
const fileName = `Proposal-${safeClient || ‘Client’}-${state.proposalRef || ‘Quote’}.pdf`;
// If html2pdf is loaded, generate direct PDF download
if (typeof window.html2pdf === ‘function’) {
const originalText = els.downloadPdfBtnText.textContent;
els.downloadPdfBtnText.textContent = ‘Generating PDF…’;
els.downloadPdfBtn.disabled = true;
try {
const opt = {
margin: [10, 10, 10, 10],
filename: fileName,
image: { type: ‘jpeg’, quality: 0.98 },
html2canvas: {
scale: 2,
useCORS: true,
backgroundColor: ‘#ffffff’,
scrollY: 0
},
jsPDF: { unit: ‘mm’, format: ‘a4’, orientation: ‘portrait’ }
};
await window.html2pdf().set(opt).from(sheetEl).save();
showToast(`Downloaded “${fileName}”!`);
} catch (err) {
// Fallback to native print dialog if canvas security blocks export
window.print();
} finally {
els.downloadPdfBtnText.textContent = originalText;
els.downloadPdfBtn.disabled = false;
}
} else {
// Fallback to native vector print dialog
window.print();
}
}
/**
* Copies text to clipboard using document.execCommand(‘copy’) for iframe compatibility
*/
function copyToClipboard(text, toastMessage) {
const textArea = document.createElement(‘textarea’);
textArea.value = text;
textArea.style.position = ‘fixed’;
textArea.style.left = ‘-9999px’;
textArea.style.top = ‘0’;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand(‘copy’);
showToast(toastMessage);
} catch (err) {
showToast(‘Could not copy automatically — please use Export PDF.’);
} finally {
document.body.removeChild(textArea);
}
}
/**
* Displays a non-intrusive toast confirmation
*/
function showToast(message) {
const toast = document.createElement(‘div’);
toast.className = ‘flex items-center gap-2.5 px-4 py-3 rounded-xl bg-slate-900 dark:bg-white text-white dark:text-slate-900 text-xs sm:text-sm font-semibold shadow-xl border border-slate-700 dark:border-slate-200 transform transition-all duration-300 translate-y-2 opacity-0’;
toast.innerHTML = `
✓
${escapeHtml(message)}
`;
els.toastContainer.appendChild(toast);
requestAnimationFrame(() => {
toast.classList.remove(‘translate-y-2’, ‘opacity-0’);
});
setTimeout(() => {
toast.classList.add(‘translate-y-2’, ‘opacity-0’);
setTimeout(() => toast.remove(), 300);
}, 2800);
}
/**
* Helper to bind a number input and range slider pair two-ways
*/
function bindInputAndSlider(inputEl, sliderEl, stateKey) {
inputEl.addEventListener(‘input’, (e) => {
const val = parseFloat(e.target.value);
if (!Number.isNaN(val)) {
state[stateKey] = val;
sliderEl.value = val;
updateUI();
}
});
sliderEl.addEventListener(‘input’, (e) => {
const val = parseFloat(e.target.value);
state[stateKey] = val;
inputEl.value = val;
updateUI();
});
}
/**
* Helper to bind text/textarea/select inputs to state
*/
function bindTextField(el, stateKey, eventName = ‘input’) {
if (!el) return;
el.addEventListener(eventName, (e) => {
state[stateKey] = e.target.value;
updateUI();
});
}
/**
* Syncs all DOM inputs from the state object
*/
function syncInputsFromState() {
els.incomeInput.value = state.monthlyIncome;
els.incomeSlider.value = state.monthlyIncome;
els.hoursInput.value = state.billableHoursPerWeek;
els.hoursSlider.value = state.billableHoursPerWeek;
els.expensesInput.value = state.monthlyExpenses;
els.expensesSlider.value = state.monthlyExpenses;
els.vacationInput.value = state.vacationWeeks;
els.vacationSlider.value = state.vacationWeeks;
els.taxInput.value = state.taxPercent;
els.taxSlider.value = state.taxPercent;
els.profitMarginInput.value = state.profitMarginPercent;
els.profitMarginSlider.value = state.profitMarginPercent;
els.projectNameInput.value = state.projectName;
els.projectHoursInput.value = state.projectHours;
els.projectHoursSlider.value = state.projectHours;
els.tier1NameInput.value = state.tier1Name;
els.tier1DeliverablesInput.value = state.tier1Deliverables;
els.tier2NameInput.value = state.tier2Name;
els.tier2DeliverablesInput.value = state.tier2Deliverables;
els.tier3NameInput.value = state.tier3Name;
els.tier3DeliverablesInput.value = state.tier3Deliverables;
els.proposalModeSelect.value = state.proposalMode;
els.studioNameInput.value = state.studioName;
els.studioContactInput.value = state.studioContact;
els.clientNameInput.value = state.clientName;
els.clientCompanyInput.value = state.clientCompany;
els.proposalRefInput.value = state.proposalRef;
els.proposalValidInput.value = state.proposalValidUntil;
els.paymentTermsInput.value = state.paymentTerms;
els.proposalNotesInput.value = state.proposalNotes;
}
// Initialize Event Listeners
function init() {
// Core numeric sliders
bindInputAndSlider(els.incomeInput, els.incomeSlider, ‘monthlyIncome’);
bindInputAndSlider(els.hoursInput, els.hoursSlider, ‘billableHoursPerWeek’);
bindInputAndSlider(els.expensesInput, els.expensesSlider, ‘monthlyExpenses’);
bindInputAndSlider(els.vacationInput, els.vacationSlider, ‘vacationWeeks’);
bindInputAndSlider(els.taxInput, els.taxSlider, ‘taxPercent’);
bindInputAndSlider(els.profitMarginInput, els.profitMarginSlider, ‘profitMarginPercent’);
bindInputAndSlider(els.projectHoursInput, els.projectHoursSlider, ‘projectHours’);
// Project & Tier editable fields
bindTextField(els.projectNameInput, ‘projectName’);
bindTextField(els.tier1NameInput, ‘tier1Name’);
bindTextField(els.tier1DeliverablesInput, ‘tier1Deliverables’);
bindTextField(els.tier2NameInput, ‘tier2Name’);
bindTextField(els.tier2DeliverablesInput, ‘tier2Deliverables’);
bindTextField(els.tier3NameInput, ‘tier3Name’);
bindTextField(els.tier3DeliverablesInput, ‘tier3Deliverables’);
// Proposal Customization fields
bindTextField(els.proposalModeSelect, ‘proposalMode’, ‘change’);
bindTextField(els.studioNameInput, ‘studioName’);
bindTextField(els.studioContactInput, ‘studioContact’);
bindTextField(els.clientNameInput, ‘clientName’);
bindTextField(els.clientCompanyInput, ‘clientCompany’);
bindTextField(els.proposalRefInput, ‘proposalRef’);
bindTextField(els.proposalValidInput, ‘proposalValidUntil’, ‘change’);
bindTextField(els.paymentTermsInput, ‘paymentTerms’);
bindTextField(els.proposalNotesInput, ‘proposalNotes’);
// Currency Selector
els.currencySelect.addEventListener(‘change’, (e) => {
const selectedOption = e.target.options[e.target.selectedIndex];
state.currencySymbol = e.target.value;
state.currencyCode = selectedOption.getAttribute(‘data-code’) || ‘USD’;
updateUI();
showToast(`Currency switched to ${state.currencySymbol} (${state.currencyCode})`);
});
// Preset Buttons
document.querySelectorAll(‘.preset-btn’).forEach(btn => {
btn.addEventListener(‘click’, () => {
const presetKey = btn.getAttribute(‘data-preset’);
if (PRESETS[presetKey]) {
state = {
…state,
…PRESETS[presetKey]
};
syncInputsFromState();
updateUI();
showToast(`Loaded “${btn.textContent.trim()}” preset`);
}
});
});
// Reset to Defaults Button
els.resetBtn.addEventListener(‘click’, () => {
state = { …DEFAULT_STATE, proposalValidUntil: getDefaultValidUntilISO() };
els.currencySelect.value = DEFAULT_STATE.currencySymbol;
syncInputsFromState();
updateUI();
showToast(‘All inputs reset to default values’);
});
// Copy Individual Tier Buttons
document.querySelectorAll(‘.copy-tier-btn’).forEach(btn => {
btn.addEventListener(‘click’, () => {
const tierNum = btn.getAttribute(‘data-tier’);
const text = generateSingleTierText(tierNum);
copyToClipboard(text, `Tier ${tierNum} client quote copied to clipboard!`);
});
});
// Copy Full 3-Tier Proposal Button
els.copyAllTiersBtn.addEventListener(‘click’, () => {
const m = calculateMetrics();
const text = generateFullProposalText(m);
copyToClipboard(text, ‘Full 3-Tier Client Proposal copied to clipboard!’);
});
// PDF Download & Print Buttons
els.downloadPdfBtn.addEventListener(‘click’, downloadPrintablePdfProposal);
els.sidebarDownloadPdfBtn.addEventListener(‘click’, downloadPrintablePdfProposal);
els.headerDownloadPdfBtn.addEventListener(‘click’, () => {
document.getElementById(‘proposalStudioSection’).scrollIntoView({ behavior: ‘smooth’ });
setTimeout(downloadPrintablePdfProposal, 450);
});
els.printVectorPdfBtn.addEventListener(‘click’, () => {
window.print();
});
// Dark Mode Toggle
els.themeToggleBtn.addEventListener(‘click’, () => {
document.documentElement.classList.toggle(‘dark’);
});
// Initial Render
syncInputsFromState();
updateUI();
}
window.addEventListener(‘DOMContentLoaded’, init);