commit ef551377f8d6911a805930953291320bc554551a Author: Todd Date: Sat Jun 21 14:58:56 2025 -0400 Upload files to "/" diff --git a/firebase-debug.js b/firebase-debug.js new file mode 100644 index 0000000..77cdb44 --- /dev/null +++ b/firebase-debug.js @@ -0,0 +1,300 @@ +// Firebase Debugging Helper +document.addEventListener('DOMContentLoaded', function() { + // Create a debugging panel + const debugPanel = document.createElement('div'); + debugPanel.id = 'firebase-debug-panel'; + debugPanel.style.position = 'fixed'; + debugPanel.style.bottom = '20px'; + debugPanel.style.right = '20px'; + debugPanel.style.width = '400px'; + debugPanel.style.maxHeight = '80vh'; + debugPanel.style.overflowY = 'auto'; + debugPanel.style.backgroundColor = '#f8f9fa'; + debugPanel.style.border = '1px solid #dee2e6'; + debugPanel.style.borderRadius = '4px'; + debugPanel.style.padding = '15px'; + debugPanel.style.boxShadow = '0 0.5rem 1rem rgba(0, 0, 0, 0.15)'; + debugPanel.style.zIndex = '9999'; + debugPanel.style.display = 'none'; + + debugPanel.innerHTML = ` +
+

Firebase Debug Console

+ +
+
+
+
+
+ + +
+ `; + + document.body.appendChild(debugPanel); + + // Add toggle button + const toggleButton = document.createElement('button'); + toggleButton.id = 'toggle-debug-panel'; + toggleButton.textContent = '🔍 Debug Firebase'; + toggleButton.style.position = 'fixed'; + toggleButton.style.bottom = '20px'; + toggleButton.style.right = '20px'; + toggleButton.style.backgroundColor = '#007bff'; + toggleButton.style.color = 'white'; + toggleButton.style.border = 'none'; + toggleButton.style.borderRadius = '4px'; + toggleButton.style.padding = '8px 15px'; + toggleButton.style.cursor = 'pointer'; + toggleButton.style.zIndex = '9998'; + + document.body.appendChild(toggleButton); + + // Event listeners + document.getElementById('toggle-debug-panel').addEventListener('click', function() { + const panel = document.getElementById('firebase-debug-panel'); + if (panel.style.display === 'none') { + panel.style.display = 'block'; + this.style.display = 'none'; + checkFirebaseConfig(); + testFirebaseConnection(); + } + }); + + document.getElementById('close-debug-panel').addEventListener('click', function() { + document.getElementById('firebase-debug-panel').style.display = 'none'; + document.getElementById('toggle-debug-panel').style.display = 'block'; + }); + + document.getElementById('test-firebase-connection').addEventListener('click', testFirebaseConnection); + + document.getElementById('clear-error-log').addEventListener('click', function() { + document.getElementById('firebase-error-log').innerHTML = ''; + }); + + // Check Firebase configuration + function checkFirebaseConfig() { + const configStatus = document.getElementById('firebase-config-status'); + + try { + if (typeof firebase === 'undefined') { + configStatus.innerHTML = ` +
+ Error: Firebase is not defined. Make sure you've included the Firebase SDK. +
+ `; + return; + } + + // Check if firebaseConfig exists and has required fields + if (typeof firebaseConfig === 'undefined') { + configStatus.innerHTML = ` +
+ Error: firebaseConfig is not defined. +
+ `; + return; + } + + const requiredFields = ['apiKey', 'authDomain', 'projectId']; + const missingFields = []; + + for (const field of requiredFields) { + if (!firebaseConfig[field]) { + missingFields.push(field); + } + } + + if (missingFields.length > 0) { + configStatus.innerHTML = ` +
+ Error: Missing required fields in firebaseConfig: ${missingFields.join(', ')} +
+ `; + return; + } + + // Check if the API key looks like a placeholder + const apiKey = firebaseConfig.apiKey; + if (apiKey.includes('AIzaSyDQzGnxw9RUq0H5Qp-1Bv_qbCR_JRQJdvM') || + apiKey.includes('your-api-key') || + apiKey.includes('placeholder')) { + configStatus.innerHTML = ` +
+ Warning: Your API key appears to be a placeholder. Replace it with your actual Firebase API key. +
+
+ How to fix: +
    +
  1. Go to the Firebase Console
  2. +
  3. Create a new project or select an existing one
  4. +
  5. Click the web icon () to add a web app
  6. +
  7. Register your app with a nickname (e.g., "Fence Estimator")
  8. +
  9. Copy the firebaseConfig object
  10. +
  11. Replace the placeholder config in index.html with yours
  12. +
