Skip to content

Instantly share code, notes, and snippets.

@callmephil
Last active February 26, 2025 14:07
Show Gist options
  • Save callmephil/bcdc21018c65b61965641781cc0846cb to your computer and use it in GitHub Desktop.
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.
<!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