Perfect Biogas

Admin Panel — Login to manage your site

Only @perfectbiogas.com accounts allowed

Admin Panel

Gallery
Blog
Contracts

Write New Blog Post

API key is shared across all admins. Get free key here (one-time setup)

Tap to select cover image

Published Posts

Loading...

Create Quotation

Client Details

Subject & Introduction

Concept Section

Technical Specifications

Working Description

SWOT Analysis

Cost Sheet

Scope of Work

Terms & Conditions

Bank Details

Sender Details

Review Settings

Create a webhook in your Google Chat space → Apps & Integrations → Webhooks. Paste the URL above.

Quotations

No quotations yet.

Review Quotation

'); printWin.document.close(); // Wait for content + images to load, then auto-trigger print var triggerPrint = function() { printWin.focus(); printWin.print(); if (afterClose) { // Give time for user to finish with print dialog var checkClosed = setInterval(function() { if (printWin.closed) { clearInterval(checkClosed); afterClose(); } }, 500); } }; // Check if images loaded printWin.onload = function() { setTimeout(triggerPrint, 400); }; // Fallback if onload doesn't fire setTimeout(function() { if (!printWin.closed) triggerPrint(); }, 2000); } function sendContract() { var name = document.getElementById('cName').value.trim(); var email = document.getElementById('cEmail').value.trim(); var org = document.getElementById('cOrg').value.trim(); var capacity = document.getElementById('cCapacity').value.trim(); var subject = document.getElementById('cSubject').value.trim(); var template = document.getElementById('cTemplate').value; if (!name) { showToast('Enter client name'); return; } if (!email) { showToast('Enter client email'); return; } // Save to Firestore - save ALL form fields for editing later var formFields = [ 'cTemplate','cDate','cName','cDesignation','cOrg','cAddress','cMobile','cEmail', 'cSubject','cSalutation','cIntro', 'cConceptBiogas','cConceptFiltration','cConceptElectricity', 'cPlantType','cCapacity','cFeedMaterial','cFeedQty','cBiogasProd','cDimension','cGasStorage','cGasHolder','cSlurryPit','cMixingUnit', 'cWorkingDesc', 'cSwotS','cSwotW','cSwotO','cSwotT', 'cOpt1Title','cOpt1Material','cOpt1Dimension','cOpt1Capacity','cOpt1Land','cOpt1Mobility','cOpt1Feed','cOpt1GasStorage','cOpt1Pipes','cOpt1Power','cOpt1Water','cOpt1Pump','cOpt1DigesterType','cOpt1Price', 'cOpt2Title','cOpt2Material','cOpt2Dimension','cOpt2Capacity','cOpt2Land','cOpt2Mobility','cOpt2Feed','cOpt2GasStorage','cOpt2Pipes','cOpt2Power','cOpt2Water','cOpt2Pump','cOpt2DigesterType','cOpt2Price', 'cDigesterPrice','cGST', 'cPayment','cValidity','cCompletionTime','cWarranty','cOtherTerms', 'cScope', 'cBankCompany','cBankName','cBankAccount','cBankIFSC', 'cSenderName','cSenderDesig','cSenderPhone','cSenderEmail' ]; var formData = {}; formFields.forEach(function(id) { var el = document.getElementById(id); if (el) formData[id] = el.value; }); // Save extra items var extras = []; for (var i = 1; i <= extraItemCount; i++) { var nameEl = document.getElementById('extraName_' + i); var priceEl = document.getElementById('extraPrice_' + i); if (nameEl && priceEl) extras.push({ name: nameEl.value, price: priceEl.value }); } formData.extraItems = extras; db.collection('contracts').add({ client: name, org: org, email: email, template: template, capacity: capacity, subject: subject, sentBy: auth.currentUser ? auth.currentUser.email : '', sentAt: firebase.firestore.FieldValue.serverTimestamp(), formData: formData, status: 'sent' }).then(function() { showToast('Quotation saved!'); loadContracts(); }); // Copy formatted HTML quotation to clipboard for Gmail paste var htmlContent = generateContractHTML(); var fullHTML = '' + htmlContent + ''; copyHTMLToClipboard(fullHTML); // Open Gmail compose var gmailSubject = 'Quotation: ' + subject + ' \u2014 Perfect Bio-Waste & Power Management'; var gmailURL = 'https://mail.google.com/mail/?view=cm' + '&to=' + encodeURIComponent(email) + '&su=' + encodeURIComponent(gmailSubject); window.open(gmailURL, '_blank'); showToast('Quotation copied! Paste (Ctrl+V) in Gmail body.'); } function downloadContract() { openPrintPDF(); } function loadContracts(filter) { if (!filter) filter = 'all'; currentFilter = filter; var query = db.collection('contracts').orderBy('sentAt', 'desc'); query.get().then(function(snap) { var container = document.getElementById('contractList'); if (snap.empty) { container.innerHTML = '
No quotations yet.
'; return; } var html = ''; var count = 0; snap.forEach(function(doc) { var d = doc.data(); var status = d.status || 'sent'; if (filter !== 'all' && status !== filter) return; count++; var date = d.sentAt ? d.sentAt.toDate().toLocaleDateString('en-IN') : 'Just now'; var badgeClass = status === 'pending_review' ? 'pending' : status === 'approved' ? 'approved' : status === 'changes_requested' ? 'changes' : 'sent'; var badgeText = status === 'pending_review' ? 'Pending Review' : status === 'approved' ? 'Approved' : status === 'changes_requested' ? 'Changes Requested' : 'Sent'; html += '
'; html += '
' + esc(d.client || '') + ''; if (d.org) html += ' (' + esc(d.org) + ')'; html += ' ' + badgeText + ''; html += '
' + date + ' \u00b7 ' + esc(d.email || '') + ' \u00b7 ' + esc(d.capacity || '') + ' m\u00b3 \u00b7 ' + (d.template === 'large' ? 'Large+Electricity' : d.template === 'two' ? 'Two Options' : 'Small Plant') + ' \u00b7 by ' + esc(d.sentBy || '') + ''; // Show review comments count if (d.reviewComments && d.reviewComments.length > 0) { html += '
' + d.reviewComments.length + ' review comment' + (d.reviewComments.length > 1 ? 's' : '') + ''; } html += '
'; html += '
'; // Review link - for pending_review or changes_requested if (d.reviewURL && (status === 'pending_review' || status === 'changes_requested')) { html += ''; } // Review button (in-admin quick review) if (status === 'pending_review' || status === 'changes_requested') { html += ''; } // Re-submit button for changes_requested if (status === 'changes_requested') { html += ''; } // Send button for approved contracts if (status === 'approved') { html += ''; } if (d.formData) html += ''; html += ''; html += '
'; }); if (count === 0) html = '
No quotations matching this filter.
'; container.innerHTML = html; }); } var currentFilter = 'all'; var reviewingDocId = null; var editingContractId = null; function editAndResubmit(docId) { editContract(docId); setTimeout(function() { showToast('Make your changes, then click "Re-submit for Review" below'); // Scroll to the preview button area and add a re-submit button var previewBtn = document.querySelector('#panel-contracts .btn-primary[onclick="previewContract()"]'); if (previewBtn) { var existing = document.getElementById('resubmitBtn'); if (existing) existing.remove(); var btn = document.createElement('button'); btn.id = 'resubmitBtn'; btn.className = 'btn-review'; btn.style.cssText = 'margin-top:8px;width:100%;'; btn.innerHTML = ' Re-submit for Review'; btn.onclick = function() { resubmitForReview(docId); }; previewBtn.parentNode.insertBefore(btn, previewBtn.nextSibling); } }, 200); } // === CLIPBOARD COPY FOR GMAIL === function copyHTMLToClipboard(html) { try { var blob = new Blob([html], {type: 'text/html'}); var textBlob = new Blob(['[Quotation - view in rich text]'], {type: 'text/plain'}); var item = new ClipboardItem({'text/html': blob, 'text/plain': textBlob}); navigator.clipboard.write([item]).catch(function() { fallbackCopyHTML(html); }); } catch(e) { fallbackCopyHTML(html); } } function fallbackCopyHTML(html) { var container = document.createElement('div'); container.innerHTML = html; container.style.cssText = 'position:fixed;left:-9999px;opacity:0;'; document.body.appendChild(container); var range = document.createRange(); range.selectNodeContents(container); var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); document.execCommand('copy'); sel.removeAllRanges(); document.body.removeChild(container); } // === GOOGLE CHAT WEBHOOK === var webhookURL = ''; function loadWebhook() { db.collection('config').doc('gchat_webhook').get().then(function(doc) { if (doc.exists && doc.data().url) { webhookURL = doc.data().url; document.getElementById('webhookURL').value = webhookURL; } }); } function saveWebhook() { var url = document.getElementById('webhookURL').value.trim(); if (!url) { showToast('Enter a webhook URL'); return; } db.collection('config').doc('gchat_webhook').set({ url: url }).then(function() { webhookURL = url; showToast('Webhook URL saved!'); }); } function sendChatNotification(message) { if (!webhookURL) return; fetch(webhookURL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: message }) }).catch(function(err) { console.error('Chat notification failed:', err); }); } // === REVIEW WORKFLOW === function getContractFormData() { var formFields = [ 'cTemplate','cDate','cName','cDesignation','cOrg','cAddress','cMobile','cEmail', 'cSubject','cSalutation','cIntro', 'cConceptBiogas','cConceptFiltration','cConceptElectricity', 'cPlantType','cCapacity','cFeedMaterial','cFeedQty','cBiogasProd','cDimension','cGasStorage','cGasHolder','cSlurryPit','cMixingUnit', 'cWorkingDesc', 'cSwotS','cSwotW','cSwotO','cSwotT', 'cOpt1Title','cOpt1Material','cOpt1Dimension','cOpt1Capacity','cOpt1Land','cOpt1Mobility','cOpt1Feed','cOpt1GasStorage','cOpt1Pipes','cOpt1Power','cOpt1Water','cOpt1Pump','cOpt1DigesterType','cOpt1Price', 'cOpt2Title','cOpt2Material','cOpt2Dimension','cOpt2Capacity','cOpt2Land','cOpt2Mobility','cOpt2Feed','cOpt2GasStorage','cOpt2Pipes','cOpt2Power','cOpt2Water','cOpt2Pump','cOpt2DigesterType','cOpt2Price', 'cDigesterPrice','cGST', 'cPayment','cValidity','cCompletionTime','cWarranty','cOtherTerms', 'cScope', 'cBankCompany','cBankName','cBankAccount','cBankIFSC', 'cSenderName','cSenderDesig','cSenderPhone','cSenderEmail' ]; var formData = {}; formFields.forEach(function(id) { var el = document.getElementById(id); if (el) formData[id] = el.value; }); var extras = []; for (var i = 1; i <= extraItemCount; i++) { var nameEl = document.getElementById('extraName_' + i); var priceEl = document.getElementById('extraPrice_' + i); if (nameEl && priceEl) extras.push({ name: nameEl.value, price: priceEl.value }); } formData.extraItems = extras; return formData; } function submitForReview() { var name = document.getElementById('cName').value.trim(); var email = document.getElementById('cEmail').value.trim(); var org = document.getElementById('cOrg').value.trim(); var capacity = document.getElementById('cCapacity').value.trim(); var subject = document.getElementById('cSubject').value.trim(); var template = document.getElementById('cTemplate').value; var submitter = auth.currentUser ? auth.currentUser.email : 'unknown'; if (!name) { showToast('Enter client name'); return; } var formData = getContractFormData(); var contractHTML = generateContractHTML(); db.collection('contracts').add({ client: name, org: org, email: email, template: template, capacity: capacity, subject: subject, sentBy: submitter, sentAt: firebase.firestore.FieldValue.serverTimestamp(), formData: formData, contractHTML: contractHTML, status: 'pending_review', reviewComments: [{ id: 'c_' + Date.now(), section: 'general', sectionName: 'General', author: submitter, text: 'Submitted for review', action: 'submitted', timestamp: new Date().toISOString(), replies: [] }] }).then(function(docRef) { var reviewURL = window.location.origin + '/review?id=' + docRef.id; // Save review URL back to document db.collection('contracts').doc(docRef.id).update({ reviewURL: reviewURL }); showToast('Quotation submitted for review!'); loadContracts(); // Send Google Chat notification with review link var chatMsg = '\ud83d\udccb *New Quotation Review Request*\n\n' + '*Client:* ' + name + (org ? ' (' + org + ')' : '') + '\n' + '*Subject:* ' + subject + '\n' + '*Capacity:* ' + (capacity || 'N/A') + ' m\u00b3\n' + '*Template:* ' + (template === 'large' ? 'Large + Electricity' : template === 'two' ? 'Two Options' : 'Small Plant') + '\n' + '*Submitted by:* ' + submitter + '\n\n' + '\ud83d\udd17 *Review here:* ' + reviewURL; sendChatNotification(chatMsg); }); } function resubmitForReview(docId) { if (!docId) return; var submitter = auth.currentUser ? auth.currentUser.email : 'unknown'; db.collection('contracts').doc(docId).get().then(function(doc) { if (!doc.exists) return; var d = doc.data(); var comments = d.reviewComments || []; // If form is filled, update the saved HTML and formData var name = document.getElementById('cName').value.trim(); var updates = { status: 'pending_review', reviewedBy: firebase.firestore.FieldValue.delete(), reviewedAt: firebase.firestore.FieldValue.delete() }; if (name) { updates.formData = getContractFormData(); updates.contractHTML = generateContractHTML(); } comments.push({ id: 'c_' + Date.now(), section: 'general', sectionName: 'General', author: submitter, text: 'Re-submitted for review after changes', action: 'submitted', timestamp: new Date().toISOString(), replies: [] }); updates.reviewComments = comments; db.collection('contracts').doc(docId).update(updates).then(function() { showToast('Re-submitted for review!'); loadContracts(); var reviewURL = d.reviewURL || (window.location.origin + '/review?id=' + docId); sendChatNotification('\ud83d\udd04 *Quotation Re-submitted for Review*\n\n' + '*Client:* ' + (d.client || '') + (d.org ? ' (' + d.org + ')' : '') + '\n' + '*Re-submitted by:* ' + submitter + '\n\n' + '\ud83d\udd17 *Review here:* ' + reviewURL); }); }); } function openReviewModal(docId) { reviewingDocId = docId; db.collection('contracts').doc(docId).get().then(function(doc) { if (!doc.exists) { showToast('Not found'); return; } var d = doc.data(); document.getElementById('reviewClientName').textContent = '\u2014 ' + (d.client || ''); document.getElementById('reviewComment').value = ''; var sectionLabels = { 'client': 'Client Details', 'subject': 'Subject & Introduction', 'concept': 'Concept', 'specs': 'Technical Specifications', 'working': 'Working Description', 'swot': 'SWOT Analysis', 'pricing': 'Cost Sheet / Pricing', 'scope': 'Scope of Work', 'terms': 'Terms & Conditions', 'bank': 'Bank Details', 'sender': 'Sender Details', 'general': 'General / Overall' }; // Render comments grouped by section var commentsHTML = ''; if (d.reviewComments && d.reviewComments.length > 0) { // Group by section var groups = {}; d.reviewComments.forEach(function(c, idx) { var section = c.section || 'general'; if (!groups[section]) groups[section] = []; c._idx = idx; groups[section].push(c); }); Object.keys(groups).forEach(function(section) { var label = sectionLabels[section] || section; commentsHTML += '
'; commentsHTML += '
' + esc(label) + '
'; groups[section].forEach(function(c) { var icon = c.action === 'approved' ? '\u2705' : c.action === 'changes_requested' ? '\u274c' : c.action === 'submitted' ? '\ud83d\udce4' : '\ud83d\udcac'; var dateStr = c.timestamp ? new Date(c.timestamp).toLocaleString('en-IN') : ''; commentsHTML += '
'; commentsHTML += '' + icon + ' ' + esc(c.author || '') + ''; commentsHTML += '' + dateStr + ''; commentsHTML += '
' + esc(c.text || '') + '
'; // Replies if (c.replies && c.replies.length > 0) { c.replies.forEach(function(r) { var rDate = r.timestamp ? new Date(r.timestamp).toLocaleString('en-IN') : ''; commentsHTML += '
'; commentsHTML += '\u21a9 ' + esc(r.author || '') + ''; commentsHTML += '' + rDate + ''; commentsHTML += '
' + esc(r.text || '') + '
'; commentsHTML += '
'; }); } // Reply input commentsHTML += '
'; commentsHTML += ''; commentsHTML += ''; commentsHTML += '
'; commentsHTML += '
'; }); commentsHTML += '
'; }); // Show review link if available if (d.reviewURL) { commentsHTML += '
Open full review page
'; } } else { commentsHTML = '

No comments yet.

'; } document.getElementById('reviewComments').innerHTML = commentsHTML; document.getElementById('reviewModal').classList.add('show'); }); } function closeReviewModal() { document.getElementById('reviewModal').classList.remove('show'); reviewingDocId = null; } function addModalReply(commentIdx) { if (!reviewingDocId) return; var input = document.getElementById('modalReply_' + commentIdx); if (!input) return; var text = input.value.trim(); if (!text) return; var author = auth.currentUser ? auth.currentUser.email : 'unknown'; db.collection('contracts').doc(reviewingDocId).get().then(function(doc) { if (!doc.exists) return; var d = doc.data(); var comments = d.reviewComments || []; if (!comments[commentIdx]) return; if (!comments[commentIdx].replies) comments[commentIdx].replies = []; comments[commentIdx].replies.push({ author: author, text: text, timestamp: new Date().toISOString() }); db.collection('contracts').doc(reviewingDocId).update({ reviewComments: comments }).then(function() { showToast('Reply added'); openReviewModal(reviewingDocId); // Refresh modal // Notify via chat sendChatNotification('\ud83d\udcac *Reply on quotation for ' + esc(d.client || '') + '*\n' + '*Section:* ' + (comments[commentIdx].sectionName || 'General') + '\n' + '*' + author + ':* ' + text + '\n\n' + '\ud83d\udd17 ' + (d.reviewURL || window.location.href)); }); }); } function approveReview() { if (!reviewingDocId) return; var reviewer = auth.currentUser ? auth.currentUser.email : 'unknown'; var comment = document.getElementById('reviewComment').value.trim(); var commentText = comment || 'Approved'; db.collection('contracts').doc(reviewingDocId).get().then(function(doc) { if (!doc.exists) return; var d = doc.data(); var comments = d.reviewComments || []; comments.push({ author: reviewer, text: commentText, action: 'approved', timestamp: new Date().toISOString() }); db.collection('contracts').doc(reviewingDocId).update({ status: 'approved', reviewComments: comments, reviewedBy: reviewer, reviewedAt: firebase.firestore.FieldValue.serverTimestamp() }).then(function() { showToast('Quotation approved!'); closeReviewModal(); loadContracts(); // Notify via Google Chat var chatMsg = '\u2705 *Quotation Approved*\n\n' + '*Client:* ' + (d.client || '') + (d.org ? ' (' + d.org + ')' : '') + '\n' + '*Subject:* ' + (d.subject || '') + '\n' + '*Approved by:* ' + reviewer + '\n'; if (comment) chatMsg += '*Comment:* ' + comment + '\n'; chatMsg += '\n\ud83d\udd17 View at: ' + window.location.href; sendChatNotification(chatMsg); }); }); } function requestChanges() { if (!reviewingDocId) return; var reviewer = auth.currentUser ? auth.currentUser.email : 'unknown'; var comment = document.getElementById('reviewComment').value.trim(); if (!comment) { showToast('Please add a comment explaining what changes are needed'); return; } db.collection('contracts').doc(reviewingDocId).get().then(function(doc) { if (!doc.exists) return; var d = doc.data(); var comments = d.reviewComments || []; comments.push({ author: reviewer, text: comment, action: 'changes_requested', timestamp: new Date().toISOString() }); db.collection('contracts').doc(reviewingDocId).update({ status: 'changes_requested', reviewComments: comments, reviewedBy: reviewer, reviewedAt: firebase.firestore.FieldValue.serverTimestamp() }).then(function() { showToast('Changes requested!'); closeReviewModal(); loadContracts(); // Notify via Google Chat var chatMsg = '\u274c *Changes Requested on Quotation*\n\n' + '*Client:* ' + (d.client || '') + (d.org ? ' (' + d.org + ')' : '') + '\n' + '*Subject:* ' + (d.subject || '') + '\n' + '*Reviewer:* ' + reviewer + '\n' + '*Changes needed:* ' + comment + '\n\n' + '\ud83d\udd17 Edit at: ' + window.location.href; sendChatNotification(chatMsg); }); }); } function editContract(docId) { editingContractId = docId; db.collection('contracts').doc(docId).get().then(function(doc) { if (!doc.exists) { showToast('Quotation not found'); return; } var d = doc.data(); if (!d.formData) { showToast('This quotation has no saved form data (older record)'); return; } var fd = d.formData; // Set template first to trigger section visibility if (fd.cTemplate) { document.getElementById('cTemplate').value = fd.cTemplate; loadTemplate(); } // Small delay to let loadTemplate finish populating defaults, then overwrite with saved values setTimeout(function() { var fields = [ 'cDate','cName','cDesignation','cOrg','cAddress','cMobile','cEmail', 'cSubject','cSalutation','cIntro', 'cConceptBiogas','cConceptFiltration','cConceptElectricity', 'cPlantType','cCapacity','cFeedMaterial','cFeedQty','cBiogasProd','cDimension','cGasStorage','cGasHolder','cSlurryPit','cMixingUnit', 'cWorkingDesc', 'cSwotS','cSwotW','cSwotO','cSwotT', 'cOpt1Title','cOpt1Material','cOpt1Dimension','cOpt1Capacity','cOpt1Land','cOpt1Mobility','cOpt1Feed','cOpt1GasStorage','cOpt1Pipes','cOpt1Power','cOpt1Water','cOpt1Pump','cOpt1DigesterType','cOpt1Price', 'cOpt2Title','cOpt2Material','cOpt2Dimension','cOpt2Capacity','cOpt2Land','cOpt2Mobility','cOpt2Feed','cOpt2GasStorage','cOpt2Pipes','cOpt2Power','cOpt2Water','cOpt2Pump','cOpt2DigesterType','cOpt2Price', 'cDigesterPrice','cGST', 'cPayment','cValidity','cCompletionTime','cWarranty','cOtherTerms', 'cScope', 'cBankCompany','cBankName','cBankAccount','cBankIFSC', 'cSenderName','cSenderDesig','cSenderPhone','cSenderEmail' ]; fields.forEach(function(id) { var el = document.getElementById(id); if (el && fd[id] !== undefined) el.value = fd[id]; }); // Restore extra items document.getElementById('extraItemsDiv').innerHTML = ''; extraItemCount = 0; if (fd.extraItems && fd.extraItems.length > 0) { fd.extraItems.forEach(function(item) { addExtraItem(item.name, item.price); }); } // Scroll to top of form document.querySelector('#panel-contracts .admin-card').scrollIntoView({ behavior: 'smooth' }); showToast('Quotation loaded for editing — "' + (fd.cName || d.client) + '"'); }, 100); }); } function deleteContract(docId) { if (!confirm('Delete this quotation record?')) return; db.collection('contracts').doc(docId).delete().then(function() { showToast('Quotation record deleted'); loadContracts(); }); } // Init: set today's date and load default template document.addEventListener('DOMContentLoaded', function() { var dateEl = document.getElementById('cDate'); if (dateEl) dateEl.valueAsDate = new Date(); loadTemplate(); // Load webhook URL after auth firebase.auth().onAuthStateChanged(function(u) { if (u) loadWebhook(); }); });