Layout Notes: ${escapeHTML(data.layout)}
3. Projected Budget
Shopping List Subtotal:
${formatCurrency(data.totals.subtotal)}
Tax/Contingency Estimate (8.0%):
${formatCurrency(data.totals.taxEstimate)}
PROJECTED GRAND TOTAL:
${formatCurrency(data.totals.grandTotal)}
4. Shopping List
| Item Description |
Unit Cost ($) |
Qty |
Line Total ($) |
${itemRowsHTML.length > 0 ? itemRowsHTML : '| No items logged. |
'}
`;
};
const downloadTxt = () => {
const data = getPlanData();
const totals = data.totals;
let content = `ROOM MAKEOVER PLAN: ${data.roomName.toUpperCase()}\n`;
content += `Style: ${data.style}\n`;
content += "========================================================\n\n";
content += "1. PROJECT VISION & GOALS\n";
content += `Vision: ${data.vision}\n\n`;
content += `Key Goals:\n${data.goals.split('\n').filter(l => l.trim()).map(l => ` - ${l}`).join('\n')}\n\n`;
content += "2. DIMENSIONS & PREP\n";
content += `Dimensions (L x W x H): ${data.length} ft x ${data.width} ft x ${data.height} ft\n`;
content += `Preparation Notes: ${data.prep}\n`;
content += `Layout Notes: ${data.layout}\n\n`;
content += "3. PROJECTED BUDGET\n";
content += `Subtotal (Items):\t${formatCurrency(totals.subtotal)}\n`;
content += `Tax/Contingency (8.0%):\t${formatCurrency(totals.taxEstimate)}\n`;
content += `GRAND TOTAL:\t\t${formatCurrency(totals.grandTotal)}\n\n`;
content += "4. SHOPPING LIST\n";
content += "--------------------------------------------------------\n";
content += "Item Description\tUnit Cost\tQty\tLine Total\n";
content += "--------------------------------------------------------\n";
data.items.forEach(item => {
const lineTotal = item.cost * item.quantity;
content += `${item.description.padEnd(25).substring(0, 25)}\t$${item.cost.toFixed(2)}\t\t${item.quantity}\t$${lineTotal.toFixed(2)}\n`;
});
content += "--------------------------------------------------------\n";
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `plan_${data.roomName.replace(/ /g, '_')}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
};
const downloadPDF = () => {
if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') {
alert('Error: jsPDF library not loaded.');
return;
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF('p', 'mm', 'a4');
const data = getPlanData();
const totals = data.totals;
const margin = 15;
let yPos = 20;
const pageWidth = doc.internal.pageSize.getWidth();
const addTitle = (text, size, color, style = 'bold') => {
if (yPos > 280) { doc.addPage(); yPos = 20; }
doc.setFontSize(size);
doc.setFont(undefined, style);
doc.setTextColor(color[0], color[1], color[2]);
doc.text(text, margin, yPos);
yPos += size / 2 + 3;
};
const addText = (text, size = 10, style = 'normal', indent = 0) => {
doc.setFontSize(size);
doc.setFont(undefined, style);
doc.setTextColor(52, 73, 94);
const lines = doc.splitTextToSize(text, pageWidth - margin * 2 - indent);
if (lines.length * 5 + yPos > 280) { doc.addPage(); yPos = 20; }
doc.text(lines, margin + indent, yPos);
yPos += (lines.length * 5) + 3;
};
// 1. Header
doc.setFontSize(22);
doc.setFont(undefined, 'bold');
doc.setTextColor(44, 62, 80);
doc.text(`Room Makeover Plan`, pageWidth / 2, yPos, { align: 'center' });
yPos += 8;
addTitle(data.roomName, 14, [217, 119, 6], 'bold');
yPos -= 5;
addText(`Style: ${data.style}`, 10, 'italic', 0);
yPos += 3;
// 2. Project Vision
addTitle("1. Project Vision & Goals", 12, [217, 119, 6]);
addText("Vision:", 10, 'bold');
addText(data.vision, 10, 'normal', 5);
addText("Key Goals:", 10, 'bold');
addText(data.goals, 10, 'normal', 5);
yPos += 3;
// 3. Dimensions & Prep
addTitle("2. Dimensions & Preparation", 12, [217, 119, 6]);
addText(`Dimensions (L x W x H): ${data.length} ft x ${data.width} ft x ${data.height} ft`, 10);
addText("Preparation Notes:", 10, 'bold');
addText(data.prep, 10, 'normal', 5);
addText("Layout Notes:", 10, 'bold');
addText(data.layout, 10, 'normal', 5);
yPos += 3;
// 4. Budget Summary
addTitle("3. Projected Budget", 12, [217, 119, 6]);
const totalsY = yPos;
const totalsX = pageWidth - margin - 50;
const addTotalLine = (label, value, isGrand = false) => {
doc.setFontSize(isGrand ? 12 : 10);
doc.setFont(undefined, isGrand ? 'bold' : 'normal');
doc.setTextColor(isGrand ? 217 : 52, isGrand ? 119 : 73, isGrand ? 6 : 94);
if (isGrand) {
doc.setDrawColor(44, 62, 80);
doc.setLineWidth(0.5);
doc.line(totalsX - 10, totalsY - 1, pageWidth - margin, totalsY - 1);
yPos += 2;
}
doc.text(label, totalsX, totalsY, { align: 'left' });
doc.text(formatCurrency(value), pageWidth - margin, totalsY, { align: 'right' });
yPos += 5;
};
addTotalLine("Shopping List Subtotal:", totals.subtotal);
addTotalLine(`Tax/Contingency (8.0%):`, totals.taxEstimate);
addTotalLine("PROJECTED GRAND TOTAL:", totals.grandTotal, true);
yPos = Math.max(yPos, totalsY) + 5; // Advance Y position past the totals block
// 5. Shopping List Table
addTitle("4. Shopping List", 12, [217, 119, 6]);
const itemHead = [['Item Description', 'Unit Cost ($)', 'Qty', 'Line Total ($)']];
const itemBody = data.items.map(item => [
item.description,
item.cost.toFixed(2),
item.quantity.toString(),
(item.cost * item.quantity).toFixed(2)
]);
doc.autoTable({
startY: yPos,
head: itemHead,
body: itemBody,
theme: 'grid',
styles: { fontSize: 9, cellPadding: 2, textColor: [52, 73, 94] },
headStyles: { fillColor: [243, 203, 137], textColor: [44, 62, 80], fontStyle: 'bold' },
columnStyles: {
0: { cellWidth: 70, fontStyle: 'normal' },
1: { halign: 'right' },
2: { halign: 'center', cellWidth: 15 },
3: { halign: 'right', fontStyle: 'bold' }
},
margin: { left: margin, right: margin }
});
doc.save(`plan_${data.roomName.replace(/ /g, '_')}.pdf`);
};
// --- Event Listeners ---
// Tab Buttons
tabButtons.forEach((btn, index) => {
btn.addEventListener('click', () => showTab(index + 1));
});
// Next/Prev Navigation
nextBtn.addEventListener('click', () => showTab(currentTab + 1));
prevBtn.addEventListener('click', () => showTab(currentTab - 1));
// Tab 3 Actions (Add, Remove, Edit)
itemInputs.addBtn.addEventListener('click', addItem);
itemInputs.tbody.addEventListener('click', (e) => {
if (e.target.dataset.removeId) {
removeItem(parseInt(e.target.dataset.removeId));
}
});
itemInputs.tbody.addEventListener('blur', (e) => {
if (e.target.tagName === 'TD' && e.target.isContentEditable) {
const id = parseInt(e.target.dataset.id);
const field = e.target.dataset.field;
updateItem(id, field, e.target.textContent);
}
}, true);
// Tab 4 Actions
downloadPdfBtn.addEventListener('click', downloadPDF);
downloadTxtBtn.addEventListener('click', downloadTxt);
// --- Initialization ---
loadSampleData();
showTab(1); // Set initial state
});