+
+ `; + return; + } + + configStatus.innerHTML = ` +
+ Firebase Config: Looks valid +
+
+ API Key: ${firebaseConfig.apiKey}
+ Project ID: ${firebaseConfig.projectId}
+ Auth Domain: ${firebaseConfig.authDomain} +
+ `; + + } catch (error) { + configStatus.innerHTML = ` +
+ Error checking config: ${error.message} +
+ `; + } + } + + // Test Firebase connection + function testFirebaseConnection() { + const connectionStatus = document.getElementById('firebase-connection-status'); + const errorLog = document.getElementById('firebase-error-log'); + + connectionStatus.innerHTML = ` +
+ Testing connection... +
+ `; + + try { + if (typeof firebase === 'undefined' || typeof firebase.firestore !== 'function') { + connectionStatus.innerHTML = ` +
+ Error: Firebase Firestore is not available. +
+ `; + return; + } + + // Try to access Firestore + const db = firebase.firestore(); + + // Try to read from a test collection + db.collection('test').limit(1).get() + .then(() => { + connectionStatus.innerHTML = ` +
+ Connection successful! Firebase is working correctly. +
+ `; + }) + .catch(error => { + let errorMessage = error.message; + let solution = ''; + + // Provide specific guidance based on error + if (error.code === 'permission-denied') { + solution = ` +
+ How to fix: +
    +
  1. Go to the Firebase Console
  2. +
  3. Select your project
  4. +
  5. Go to Firestore Database
  6. +
  7. Go to Rules tab
  8. +
  9. Update your rules to allow read/write access (for testing):
  10. +
    +rules_version = '2';
    +service cloud.firestore {
    +  match /databases/{database}/documents {
    +    match /{document=**} {
    +      allow read, write: if true;  // WARNING: Only for testing!
    +    }
    +  }
    +}
    +                                    
    +
  11. Click "Publish"
  12. +
+
+ `; + } else if (error.code === 'invalid-argument' || errorMessage.includes('400')) { + solution = ` +
+ How to fix: +
    +
  1. The 400 error usually means your Firebase configuration is invalid
  2. +
  3. Make sure you've replaced the placeholder config with your own
  4. +
  5. Check that your project exists in the Firebase Console
  6. +
  7. Verify that Firestore is enabled in your project
  8. +
