No items added yet.
`;
}
lineItems.forEach((item, index) => {
const amount = calculateItemAmount(item.qty, item.price);
const tr = document.createElement('tr');
tr.dataset.id = item.id;
tr.innerHTML = `
${index + 1} |
${escapeHtml(item.desc)} |
${item.qty} |
${formatUSD(item.price)} |
${formatUSD(amount)} |
|
`;
tableBody.appendChild(tr);
});
qegCalculateFinalTotal(); // Recalculate totals after rendering
}
// === LINE ITEM CRUD ===
/** Adds a new line item */
window.qegAddItem = () => {
if (!descInput || !qtyInput || !priceInput) return;
const desc = descInput.value.trim();
const qty = parseInt(qtyInput.value);
const price = parseFloat(priceInput.value);
if (isNaN(qty) || qty < 1 || isNaN(price) || price < 0.01) {
showMessage("Please enter valid Quantity (>0) and Unit Price (>0).", true);
return;
}
lineItems.push({
id: 'li' + Date.now(),
desc,
qty,
price
});
document.getElementById('qeg-add-item-form').reset();
qtyInput.value = 1; // Reset quantity to 1
showMessage(null);
renderLineItemTable();
};
/** Deletes a line item */
window.qegDeleteItem = (id) => {
if (!confirm('Are you sure you want to delete this line item?')) return;
lineItems = lineItems.filter(item => item.id !== id);
renderLineItemTable();
};
/** Clears all line items */
window.qegClearAllItems = () => {
if (lineItems.length === 0) { showMessage("The item list is already empty.", false); return; }
if (!confirm('Are you sure you want to clear ALL line items?')) return;
lineItems = [];
renderLineItemTable();
};
// === TAB AND NAV LOGIC ===
/** Switches tabs */
window.qegShowTab = (tabId, element) => {
if (!container || !tabLinks) return;
container.querySelectorAll('.qeg-tab-content').forEach(tab => tab.classList.remove('qeg-active'));
container.querySelectorAll('.qeg-tab-link').forEach(link => link.classList.remove('qeg-active'));
const tabToShow = container.querySelector('#' + tabId);
if (tabToShow) tabToShow.classList.add('qeg-active');
if (element) element.classList.add('qeg-active');
currentTabId = tabId;
updateNavButtons();
// Recalculate and update summary outputs when switching to summary tab
if (tabId === 'qeg-tab-summary') {
qegCalculateFinalTotal();
}
};
/** Handles nav buttons */
window.qegNavigateTabs = (isNext) => {
if (!container || !tabLinks) return;
const targetTabId = isNext ? 'qeg-tab-summary' : 'qeg-tab-line-items';
const targetTabLink = container.querySelector(`.qeg-tab-link[onclick*="'${targetTabId}'"]`);
if(targetTabLink) targetTabLink.click();
};
/** Updates nav button states */
function updateNavButtons() {
if (!prevBtn || !nextBtn) return;
prevBtn.disabled = currentTabId === 'qeg-tab-line-items';
nextBtn.disabled = currentTabId === 'qeg-tab-summary';
}
// === PDF EXPORT ===
window.qegDownloadPDF = () => {
if (typeof jsPDF === 'undefined' || !jsPDF.autoTable) { showMessage("PDF library not loaded.", true); return; }
if (lineItems.length === 0) { showMessage("Add line items before downloading the quote.", true); return; }
// Recalculate one last time
qegCalculateFinalTotal();
const projectName = projectNameInput?.value || 'Untitled Project Estimate (USA)';
const quoteNumber = quoteNumberInput?.value || 'N/A';
const clientName = clientNameInput?.value || 'Client Name';
const subtotal = lineItems.reduce((acc, item) => acc + calculateItemAmount(item.qty, item.price), 0);
const taxRate = parseFloat(taxRateInput?.value || 0);
const discount = parseFloat(discountInput?.value || 0);
const taxAmount = subtotal * (taxRate / 100);
const finalTotal = subtotal + taxAmount - discount;
// PDF Colors (Matching theme)
const PRIMARY_COLOR_HEX = '#3b82f6';
const DARK_GRAY_HEX = '#4b5563';
const SECONDARY_COLOR_HEX = '#10b981';
try {
const doc = new jsPDF();
const pageMargin = 40;
const pageWidth = doc.internal.pageSize.getWidth();
let y = pageMargin;
// 1. Header Block (Title, Quote #, Date)
doc.setFontSize(22);
doc.setFont(undefined, 'bold');
doc.setTextColor(PRIMARY_COLOR_HEX);
doc.text("QUOTE / ESTIMATE", pageWidth - pageMargin, y, { align: 'right' });
y += 28;
doc.setFontSize(10);
doc.setFont(undefined, 'normal');
doc.setTextColor(DARK_GRAY_HEX);
doc.text(`Project: ${projectName}`, pageMargin, y);
doc.text(`Client: ${clientName}`, pageMargin, y + 12);
y += 12;
doc.text(`Quote #: ${quoteNumber}`, pageWidth - pageMargin, y, { align: 'right' });
y += 12;
doc.text(`Date: ${new Date().toLocaleDateString('en-US')}`, pageWidth - pageMargin, y, { align: 'right' });
y += 35;
// 2. Line Items Table
const tableHead = [["#", "Description", "Qty", "Unit Price", "Amount"]];
const tableBody = lineItems.map((item, index) => [
index + 1,
item.desc,
item.qty.toString(),
formatUSD(item.price),
formatUSD(calculateItemAmount(item.qty, item.price))
]);
doc.autoTable({
startY: y,
head: tableHead,
body: tableBody,
theme: 'striped',
styles: { fontSize: 9, cellPadding: 4, valign: 'middle' },
headStyles: { fillColor: [59, 130, 246], textColor: 255 }, // Primary Blue
columnStyles: {
2: { cellWidth: 30, halign: 'right' },
3: { halign: 'right' },
4: { halign: 'right', fontStyle: 'bold' }
}
});
y = doc.autoTable.previous.finalY + 15;
// 3. Financial Summary
doc.setFontSize(10);
doc.setFont(undefined, 'normal');
doc.setTextColor(DARK_GRAY_HEX);
const summaryX = pageWidth - pageMargin - 120; // Start summary tables 120pt from right
const summaryLines = [
['Subtotal', formatUSD(subtotal), ''],
['Tax Rate', `${taxRate}%`, formatUSD(taxAmount)],
['Discount', '', formatUSD(discount * -1)],
];
summaryLines.forEach(line => {
doc.text(line[0], summaryX, y, { align: 'right' });
doc.text(line[1], summaryX + 75, y, { align: 'right' });
if (line[2]) { doc.text(line[2], summaryX + 150, y, { align: 'right' }); }
y += 12;
});
doc.setDrawColor(DARK_GRAY_HEX);
doc.setLineWidth(1);
doc.line(summaryX + 50, y, summaryX + 150, y); // Summary divider
y += 15;
doc.setFontSize(14);
doc.setFont(undefined, 'bold');
doc.setTextColor(SECONDARY_COLOR_HEX); // Accent Green for Total
doc.text("GRAND TOTAL:", summaryX - 25, y, { align: 'right' });
doc.text(formatUSD(finalTotal), summaryX + 150, y, { align: 'right' });
doc.save(`Quote_${projectName.replace(/\s/g, '_')}_${new Date().getFullYear()}.pdf`);
} catch (e) {
console.error("Error generating PDF:", e);
showMessage("An error occurred while generating the PDF.", true);
}
};
/** Basic HTML escaping */
function escapeHtml(unsafe) {
if (typeof unsafe !== 'string') return unsafe ?? ''; // Handle null/undefined
return unsafe
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
/** Displays messages globally */
function showMessage(message, isError = false) {
if (!messageArea) return;
if (!message) { messageArea.style.display = 'none'; return; }
const colorClass = isError ? 'text-red-600' : 'text-blue-600';
messageArea.textContent = message;
messageArea.className = `mt-4 text-center font-medium ${colorClass}`;
messageArea.style.display = 'block';
setTimeout(() => { if(messageArea && messageArea.textContent === message) { messageArea.style.display = 'none'; } }, 4000);
}
// === Initial Load ===
renderLineItemTable();
qegCalculateFinalTotal();
});