Last active
February 26, 2025 14:07
-
-
Save callmephil/bcdc21018c65b61965641781cc0846cb to your computer and use it in GitHub Desktop.
Quotation generator: Generate a quotation and allow you to download it as pdf.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>Quotation</title> | |
<style> | |
body { | |
font-family: Arial, sans-serif; | |
margin: 0; | |
padding: 20px; | |
background-color: #fff; | |
width: 21cm; | |
height: 29.7cm; | |
margin: 0 auto; | |
} | |
.quotation-box { | |
margin: auto; | |
padding: 30px; | |
background-color: #fff; | |
} | |
.quotation-box table { | |
width: 100%; | |
line-height: inherit; | |
text-align: left; | |
} | |
.quotation-box table td { | |
padding: 5px; | |
vertical-align: top; | |
} | |
.quotation-box table tr td:nth-child(2) { | |
text-align: right; | |
} | |
.quotation-box table tr.top table td { | |
padding-bottom: 20px; | |
} | |
.quotation-box table tr.information table td { | |
padding-bottom: 40px; | |
} | |
.quotation-box table tr.heading td { | |
background: #eee; | |
border-bottom: 1px solid #ddd; | |
font-weight: bold; | |
} | |
.quotation-box table tr.details td { | |
padding-bottom: 20px; | |
} | |
.quotation-box table tr.item td { | |
border-bottom: 1px solid #eee; | |
} | |
.quotation-box table tr.item.last td { | |
border-bottom: none; | |
} | |
.quotation-box table tr.total td:nth-child(2) { | |
border-top: 2px solid #eee; | |
font-weight: bold; | |
} | |
.terms { | |
margin-top: 10px; | |
font-size: 14px; | |
color: #555; | |
} | |
.signature { | |
margin-top: 0px; | |
display: flex; | |
justify-content: space-between; | |
font-size: 12px; | |
color: #555; | |
} | |
.signature .sign-section { | |
width: 45%; | |
} | |
.signature .sign-section p { | |
margin-top: 50px; /* Space for signatures */ | |
} | |
/* Hide elements with class "no-print" when printing or generating PDF */ | |
@media print { | |
.no-print { | |
display: none; | |
} | |
} | |
</style> | |
<!-- Include html2pdf library --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.9.3/html2pdf.bundle.min.js"></script> | |
</head> | |
<body> | |
<!-- Download PDF Button (hidden when printing) --> | |
<div class="no-print" style="text-align: center; margin-top: 20px"> | |
<button id="download-pdf">Download as PDF</button> | |
</div> | |
<div class="quotation-box"> | |
<table> | |
<tr class="information"> | |
<td colspan="2"> | |
<table> | |
<tr> | |
<td id="company-info"> | |
<!-- Company information will be inserted here --> | |
</td> | |
<td> | |
<span id="client-name"></span><br /> | |
<span id="client-email"></span><br /> | |
<span id="client-phone"></span> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<tr class="heading"> | |
<td>Item</td> | |
<td>Price</td> | |
</tr> | |
<tbody id="items-body"> | |
<!-- Items will be dynamically inserted here --> | |
</tbody> | |
<tr class="item last"> | |
<td>Discount</td> | |
<td id="discount"></td> | |
</tr> | |
<tr class="total"> | |
<td></td> | |
<td>Total: <span id="total-price"></span></td> | |
</tr> | |
</table> | |
<div class="terms"> | |
<p><strong>Terms:</strong></p> | |
<div id="payment-info"></div> | |
</div> | |
<div class="signature"> | |
<div class="sign-section"> | |
<p> | |
<strong>Client:</strong><br /> | |
<span id="client-signature-name"></span><br /><br /><br /> | |
Date: ______________________<br /><br /><br /> | |
Signature: ______________________ | |
</p> | |
</div> | |
<div class="sign-section"> | |
<p> | |
<strong>Contractor:</strong><br /> | |
<span id="signer-name"></span><br /><br /><br /> | |
Date: ______________________<br /><br /><br /> | |
Signature: ______________________ | |
</p> | |
</div> | |
</div> | |
</div> | |
<script> | |
// Dummy company information | |
const myCompanyInfo = { | |
"company-name": "Dummy Company LTD", | |
"company-address": "123 Dummy Street, Suite 456,<br>Faketown, FT 78910,<br>Dummy Country", | |
}; | |
// Dummy client information | |
const myName = { | |
"client-name": "John Doe", | |
"client-phone": "123-456-7890", | |
"client-email": "[email protected]", | |
}; | |
// Dummy cost information with two items (Design fee and Development fee) | |
const myCost = { | |
items: [ | |
{ | |
desc: "Design fee", | |
price: "500", | |
}, | |
{ | |
desc: "Development fee", | |
price: "1500", | |
}, | |
], | |
discount: 0.0, | |
}; | |
// Dummy payment terms with placeholders | |
const myPaymentTerms = { | |
"payment-info": [ | |
"An initial payment of {initial_payment} will be required after design approval. A deposit of {deposit} is needed before development, and the final payment of {final_payment} is due upon completion, for a total of {total}.", | |
"Design ownership transfers to the client after the initial payment.", | |
"The client must provide all necessary materials; additional charges may apply for extra work.", | |
"Hosting will be provided free of charge on our dummy server at https://dummyhosting.example.com.", | |
"All work is subject to a non-disclosure agreement. No analytics or tracking will be included unless requested.", | |
], | |
}; | |
// Dummy signature fields | |
const mySignatureFields = { | |
"signer-name": "Alice Smith", | |
"client-signature-name": "Bob's Enterprises Ltd", | |
}; | |
// Merge all data into a single object | |
const quotationData = { | |
...myCompanyInfo, | |
...myName, | |
...myCost, | |
...myPaymentTerms, | |
...mySignatureFields, | |
}; | |
// Calculate total cost after discount | |
function calculateTotal(items, discount) { | |
const total = items.reduce((sum, item) => sum + (parseFloat(item.price) || 0), 0); | |
return total - discount; | |
} | |
// Calculate fee breakdown based on the first item being the design fee | |
function calculateFees(items, discount) { | |
const designFee = parseFloat(items[0].price) || 0; | |
const total = calculateTotal(items, discount); | |
const remaining = total - designFee; | |
const deposit = remaining * 0.3; | |
const finalPayment = remaining - deposit; | |
return { designFee, deposit, finalPayment, total }; | |
} | |
// Update the HTML content of the quotation | |
function updateQuotationContent(data) { | |
// Update company information | |
const companyInfoElem = document.getElementById("company-info"); | |
companyInfoElem.innerHTML = data["company-name"] + "<br>" + data["company-address"]; | |
document.getElementById("client-name").textContent = data["client-name"]; | |
document.getElementById("client-phone").textContent = data["client-phone"]; | |
document.getElementById("client-email").textContent = data["client-email"]; | |
const itemsBody = document.getElementById("items-body"); | |
itemsBody.innerHTML = ""; // Clear any existing items | |
data.items.forEach((item, index) => { | |
const row = document.createElement("tr"); | |
row.className = "item"; | |
if (index === data.items.length - 1) { | |
row.classList.add("last"); | |
} | |
const descCell = document.createElement("td"); | |
descCell.innerHTML = item.desc; | |
row.appendChild(descCell); | |
const priceCell = document.createElement("td"); | |
priceCell.textContent = `$${(parseFloat(item.price) || 0).toFixed(2)}`; | |
row.appendChild(priceCell); | |
itemsBody.appendChild(row); | |
}); | |
document.getElementById("discount").textContent = `-$${data.discount.toFixed(2)}`; | |
const totalPrice = calculateTotal(data.items, data.discount); | |
document.getElementById("total-price").textContent = `$${totalPrice.toFixed(2)}`; | |
const paymentInfoElement = document.getElementById("payment-info"); | |
paymentInfoElement.innerHTML = ""; // Clear existing payment info | |
data["payment-info"].forEach((paragraph) => { | |
const p = document.createElement("p"); | |
p.textContent = paragraph; | |
paymentInfoElement.appendChild(p); | |
}); | |
document.getElementById("signer-name").textContent = data["signer-name"]; | |
document.getElementById("client-signature-name").textContent = | |
data["client-signature-name"]; | |
} | |
// Replace placeholders in the payment terms with calculated fee values | |
function updatePaymentTerms() { | |
const fees = calculateFees(quotationData.items, quotationData.discount); | |
const paymentInfo = quotationData["payment-info"].map((paragraph) => | |
paragraph | |
.replace("{initial_payment}", `$${fees.designFee.toFixed(2)}`) | |
.replace("{deposit}", `$${fees.deposit.toFixed(2)}`) | |
.replace("{final_payment}", `$${fees.finalPayment.toFixed(2)}`) | |
.replace("{total}", `$${fees.total.toFixed(2)}`) | |
); | |
quotationData["payment-info"] = paymentInfo; | |
} | |
updatePaymentTerms(); | |
updateQuotationContent(quotationData); | |
// Set up PDF download functionality using html2pdf | |
document.getElementById("download-pdf").addEventListener("click", function () { | |
const element = document.querySelector(".quotation-box"); | |
const opt = { | |
margin: 0.5, | |
filename: "quotation.pdf", | |
image: { type: "jpeg", quality: 0.98 }, | |
html2canvas: { scale: 2 }, | |
jsPDF: { unit: "in", format: "letter", orientation: "portrait" }, | |
}; | |
html2pdf().set(opt).from(element).save(); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment