Ziggie Exterior Presentation Scorecard | Gold CoastComplimentary 10-Point Scorecard
See what your building needs before people complain.
Capture the exterior presentation score for a Gold Coast commercial building, identify the priority areas, and generate a clear report for the facilities manager.
Ziggie Exterior Presentation Control Plan
Accountable exterior presentation for your commercial building — scheduled, reported and managed for you three times a year.
- Assess the areas tenants notice first
- Score presentation risk out of 100
- Recommend priority-area action
- Generate a professional report
Ready
`;
}function generatePlainTextReport(data) {
const assessorName = data.assessor || 'The assessor';
const priorities = priorityAreas(data.scores);
return [
'ZIGGIE 10-POINT EXTERIOR PRESENTATION SCORECARD',
'www.ziggiegroup.com.au',
'',
`Property: ${data.propertyName || 'Not supplied'}`,
`Assessor: ${assessorName}`,
`Score: ${data.total}/100`,
`Status: ${data.risk.label}`,
'',
'EXECUTIVE SUMMARY',
`${assessorName} assessed ${data.propertyName || 'this commercial building'} and recorded an exterior presentation score of ${data.total}/100.`,
'',
'TOP 3 PRIORITY AREAS',
...priorities.map((item, i) => `${i+1}. ${item.title}: ${item.score}/10 — ${item.action || recommendedActionForScore(item, item.score)}`),
'',
'PHOTOS CAPTURED',
...data.scores.filter(item => item.photos && item.photos.length).map(item => `${item.title}: ${item.photos.length} photo${item.photos.length === 1 ? '' : 's'}`),
'',
'RECOMMENDED NEXT STEPS',
'The upkeep of the building exterior should not be expensive or difficult. This report helps identify the highest-priority exterior areas so the first job can focus on what matters most and avoid wasted spend on low-priority work.',
'Ziggie can help keep the building up over time with scheduled exterior washes, photo reporting, follow-up communication and an improvement dashboard using future scorecard assessments.',
'',
`Book a Free 30-Minute Call with Ziggie: ${CALENDLY_URL}`
].join('\n');
}function isValidEmail(email) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(email || '').trim()); }
function getRecipients(data) { const recipients = [ZIGGIE_REPORT_EMAIL]; if (isValidEmail(data.fmEmail) && data.fmEmail.toLowerCase() !== ZIGGIE_REPORT_EMAIL.toLowerCase()) recipients.push(data.fmEmail); return recipients; }
function subject(data) { return `Ziggie Exterior Presentation Scorecard — ${data.propertyName || 'Commercial Building'} — ${data.total}/100`; }function pdfAttachmentFilename(data) {
const safeName = (data.propertyName || 'ziggie-scorecard')
.toLowerCase()
.replace(/[^a-z0-9]+/g,'-')
.replace(/^-|-$/g,'') || 'ziggie-scorecard';
return `${safeName}-exterior-scorecard.pdf`;
}function cleanBase64Pdf(value) {
return String(value || '')
.replace(/^data:application\/pdf;base64,/i, '')
.replace(/\s+/g, '');
}function looksLikePdfBase64(value) {
const clean = cleanBase64Pdf(value);
return clean.length > 500 && clean.startsWith('JVBERi0');
}function escapePdfText(text) {
return String(text || '')
.replace(/[\u2013\u2014]/g, '-')
.replace(/[\u2018\u2019]/g, "'")
.replace(/[\u201C\u201D]/g, '"')
.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, ' ')
.replace(/\\/g, '\\\\')
.replace(/\(/g, '\\(')
.replace(/\)/g, '\\)');
}function wrapPdfTextLine(line, maxChars) {
const words = String(line || '').split(/\s+/).filter(Boolean);
if (!words.length) return [''];
const lines = [];
let current = '';
words.forEach(word => {
if (!current) current = word;
else if ((current + ' ' + word).length <= maxChars) current += ' ' + word;
else { lines.push(current); current = word; }
});
if (current) lines.push(current);
return lines;
}function makeSimplePdfBase64FromText(text) {
const rawLines = String(text || '').split(/\r?\n/);
const wrappedLines = [];
rawLines.forEach(line => {
wrapPdfTextLine(line, 88).forEach(wrapped => wrappedLines.push(wrapped));
});const linesPerPage = 47;
const pages = [];
for (let i = 0; i < wrappedLines.length; i += linesPerPage) pages.push(wrappedLines.slice(i, i + linesPerPage));
if (!pages.length) pages.push(['Ziggie Exterior Presentation Scorecard Report']);const objects = [];
function setObject(id, value) { objects[id] = value; }
function addObject(value) { objects.push(value); return objects.length - 1; }setObject(1, '<< /Type /Catalog /Pages 2 0 R >>');
const fontId = 3;
setObject(fontId, '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>');const pageIds = [];
pages.forEach(pageLines => {
let stream = 'BT\n/F1 10 Tf\n14 TL\n42 790 Td\n';
pageLines.forEach(line => { stream += `(${escapePdfText(line)}) Tj\nT*\n`; });
stream += 'ET\n';
const contentId = addObject(`<< /Length ${stream.length} >>\nstream\n${stream}endstream`);
const pageId = addObject(`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 ${fontId} 0 R >> >> /Contents ${contentId} 0 R >>`);
pageIds.push(pageId);
});setObject(2, `<< /Type /Pages /Kids [${pageIds.map(id => `${id} 0 R`).join(' ')}] /Count ${pageIds.length} >>`);let pdf = '%PDF-1.4\n';
const offsets = [0];
for (let id = 1; id < objects.length; id++) {
offsets[id] = pdf.length;
pdf += `${id} 0 obj\n${objects[id]}\nendobj\n`;
}
const xrefOffset = pdf.length;
pdf += `xref\n0 ${objects.length}\n`;
pdf += '0000000000 65535 f \n';
for (let id = 1; id < objects.length; id++) {
pdf += `${String(offsets[id]).padStart(10, '0')} 00000 n \n`;
}
pdf += `trailer\n<< /Size ${objects.length} /Root 1 0 R >>\nstartxref\n${xrefOffset}\n%%EOF`;return btoa(pdf);
}function makeJsPdfBase64FromText(text, data) {
if (!window.jspdf || !window.jspdf.jsPDF) {
throw new Error('jsPDF library is not available');
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
const pageWidth = doc.internal.pageSize.getWidth();
const pageHeight = doc.internal.pageSize.getHeight();
const margin = 14;
const usableWidth = pageWidth - margin * 2;
let y = 18;function addLine(line, opts = {}) {
const fontSize = opts.size || 10;
const lineHeight = opts.lineHeight || 5.3;
doc.setFont('helvetica', opts.bold ? 'bold' : 'normal');
doc.setFontSize(fontSize);
const wrapped = doc.splitTextToSize(String(line || ' '), usableWidth);
wrapped.forEach(part => {
if (y > pageHeight - 18) {
doc.addPage();
y = 18;
}
doc.text(part, margin, y);
y += lineHeight;
});
}addLine('ZIGGIE EXTERIOR PRESENTATION SCORECARD REPORT', { size: 14, bold: true, lineHeight: 7 });
addLine('www.ziggiegroup.com.au | info@ziggiegroup.com.au', { size: 9, lineHeight: 6 });
addLine('', { lineHeight: 4 });const rawLines = String(text || 'No report content was generated.').split(/\r?\n/);
rawLines.forEach(line => {
const trimmed = line.trim();
if (!trimmed) {
y += 3;
return;
}
const isHeading = /^[A-Z0-9 \-]{8,}$/.test(trimmed) || ['EXECUTIVE SUMMARY','TOP 3 PRIORITY AREAS','PHOTOS CAPTURED','RECOMMENDED NEXT STEPS'].includes(trimmed);
addLine(trimmed, { size: isHeading ? 11 : 9.5, bold: isHeading, lineHeight: isHeading ? 6.4 : 5.2 });
});const dataUri = doc.output('datauristring');
return cleanBase64Pdf(dataUri);
}function createFallbackPdfAttachment(data) {
const text = generatePlainTextReport(data);
return {
filename: pdfAttachmentFilename(data),
mimeType: 'application/pdf',
base64: makeSimplePdfBase64FromText(text),
source: 'fallback-pdf-generator'
};
}async function createPdfAttachment(data) {
// Stable no-CDN PDF generator. This creates a real, text-based PDF
// whose decoded file starts with %PDF- and contains the report text.
// It avoids browser screenshot/image PDF generation, which caused blank or damaged attachments.
const reportText = generatePlainTextReport(data);
const clean = cleanBase64Pdf(makeSimplePdfBase64FromText(reportText));if (!looksLikePdfBase64(clean)) {
throw new Error('PDF attachment was not generated correctly.');
}return {
filename: pdfAttachmentFilename(data),
mimeType: 'application/pdf',
base64: clean,
source: 'stable-no-cdn-text-pdf-generator'
};
}function openMailto(data) {
const recipients = getRecipients(data);
const params = new URLSearchParams({ subject: subject(data), body: generatePlainTextReport(data) });
if (recipients.length > 1) params.set('cc', recipients.slice(1).join(','));// Use a temporary link instead of changing window.location.href.
// This keeps the generated report visible on the page while still preparing the email.
const link = document.createElement('a');
link.href = `mailto:${recipients[0]}?${params.toString()}`;
link.target = '_blank';
link.rel = 'noopener';
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}function reportAttachmentFilename(data) {
const safeName = (data.propertyName || 'ziggie-scorecard')
.toLowerCase()
.replace(/[^a-z0-9]+/g,'-')
.replace(/^-|-$/g,'') || 'ziggie-scorecard';
return `${safeName}-exterior-scorecard-report.html`;
}function wordAttachmentFilename(data) {
return reportAttachmentFilename(data).replace(/\.html$/i, '.doc');
}function buildWordCompatibleReportAttachment(data) {
const reportHtmlDocument = buildStandaloneReportAttachmentHtml(data);
return `
Ziggie Exterior Scorecard Report
${reportHtmlDocument} `;
}function buildGoogleDocsCleanReportContent(data) {
const assessorName = escapeHtml(data.assessor || 'The assessor');
const property = escapeHtml(data.propertyName || 'this commercial building');
const priorities = priorityAreas(data.scores);
const priorityRows = priorities.map((item, i) => `
| ${i + 1}. ${escapeHtml(item.title)} | ${Number(item.score || 0)}/10 | ${escapeHtml(item.action || recommendedActionForScore(item, item.score))} |
`).join('');
const scoreRows = data.scores.map(item => `
| ${escapeHtml(item.title)} | ${Number(item.score || 0)}/10 ${escapeHtml(scoreBandLabel(item.score))} | ${escapeHtml(item.notes || 'No notes supplied')} | ${escapeHtml(item.action || recommendedActionForScore(item, item.score))} | ${(item.photos || []).length} |
`).join('');
const photoRows = data.scores.filter(item => item.photos && item.photos.length).map(item => `
| ${escapeHtml(item.title)} | ${item.photos.length} photo${item.photos.length === 1 ? '' : 's'} captured |
`).join('');// This is intentionally an HTML fragment, not a full document.
// Google Docs accepts this more reliably when Make creates the document,
// then the separate Google Docs download step exports it as a PDF.
return `
Ziggie Exterior Presentation Scorecard
${property}
Prepared by ${assessorName} on ${escapeHtml(formatDate(data.reportDate))}
Executive Summary
${assessorName} assessed ${property} and recorded an exterior presentation score of ${data.total}/100. The current status is ${escapeHtml(data.risk.label)}.
${escapeHtml(data.risk.recommendation)}
| Total score | ${data.total}/100 — ${escapeHtml(data.risk.label)} |
| Building type | ${escapeHtml(data.buildingType || 'Not supplied')} |
| Site address | ${escapeHtml(data.siteAddress || 'Not supplied')} |
| Facilities contact | ${escapeHtml(data.fmName || 'Not supplied')} ${escapeHtml(data.fmEmail || 'No email supplied')} ${escapeHtml(data.fmPhone || 'No phone supplied')} |
| Current frequency | ${escapeHtml(data.frequency || 'Not supplied')} |
Why Regular Checks Save Money and Time
Exterior presentation problems are easier to manage when they are found early. Once they become tenant complaints, owner pressure or urgent safety perception concerns, the facilities manager often spends more time coordinating people, explaining delays and approving reactive work.
Scheduled control Inspect → prioritise → complete targeted work → report with photos → re-score. | Reactive response Wait → receive complaint → arrange urgent work → explain delays → repeat. |
Cost and time pressure curve
| Early issue | Regular plan: ███ Low cost/time Reactive: ████ Low–medium cost/time |
| Left unmanaged | Regular plan: ████ Controlled planned work Reactive: ███████ Medium–high cost/time |
| After complaint | Regular plan: █████ Evidence of action Reactive: ██████████ High pressure, disruption and cost |
Simple value: regular checks help the facilities manager spend money on the right areas first, reduce avoidable follow-up and show visible improvement over time.
Top 3 Priority Areas
These are the areas most likely to create complaint risk, owner pressure or poor first impressions if left unmanaged.
| Priority area | Score | Recommended action |
${priorityRows}
Detailed Scorecard Breakdown
| Area | Score | Notes | Recommended Action | Photos |
${scoreRows}
${photoRows ? `
Assessment Photos Captured
Photo files were captured during the scorecard assessment and should be retained with the job record.
` : ''}
Recommended Next Steps
Keeping the exterior of the building clean, safe-looking and well presented should not be expensive or difficult. This scorecard shows where the highest-impact areas are, so Ziggie can help focus the first job on the priority areas that matter most instead of spending money on low-priority work.
- Complete a priority-area exterior reset focused on the lowest-scoring areas.
- Provide before-and-after photos so the facilities manager has proof of action.
- Move the property onto a scheduled exterior presentation control plan.
- Re-score the building over time to show measurable improvement.
Suggested approach: Assess. Prioritise. Complete the priority-area job. Report with photos. Re-score over time.
Book a free 30-minute call with Ziggie: ${CALENDLY_URL}
Ziggie | www.ziggiegroup.com.au | info@ziggiegroup.com.au
`;
}function buildStandaloneReportAttachmentHtml(data) {
const assessorName = escapeHtml(data.assessor || 'The assessor');
const property = escapeHtml(data.propertyName || 'this commercial building');
const priorities = priorityAreas(data.scores);
const priorityRows = priorities.map((item, i) => `
| ${i + 1}. ${escapeHtml(item.title)} | ${Number(item.score || 0)}/10 | ${escapeHtml(item.action || recommendedActionForScore(item, item.score))} |
`).join('');
const scoreRows = data.scores.map(item => `
| ${escapeHtml(item.title)} | ${Number(item.score || 0)}/10 ${escapeHtml(scoreBandLabel(item.score))} | ${escapeHtml(item.notes || 'No notes supplied')} | ${escapeHtml(item.action || recommendedActionForScore(item, item.score))} | ${(item.photos || []).length} |
`).join('');
const photoRows = data.scores.filter(item => item.photos && item.photos.length).map(item => `
| ${escapeHtml(item.title)} | ${item.photos.length} photo${item.photos.length === 1 ? '' : 's'} captured |
`).join('');return `
Ziggie Exterior Scorecard ReportZiggie Exterior Presentation Scorecard
${property}
Prepared by ${assessorName} on ${escapeHtml(formatDate(data.reportDate))}
Executive Summary
${assessorName} assessed ${property} and recorded an exterior presentation score of ${data.total}/100. The current status is ${escapeHtml(data.risk.label)}.
${escapeHtml(data.risk.recommendation)}
| Total score | ${data.total}/100 — ${escapeHtml(data.risk.label)} |
|---|
| Building type | ${escapeHtml(data.buildingType || 'Not supplied')} |
|---|
| Site address | ${escapeHtml(data.siteAddress || 'Not supplied')} |
|---|
| Facilities contact | ${escapeHtml(data.fmName || 'Not supplied')} ${escapeHtml(data.fmEmail || 'No email supplied')} ${escapeHtml(data.fmPhone || 'No phone supplied')} |
|---|
| Current frequency | ${escapeHtml(data.frequency || 'Not supplied')} |
|---|
Why Regular Checks Save Money and Time
Exterior presentation problems are easier to manage when they are found early. Once they become tenant complaints, owner pressure or urgent safety perception concerns, the facilities manager often spends more time coordinating people, explaining delays and approving reactive work.
Scheduled control Inspect → prioritise → complete targeted work → report with photos → re-score. | Reactive response Wait → receive complaint → arrange urgent work → explain delays → repeat. |
Illustrative cost and time pressure curve
| Timing | Regular maintenance plan | Reactive / complaint-driven work |
|---|
| Early issue | Low cost/time — small check and targeted clean | Low to medium — issue is visible but not urgent |
| Left unmanaged | Still controlled — planned work is already scheduled | Medium to high — complaints, follow-up and urgency increase |
| After complaint | Clear proof — photos and scorecard show action | High — reactive cost, stakeholder pressure and disruption |
Simple value: regular checks help the facilities manager spend money on the right areas first, reduce avoidable follow-up and show visible improvement over time.
Top 3 Priority Areas
These are the areas most likely to create complaint risk, owner pressure or poor first impressions if left unmanaged.
| Priority area | Score | Recommended action |
${priorityRows}
Detailed Scorecard Breakdown
| Area | Score | Notes | Recommended Action | Photos |
${scoreRows}
${photoRows ? `
Assessment Photos Captured
Photo files were captured during the scorecard assessment and should be retained with the job record.
` : ''}
Recommended Next Steps
Keeping the exterior of the building clean, safe-looking and well presented should not be expensive or difficult. This scorecard shows where the highest-impact areas are, so Ziggie can help focus the first job on the priority areas that matter most instead of spending money on low-priority work.
- Complete a priority-area exterior reset focused on the lowest-scoring areas.
- Provide before-and-after photos so the facilities manager has proof of action.
- Move the property onto a scheduled exterior presentation control plan.
- Re-score the building over time to show measurable improvement.
Suggested approach: Assess. Prioritise. Complete the priority-area job. Report with photos. Re-score over time.
Book a free 30-minute call with Ziggie: ${CALENDLY_URL}
Ziggie | www.ziggiegroup.com.au | info@ziggiegroup.com.au
`;
}function generateGoogleDocsTemplateFields(data) {
const priorities = priorityAreas(data.scores);
const getPriority = (index) => priorities[index] || { title:'Not supplied', score:'', action:'No priority recorded' };
const p1 = getPriority(0);
const p2 = getPriority(1);
const p3 = getPriority(2);
const assessorName = data.assessor || 'The assessor';
const property = data.propertyName || 'the property';
const detailedBreakdown = data.scores.map((item, index) => {
const score = Number(item.score || 0);
const notes = item.notes || 'No notes supplied';
const action = item.action || recommendedActionForScore(item, score);
const photos = (item.photos || []).length;
return `${index + 1}. ${item.title}\nScore: ${score}/10 — ${scoreBandLabel(score)}\nNotes: ${notes}\nRecommended action: ${action}\nPhotos captured: ${photos}`;
}).join('\n\n');
const maintenanceGraph = [
'Reactive complaint-driven work: Cost ████████████████████ Time ██████████████████ Risk ███████████████████',
'Scheduled exterior checks: Cost ███████ Time ██████ Risk ████',
'',
'Regular scorecard checks help identify small exterior issues before they become visible tenant complaints, owner pressure or urgent make-good work.'
].join('\n');
const nextSteps = [
'1. Complete a priority-area exterior reset focused on the lowest-scoring areas.',
'2. Provide before-and-after photos so the facilities manager has proof of action.',
'3. Move the property onto a scheduled exterior presentation control plan.',
'4. Re-score the building over time to show measurable improvement.'
].join('\n');
return {
templateReportTitle: 'Ziggie Exterior Presentation Scorecard Report',
templateReportDate: formatDate(data.reportDate),
templateAssessorName: assessorName,
templatePropertyName: data.propertyName || 'Not supplied',
templateBuildingType: data.buildingType || 'Not supplied',
templateSiteAddress: data.siteAddress || 'Not supplied',
templateFacilitiesContact: data.fmName || 'Not supplied',
templateFacilitiesEmail: data.fmEmail || 'Not supplied',
templateFacilitiesPhone: data.fmPhone || 'Not supplied',
templateMaintenanceFrequency: data.frequency || 'Not supplied',
templateCurrentConcerns: data.currentConcerns || 'No concerns supplied',
templateTotalScore: String(data.total),
templateScoreOutOf: `${data.total}/100`,
templateRiskLabel: data.risk.label,
templateRiskRecommendation: data.risk.recommendation,
templateExecutiveSummary: `${assessorName} assessed ${property} and recorded an exterior presentation score of ${data.total}/100. The current status is ${data.risk.label}.`,
templatePriority1Title: p1.title || 'Not supplied',
templatePriority1Score: p1.score !== '' ? `${p1.score}/10` : '',
templatePriority1Action: p1.action || recommendedActionForScore(p1, p1.score),
templatePriority2Title: p2.title || 'Not supplied',
templatePriority2Score: p2.score !== '' ? `${p2.score}/10` : '',
templatePriority2Action: p2.action || recommendedActionForScore(p2, p2.score),
templatePriority3Title: p3.title || 'Not supplied',
templatePriority3Score: p3.score !== '' ? `${p3.score}/10` : '',
templatePriority3Action: p3.action || recommendedActionForScore(p3, p3.score),
templateMaintenanceGraph: maintenanceGraph,
templateDetailedBreakdown: detailedBreakdown,
templateNextSteps: nextSteps,
templateCalendlyUrl: CALENDLY_URL,
templateFooter: 'Ziggie | www.ziggiegroup.com.au | info@ziggiegroup.com.au'
};
}function generateGoogleDocsPlainPdfContent(data) {
const property = data.propertyName || 'Not supplied';
const assessorName = data.assessor || 'The assessor';
const priorities = priorityAreas(data.scores);
const divider = '────────────────────────────────────────';
const rows = data.scores.map(item => {
const action = item.action || recommendedActionForScore(item, item.score);
const notes = item.notes || 'No notes supplied';
const photos = (item.photos || []).length;
return `${item.title}\nScore: ${Number(item.score || 0)}/10 — ${scoreBandLabel(item.score)}\nNotes: ${notes}\nRecommended action: ${action}\nPhotos captured: ${photos}\n`;
});
const graph = [
'WHY REGULAR CHECKS SAVE MONEY AND TIME',
'',
'The aim is to move exterior care from reactive complaint-driven work to planned presentation control.',
'',
'Reactive maintenance after complaints',
'Cost exposure: ████████████████████ High',
'Time pressure: ██████████████████ High',
'Complaint risk: ███████████████████ High',
'',
'Scheduled checks and planned exterior care',
'Cost exposure: ███████ Controlled',
'Time pressure: ██████ Lower',
'Complaint risk: ████ Lower',
'',
'Why this matters: small exterior issues are usually cheaper and easier to correct before tenants, owners, visitors or managers start raising complaints. Regular scorecard checks make the work visible, prioritised and easier to budget.'
].join('\n');
return [
'ZIGGIE EXTERIOR PRESENTATION SCORECARD REPORT',
'www.ziggiegroup.com.au | info@ziggiegroup.com.au',
divider,
'',
'EXECUTIVE SUMMARY',
`${assessorName} assessed ${property} and recorded an exterior presentation score of ${data.total}/100. The current status is ${data.risk.label}.`,
'',
data.risk.recommendation,
'',
`Building type: ${data.buildingType || 'Not supplied'}`,
`Site address: ${data.siteAddress || 'Not supplied'}`,
`Facilities contact: ${data.fmName || 'Not supplied'}`,
`Email: ${data.fmEmail || 'Not supplied'}`,
`Phone: ${data.fmPhone || 'Not supplied'}`,
'',
divider,
'',
'TOP 3 PRIORITY AREAS',
'These are the areas most likely to create complaint risk, owner pressure or poor first impressions if left unmanaged.',
'',
...priorities.map((item, i) => `${i + 1}. ${item.title}\nScore: ${Number(item.score || 0)}/10\nRecommended action: ${item.action || recommendedActionForScore(item, item.score)}\n`),
'',
divider,
'',
graph,
'',
divider,
'',
'DETAILED SCORECARD BREAKDOWN',
...rows,
divider,
'',
'RECOMMENDED NEXT STEPS',
'1. Complete a priority-area exterior reset focused on the lowest-scoring areas.',
'2. Provide before-and-after photos so the facilities manager has proof of action.',
'3. Move the property onto a scheduled exterior presentation control plan.',
'4. Re-score the building over time to show measurable improvement.',
'',
'Suggested approach: Assess. Prioritise. Complete the priority-area job. Report with photos. Re-score over time.',
'',
`Book a free 30-minute call with Ziggie: ${CALENDLY_URL}`,
'',
'Ziggie | www.ziggiegroup.com.au | info@ziggiegroup.com.au'
].join('\n');
}async function emailReport(data) {
if (EMAIL_WEBHOOK_ENDPOINT) {
const emailHtml = generateNiceEmailHtml(data);
const reportText = generatePlainTextReport(data);
const screenReportHtml = renderReport(data);
const clientEmail = isValidEmail(data.fmEmail) ? data.fmEmail.trim() : '';
const allRecipients = getRecipients(data).join(',');// No-API attachment workflow: send a Word-compatible report attachment directly to Gmail.
// This avoids third-party HTML-to-PDF API keys while keeping a formatted downloadable report.
const reportHtmlDocument = buildStandaloneReportAttachmentHtml(data);
const googleDocsReportContent = generateGoogleDocsPlainPdfContent(data);
const googleDocsTemplateFields = generateGoogleDocsTemplateFields(data);
const pdfOutputFilename = reportAttachmentFilename(data).replace(/\.html$/i, '.pdf');
const htmlPreviewFilename = reportAttachmentFilename(data);
const htmlPreviewData = reportHtmlDocument;
const wordFilename = wordAttachmentFilename(data);
const wordData = buildWordCompatibleReportAttachment(data);const makeAttachment = {
filename: wordFilename,
fileName: wordFilename,
name: wordFilename,
mimeType: 'application/msword',
contentType: 'application/msword',
data: wordData
};const res = await fetch(EMAIL_WEBHOOK_ENDPOINT, {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({
to:getRecipients(data),
allRecipients,
ziggieEmail:ZIGGIE_REPORT_EMAIL,
clientEmail,
formEmail:clientEmail,
subject:subject(data),
...googleDocsTemplateFields,
reportVersion:'google-docs-coloured-template-fields',
body:emailHtml,
content:emailHtml,
emailHtml,
// Keep the old field names too, but point them to the cleaned report so existing Make mappings do not send the old version.
reportHtml: googleDocsReportContent,
screenReportHtml,
reportText,
reportHtmlDocument: googleDocsReportContent,
// Simple root-level fields for Make Google Docs PDF workflow.
// Map one of these into Google Docs > Create a Document > Content.
googleDocContent: googleDocsReportContent,
googleDocsContent: googleDocsReportContent,
googleDocumentContent: googleDocsReportContent,
googleDocsPdfContent: googleDocsReportContent,
makeGoogleDocContent: googleDocsReportContent,
pdfReportContent: googleDocsReportContent,
documentContent: googleDocsReportContent,
docContent: googleDocsReportContent,
reportForGoogleDocs: googleDocsReportContent,
googleDocTitle: 'Ziggie Exterior Scorecard Report',
pdfFileName: pdfOutputFilename,
pdfOutputFileName: pdfOutputFilename,
reportHtmlForPdf: googleDocsReportContent,
htmlForPdf: googleDocsReportContent,
makePdfHtml: googleDocsReportContent,
pdfOutputFilename,
pdfFilename: pdfOutputFilename,
pdfFileName: pdfOutputFilename,
pdfAttachmentFilename: pdfOutputFilename,
pdfAttachmentFileName: pdfOutputFilename,
googleDocsPdfFilename: pdfOutputFilename,
googleDocsPdfFileName: pdfOutputFilename,
makePdfAttachmentFilename: pdfOutputFilename,
makePdfAttachmentFileName: pdfOutputFilename,
// Keep these explicit flags for the Make scenario: the Gmail attachment should come from
// Google Docs > Download a Document > Data, using the PDF filename above.
includePdfAttachment: true,
pdfAttachmentSource: 'Google Docs Download a Document > Data',
gmailAttachmentDataSource: 'Map the Data output from Google Docs Download a Document',
attachment: makeAttachment,
attachmentFilename: wordFilename,
attachmentFileName: wordFilename,
attachmentMimeType: 'application/msword',
attachmentData: wordData,
wordAttachmentFilename: wordFilename,
wordAttachmentFileName: wordFilename,
wordAttachmentMimeType: 'application/msword',
wordAttachmentData: wordData,
emailAttachmentFilename: wordFilename,
emailAttachmentFileName: wordFilename,
emailAttachmentMimeType: 'application/msword',
emailAttachmentData: wordData,
htmlAttachmentFilename: htmlPreviewFilename,
htmlAttachmentFileName: htmlPreviewFilename,
htmlAttachmentMimeType: 'text/html',
htmlAttachmentData: htmlPreviewData,
attachmentFormat:'word-compatible-html-doc-no-api',
pdfAttachmentStrategy:'browser-open-report-or-use-third-party-pdf-api',
// Do not map these old browser-generated PDF fields in Make.
pdfbase64:'',
pdfBase64:'',
pdfBase64Clean:'',
pdfFirstCharacters:'',
pdfSource:'disabled-use-make-html-to-pdf',
reportTextPreview:reportText.slice(0, 500),
data
})
});
if (!res.ok) throw new Error('Email webhook failed');
showToast('Report emailed to Ziggie and contact');
} else {
openMailto(data);
showToast('Email prepared. Add webhook URL for automatic report email.');
}
}function generateReport() {
ensureScoreInputsRendered();
const data = collectData();
latestReportData = data;
$('reportOutput').innerHTML = renderReport(data);
$('reportWrap').classList.add('visible');
updateLiveScore();
updateScoreMeanings();
saveReportSnapshot(data);
setTimeout(() => $('reportWrap').scrollIntoView({behavior:'smooth', block:'start'}), 80);
showToast('Scorecard report generated');
return data;
}async function emailCurrentReport() {
// Keep the Email Report button aligned with Generate Scorecard Report:
// always rebuild the report from the current form data first, then send it to Make.
const data = generateReport();
try {
await emailReport(data);
} catch(e) {
console.warn(e);
showToast('Report generated, but automatic email failed. Check Make scenario.');
}
}function printReport() {
if (!$('reportWrap').classList.contains('visible')) generateReport();
window.print();
}async function downloadEmailPdfTest() {
const data = latestReportData || generateReport();
try {
const html = buildStandaloneReportAttachmentHtml(data);
const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
const url = URL.createObjectURL(blob);
const win = window.open(url, '_blank', 'noopener,noreferrer');
if (!win) {
// Popup blockers can prevent a new tab; fall back to direct navigation in a hidden link.
const link = document.createElement('a');
link.href = url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
setTimeout(() => URL.revokeObjectURL(url), 60000);
showToast('Report opened in a new browser window');
} catch (e) {
console.warn(e);
showToast('Could not open the report in a new browser window.');
}
}function resetForm() {
if (!confirm('Clear the scorecard form?')) return;
document.querySelectorAll('input, textarea').forEach(el => el.value = '');
document.querySelectorAll('select').forEach(el => { el.selectedIndex = 0; });
document.querySelectorAll('.score-select').forEach(el => el.value = '5');
Object.keys(photosByKey).forEach(key => { photosByKey[key] = []; renderPhotoPreview(key); });
latestReportData = null;
$('reportWrap').classList.remove('visible');
setToday(); updateLiveScore(); updateScoreMeanings();
}function initialiseScorecardApp() {
if (window.ziggieScorecardInitialised) return;
window.ziggieScorecardInitialised = true;setToday();
renderScoreInputs();
updateScoreMeanings();
updateLiveScore();const generateBtn = $('generateReportBtn');
if (generateBtn) {
generateBtn.addEventListener('click', async () => {
const data = generateReport();
try {
await emailReport(data);
} catch(e) {
console.warn(e);
showToast('Report generated, but automatic email failed. Check Make scenario.');
}
});
}const resetBtn = $('resetBtn');
if (resetBtn) resetBtn.addEventListener('click', resetForm);const emailBtn = $('emailReportBtn');
if (emailBtn) emailBtn.addEventListener('click', emailCurrentReport);const downloadEmailPdfBtn = $('downloadEmailPdfBtn');
if (downloadEmailPdfBtn) downloadEmailPdfBtn.addEventListener('click', downloadEmailPdfTest);['reportDate','assessor','propertyName','buildingType','siteAddress','fmName','fmEmail','fmPhone','frequency','currentConcerns'].forEach(id => {
const el = $(id);
if (el) {
el.addEventListener('input', updateLiveScore);
el.addEventListener('change', updateLiveScore);
}
});
}if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialiseScorecardApp);
} else {
initialiseScorecardApp();
}