import { calculate, quoteLines, addLines, fmt } from './math.mjs'; const kind = document.body.dataset.calculator; const form = document.querySelector('#calculator-form'); const resultBox = document.querySelector('#result'); const add = document.querySelector('#add-list'); const error = document.querySelector('#form-error'); const exampleNote = document.querySelector('#example-note'); const addedNote = document.querySelector('#added-note'); let current = null; let lastAdded = ''; function element(tag, text, className) { const node = document.createElement(tag); node.textContent = text; if (className) node.className = className; return node; } function showError(message) { error.textContent = message; error.hidden = false; } function renderList() { const list = document.querySelector('#material-list'); list.replaceChildren(); try { if (!window.elQuote) throw new Error('List unavailable'); const items = window.elQuote.get(); if (!Array.isArray(items)) throw new Error('Invalid list'); document.querySelector('#list-empty').hidden = items.length > 0; items.forEach((item, index) => { const row = element('li', ''); const detail = element('div', ''); detail.append(element('strong', `${item.qty || 1} × ${item.name}`), element('span', item.note || '', 'small')); const remove = element('button', 'Remove'); remove.type = 'button'; remove.setAttribute('aria-label', `Remove ${item.name}`); remove.addEventListener('click', () => { try { window.elQuote.removeAt(index); lastAdded = ''; if (current) add.disabled = false; addedNote.textContent = ''; renderList(); } catch { storageError(); } }); row.append(detail, remove); list.append(row); }); document.querySelector('#storage-error').hidden = true; } catch { storageError(); } } function storageError() { const message = document.querySelector('#storage-error'); message.hidden = false; message.textContent = 'Your list could not be saved or read in this browser. Keep the counts on this page and call or text (718) 851-2100.'; } function invalidate() { current = null; add.disabled = true; add.textContent = 'Add to my list'; addedNote.textContent = ''; error.hidden = true; document.querySelector('#result-heading').textContent = 'Ready when you are.'; resultBox.replaceChildren(element('p','Tap Calculate to use your current measurements.')); } form.addEventListener('input', () => { invalidate(); exampleNote.hidden = true; }); form.addEventListener('reset', () => { invalidate(); exampleNote.hidden = true; }); form.addEventListener('invalid', () => { invalidate(); showError('Check the highlighted field. All measurements and the waste allowance are required.'); }, true); document.querySelector('#use-example').addEventListener('click', () => { invalidate(); const example = JSON.parse(document.querySelector('#example-data').textContent); Object.entries(example).forEach(([key,value]) => { form.elements.namedItem(key).value = value; }); exampleNote.hidden = false; form.requestSubmit(); }); form.addEventListener('submit', event => { event.preventDefault(); invalidate(); try { const values = Object.fromEntries(new FormData(form)); const r = calculate(kind, values); const lines = quoteLines(kind, values, r); const fingerprint = JSON.stringify(lines); current = { lines, fingerprint }; document.querySelector('#result-heading').textContent = 'Here is your count.'; resultBox.replaceChildren(element('p', fmt(r.quantity), 'result-number'), element('p', { board:'pieces of lumber',drywall:'drywall sheets',studs:'metal studs',stairs:'stringer blanks', siding:'siding packs',insulation:'insulation packs',balusters:'balusters to request',ceiling:'ceiling tile packs' }[kind], 'result-unit')); const rows = kind === 'board' ? [['Before waste',`${fmt(r.base)} board feet`],['Per piece',`${fmt(r.perPiece)} board feet`],['Rounded order total',`${fmt(r.total)} board feet`]] : kind === 'drywall' ? [['Area after layers',`${fmt(r.netArea)} sq ft`],['Area per sheet',`${fmt(r.sheetArea)} sq ft`],['Before waste',`${fmt(r.base)} sheets`]] : kind === 'studs' ? [['Base studs (both ends)',fmt(r.base)],['Extra studs entered',fmt(r.extras)],['Track before waste',`${fmt(r.trackFeet)} ft`],['Track pieces with waste',fmt(r.tracks)]] : kind === 'siding' || kind === 'insulation' ? [['Area after openings and layers',`${fmt(r.netArea)} sq ft`],['Area with waste',`${fmt(r.orderArea)} sq ft`],['Entered coverage per pack',`${fmt(r.coverage)} sq ft`]] : kind === 'balusters' ? [['Plan count before waste',fmt(r.base)],['Equal gaps per section',fmt(r.gaps)],['Gap from your plan (approx.)',`${fmt(r.gap)} in`]] : kind === 'ceiling' ? [['Net tile area',`${fmt(r.netArea)} sq ft`],['Main tees with waste',fmt(r.mains)],['Cross tees with waste',fmt(r.crosses)],['Wall angle pieces with waste',fmt(r.trims)]] : [['Riser height',`${fmt(r.riserHeight)} in`],['Treads below landing',fmt(r.treads)],['Horizontal run',`${fmt(r.run)} in`],['Sloped distance only',`${fmt(r.slopeFeet)} ft`],['Requested blank length',`${fmt(r.blankLength)} ft`]]; rows.push(['Your waste allowance',`${fmt(r.allowance)}%`]); const dl = element('dl','','result-rows'); rows.forEach(([label,value]) => { const row=element('div','');row.append(element('dt',label),element('dd',value));dl.append(row); }); resultBox.append(dl); if (kind === 'stairs') resultBox.append(element('p','Sloped distance is not a cutting length. Confirm blank size and end cuts with your engineer or architect.','small')); if (kind === 'studs') resultBox.append(element('p','Spacing, extras and track runs came from your inputs. Confirm with your engineer or architect.','small')); if (kind === 'balusters' || kind === 'ceiling' || kind === 'insulation') resultBox.append(element('p','This is a quantity and arithmetic check of your inputs. Confirm the layout and specification with your engineer or architect.','small')); add.disabled = fingerprint === lastAdded; if (add.disabled) addedNote.textContent = 'This result is already in your list.'; } catch (e) { current = null; add.disabled = true; showError(e.message); } }); add.addEventListener('click', () => { if (!current || current.fingerprint === lastAdded) return; try { if (!window.elQuote) throw new Error('List unavailable'); addLines(window.elQuote, current.lines); lastAdded = current.fingerprint; add.disabled = true; addedNote.replaceChildren(element('span','Added to my list. ')); const link=element('a','Review & send');link.href='/quote/';addedNote.append(link); renderList(); } catch { storageError(); addedNote.textContent = 'Could not save the result. Call or text the yard with these counts.'; } }); window.addEventListener('storage', () => { lastAdded=''; if(current) add.disabled=false;renderList(); }); window.addEventListener('pageshow', renderList); renderList();