// ---- questions.js ----
const SECTIONS = [
{
"title": "Accuracy and reliability",
"questions": [
{
"id": 1,
"text": "Are all business bank and credit-card accounts reconciled to their statements every month, with differences investigated rather than forced to balance?"
},
{
"id": 2,
"text": "Are transactions recorded in the correct accounts without unresolved duplicates, omissions, or uncategorized entries?"
},
{
"id": 3,
"text": "Can recorded transactions be traced to invoices, receipts, contracts, or other appropriate supporting records?"
},
{
"id": 4,
"text": "Are balance-sheet balances, such as loans, customer amounts due, and supplier amounts owed, checked against supporting schedules or statements?"
}
]
},
{
"title": "Financial visibility",
"questions": [
{
"id": 5,
"text": "Do you receive a monthly profit-and-loss statement and balance sheet that you can understand?"
},
{
"id": 6,
"text": "Can you explain the main reasons profit increased or decreased compared with the previous period?"
},
{
"id": 7,
"text": "Can you see which customers owe you money and which supplier bills are due or overdue?"
},
{
"id": 8,
"text": "Can you explain why reported profit differs from the change in your bank balance?"
}
]
},
{
"title": "Accounting methodology and consistency",
"questions": [
{
"id": 9,
"text": "Has an appropriate accounting professional confirmed the accounting basis used for your management reports, such as cash or accrual, and explained why it fits your needs?"
},
{
"id": 10,
"text": "Are income and expenses recorded consistently in the correct periods under that chosen basis?"
},
{
"id": 11,
"text": "Are customer deposits and advance payments treated under a documented policy appropriate to your accounting basis?"
},
{
"id": 12,
"text": "Are equipment purchases, loan principal, interest, and owner contributions or withdrawals recorded distinctly and treated consistently?"
}
]
},
{
"title": "Cost accounting and profitability",
"questions": [
{
"id": 13,
"text": "Are direct delivery costs, such as materials, production labor, and subcontractors, distinguished consistently from general operating expenses?"
},
{
"id": 14,
"text": "Are direct costs assigned to the relevant job, project, product, or service rather than left in a general expense pool?"
},
{
"id": 15,
"text": "Where shared costs need to be allocated, is there a documented, reasonable allocation method that is applied consistently?"
},
{
"id": 16,
"text": "Can you review actual gross margins for your main revenue streams and investigate unexpected changes?"
}
]
},
{
"title": "Inventory, work in progress, and assets",
"questions": [
{
"id": 17,
"text": "Where you hold inventory, are recorded quantities checked against physical counts and differences investigated?"
},
{
"id": 18,
"text": "Where you hold inventory, are inventory values and the costs released when items are sold reviewed under a consistent accounting policy?"
},
{
"id": 19,
"text": "Where you have unfinished jobs or production, is work in progress tracked and reviewed for the appropriate period-end treatment?"
},
{
"id": 20,
"text": "Is there an up-to-date record of significant business assets, including purchases, disposals, and depreciation where applicable?"
}
]
},
{
"title": "Customer billing and collections",
"questions": [
{
"id": 21,
"text": "Are invoices issued promptly and checked against agreed prices, completed work, or delivered goods?"
},
{
"id": 22,
"text": "Are customer receipts applied to the correct invoices or sales records, with differences investigated?"
},
{
"id": 23,
"text": "Is someone responsible for reviewing overdue invoices and following up with customers?"
},
{
"id": 24,
"text": "Are customer credits, refunds, disputes, and write-offs documented and approved?"
}
]
},
{
"title": "Supplier bills and cash commitments",
"questions": [
{
"id": 25,
"text": "Are supplier bills entered promptly and checked for duplicates?"
},
{
"id": 26,
"text": "Can you identify bills due over the next 30 days from current, reliable records?"
},
{
"id": 27,
"text": "Are payments matched to approved bills or other supporting documents?"
},
{
"id": 28,
"text": "Are recurring charges and supplier balances reviewed so errors and unexpected charges are investigated?"
}
]
},
{
"title": "Payroll and tax-record readiness",
"questions": [
{
"id": 29,
"text": "Are payroll totals and related liabilities reconciled between the payroll system, accounting records, and bank activity?"
},
{
"id": 30,
"text": "Is responsibility for payroll filings and remittances clearly assigned, with completion checked?"
},
{
"id": 31,
"text": "Are sales-tax or similar transaction-tax balances reconciled to the relevant filings and payments where applicable?"
},
{
"id": 32,
"text": "Are contractor records and year-end accounting schedules kept organized for the person responsible for required reporting and tax preparation?"
}
]
},
{
"title": "Controls and accountability",
"questions": [
{
"id": 33,
"text": "Are business transactions kept separate from personal spending, with exceptions identified and recorded appropriately?"
},
{
"id": 34,
"text": "Are payment approvals and independent checks defined, including a compensating owner review where duties cannot be separated?"
},
{
"id": 35,
"text": "Are accounting and banking permissions limited to each person's responsibilities and removed when no longer needed?"
},
{
"id": 36,
"text": "Are significant adjustments supported by explanations and reviewed by someone other than the preparer?"
}
]
},
{
"title": "Timeliness, capacity, and continuity",
"questions": [
{
"id": 37,
"text": "Are the books completed and reviewed by an agreed monthly deadline that supports your decisions?"
},
{
"id": 38,
"text": "Is there a clear owner for bookkeeping tasks, deadlines, and resolving outstanding questions?"
},
{
"id": 39,
"text": "Can routine bookkeeping be completed without regularly pulling you or other key staff away from sales, customers, or operations?"
},
{
"id": 40,
"text": "Is there a documented process and backup arrangement so bookkeeping can continue during absence, turnover, or growth?"
}
]
}
];
// ---- model.js ----
const VERSION = 1;
const WEIGHTS = [15,15,10,15,10,5,5,10,10,5];
const SCORING_VERSION = 'risk-weighted-health-v1';
const OPTIONS = [
{value:'yes', label:'Yes, consistently', short:'Yes', points:0},
{value:'partly', label:'Partly', short:'Partly', points:1},
{value:'no', label:'No', short:'No', points:2},
{value:'unsure', label:'Not sure', short:'Not sure', points:2},
{value:'na', label:'Not applicable', short:'N/A', points:null},
];
const FLAGS = [
'A bank or credit-card account has not been reconciled for three months or more.',
'Unexplained differences are being cleared through unsupported adjustments.',
'There are unexplained payments, missing funds, or suspected unauthorized access.',
'A payroll or tax filing/payment may be overdue, or a related notice remains unresolved.',
'You cannot reliably determine current cash, customer balances, or supplier obligations.',
'Missing or unreliable records are preventing an imminent financing, tax, or transaction deadline from being met.',
];
const SHORT_NAMES = ['Accuracy','Visibility','Accounting methods','Cost accounting','Inventory & assets','Billing & collections','Bills & commitments','Payroll & tax records','Controls','Timeliness & capacity'];
const GUIDANCE = [
{title:'Start with the evidence.', text:'Think about your latest reconciliations and supporting records, not simply whether the software shows a balance.'},
{title:'Can you use your numbers?', text:'Answer based on the reports you actually receive and understand, not the reports your software could produce.'},
{title:'Consistency matters.', text:'Cash-basis accounting is not automatically a weakness. Consider whether your method fits its purpose and is applied consistently.'},
{title:'Follow the cost of delivery.', text:'Apply these questions to the jobs, projects, products, services, or customers that matter to your business.'},
{title:'Exclude only what does not apply.', text:'No inventory? Use Not applicable for inventory questions, but assess equipment and other business assets separately.'},
{title:'From invoice to payment.', text:'Consider how customer transactions are recorded and followed up. Exclude only activities that genuinely do not apply.'},
{title:'Know what you owe.', text:'Think about whether your records help you make payment decisions before money leaves the bank.'},
{title:'Records, not tax advice.', text:'This section tests recordkeeping and responsibility, not the legal correctness of a tax treatment.'},
{title:'Who checks the work?', text:'In a small team, a documented owner review can compensate where full separation of duties is impractical.'},
{title:'Make the process dependable.', text:'Consider the usual workload and what happens when your regular bookkeeper is unavailable.'},
];
function emptyState() {
return {version:VERSION, answers:{}, reasons:{}, flags:[], flagsReviewed:false, business:'', respondent:'', firstName:'', lastName:'', email:'', deliveryConsent:false, industry:'', software:'', provider:'', reconciled:'', section:0, plan:Array.from({length:3},()=>({gap:'',action:'',owner:'',date:''}))};
}
function complete(answer, reason='') {
return OPTIONS.some(x=>x.value===answer) && (answer!=='na' || reason.trim().length>0);
}
function calculate(ids, answers) {
const values=ids.map(id=>answers[id]).filter(v=>OPTIONS.some(o=>o.value===v));
const applicable=values.filter(v=>v!=='na');
const points=applicable.reduce((n,v)=>n+OPTIONS.find(o=>o.value===v).points,0);
return {answered:values.length, applicable:applicable.length, points, max:applicable.length*2, excluded:values.filter(v=>v==='na').length, unsure:values.filter(v=>v==='unsure').length, score:applicable.length?Math.round(points/(applicable.length*2)*100):null};
}
// Internal gap-point calculations remain available for the existing diagnostic rules.
// Public-facing scores use the positive, 0–100 health scale below.
function weightedAverage(sectionResults, weights=WEIGHTS) {
if(sectionResults.length!==weights.length || weights.some(w=>!Number.isFinite(w)||w<0)) throw Error('Invalid section weights.');
const activeWeight=sectionResults.reduce((sum,s,i)=>sum+(s.applicable?weights[i]:0),0);
const sections=sectionResults.map((s,i)=>{
const raw=s.applicable?100*(1-s.points/s.max):null;
const effectiveWeight=s.applicable&&activeWeight?weights[i]/activeWeight:0;
return {...s,rawHealth:raw,healthScore:raw===null?null:Math.round(raw),weight:weights[i],effectiveWeight,contribution:raw===null?0:raw*effectiveWeight};
});
const rawScore=activeWeight?sections.reduce((sum,s)=>sum+s.contribution,0):null;
return {score:rawScore===null?null:Math.round(rawScore),rawScore,activeWeight,sections,activeSections:sections.filter(s=>s.applicable).length};
}
function healthBand(score) {
return band(score===null?null:100-score);
}
function contactError(state) {
if(!state.firstName?.trim())return 'Please enter your first name.';
if(!state.lastName?.trim())return 'Please enter your last name.';
if(!state.business?.trim())return 'Please enter your business name.';
if(!/^[^\s@<>]+@[^\s@<>]+\.[^\s@<>]+$/.test(state.email?.trim()||''))return 'Please enter a valid email address for your results.';
if(!state.deliveryConsent)return 'Please confirm how your contact details and results will be used.';
return '';
}
function band(score) {
if(score===null) return {label:'Not enough applicable information', description:'All questions were excluded. No score or service-fit recommendation can be calculated. Review the exclusions before using this assessment.', action:'Review your answers and confirm which activities genuinely do not apply.', tone:'neutral'};
if(score<20) return {label:'An established foundation', description:'Your current process appears broadly established based on your answers. Check the exceptions before deciding whether additional support is needed.', action:'Verify exceptions and consider periodic review. Ongoing outsourced bookkeeping may not be necessary.', tone:'good'};
if(score<40) return {label:'Targeted improvements needed', description:'Specific weaknesses or inconsistencies need attention. A focused review can help distinguish one-time repair work from recurring needs.', action:'Consider a targeted review, cleanup, process improvement, training, or limited recurring support.', tone:'watch'};
if(score<60) return {label:'Structured support worth reviewing', description:'Gaps span enough activities to justify a structured support review. Decide who should own the routine work and who should review it.', action:'Evaluate recurring bookkeeping support and the level of accounting review required.', tone:'watch'};
return {label:'A diagnostic review comes first', description:'Your answers indicate extensive gaps or uncertainty. Establish what is reliable, identify catch-up work, and define clear ongoing ownership.', action:'Prioritize a diagnostic review before scoping cleanup and ongoing support.', tone:'high'};
}
function recommendations(sectionResults, state) {
const high=i=>sectionResults[i].score!==null && sectionResults[i].score>=50;
const out=[];
if(state.flags.length) out.push({title:'Review priority flags first',text:'Arrange a focused review of the flagged issues rather than relying on the overall score. Flags do not by themselves establish fraud, insolvency, or noncompliance.'});
if(state.flags.includes(2)) out.push({title:'Review bank and system access',text:'For suspected unauthorized transactions, consider immediate bank and access-control review.'});
if(state.flags.includes(3)||high(7)) out.push({title:'Payroll or tax specialist',text:'Verify filing responsibilities, reconcile related balances, and involve the appropriate specialist for overdue filings, notices, or uncertain tax treatment.'});
if(high(0)) out.push({title:'Bookkeeping review and possible cleanup',text:'Inspect reconciliations and supporting records. Define any historical catch-up or correction work before deciding the recurring scope.'});
if(high(2)||high(8)) out.push({title:'Controller or accounting oversight',text:'Review accounting policies, balance-sheet support, adjustments, permissions, and approval controls alongside the bookkeeping process.'});
if(high(3)||high(4)) out.push({title:'Cost-accounting or industry support',text:'Review job costing, cost allocations, inventory valuation, work in progress, and asset records as applicable.'});
if(high(5)||high(6)||high(9)) out.push({title:'Recurring bookkeeping and capacity',text:'Set clear ownership and deadlines for invoicing, supplier bills, reconciliations, and monthly reporting. Include a backup arrangement.'});
if(high(1)) out.push({title:sectionResults[0].score!==null && sectionResults[0].score<20 && sectionResults[2].score!==null && sectionResults[2].score<20?'Financial interpretation or CFO advice':'Improve reporting after verifying the books',text:'Clarify which reports and explanations support owner decisions. Establish reliable records first; consider advisory help where the remaining need is interpretation or planning.'});
if(!out.length) out.push({title:'Verify exceptions before adding services',text:'Review every Partly, No, and Not sure response. Target the specific gaps; do not assume a full ongoing engagement is needed.'});
return out;
}
function validateImport(raw) {
if(!raw || raw.version!==VERSION || !raw.answers || typeof raw.answers!=='object' || Array.isArray(raw.answers)) throw Error('This is not a compatible saved assessment.');
const clean=emptyState();
for(const [id,v] of Object.entries(raw.answers)) {
if(!/^\d+$/.test(id) || +id<1 || +id>40 || !OPTIONS.some(o=>o.value===v)) throw Error('The saved assessment contains an invalid answer.');
clean.answers[id]=v;
}
if(raw.reasons && typeof raw.reasons==='object') for(const [id,v] of Object.entries(raw.reasons)) {
if(/^\d+$/.test(id) && +id>=1 && +id<=40 && typeof v==='string') clean.reasons[id]=v.slice(0,500);
}
for(const key of ['business','respondent','firstName','lastName','email','industry','software','provider','reconciled']) if(typeof raw[key]==='string') clean[key]=raw[key].slice(0,160);
clean.deliveryConsent=raw.deliveryConsent===true;
clean.section=Number.isInteger(raw.section)&&raw.section>=0&&raw.section<=10?raw.section:0;
if(Array.isArray(raw.flags)) clean.flags=[...new Set(raw.flags.filter(i=>Number.isInteger(i)&&i>=0&&i<6))];
clean.flagsReviewed=raw.flagsReviewed===true;
if(Array.isArray(raw.plan)) clean.plan=clean.plan.map((r,i)=>Object.fromEntries(Object.keys(r).map(k=>[k,typeof raw.plan[i]?.[k]==='string'?raw.plan[i][k].slice(0,500):''])));
const firstIncomplete=Array.from({length:40},(_,i)=>i+1).find(id=>!complete(clean.answers[id],clean.reasons[id]));
if(firstIncomplete!==undefined) clean.section=Math.min(clean.section,Math.floor((firstIncomplete-1)/4));
return clean;
}
// ---- email-results.js ----
const BOOKING_URL='https://stan-bcfoforhire.zohobookings.com/#/customer/bcfoforhire';
const FREE_RESOURCES_URL='https://businesscfoforhire.com/free-resources/';
const EMAIL_CC='stan@bcfoforhire.com';
const EMAIL_SUBJECT='Next Steps Following Your Bookkeeping Assessment';
const CONTACT_NOTICE='The contact details entered here will be used to email your assessment results, with a copy kept on file at Business CFO for hire. When you request your results email, you explicitly authorize Business CFO for Hire to follow up and add you to the monthly mailing list. No passwords are required.';
const escapeHtml=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
function nextStepsParagraphs(firstName,score) {
return [
`Hi ${firstName.trim()},`,
'Thank you for completing our bookkeeping assessment.',
score===null?'Your weighted average score could not be calculated because every question was marked not applicable.':`Your weighted average score is ${score} out of 100.`,
'The closer you get to an ideal score, the better visibility, accuracy and transparency allows you to base decisions on facts and data not on gut-feel.',
'Your responses helped us understand your current processes, the challenges you’re facing, and where additional support could make the biggest difference.',
'The next step is a short conversation to review your responses, clarify any gaps, and discuss what’s working well and what could be improved. From there, we can recommend a practical scope of support and outline the associated fees before you make any commitment.',
'Our goal is to help you keep your books accurate, current, and useful, so you can spend less time sorting out the numbers and more time running your business.',
`Please reply with a couple of convenient times for a 20–30-minute call, or book directly here: ${BOOKING_URL}.`,
'I look forward to discussing the best next steps for your business.',
'Best,',
'Stan Alhadeff\nBusiness CFO for Hire',
];
}
function letterHtml(firstName,score) {
return nextStepsParagraphs(firstName,score).map(p=>{
if(p.includes(BOOKING_URL))return `
Please reply with a couple of convenient times for a 20–30-minute call, or book directly here: Schedule your 20–30-minute call.
`;
return `
${escapeHtml(p).replace(/\n/g,' ')}
`;
}).join('');
}
// A server or Zoho Flow adapter must call this function after validating the
// raw answers. Do not trust client-supplied scores or permit arbitrary CCs.
function prepareEmail(rawState,submissionId,now=new Date()) {
const state=validateImport(rawState);
const err=contactError(state);
if(err)throw Error(err);
if(!/^[a-zA-Z0-9_-]{12,100}$/.test(submissionId))throw Error('Invalid submission identifier.');
if(!state.flagsReviewed)throw Error('Priority flags must be reviewed.');
if(SECTIONS.some(s=>s.questions.some(q=>!complete(state.answers[q.id],state.reasons[q.id]))))throw Error('Complete all questions and exclusion reasons.');
const sectionGaps=SECTIONS.map(s=>calculate(s.questions.map(q=>q.id),state.answers));
const weighted=weightedAverage(sectionGaps);
const result=healthBand(weighted.score);
const suggestions=recommendations(sectionGaps,state);
const scoreText=value=>value===null?'N/A':`${value}/100`;
const name=value=>OPTIONS.find(o=>o.value===value)?.label||'Not answered';
const details=[
'ASSESSMENT RESULTS',
`Business: ${state.business}`,
`Completed by: ${state.firstName} ${state.lastName}`,
`Completed: ${now.toISOString()}`,
`Assessment ID: ${submissionId}`,
`Weighted average score: ${scoreText(weighted.score)}. Ideal score: 100.`,
state.flags.length?'Priority review needed: flagged issues take precedence over a reassuring score.':result.label,
result.description,
'SECTION SCORES AND WEIGHTS',
...weighted.sections.map((s,i)=>`${SECTIONS[i].title}: ${scoreText(s.healthScore)}; base weight ${s.weight}%; effective weight ${(s.effectiveWeight*100).toFixed(2)}%; ${s.applicable}/4 applicable questions.`),
'PRIORITY FLAGS',
...(state.flags.length?state.flags.map(i=>FLAGS[i]):['None selected.']),
...(state.flags.includes(2)?['For suspected unauthorized transactions, consider immediate bank and access-control review.']:[]),
...(state.flags.includes(3)?['For overdue filings, tax notices, or uncertain tax treatment, involve the appropriate payroll or tax professional.']:[]),
'YOUR NEXT THREE ACTIONS',
...state.plan.map((p,i)=>`${i+1}. Gap: ${p.gap||'Not entered'} | Action: ${p.action||'Not entered'} | Owner: ${p.owner||'Not entered'} | Target: ${p.date||'Not entered'}`),
'SUPPORT TO CONSIDER',
...(weighted.score===null?['Confirm applicability before drawing any service-fit conclusion.']:suggestions.map(r=>`${r.title}: ${r.text}`)),
'COMPLETE ANSWER RECORD',
...SECTIONS.flatMap(s=>[s.title,...s.questions.map(q=>`${q.id}. ${q.text}\nAnswer: ${name(state.answers[q.id])}${state.answers[q.id]==='na'?`\nExclusion reason: ${state.reasons[q.id]}`:''}`)]),
'METHOD AND LIMITATIONS',
`Yes = 100, Partly = 50, No and Not sure = 0. Each section averages its applicable answers. Final score = sum of unrounded section scores × section weights / sum of applicable section weights. Round only the final overall score. Base weights: ${WEIGHTS.join(', ')} percent in section order.`,
'Not applicable answers are excluded within a section; a wholly excluded section is removed and remaining weights are rescaled. Not sure is scored conservatively and requires verification; it does not prove an error.',
'Interpretive bands: 81–100 established foundation; 61–80 targeted improvements; 41–60 structured support review; 0–40 diagnostic review first. Sections at 50 or below need follow-up. These weights and bands are screening choices, not validated benchmarks or an audit opinion. Priority flags take precedence over a reassuring total.',
'This self-assessment does not replace an audit or professional accounting, tax, or legal advice.',
];
const text=nextStepsParagraphs(state.firstName,weighted.score).join('\n\n')+'\n\n'+details.join('\n\n');
const html=`
${letterHtml(state.firstName,weighted.score)}
${escapeHtml(details.join('\n\n'))}
`;
return {
assessmentId:submissionId,scoringVersion:SCORING_VERSION,completedAt:now.toISOString(),
contact:{First_Name:state.firstName.trim(),Last_Name:state.lastName.trim(),Email:state.email.trim(),Business_Name:state.business.trim()},
consent:{accepted:true,notice:CONTACT_NOTICE},
weightedScore:weighted.score,weightedRawScore:weighted.rawScore,sections:weighted.sections.map((s,i)=>({name:SECTIONS[i].title,score:s.healthScore,rawScore:s.rawHealth,weight:s.weight,effectiveWeight:s.effectiveWeight,applicable:s.applicable})),
priorityFlags:state.flags.map(i=>FLAGS[i]),actionPlan:state.plan,
email:{to:state.email.trim(),cc:[EMAIL_CC],subject:EMAIL_SUBJECT,text,html},
answers:SECTIONS.flatMap(s=>s.questions.map(q=>({id:q.id,question:q.text,answer:state.answers[q.id],exclusionReason:state.answers[q.id]==='na'?state.reasons[q.id]:null}))),
};
}
// ---- integration.js ----
// Set only to an authenticated/rate-limited server-side intake endpoint after
// the Zoho module, fields, verified sender, and workflow are confirmed.
// Never put a Zoho API credential, Flow webhook secret, or OAuth token here.
const EMAIL_ENDPOINT = null;
const EMAIL_SETUP_MESSAGE = 'Email delivery is awaiting Zoho CRM setup. No results have been sent. You can still review your results and schedule a call.';
async function submitEmailRequest(assessment,submissionId,endpoint=EMAIL_ENDPOINT) {
if(!endpoint)throw Error(EMAIL_SETUP_MESSAGE);
const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),25000);
try {
const response=await fetch(endpoint,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({assessment,submissionId}),signal:controller.signal});
const data=await response.json().catch(()=>null);
if(!response.ok||!data||!['queued','sent'].includes(data.status))throw Error('Zoho did not confirm this email request. No successful send has been confirmed; please contact us before retrying.');
return data;
} catch(err) {
if(err.name==='AbortError')throw Error('The request timed out. Delivery is unconfirmed; please contact us before retrying to avoid duplicate emails.');
throw err;
} finally {clearTimeout(timer);}
}
// ---- app.js ----
let state=emptyState(),view='assessment',error='',invalid=[];
let theme=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';
let emailStatus='idle',emailMessage='',submissionId=makeId();
const app=document.querySelector('#bkh-app');
const allQuestions=SECTIONS.flatMap(s=>s.questions),ids=allQuestions.map(q=>q.id);
const escape=escapeHtml;
const answeredCount=()=>ids.filter(id=>complete(state.answers[id],state.reasons[id])).length;
const sectionCount=i=>SECTIONS[i].questions.filter(q=>complete(state.answers[q.id],state.reasons[q.id])).length;
const stats=()=>SECTIONS.map(s=>calculate(s.questions.map(q=>q.id),state.answers));
const optionName=v=>OPTIONS.find(o=>o.value===v)?.label||'Not answered';
const logo=``;
function makeId(){return 'bk_'+(globalThis.crypto?.randomUUID?.()||`${Date.now()}_${Math.random().toString(36).slice(2)}`);}
function field(key,label,placeholder='',required=false,type='text') {
return ``;
}
const errorHtml=()=>error?`
${escape(error)}
`:'';
function render({focus=null,scroll=false}={}) {
document.documentElement.dataset.theme=theme;
const done=answeredCount(),isResults=view==='results',section=state.section;
const progress=SECTIONS.map((s,i)=>`
${isResults?'Your next step, made clearer.':'How well are your books working?'}
${isResults?`Your results${state.business?` for ${escape(state.business)}`:''} highlight where to focus and the type of support to consider.`:'Find the gaps in your bookkeeping, understand what needs attention, and see which level of support fits your business.'}
40 questions · 10 sections
${!isResults?`
${done} of 40 questions complete${Math.round(done/40*100)}%
${state.flags.length} priority flag${state.flags.length===1?'':'s'} to address first
Do not let a high score delay a focused review. These flags take precedence over a reassuring interpretation without changing your numeric score.
${state.flags.map(i=>`
${FLAGS[i]}
`).join('')}
${state.flags.includes(2)?'
For suspected unauthorized transactions, consider immediate bank and access-control review.
':''}${state.flags.includes(3)?'
For overdue filings, tax notices, or uncertain tax treatment, involve the appropriate payroll or tax professional.
':''}`:''}
${weighted.score??'N/A'}${weighted.score===null?'no applicable score':'out of 100'}
Your weighted average score · 100 is ideal
${concern?'A focused review is your first step.':result.label}
${result.description}
${concern?'Address the priority flags first. ':''}${result.action}
${weighted.activeSections} of 10Applicable sections
${overall.applicable} of 40Applicable questions
${overall.unsure}Not sure responses
${overall.excluded}Excluded with a reason
To: ${escape(state.email)} · CC: ${EMAIL_CC}. The email includes your score, section results, answers, priority flags, and action plan.
${escape(emailMessage||(!EMAIL_ENDPOINT?EMAIL_SETUP_MESSAGE:'Review your results, then select Email my results. Nothing has been sent yet.'))}
${overall.unsure||exclusions.length?`
Verify the coverage before relying on this result.
${overall.unsure?`
${overall.unsure} “Not sure” response${overall.unsure===1?' needs':'s need'} verification. It receives zero on the health scale but is not proof of an error.
`:''}${exclusions.length?`
Entire sections excluded: ${escape(exclusions.join(', '))}. Confirm these exclusions in a records review. Remaining section weights have been rescaled.
`:''}
`:''}
${actionPlanHtml()}
Where to focus
100 is ideal. Sections scoring 50 or below need follow-up. Weights reflect each area’s contribution to the overall assessment.
Yes = 100; Partly = 50; No and Not sure = 0. Each section averages its applicable answers. Your final score is the weighted average of the unrounded section scores, rounded once to a whole number out of 100.
Final score = sum of (section score × base weight) ÷ sum of applicable section weights. N/A answers are excluded within a section. A wholly excluded section has no score or weight; remaining weights are rescaled. With no applicable sections, no overall score is calculated.
${sections.map((s,i)=>`
${SHORT_NAMES[i]}${s.weight}%
`).join('')}
Screening bands: 81–100 established foundation; 61–80 targeted improvements; 41–60 structured support review; 0–40 diagnostic review first. These risk weights and bands are screening choices, not statistically validated benchmarks. Priority flags take precedence over a reassuring score.
`;
}
function navigate(section){state.section=section;view='assessment';error='';invalid=[];render({scroll:true});document.getElementById('main').focus({preventScroll:true});}
function next() {
if(state.section===0&&contactError(state)){error=contactError(state);render({scroll:true});return;}
const missing=SECTIONS[state.section].questions.filter(q=>!complete(state.answers[q.id],state.reasons[q.id]));
if(missing.length){invalid=missing.map(q=>q.id);error='Please answer every question in this section and provide a reason for each Not applicable answer.';render();document.getElementById(`question-${missing[0].id}`).scrollIntoView({block:'center'});return;}
navigate(Math.min(10,state.section+1));
}
function showResults() {
const contactIssue=contactError(state);
if(contactIssue){view='assessment';state.section=0;error=contactIssue;render({scroll:true});return;}
const missing=allQuestions.filter(q=>!complete(state.answers[q.id],state.reasons[q.id]));
if(missing.length){view='assessment';state.section=Math.floor((missing[0].id-1)/4);invalid=missing.map(q=>q.id);error=`${missing.length} question${missing.length===1?' still needs':'s still need'} an answer or an exclusion reason before results can be calculated.`;render({scroll:true});return;}
if(!state.flagsReviewed){state.section=10;view='assessment';error='Please review the priority flags and check the confirmation box before viewing results.';render({scroll:true});return;}
view='results';error='';invalid=[];render({scroll:true});document.getElementById('main').focus({preventScroll:true});
}
function changed() {
if(['sent','queued'].includes(emailStatus))submissionId=makeId();
emailStatus='idle';emailMessage='';
}
function reviewEmail() {
if(!EMAIL_ENDPOINT)return;
const payload=prepareEmail(state,submissionId);
document.querySelector('#email-preview').innerHTML=`
To: ${escape(payload.email.to)} CC: ${EMAIL_CC}
Your contact details and complete assessment results will be recorded in Zoho CRM. The email will contain the following message, followed by your full section scores, answers, priority flags, and action plan.