|
|
| document.addEventListener('DOMContentLoaded', function() { |
| const questions = [ |
| { |
| question: "What is the width of the deck (left to right)?", |
| type: "number", |
| key: "width" |
| }, |
| { |
| question: "What is the depth of the deck (distance from the house outward)?", |
| type: "number", |
| key: "depth" |
| }, |
| { |
| question: "Is the deck a simple rectangle, or does it have angles/irregularities?", |
| type: "select", |
| options: ["Simple rectangle", "Angles/irregularities"], |
| key: "shape" |
| }, |
| { |
| question: "Is the deck tying into the house with a ledger?", |
| type: "yesno", |
| key: "hasLedger" |
| }, |
| { |
| question: "Will deck boards run parallel or perpendicular to the house?", |
| type: "select", |
| options: ["Parallel", "Perpendicular"], |
| key: "boardOrientation" |
| }, |
| { |
| question: "Are we picture framing the deck?", |
| type: "yesno", |
| key: "hasPictureFrame", |
| followups: [ |
| { |
| question: "What color is the picture frame?", |
| type: "select", |
| options: ["Black", "White", "Gray", "Brown", "Custom"], |
| key: "frameColor" |
| }, |
| { |
| question: "What color is the field decking?", |
| type: "select", |
| options: ["Black", "White", "Gray", "Brown", "Custom"], |
| key: "fieldColor" |
| } |
| ] |
| }, |
| { |
| question: "Are there steps?", |
| type: "yesno", |
| key: "hasSteps", |
| followups: [ |
| { |
| question: "How many sets of steps?", |
| type: "number", |
| key: "stepSets" |
| }, |
| { |
| question: "What is the width of each step run?", |
| type: "number", |
| key: "stepWidth" |
| }, |
| { |
| question: "What is the deck height off the ground?", |
| type: "number", |
| key: "deckHeight" |
| }, |
| { |
| question: "Is there a landing?", |
| type: "yesno", |
| key: "hasLanding", |
| followups: [ |
| { |
| question: "What size is the landing?", |
| type: "text", |
| key: "landingSize" |
| } |
| ] |
| } |
| ] |
| }, |
| { |
| question: "Are we installing railing on the deck?", |
| type: "yesno", |
| key: "hasRailing", |
| followups: [ |
| { |
| question: "Which sides of the deck will have railing? (Select all that apply)", |
| type: "multiselect", |
| options: ["Front", "Left", "Right", "Back"], |
| key: "railingSides" |
| } |
| ] |
| }, |
| { |
| question: "Do we need ADA handrail?", |
| type: "yesno", |
| key: "hasAdaRail", |
| followups: [ |
| { |
| question: "Choose ADA handrail length", |
| type: "select", |
| options: ["8'8\"", "16'"], |
| key: "adaRailLength" |
| } |
| ] |
| }, |
| { |
| question: "Will the deck be skirted?", |
| type: "yesno", |
| key: "hasSkirting" |
| }, |
| { |
| question: "Are we installing fascia?", |
| type: "yesno", |
| key: "hasFascia", |
| followups: [ |
| { |
| question: "What color is the fascia?", |
| type: "select", |
| options: ["Black", "White", "Gray", "Brown", "Custom"], |
| key: "fasciaColor" |
| } |
| ] |
| }, |
| { |
| question: "Any customer preferences, township requirements, or special framing considerations?", |
| type: "textarea", |
| key: "notes" |
| } |
| ]; |
|
|
| let currentQuestion = 0; |
| let answers = {}; |
| const questionContainer = document.getElementById('current-question'); |
| const answerInput = document.getElementById('answer-input'); |
| const nextBtn = document.getElementById('next-btn'); |
|
|
| function renderQuestion(index) { |
| const q = questions[index]; |
| questionContainer.textContent = q.question; |
| answerInput.innerHTML = ''; |
|
|
| switch(q.type) { |
| case 'number': |
| answerInput.innerHTML = ` |
| <input type="number" class="w-full px-4 py-2 border border-gray-300 rounded-lg" |
| id="answer" placeholder="Enter measurement in feet"> |
| `; |
| break; |
| case 'yesno': |
| answerInput.innerHTML = ` |
| <div class="flex space-x-4"> |
| <button type="button" class="yesno-btn px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-100" data-value="yes">Yes</button> |
| <button type="button" class="yesno-btn px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-100" data-value="no">No</button> |
| </div> |
| `; |
| |
| document.querySelectorAll('.yesno-btn').forEach(btn => { |
| btn.addEventListener('click', function() { |
| document.querySelectorAll('.yesno-btn').forEach(b => b.classList.remove('bg-blue-500', 'text-white', 'border-blue-500')); |
| this.classList.add('bg-blue-500', 'text-white', 'border-blue-500'); |
| }); |
| }); |
| break; |
| case 'select': |
| answerInput.innerHTML = ` |
| <select class="w-full px-4 py-2 border border-gray-300 rounded-lg" id="answer"> |
| ${q.options.map(opt => `<option value="${opt}">${opt}</option>`).join('')} |
| </select> |
| `; |
| break; |
| case 'multiselect': |
| answerInput.innerHTML = ` |
| <div class="space-y-2"> |
| ${q.options.map(opt => ` |
| <label class="flex items-center"> |
| <input type="checkbox" class="mr-2" value="${opt}"> |
| ${opt} |
| </label> |
| `).join('')} |
| </div> |
| `; |
| break; |
| case 'textarea': |
| answerInput.innerHTML = ` |
| <textarea class="w-full px-4 py-2 border border-gray-300 rounded-lg" |
| id="answer" rows="3"></textarea> |
| `; |
| break; |
| } |
| } |
| function collectAnswer() { |
| const q = questions[currentQuestion]; |
| let answer; |
|
|
| switch(q.type) { |
| case 'number': |
| answer = parseFloat(document.getElementById('answer').value) || 0; |
| break; |
| case 'select': |
| case 'textarea': |
| answer = document.getElementById('answer').value; |
| break; |
| case 'yesno': |
| const selectedBtn = document.querySelector('.yesno-btn.bg-blue-500'); |
| answer = selectedBtn ? selectedBtn.dataset.value : null; |
| break; |
| case 'multiselect': |
| answer = Array.from(document.querySelectorAll('input[type="checkbox"]:checked')) |
| .map(cb => cb.value); |
| break; |
| } |
| |
| |
| if (q.key === 'notes') { |
| answers[q.key] = answer || ''; |
| return true; |
| } |
|
|
| |
| if (answer === null || answer === undefined || (Array.isArray(answer) && answer.length === 0)) { |
| return false; |
| } |
|
|
| |
| answers[q.key] = answer; |
| return true; |
| } |
| nextBtn.addEventListener('click', function() { |
| if (!collectAnswer()) { |
| alert('Please provide an answer before continuing'); |
| return; |
| } |
| currentQuestion++; |
| if (currentQuestion < questions.length) { |
| renderQuestion(currentQuestion); |
| } else { |
| |
| document.getElementById('question-container').classList.add('hidden'); |
| document.getElementById('results-section').classList.remove('hidden'); |
| } |
| }); |
| |
| renderQuestion(0); |
|
|
| |
| if (document.getElementById('stairs').checked) { |
| document.getElementById('stairs-details').classList.remove('hidden'); |
| } |
| |
| document.getElementById('stairs').addEventListener('change', function() { |
| document.getElementById('stairs-details').classList.toggle('hidden', !this.checked); |
| }); |
|
|
| |
| document.getElementById('railing').addEventListener('change', function() { |
| |
| }); |
| |
| document.getElementById('calculate-btn').addEventListener('click', function() { |
| |
| while (currentQuestion < questions.length) { |
| if (!collectAnswer()) { |
| alert('Please answer all questions before calculating materials'); |
| document.getElementById('question-container').classList.remove('hidden'); |
| document.getElementById('results-section').classList.add('hidden'); |
| renderQuestion(currentQuestion); |
| return; |
| } |
| currentQuestion++; |
| } |
|
|
| |
| generateMaterialList(); |
| document.getElementById('question-container').classList.add('hidden'); |
| document.getElementById('results-section').classList.remove('hidden'); |
| document.getElementById('results-section').scrollIntoView({ behavior: 'smooth' }); |
| |
| |
| currentQuestion = 0; |
| answers = {}; |
| renderQuestion(0); |
| |
| |
| console.log('User answers:', answers); |
| }); |
| function generateMaterialList() { |
| |
| const width = answers.width || 10; |
| const depth = answers.depth || 10; |
| const deckArea = width * depth; |
| |
| |
| const boardType = document.getElementById('board-type').value; |
| const hasStairs = document.getElementById('stairs').checked; |
| const stepCount = parseInt(document.getElementById('step-count').value) || 0; |
| const hasRailing = document.getElementById('railing').checked; |
| const hasRoof = document.getElementById('roof').checked; |
|
|
| |
| const boardWidth = 5.5; |
| const boardSpacing = 0.25; |
| const effectiveWidth = boardWidth + boardSpacing; |
| |
| |
| const boardsAlongDepth = Math.ceil((depth * 12) / effectiveWidth); |
| |
| |
| let deckBoards; |
| if (boardType === 'trex-grooved') { |
| |
| deckBoards = calculateOptimalBoards(width, boardsAlongDepth, [12, 16, 20]); |
| } else if (boardType === 'trex-square') { |
| |
| deckBoards = calculateOptimalBoards(width, boardsAlongDepth, [20]); |
| } else { |
| |
| deckBoards = calculateOptimalBoards(width, boardsAlongDepth, [12]); |
| } |
|
|
| |
| const joistSpacing = 16; |
| const joists = Math.ceil((width * 12) / joistSpacing) + 1; |
| const beams = Math.ceil(width / 8); |
| const posts = Math.ceil(width / 6) * 2; |
| const concrete = posts * 3; |
|
|
| |
| const screwSpacing = 12; |
| const screwsPerBoard = Math.ceil((depth * 12) / screwSpacing) * 2; |
| const totalScrews = screwsPerBoard * boardsAlongDepth; |
| const fastenerBoxes = Math.ceil(totalScrews / 250); |
| |
| |
| document.getElementById('deck-boards').textContent = `${deckBoards.total} ${boardType.includes('trex') ? 'Trex' : boardType === 'wood' ? 'Wood' : 'Composite'} boards`; |
| document.getElementById('joists').textContent = `${joists} (2×8 @ 16" OC)`; |
| document.getElementById('beams').textContent = `${beams} (4×6)`; |
| document.getElementById('posts').textContent = `${posts} (6×6)`; |
| document.getElementById('concrete').textContent = `${concrete} bags`; |
| document.getElementById('fasteners').textContent = `${fastenerBoxes} boxes`; |
|
|
| |
| if (hasStairs) { |
| document.getElementById('deck-boards').textContent += ` + ${stepCount} stair treads`; |
| document.getElementById('joists').textContent += ` + ${stepCount} stair stringers`; |
| } |
| if (hasRailing) { |
| const railingLength = width * 2; |
| const railingPosts = Math.ceil(railingLength / 6) * 2; |
| document.getElementById('deck-boards').textContent += ` + ${Math.ceil(railingLength / 6)} railing sections`; |
| document.getElementById('posts').textContent += ` + ${railingPosts} railing posts`; |
| } |
| if (hasRoof) { |
| document.getElementById('joists').textContent += ` + ${Math.ceil(width / 2)} roof rafters`; |
| document.getElementById('posts').textContent += ` + ${Math.ceil(width / 8) * 2} roof support posts`; |
| } |
| } |
|
|
| function calculateOptimalBoards(width, count, availableLengths) { |
| availableLengths.sort((a, b) => b - a); |
| |
| const result = { |
| total: 0, |
| lengths: {} |
| }; |
| |
| for (let i = 0; i < count; i++) { |
| let remaining = width; |
| let currentCut = {}; |
| |
| while (remaining > 0) { |
| for (const len of availableLengths) { |
| if (len <= remaining) { |
| currentCut[len] = (currentCut[len] || 0) + 1; |
| remaining -= len; |
| break; |
| } |
| } |
| } |
| |
| |
| for (const [len, qty] of Object.entries(currentCut)) { |
| result.lengths[len] = (result.lengths[len] || 0) + qty; |
| result.total += qty; |
| } |
| } |
| |
| return result; |
| } |
| }); |