+
+ `; + } + + connectionStatus.innerHTML = ` +
+ Connection failed: ${errorMessage} +
Error code: ${error.code || 'unknown'}
+
+ ${solution} + `; + + // Add to error log + const timestamp = new Date().toLocaleTimeString(); + errorLog.innerHTML += ` +
+
${timestamp}
+
Error: ${errorMessage}
+
Code: ${error.code || 'unknown'}
+
+ `; + }); + + } catch (error) { + connectionStatus.innerHTML = ` +
+ Error testing connection: ${error.message} +
+ `; + + // Add to error log + const timestamp = new Date().toLocaleTimeString(); + errorLog.innerHTML += ` +
+
${timestamp}
+
Error: ${error.message}
+
+ `; + } + } + + // Monitor for Firebase errors + const originalConsoleError = console.error; + console.error = function() { + originalConsoleError.apply(console, arguments); + + // Check if this is a Firebase error + const errorString = Array.from(arguments).join(' '); + if (errorString.includes('firebase') || errorString.includes('firestore') || errorString.includes('400')) { + const errorLog = document.getElementById('firebase-error-log'); + const timestamp = new Date().toLocaleTimeString(); + + errorLog.innerHTML += ` +
+
${timestamp}
+
Console Error: ${errorString}
+
+ `; + + // Show the debug button with an alert indicator + const toggleButton = document.getElementById('toggle-debug-panel'); + if (toggleButton.style.display !== 'none') { + toggleButton.textContent = '🔴 Debug Firebase'; + toggleButton.style.backgroundColor = '#dc3545'; + } + } + }; +}); diff --git a/index.html b/index.html new file mode 100644 index 0000000..3fe140b --- /dev/null +++ b/index.html @@ -0,0 +1,684 @@ + + + + + Fence & Perimeter Calculator (Google Maps) + + + + + + + + + + + +
+
+

Fence & Perimeter Calculator

+

Find an address, draw with arrows, or use the pencil for freehand mapping.

+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+

Use the controls below to map your fence line

+
+
+ +
+ + + +
+
+ + + + + +
+
+
+ + +
+

Tools & Calculations

+ +
+

Find Address

+
+ +
+
+ +
+

Mapped Perimeter

+
+ 0 + linear ft. +
+
+ +
+

Post Calculation

+
+
+ End Posts: + 0 +
+
+ Corner Posts: + 0 +
+
+ Line Posts: + 0 +
+
+ Gate Posts: + 0 +
+
+
+ +
+

Gates

+
+ + +
+
+
+ +
+

Fence Section Estimation

+
+
+ + +
+
+ + +
+ +
+
+ + + +
+
+
+ + + + + + + + + + + + + + + + diff --git a/materials-db.js b/materials-db.js new file mode 100644 index 0000000..d055b21 --- /dev/null +++ b/materials-db.js @@ -0,0 +1,404 @@ +// Materials Database Functions +async function loadMaterials() { + if (typeof window.materialsCollection === 'undefined') { + console.error('Firebase materialsCollection is not defined.'); + return; + } + + const tableBody = document.getElementById('materialsTableBody'); + tableBody.innerHTML = 'Loading...'; + let tableHTML = ''; + materials = []; + + try { + const snapshot = await window.materialsCollection.get(); + if (snapshot.empty) { + tableBody.innerHTML = 'No materials found.'; + return; + } + + for (const doc of snapshot.docs) { + const group = doc.data(); + group.id = doc.id; + materials.push(group); + + // Render group row + tableHTML += ` + + ${group.description || 'Group'} + + + + + + `; + + const itemsSnapshot = await doc.ref.collection('items').get(); + itemsSnapshot.forEach(itemDoc => { + const item = itemDoc.data(); + item.id = itemDoc.id; + item.groupId = group.id; + materials.push(item); + + // Render item row + tableHTML += ` + + ${item.partNumber || ''} + ${item.description || ''} + ${item.oldStyle || ''} + ${item.newStyle || ''} + ${item.color || ''} + $${parseFloat(item.price || 0).toFixed(2)} + + + + + + `; + }); + } + + tableBody.innerHTML = tableHTML; + attachActionListeners(); + updateProductSelect(); + + } catch (error) { + console.error("Error loading materials: ", error); + tableBody.innerHTML = 'Error loading materials.'; + } +} + +function attachActionListeners() { + document.querySelectorAll('.edit-btn').forEach(button => { + button.addEventListener('click', function() { + const id = this.getAttribute('data-id'); + const groupId = this.getAttribute('data-group-id'); + editMaterial(id, groupId); + }); + }); + + document.querySelectorAll('.delete-btn').forEach(button => { + button.addEventListener('click', function() { + const id = this.getAttribute('data-id'); + const groupId = this.getAttribute('data-group-id'); + deleteMaterial(id, groupId); + }); + }); +} + +function updateProductSelect() { + const productSelect = document.getElementById('productSelect'); + productSelect.innerHTML = ''; + + materials.forEach(material => { + const option = document.createElement('option'); + option.value = material.id; + option.textContent = `${material.description} (${material.color}) - $${parseFloat(material.price).toFixed(2)}`; + productSelect.appendChild(option); + }); + + // If no materials, add default options + if (materials.length === 0) { + const defaultOptions = ['Wood Panel', 'Chain Link', 'Vinyl Panel', 'Wrought Iron']; + defaultOptions.forEach(option => { + const optionElement = document.createElement('option'); + optionElement.textContent = option; + productSelect.appendChild(optionElement); + }); + } +} + +function saveMaterial(event) { + event.preventDefault(); + + const materialData = { + partNumber: document.getElementById('partNumber').value, + description: document.getElementById('description').value, + oldStyle: document.getElementById('oldStyle').value, + newStyle: document.getElementById('newStyle').value, + color: document.getElementById('color').value, + price: parseFloat(document.getElementById('price').value) + }; +let currentEditGroupId = null; + + let promise; + if (currentEditId) { + let docRef; + if (currentEditGroupId) { + docRef = window.materialsCollection.doc(currentEditGroupId).collection('items').doc(currentEditId); + } else { + docRef = window.materialsCollection.doc(currentEditId); + } + promise = docRef.update(materialData); + } else { + promise = window.materialsCollection.add(materialData); + } + + promise.then(() => { + resetForm(); + loadMaterials(); + alert('Material saved successfully!'); + }).catch(error => { + console.error("Error saving material: ", error); + alert("Error saving material. Please try again."); + }); +} + +function editMaterial(id, groupId) { + const material = materials.find(m => m.id === id); + if (material) { + document.getElementById('materialId').value = id; + document.getElementById('partNumber').value = material.partNumber || ''; + document.getElementById('description').value = material.description || ''; + document.getElementById('oldStyle').value = material.oldStyle || ''; + document.getElementById('newStyle').value = material.newStyle || ''; + document.getElementById('color').value = material.color || ''; + document.getElementById('price').value = material.price || ''; + + document.getElementById('formTitle').textContent = 'Edit Material'; + document.getElementById('cancelEdit').classList.remove('hidden'); + currentEditId = id; + currentEditGroupId = groupId; + } +} + +function deleteMaterial(id, groupId) { + if (confirm('Are you sure you want to delete this material?')) { + let docRef; + if (groupId) { + docRef = window.materialsCollection.doc(groupId).collection('items').doc(id); + } else { + docRef = window.materialsCollection.doc(id); + } + + docRef.delete() + .then(() => { + resetForm(); + loadMaterials(); + alert('Material deleted successfully!'); + }) + .catch(error => { + console.error("Error deleting material: ", error); + alert("Error deleting material. Please try again."); + }); + } +} + +function resetForm() { + document.getElementById('materialForm').reset(); + document.getElementById('materialId').value = ''; + document.getElementById('formTitle').textContent = 'Add New Material'; + document.getElementById('cancelEdit').classList.add('hidden'); + currentEditId = null; +} + +// Excel Import Functionality +const requiredColumns = ['Part Number', 'Description', 'Old Style', 'New Style', 'Color', 'Price']; +document.addEventListener('DOMContentLoaded', function() { +document.getElementById('importExcelBtn').addEventListener('click', function() { + const fileInput = document.getElementById('excelFileInput'); + const importStatus = document.getElementById('importStatus'); + + if (!fileInput.files.length) { + alert('Please select an Excel file to import.'); + return; + } + + const file = fileInput.files[0]; + const reader = new FileReader(); + + // Show status with more visibility + importStatus.classList.remove('hidden'); + importStatus.classList.remove('text-red-600', 'text-green-600'); + importStatus.classList.add('text-blue-600', 'font-bold', 'p-3', 'bg-blue-50', 'rounded'); + importStatus.textContent = '📂 Reading file: ' + file.name; + console.log('Starting import process for file:', file.name); + + reader.onload = async function(e) { + try { + const data = e.target.result; + const workbook = XLSX.read(data, { type: 'binary', cellStyles: true }); + const firstSheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[firstSheetName]; + const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); + + if (!jsonData.length) { + importStatus.textContent = '❌ Error: No data found in the Excel file.'; + return; + } + + importStatus.innerHTML = `⏳ Processing ${jsonData.length} rows...`; + + let headerRowIndex = -1; + for (let i = 0; i < Math.min(5, jsonData.length); i++) { + const row = jsonData[i]; + const hasAllHeaders = requiredColumns.every(col => row.some(cell => cell?.toString().toLowerCase().trim() === col.toLowerCase().trim())); + if (hasAllHeaders) { + headerRowIndex = i; + break; + } + } + + if (headerRowIndex === -1) { + importStatus.innerHTML = `❌ Required columns not found. Please ensure your Excel file contains: ${requiredColumns.join(', ')}.`; + return; + } + + const headers = jsonData[headerRowIndex].map(h => h.toString().trim()); + const dataRows = jsonData.slice(headerRowIndex + 1); + + let batches = []; + let currentBatch = window.db.batch(); + let operationCount = 0; + let currentGroupRef = null; + let totalSuccessCount = 0; + let totalErrorCount = 0; + const errorDetails = []; + + dataRows.forEach((row, index) => { + const rowIndexInSheet = headerRowIndex + 1 + index; + + // Skip empty rows + if (row.every(cell => cell === null || cell === '')) { + return; + } + + const firstCellRef = XLSX.utils.encode_cell({ r: rowIndexInSheet, c: 0 }); + const firstCell = worksheet[firstCellRef]; + const hasFillColor = firstCell && firstCell.s && firstCell.s.fgColor && (firstCell.s.fgColor.rgb || firstCell.s.fgColor.theme); + + const materialData = {}; + headers.forEach((header, i) => { + const fieldName = { + 'part number': 'partNumber', + 'description': 'description', + 'old style': 'oldStyle', + 'new style': 'newStyle', + 'color': 'color', + 'price': 'price' + }[header.trim().toLowerCase()]; + + if (fieldName) { + if (row[i] !== null && row[i] !== undefined) { + materialData[fieldName] = row[i].toString(); + } else { + materialData[fieldName] = ''; + } + } + }); + + try { + const price = parseFloat(materialData.price); + if (isNaN(price)) { + totalErrorCount++; + errorDetails.push(`Row ${rowIndexInSheet + 1}: Invalid or missing 'Price'.`); + return; + } + materialData.price = price; + + if (!materialData.description) { + totalErrorCount++; + errorDetails.push(`Row ${rowIndexInSheet + 1}: Missing 'Description'.`); + return; + } + + if (operationCount >= 499) { + batches.push(currentBatch); + currentBatch = window.db.batch(); + operationCount = 0; + } + + if (hasFillColor) { + currentGroupRef = window.materialsCollection.doc(); + currentBatch.set(currentGroupRef, materialData); + } else if (currentGroupRef) { + const itemRef = currentGroupRef.collection('items').doc(); + currentBatch.set(itemRef, materialData); + } else { + const docRef = window.materialsCollection.doc(); + currentBatch.set(docRef, materialData); + } + operationCount++; + + } catch (error) { + totalErrorCount++; + errorDetails.push(`Row ${rowIndexInSheet + 1}: Error - ${error.message}`); + } + }); + + if (operationCount > 0) { + batches.push(currentBatch); + } + + importStatus.innerHTML = `🔄 Uploading ${batches.length} batch(es) to the database...`; + + for (const [index, batch] of batches.entries()) { + try { + await batch.commit(); + totalSuccessCount += 1; // Counting successful batches, not individual ops + importStatus.innerHTML = `🔄 Uploading batch ${index + 1} of ${batches.length}... Success!`; + } catch (error) { + totalErrorCount += 1; + errorDetails.push(`Batch ${index + 1} failed: ${error.message}`); + console.error(`Batch ${index + 1} upload error:`, error); + } + } + + let statusHTML = `
✅ Import complete!
+
+ ${totalSuccessCount} batch(es) imported successfully. +
`; + if (totalErrorCount > 0) { + statusHTML += `
+ ${totalErrorCount} errors occurred. +
+
Error Details:
${errorDetails.join('\n')}
`; + } + statusHTML += `
+ Your materials have been imported. Refreshing list... +
`; + importStatus.innerHTML = statusHTML; + loadMaterials(); + + } catch (error) { + importStatus.innerHTML = `❌ Error processing file: ${error.message}`; + console.error('Excel processing error:', error); + } + }; + reader.readAsBinaryString(file); + }); +}); + +// Set up event listeners for the material form and refresh button +document.addEventListener('DOMContentLoaded', function() { + document.getElementById('materialForm').addEventListener('submit', saveMaterial); + document.getElementById('cancelEdit').addEventListener('click', resetForm); + + // Add event listener for the refresh materials button + document.getElementById('refreshMaterials').addEventListener('click', function() { + const button = this; + const originalText = button.innerHTML; + + // Show loading state + button.innerHTML = '🔄 Refreshing...'; + button.disabled = true; + + // Add a small delay to make the refresh action more visible + setTimeout(function() { + loadMaterials(); + + // Show success state briefly + button.innerHTML = ' Refreshed!'; + button.classList.remove('bg-indigo-600', 'hover:bg-indigo-700'); + button.classList.add('bg-green-600', 'hover:bg-green-700'); + + // Reset button after a moment + setTimeout(function() { + button.innerHTML = originalText; + button.disabled = false; + button.classList.remove('bg-green-600', 'hover:bg-green-700'); + button.classList.add('bg-indigo-600', 'hover:bg-indigo-700'); + }, 1500); + }, 500); + }); +});