Upload files to "/"
This commit is contained in:
300
firebase-debug.js
Normal file
300
firebase-debug.js
Normal file
@ -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 = `
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<h3 style="margin: 0; font-size: 16px;">Firebase Debug Console</h3>
|
||||
<button id="close-debug-panel" style="background: none; border: none; cursor: pointer; font-size: 16px;">×</button>
|
||||
</div>
|
||||
<div id="firebase-config-status"></div>
|
||||
<div id="firebase-connection-status"></div>
|
||||
<div id="firebase-error-log" style="margin-top: 10px;"></div>
|
||||
<div style="margin-top: 15px;">
|
||||
<button id="test-firebase-connection" style="background-color: #007bff; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;">Test Connection</button>
|
||||
<button id="clear-error-log" style="background-color: #6c757d; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; margin-left: 5px;">Clear Log</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div style="padding: 10px; background-color: #f8d7da; color: #721c24; border-radius: 4px; margin-bottom: 10px;">
|
||||
<strong>Error:</strong> Firebase is not defined. Make sure you've included the Firebase SDK.
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if firebaseConfig exists and has required fields
|
||||
if (typeof firebaseConfig === 'undefined') {
|
||||
configStatus.innerHTML = `
|
||||
<div style="padding: 10px; background-color: #f8d7da; color: #721c24; border-radius: 4px; margin-bottom: 10px;">
|
||||
<strong>Error:</strong> firebaseConfig is not defined.
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<div style="padding: 10px; background-color: #f8d7da; color: #721c24; border-radius: 4px; margin-bottom: 10px;">
|
||||
<strong>Error:</strong> Missing required fields in firebaseConfig: ${missingFields.join(', ')}
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<div style="padding: 10px; background-color: #fff3cd; color: #856404; border-radius: 4px; margin-bottom: 10px;">
|
||||
<strong>Warning:</strong> Your API key appears to be a placeholder. Replace it with your actual Firebase API key.
|
||||
</div>
|
||||
<div style="padding: 10px; background-color: #f8f9fa; border-radius: 4px; margin-bottom: 10px;">
|
||||
<strong>How to fix:</strong>
|
||||
<ol style="margin-top: 5px; padding-left: 20px;">
|
||||
<li>Go to the <a href="https://console.firebase.google.com/" target="_blank">Firebase Console</a></li>
|
||||
<li>Create a new project or select an existing one</li>
|
||||
<li>Click the web icon (</>) to add a web app</li>
|
||||
<li>Register your app with a nickname (e.g., "Fence Estimator")</li>
|
||||
<li>Copy the firebaseConfig object</li>
|
||||
<li>Replace the placeholder config in index.html with yours</li>
|
||||
</ol>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
configStatus.innerHTML = `
|
||||
<div style="padding: 10px; background-color: #d4edda; color: #155724; border-radius: 4px; margin-bottom: 10px;">
|
||||
<strong>Firebase Config:</strong> Looks valid
|
||||
</div>
|
||||
<div style="font-size: 12px; margin-bottom: 10px;">
|
||||
<strong>API Key:</strong> ${firebaseConfig.apiKey}<br>
|
||||
<strong>Project ID:</strong> ${firebaseConfig.projectId}<br>
|
||||
<strong>Auth Domain:</strong> ${firebaseConfig.authDomain}
|
||||
</div>
|
||||
`;
|
||||
|
||||
} catch (error) {
|
||||
configStatus.innerHTML = `
|
||||
<div style="padding: 10px; background-color: #f8d7da; color: #721c24; border-radius: 4px; margin-bottom: 10px;">
|
||||
<strong>Error checking config:</strong> ${error.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Test Firebase connection
|
||||
function testFirebaseConnection() {
|
||||
const connectionStatus = document.getElementById('firebase-connection-status');
|
||||
const errorLog = document.getElementById('firebase-error-log');
|
||||
|
||||
connectionStatus.innerHTML = `
|
||||
<div style="padding: 10px; background-color: #cce5ff; color: #004085; border-radius: 4px; margin-bottom: 10px;">
|
||||
<strong>Testing connection...</strong>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
if (typeof firebase === 'undefined' || typeof firebase.firestore !== 'function') {
|
||||
connectionStatus.innerHTML = `
|
||||
<div style="padding: 10px; background-color: #f8d7da; color: #721c24; border-radius: 4px; margin-bottom: 10px;">
|
||||
<strong>Error:</strong> Firebase Firestore is not available.
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<div style="padding: 10px; background-color: #d4edda; color: #155724; border-radius: 4px; margin-bottom: 10px;">
|
||||
<strong>Connection successful!</strong> Firebase is working correctly.
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.catch(error => {
|
||||
let errorMessage = error.message;
|
||||
let solution = '';
|
||||
|
||||
// Provide specific guidance based on error
|
||||
if (error.code === 'permission-denied') {
|
||||
solution = `
|
||||
<div style="padding: 10px; background-color: #f8f9fa; border-radius: 4px; margin-top: 10px;">
|
||||
<strong>How to fix:</strong>
|
||||
<ol style="margin-top: 5px; padding-left: 20px;">
|
||||
<li>Go to the <a href="https://console.firebase.google.com/" target="_blank">Firebase Console</a></li>
|
||||
<li>Select your project</li>
|
||||
<li>Go to Firestore Database</li>
|
||||
<li>Go to Rules tab</li>
|
||||
<li>Update your rules to allow read/write access (for testing):</li>
|
||||
<pre style="background-color: #f1f1f1; padding: 10px; border-radius: 4px; overflow-x: auto;">
|
||||
rules_version = '2';
|
||||
service cloud.firestore {
|
||||
match /databases/{database}/documents {
|
||||
match /{document=**} {
|
||||
allow read, write: if true; // WARNING: Only for testing!
|
||||
}
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
<li>Click "Publish"</li>
|
||||
</ol>
|
||||
</div>
|
||||
`;
|
||||
} else if (error.code === 'invalid-argument' || errorMessage.includes('400')) {
|
||||
solution = `
|
||||
<div style="padding: 10px; background-color: #f8f9fa; border-radius: 4px; margin-top: 10px;">
|
||||
<strong>How to fix:</strong>
|
||||
<ol style="margin-top: 5px; padding-left: 20px;">
|
||||
<li>The 400 error usually means your Firebase configuration is invalid</li>
|
||||
<li>Make sure you've replaced the placeholder config with your own</li>
|
||||
<li>Check that your project exists in the <a href="https://console.firebase.google.com/" target="_blank">Firebase Console</a></li>
|
||||
<li>Verify that Firestore is enabled in your project</li>
|
||||
</ol>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
connectionStatus.innerHTML = `
|
||||
<div style="padding: 10px; background-color: #f8d7da; color: #721c24; border-radius: 4px; margin-bottom: 10px;">
|
||||
<strong>Connection failed:</strong> ${errorMessage}
|
||||
<div style="margin-top: 5px;"><strong>Error code:</strong> ${error.code || 'unknown'}</div>
|
||||
</div>
|
||||
${solution}
|
||||
`;
|
||||
|
||||
// Add to error log
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
errorLog.innerHTML += `
|
||||
<div style="padding: 10px; background-color: #f8f9fa; border-radius: 4px; margin-bottom: 10px; font-size: 12px;">
|
||||
<div><strong>${timestamp}</strong></div>
|
||||
<div><strong>Error:</strong> ${errorMessage}</div>
|
||||
<div><strong>Code:</strong> ${error.code || 'unknown'}</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
connectionStatus.innerHTML = `
|
||||
<div style="padding: 10px; background-color: #f8d7da; color: #721c24; border-radius: 4px; margin-bottom: 10px;">
|
||||
<strong>Error testing connection:</strong> ${error.message}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add to error log
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
errorLog.innerHTML += `
|
||||
<div style="padding: 10px; background-color: #f8f9fa; border-radius: 4px; margin-bottom: 10px; font-size: 12px;">
|
||||
<div><strong>${timestamp}</strong></div>
|
||||
<div><strong>Error:</strong> ${error.message}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 += `
|
||||
<div style="padding: 10px; background-color: #f8f9fa; border-radius: 4px; margin-bottom: 10px; font-size: 12px;">
|
||||
<div><strong>${timestamp}</strong></div>
|
||||
<div><strong>Console Error:</strong> ${errorString}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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';
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
684
index.html
Normal file
684
index.html
Normal file
@ -0,0 +1,684 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Fence & Perimeter Calculator (Google Maps)</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<!-- Firebase SDK -->
|
||||
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js"></script>
|
||||
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore-compat.js"></script>
|
||||
<!-- SheetJS (xlsx) for Excel import -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
#map {
|
||||
height: 100%;
|
||||
}
|
||||
.control-button {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
.control-button:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.control-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 text-gray-800">
|
||||
|
||||
<div class="container mx-auto p-4 md:p-8 max-w-7xl">
|
||||
<header class="text-center mb-8">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-gray-900">Fence & Perimeter Calculator</h1>
|
||||
<p class="text-lg text-gray-600 mt-2">Find an address, draw with arrows, or use the pencil for freehand mapping.</p>
|
||||
</header>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<div class="flex justify-center mb-8">
|
||||
<div class="inline-flex rounded-md shadow-sm" role="group">
|
||||
<button id="calculatorTab" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-blue-600 rounded-l-lg hover:bg-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-white">
|
||||
Calculator
|
||||
</button>
|
||||
<button id="materialsTab" class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-r-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700">
|
||||
Materials Database
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calculator Section -->
|
||||
<div id="calculatorSection" class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Left Column: Map and Controls -->
|
||||
<div class="lg:col-span-2 bg-white p-6 rounded-2xl shadow-lg flex flex-col">
|
||||
<div id="map" class="w-full h-96 md:h-[500px] flex-grow bg-gray-100 rounded-lg border-2 border-gray-200"></div>
|
||||
<div class="mt-6 flex flex-col items-center flex-shrink-0">
|
||||
<p class="text-gray-600 mb-2">Use the controls below to map your fence line</p>
|
||||
<div class="grid grid-cols-3 gap-2 w-48 mb-4">
|
||||
<div></div>
|
||||
<button id="up" class="control-button bg-blue-500 text-white p-3 rounded-md shadow-md hover:bg-blue-600">↑</button>
|
||||
<div></div>
|
||||
<button id="left" class="control-button bg-blue-500 text-white p-3 rounded-md shadow-md hover:bg-blue-600">←</button>
|
||||
<button id="down" class="control-button bg-blue-500 text-white p-3 rounded-md shadow-md hover:bg-blue-600">↓</button>
|
||||
<button id="right" class="control-button bg-blue-500 text-white p-3 rounded-md shadow-md hover:bg-blue-600">→</button>
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
<button id="pencil" class="control-button bg-indigo-500 text-white py-2 px-4 rounded-md shadow-md hover:bg-indigo-600">Pencil</button>
|
||||
<button id="place-gate" class="control-button bg-teal-500 text-white py-2 px-4 rounded-md shadow-md hover:bg-teal-600">Place Gate</button>
|
||||
<button id="set-start-point" class="control-button bg-purple-500 text-white py-2 px-4 rounded-md shadow-md hover:bg-purple-600">Set Start Point</button>
|
||||
<button id="undo" class="control-button bg-yellow-500 text-white py-2 px-4 rounded-md shadow-md hover:bg-yellow-600">Undo</button>
|
||||
<button id="reset" class="control-button bg-red-500 text-white py-2 px-4 rounded-md shadow-md hover:bg-red-600">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Calculations -->
|
||||
<div class="bg-white p-6 rounded-2xl shadow-lg">
|
||||
<h2 class="text-2xl font-bold mb-4 border-b pb-2">Tools & Calculations</h2>
|
||||
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-semibold mb-2">Find Address</h3>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<input type="text" id="address-input" placeholder="Enter an address or place" class="w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-semibold mb-2">Mapped Perimeter</h3>
|
||||
<div class="bg-blue-50 p-4 rounded-lg text-center">
|
||||
<span id="perimeterDisplay" class="text-3xl font-bold text-blue-800">0</span>
|
||||
<span class="text-gray-600">linear ft.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-semibold mb-2">Post Calculation</h3>
|
||||
<div class="space-y-2 bg-blue-50 p-4 rounded-lg">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">End Posts:</span>
|
||||
<span id="endPosts" class="text-xl font-bold text-blue-800">0</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">Corner Posts:</span>
|
||||
<span id="cornerPosts" class="text-xl font-bold text-blue-800">0</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">Line Posts:</span>
|
||||
<span id="linePosts" class="text-xl font-bold text-blue-800">0</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">Gate Posts:</span>
|
||||
<span id="gatePosts" class="text-xl font-bold text-blue-800">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-semibold mb-2">Gates</h3>
|
||||
<div class="space-y-2">
|
||||
<label for="gate-width-input" class="block text-sm font-medium text-gray-700">Gate Width (ft)</label>
|
||||
<input type="number" id="gate-width-input" value="4" class="w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm">
|
||||
<div id="gate-list" class="mt-2 space-y-1 max-h-24 overflow-y-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-2">Fence Section Estimation</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="productSelect" class="block text-sm font-medium text-gray-700">Fence Type</label>
|
||||
<select id="productSelect" class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<option>Wood Panel</option>
|
||||
<option>Chain Link</option>
|
||||
<option>Vinyl Panel</option>
|
||||
<option>Wrought Iron</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="coverageInput" class="block text-sm font-medium text-gray-700">Length per Fence Section (ft.)</label>
|
||||
<input type="number" id="coverageInput" value="8" class="mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
</div>
|
||||
<button id="calculateMaterial" class="w-full bg-green-500 text-white py-3 px-4 rounded-md shadow-md hover:bg-green-600 font-bold text-lg control-button">Calculate Sections</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="result" class="mt-6 p-4 bg-green-50 rounded-lg hidden">
|
||||
<h4 class="text-lg font-bold text-green-900 mb-2 text-center">Required Materials</h4>
|
||||
<div id="result-list" class="space-y-2"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Materials Database Section -->
|
||||
<div id="materialsSection" class="container mx-auto p-4 md:p-8 max-w-7xl hidden">
|
||||
<div class="bg-white p-6 rounded-2xl shadow-lg">
|
||||
<h2 class="text-2xl font-bold mb-6 border-b pb-2">Materials Database</h2>
|
||||
|
||||
<!-- Materials Table with Refresh Button -->
|
||||
<div class="mb-8">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold">Materials List</h3>
|
||||
<button id="refreshMaterials" class="py-2 px-4 bg-indigo-600 text-white rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
<span class="inline-block mr-1">🔄</span> Refresh Materials
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full bg-white border border-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="py-2 px-4 border-b border-gray-200 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Part Number</th>
|
||||
<th class="py-2 px-4 border-b border-gray-200 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
|
||||
<th class="py-2 px-4 border-b border-gray-200 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Old Style</th>
|
||||
<th class="py-2 px-4 border-b border-gray-200 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">New Style</th>
|
||||
<th class="py-2 px-4 border-b border-gray-200 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Color</th>
|
||||
<th class="py-2 px-4 border-b border-gray-200 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Price ($)</th>
|
||||
<th class="py-2 px-4 border-b border-gray-200 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="materialsTableBody">
|
||||
<!-- Materials will be loaded here dynamically -->
|
||||
<tr>
|
||||
<td class="py-4 px-4 border-b border-gray-200 text-sm" colspan="7">Loading materials...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Excel Import Section -->
|
||||
<div class="bg-blue-50 p-6 rounded-lg mb-6">
|
||||
<h3 class="text-lg font-semibold mb-4">Import Materials from Excel</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
Your Excel file should have columns for: Part Number, Description, Old Style, New Style, Color, and Price.
|
||||
The first row should be headers.
|
||||
</p>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<input type="file" id="excelFileInput" accept=".xlsx, .xls, .csv" class="block w-full text-sm text-gray-500
|
||||
file:mr-4 file:py-2 file:px-4
|
||||
file:rounded-md file:border-0
|
||||
file:text-sm file:font-semibold
|
||||
file:bg-blue-50 file:text-blue-700
|
||||
hover:file:bg-blue-100">
|
||||
<button id="importExcelBtn" class="py-2 px-4 bg-blue-600 text-white rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Import Materials
|
||||
</button>
|
||||
<div id="importStatus" class="text-sm hidden"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Material Form -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h3 class="text-lg font-semibold mb-4" id="formTitle">Add New Material</h3>
|
||||
<form id="materialForm" class="space-y-4">
|
||||
<input type="hidden" id="materialId">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label for="partNumber" class="block text-sm font-medium text-gray-700">Part Number</label>
|
||||
<input type="text" id="partNumber" class="mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<input type="text" id="description" class="mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="oldStyle" class="block text-sm font-medium text-gray-700">Old Style</label>
|
||||
<input type="text" id="oldStyle" class="mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="newStyle" class="block text-sm font-medium text-gray-700">New Style</label>
|
||||
<input type="text" id="newStyle" class="mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="color" class="block text-sm font-medium text-gray-700">Color</label>
|
||||
<input type="text" id="color" class="mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="price" class="block text-sm font-medium text-gray-700">Price ($)</label>
|
||||
<input type="number" id="price" step="0.01" min="0" class="mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" id="cancelEdit" class="py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 hidden">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
Save Material
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Firebase Configuration
|
||||
// ⚠️ IMPORTANT: You MUST replace this with your own Firebase configuration ⚠️
|
||||
// The placeholder values below will cause 400 errors when trying to use the database
|
||||
//
|
||||
// Instructions to get your Firebase config:
|
||||
// 1. Go to https://console.firebase.google.com/
|
||||
// 2. Click "Add project" or select your existing project
|
||||
// 3. After project creation, click the web icon (</>) to add a web app
|
||||
// 4. Register your app with a nickname (e.g., "Fence Estimator")
|
||||
// 5. Copy the firebaseConfig object shown after registration
|
||||
// 6. Replace this entire configuration object with yours
|
||||
//
|
||||
// If you're seeing 400 errors, click the "Debug Firebase" button at the bottom right
|
||||
// of the screen for troubleshooting help.
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyAfG-uAp2ePfOntuLUcgUB7rDNbUPi1dro", // ⚠️ REPLACE THIS
|
||||
authDomain: "fence-estimator-a8923.firebaseapp.com", // ⚠️ REPLACE THIS
|
||||
projectId: "fence-estimator-a8923", // ⚠️ REPLACE THIS
|
||||
storageBucket: "fence-estimator-a8923.firebasestorage.app", // ⚠️ REPLACE THIS
|
||||
messagingSenderId: "635809080450", // ⚠️ REPLACE THIS
|
||||
appId: "1:635809080450:web:4997e21d8df34a6c61809b", // ⚠️ REPLACE THIS
|
||||
measurementId: "G-F2E2R3JTMR"
|
||||
|
||||
};
|
||||
|
||||
// Declare Firebase variables in global scope and explicitly attach to window
|
||||
window.db = null;
|
||||
window.materialsCollection = null;
|
||||
|
||||
// Initialize Firebase with error handling
|
||||
try {
|
||||
firebase.initializeApp(firebaseConfig);
|
||||
window.db = firebase.firestore();
|
||||
window.materialsCollection = window.db.collection('materials');
|
||||
|
||||
// Also set regular variables for backward compatibility
|
||||
db = window.db;
|
||||
materialsCollection = window.materialsCollection;
|
||||
|
||||
// Test connection to catch 400 errors early
|
||||
db.collection('test').limit(1).get()
|
||||
.catch(error => {
|
||||
console.error('Firebase connection test failed:', error);
|
||||
if (error.code === 'permission-denied') {
|
||||
console.error('Firebase permission denied. Check your Firestore rules.');
|
||||
} else if (error.message && error.message.includes('400')) {
|
||||
console.error('400 Bad Request: Your Firebase configuration is likely invalid.');
|
||||
console.error('Make sure you have replaced the placeholder values with your own Firebase config.');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Firebase initialization error:', error);
|
||||
alert('Error initializing Firebase. Check the console for details (F12) or click the Debug Firebase button.');
|
||||
}
|
||||
|
||||
// Materials Database Variables
|
||||
let currentEditId = null;
|
||||
let materials = [];
|
||||
|
||||
let map;
|
||||
let currentPolyline;
|
||||
let drawingManager;
|
||||
let isSettingStartPoint = false;
|
||||
let isPlacingGate = false;
|
||||
let gateMarkers = [];
|
||||
|
||||
const METERS_TO_FEET = 3.28084;
|
||||
const STEP_METERS = 3.048; // Approx 10 feet
|
||||
const CORNER_DEGREE_THRESHOLD = 15;
|
||||
|
||||
function initMap() {
|
||||
const chathamON = { lat: 42.4048, lng: -82.1910 };
|
||||
|
||||
map = new google.maps.Map(document.getElementById("map"), {
|
||||
zoom: 18,
|
||||
center: chathamON,
|
||||
mapTypeId: "satellite",
|
||||
disableDefaultUI: true,
|
||||
zoomControl: true,
|
||||
mapTypeControl: true,
|
||||
streetViewControl: true,
|
||||
fullscreenControl: true,
|
||||
});
|
||||
|
||||
currentPolyline = new google.maps.Polyline({
|
||||
strokeColor: "#0000FF",
|
||||
strokeOpacity: 1.0,
|
||||
strokeWeight: 3,
|
||||
editable: true,
|
||||
map: map,
|
||||
});
|
||||
|
||||
drawingManager = new google.maps.drawing.DrawingManager({
|
||||
drawingMode: null,
|
||||
drawingControl: false,
|
||||
polylineOptions: {
|
||||
editable: true,
|
||||
strokeColor: "#0000FF",
|
||||
strokeWeight: 3,
|
||||
},
|
||||
});
|
||||
drawingManager.setMap(map);
|
||||
|
||||
resetPolyline(map.getCenter());
|
||||
setupEventListeners();
|
||||
}
|
||||
|
||||
function listenForPolylineChanges(poly) {
|
||||
google.maps.event.addListener(poly.getPath(), 'insert_at', calculateAllMetrics);
|
||||
google.maps.event.addListener(poly.getPath(), 'remove_at', calculateAllMetrics);
|
||||
google.maps.event.addListener(poly.getPath(), 'set_at', calculateAllMetrics);
|
||||
}
|
||||
|
||||
function resetPolyline(position) {
|
||||
const path = currentPolyline.getPath();
|
||||
path.clear();
|
||||
if (position) {
|
||||
path.push(position);
|
||||
map.setCenter(position);
|
||||
}
|
||||
calculateAllMetrics();
|
||||
document.getElementById('result').classList.add('hidden');
|
||||
gateMarkers.forEach(marker => marker.setMap(null));
|
||||
gateMarkers = [];
|
||||
renderGateList();
|
||||
}
|
||||
|
||||
function calculateAllMetrics() {
|
||||
const path = currentPolyline.getPath();
|
||||
const pointsArray = path.getArray();
|
||||
let perimeter = 0;
|
||||
let cornerPosts = 0;
|
||||
let endPosts = 0;
|
||||
|
||||
if (pointsArray.length >= 2) {
|
||||
perimeter = google.maps.geometry.spherical.computeLength(path);
|
||||
endPosts = 2; // A line always has a start and an end.
|
||||
}
|
||||
|
||||
if (pointsArray.length > 2) {
|
||||
for (let i = 1; i < pointsArray.length - 1; i++) {
|
||||
const p0 = pointsArray[i - 1];
|
||||
const p1 = pointsArray[i];
|
||||
const p2 = pointsArray[i + 1];
|
||||
|
||||
const heading1 = google.maps.geometry.spherical.computeHeading(p0, p1);
|
||||
const heading2 = google.maps.geometry.spherical.computeHeading(p1, p2);
|
||||
|
||||
const angleChange = 180 - Math.abs(Math.abs(heading1 - heading2) - 180);
|
||||
|
||||
if (angleChange > CORNER_DEGREE_THRESHOLD) {
|
||||
cornerPosts++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lengthInFeet = (perimeter * METERS_TO_FEET).toFixed(2);
|
||||
document.getElementById('perimeterDisplay').textContent = lengthInFeet;
|
||||
document.getElementById('endPosts').textContent = endPosts;
|
||||
document.getElementById('cornerPosts').textContent = cornerPosts;
|
||||
document.getElementById('linePosts').textContent = 0; // Reset line posts
|
||||
document.getElementById('gatePosts').textContent = 0; // Reset gate posts
|
||||
}
|
||||
|
||||
function renderGateList() {
|
||||
const gateListEl = document.getElementById('gate-list');
|
||||
gateListEl.innerHTML = '';
|
||||
gateMarkers.forEach(marker => {
|
||||
const gateEl = document.createElement('div');
|
||||
gateEl.className = 'flex justify-between items-center bg-gray-100 p-1 rounded text-sm';
|
||||
gateEl.innerHTML = `
|
||||
<span>${marker.gateWidth} ft. Gate</span>
|
||||
<button data-id="${marker.gateId}" class="remove-gate text-red-500 hover:text-red-700 font-bold text-lg leading-none p-1">×</button>
|
||||
`;
|
||||
gateListEl.appendChild(gateEl);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.remove-gate').forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
const gateIdToRemove = parseInt(e.target.dataset.id);
|
||||
const markerIndex = gateMarkers.findIndex(m => m.gateId === gateIdToRemove);
|
||||
if (markerIndex > -1) {
|
||||
gateMarkers[markerIndex].setMap(null); // Remove from map
|
||||
gateMarkers.splice(markerIndex, 1); // Remove from array
|
||||
}
|
||||
renderGateList(); // Re-render the list
|
||||
document.getElementById('result').classList.add('hidden'); // Hide old results
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function placeGateOnLine(clickEvent) {
|
||||
if (!google.maps.geometry.poly.isLocationOnEdge(clickEvent.latLng, currentPolyline, 1e-4)) {
|
||||
alert("Please click directly on the fence line to place a gate.");
|
||||
return;
|
||||
}
|
||||
|
||||
const gateWidth = parseFloat(document.getElementById('gate-width-input').value);
|
||||
if (isNaN(gateWidth) || gateWidth <= 0) {
|
||||
alert("Please enter a valid number for the gate width.");
|
||||
return;
|
||||
}
|
||||
|
||||
const gateMarker = new google.maps.Marker({
|
||||
position: clickEvent.latLng,
|
||||
map: map,
|
||||
icon: {
|
||||
path: google.maps.SymbolPath.CIRCLE,
|
||||
scale: 7,
|
||||
fillColor: '#10B981',
|
||||
fillOpacity: 1,
|
||||
strokeColor: 'white',
|
||||
strokeWeight: 2,
|
||||
},
|
||||
title: `${gateWidth} ft. Gate`
|
||||
});
|
||||
|
||||
gateMarker.gateWidth = gateWidth;
|
||||
gateMarker.gateId = Date.now(); // Unique ID for removal
|
||||
gateMarkers.push(gateMarker);
|
||||
renderGateList();
|
||||
document.getElementById('result').classList.add('hidden');
|
||||
}
|
||||
|
||||
function move(direction) {
|
||||
const path = currentPolyline.getPath();
|
||||
if (path.getLength() === 0) {
|
||||
path.push(map.getCenter());
|
||||
}
|
||||
const lastPoint = path.getAt(path.getLength() - 1);
|
||||
let heading;
|
||||
|
||||
switch (direction) {
|
||||
case 'up': heading = 0; break;
|
||||
case 'down': heading = 180; break;
|
||||
case 'left': heading = 270; break;
|
||||
case 'right': heading = 90; break;
|
||||
}
|
||||
|
||||
const newPoint = google.maps.geometry.spherical.computeOffset(lastPoint, STEP_METERS, heading);
|
||||
path.push(newPoint);
|
||||
map.panTo(newPoint);
|
||||
calculateAllMetrics();
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
const resultDiv = document.getElementById('result');
|
||||
const resultList = document.getElementById('result-list');
|
||||
const setStartPointBtn = document.getElementById('set-start-point');
|
||||
const pencilBtn = document.getElementById('pencil');
|
||||
const placeGateBtn = document.getElementById('place-gate');
|
||||
const addressInput = document.getElementById('address-input');
|
||||
|
||||
const autocomplete = new google.maps.places.Autocomplete(addressInput, { fields: ["geometry", "name"], types: ["address"] });
|
||||
autocomplete.bindTo("bounds", map);
|
||||
autocomplete.addListener("place_changed", () => {
|
||||
const place = autocomplete.getPlace();
|
||||
if (place.geometry && place.geometry.location) {
|
||||
resetPolyline(place.geometry.location);
|
||||
map.setZoom(19);
|
||||
} else {
|
||||
alert("Could not find address. Please try again.");
|
||||
}
|
||||
});
|
||||
|
||||
google.maps.event.addListener(drawingManager, 'overlaycomplete', function(event) {
|
||||
drawingManager.setDrawingMode(null);
|
||||
pencilBtn.textContent = 'Pencil';
|
||||
pencilBtn.classList.remove('bg-green-600');
|
||||
pencilBtn.classList.add('bg-indigo-500');
|
||||
|
||||
if (event.type === google.maps.drawing.OverlayType.POLYLINE) {
|
||||
currentPolyline.setPath([]);
|
||||
currentPolyline = event.overlay;
|
||||
listenForPolylineChanges(currentPolyline);
|
||||
calculateAllMetrics();
|
||||
}
|
||||
});
|
||||
|
||||
pencilBtn.addEventListener('click', () => {
|
||||
const isDrawing = drawingManager.getDrawingMode() != null;
|
||||
if (isDrawing) {
|
||||
drawingManager.setDrawingMode(null);
|
||||
pencilBtn.textContent = 'Pencil';
|
||||
pencilBtn.classList.remove('bg-green-600');
|
||||
pencilBtn.classList.add('bg-indigo-500');
|
||||
} else {
|
||||
drawingManager.setDrawingMode(google.maps.drawing.OverlayType.POLYLINE);
|
||||
pencilBtn.textContent = 'Drawing...';
|
||||
pencilBtn.classList.add('bg-green-600');
|
||||
pencilBtn.classList.remove('bg-indigo-500');
|
||||
}
|
||||
});
|
||||
|
||||
placeGateBtn.addEventListener('click', () => {
|
||||
isPlacingGate = true;
|
||||
map.setOptions({ draggableCursor: 'crosshair' });
|
||||
placeGateBtn.textContent = 'Click on line...';
|
||||
placeGateBtn.classList.add('bg-green-600');
|
||||
placeGateBtn.classList.remove('bg-teal-500');
|
||||
});
|
||||
|
||||
document.getElementById('up').addEventListener('click', () => move('up'));
|
||||
document.getElementById('down').addEventListener('click', () => move('down'));
|
||||
document.getElementById('left').addEventListener('click', () => move('left'));
|
||||
document.getElementById('right').addEventListener('click', () => move('right'));
|
||||
|
||||
setStartPointBtn.addEventListener('click', () => {
|
||||
isSettingStartPoint = true;
|
||||
map.setOptions({ draggableCursor: 'crosshair' });
|
||||
setStartPointBtn.textContent = 'Click on map...';
|
||||
setStartPointBtn.classList.add('bg-green-600');
|
||||
setStartPointBtn.classList.remove('bg-purple-500');
|
||||
});
|
||||
|
||||
map.addListener('click', (event) => {
|
||||
if (isSettingStartPoint) {
|
||||
resetPolyline(event.latLng);
|
||||
isSettingStartPoint = false;
|
||||
map.setOptions({ draggableCursor: null });
|
||||
setStartPointBtn.textContent = 'Set Start Point';
|
||||
setStartPointBtn.classList.remove('bg-green-600');
|
||||
setStartPointBtn.classList.add('bg-purple-500');
|
||||
} else if (isPlacingGate) {
|
||||
placeGateOnLine(event);
|
||||
isPlacingGate = false;
|
||||
map.setOptions({ draggableCursor: null });
|
||||
placeGateBtn.textContent = 'Place Gate';
|
||||
placeGateBtn.classList.remove('bg-green-600');
|
||||
placeGateBtn.classList.add('bg-teal-500');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('undo').addEventListener('click', () => {
|
||||
const path = currentPolyline.getPath();
|
||||
if (path.getLength() > 1) {
|
||||
path.pop();
|
||||
calculateAllMetrics();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('reset').addEventListener('click', () => {
|
||||
resetPolyline(map.getCenter());
|
||||
});
|
||||
|
||||
document.getElementById('calculateMaterial').addEventListener('click', () => {
|
||||
const perimeter = parseFloat(document.getElementById('perimeterDisplay').textContent);
|
||||
const sectionLength = parseFloat(document.getElementById('coverageInput').value);
|
||||
const endPosts = parseInt(document.getElementById('endPosts').textContent);
|
||||
const cornerPosts = parseInt(document.getElementById('cornerPosts').textContent);
|
||||
const productSelect = document.getElementById('productSelect');
|
||||
const selectedOption = productSelect.options[productSelect.selectedIndex];
|
||||
const productId = productSelect.value;
|
||||
|
||||
if (isNaN(perimeter) || isNaN(sectionLength) || perimeter <= 0 || sectionLength <= 0) {
|
||||
resultDiv.classList.remove('hidden');
|
||||
resultDiv.querySelector('p').textContent = "Please map a fence line and set a valid section length.";
|
||||
resultDiv.classList.remove('bg-green-50', 'text-green-800');
|
||||
resultDiv.classList.add('bg-red-50', 'text-red-800');
|
||||
return;
|
||||
}
|
||||
|
||||
const totalGateWidth = gateMarkers.reduce((sum, marker) => sum + marker.gateWidth, 0);
|
||||
const fencingPerimeter = Math.max(0, perimeter - totalGateWidth);
|
||||
const sectionsNeeded = Math.ceil(fencingPerimeter / sectionLength);
|
||||
|
||||
const gatePosts = gateMarkers.length * 2;
|
||||
const linePosts = Math.max(0, sectionsNeeded - 1 - cornerPosts);
|
||||
const totalPosts = endPosts + cornerPosts + linePosts + gatePosts;
|
||||
|
||||
let gateSummary = gateMarkers.length > 0 ? `<div class="flex justify-between"><span>Gates (${gateMarkers.length}):</span> <span class="font-bold">${totalGateWidth.toFixed(2)} ft. total</span></div>` : '';
|
||||
|
||||
resultList.innerHTML = `
|
||||
<div class="flex justify-between"><span>Fence Panels/Sections:</span> <span class="font-bold">${sectionsNeeded}</span></div>
|
||||
${gateSummary}
|
||||
<hr class="my-1">
|
||||
<div class="flex justify-between"><span>End Posts:</span> <span class="font-bold">${endPosts}</span></div>
|
||||
<div class="flex justify-between"><span>Corner Posts:</span> <span class="font-bold">${cornerPosts}</span></div>
|
||||
<div class="flex justify-between"><span>Gate Posts:</span> <span class="font-bold">${gatePosts}</span></div>
|
||||
<div class="flex justify-between"><span>Line Posts:</span> <span class="font-bold">${linePosts}</span></div>
|
||||
<hr class="my-1 border-t-2 border-green-200">
|
||||
<div class="flex justify-between text-green-900"><strong>Total Posts:</strong> <strong class="font-bold">${totalPosts}</strong></div>
|
||||
`;
|
||||
resultDiv.classList.remove('hidden');
|
||||
});
|
||||
|
||||
listenForPolylineChanges(currentPolyline);
|
||||
}
|
||||
|
||||
// Tab Switching
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const calculatorTab = document.getElementById('calculatorTab');
|
||||
const materialsTab = document.getElementById('materialsTab');
|
||||
const calculatorSection = document.getElementById('calculatorSection');
|
||||
const materialsSection = document.getElementById('materialsSection');
|
||||
|
||||
calculatorTab.addEventListener('click', function() {
|
||||
calculatorSection.classList.remove('hidden');
|
||||
materialsSection.classList.add('hidden');
|
||||
calculatorTab.classList.remove('bg-white', 'text-gray-900');
|
||||
calculatorTab.classList.add('bg-blue-600', 'text-white');
|
||||
materialsTab.classList.remove('bg-blue-600', 'text-white');
|
||||
materialsTab.classList.add('bg-white', 'text-gray-900');
|
||||
});
|
||||
|
||||
materialsTab.addEventListener('click', function() {
|
||||
calculatorSection.classList.add('hidden');
|
||||
materialsSection.classList.remove('hidden');
|
||||
materialsTab.classList.remove('bg-white', 'text-gray-900');
|
||||
materialsTab.classList.add('bg-blue-600', 'text-white');
|
||||
calculatorTab.classList.remove('bg-blue-600', 'text-white');
|
||||
calculatorTab.classList.add('bg-white', 'text-gray-900');
|
||||
loadMaterials();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Materials Database Script -->
|
||||
<script src="materials-db.js"></script>
|
||||
|
||||
<!-- Firebase Debug Helper -->
|
||||
<script src="firebase-debug.js"></script>
|
||||
|
||||
<!-- Google Maps API -->
|
||||
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyAhN9FDQeQe3CUGR5ZXb7TYCvciu1sfcNw&callback=initMap&libraries=geometry,drawing,places&v=weekly" async defer></script>
|
||||
</body>
|
||||
</html>
|
||||
404
materials-db.js
Normal file
404
materials-db.js
Normal file
@ -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 = '<tr><td colspan="7">Loading...</td></tr>';
|
||||
let tableHTML = '';
|
||||
materials = [];
|
||||
|
||||
try {
|
||||
const snapshot = await window.materialsCollection.get();
|
||||
if (snapshot.empty) {
|
||||
tableBody.innerHTML = '<tr><td colspan="7">No materials found.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
for (const doc of snapshot.docs) {
|
||||
const group = doc.data();
|
||||
group.id = doc.id;
|
||||
materials.push(group);
|
||||
|
||||
// Render group row
|
||||
tableHTML += `
|
||||
<tr class="bg-yellow-200 font-bold">
|
||||
<td class="py-2 px-4 border-b border-gray-200" colspan="6">${group.description || 'Group'}</td>
|
||||
<td class="py-2 px-4 border-b border-gray-200">
|
||||
<button class="edit-btn text-blue-600 hover:text-blue-800 mr-2" data-id="${group.id}">Edit</button>
|
||||
<button class="delete-btn text-red-600 hover:text-red-800" data-id="${group.id}">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
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 += `
|
||||
<tr class="pl-4">
|
||||
<td class="py-2 px-4 border-b border-gray-200">${item.partNumber || ''}</td>
|
||||
<td class="py-2 px-4 border-b border-gray-200">${item.description || ''}</td>
|
||||
<td class="py-2 px-4 border-b border-gray-200">${item.oldStyle || ''}</td>
|
||||
<td class="py-2 px-4 border-b border-gray-200">${item.newStyle || ''}</td>
|
||||
<td class="py-2 px-4 border-b border-gray-200">${item.color || ''}</td>
|
||||
<td class="py-2 px-4 border-b border-gray-200">$${parseFloat(item.price || 0).toFixed(2)}</td>
|
||||
<td class="py-2 px-4 border-b border-gray-200">
|
||||
<button class="edit-btn text-blue-600 hover:text-blue-800 mr-2" data-id="${item.id}" data-group-id="${group.id}">Edit</button>
|
||||
<button class="delete-btn text-red-600 hover:text-red-800" data-id="${item.id}" data-group-id="${group.id}">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
tableBody.innerHTML = tableHTML;
|
||||
attachActionListeners();
|
||||
updateProductSelect();
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error loading materials: ", error);
|
||||
tableBody.innerHTML = '<tr><td colspan="7">Error loading materials.</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
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 = `<div class="font-bold">✅ Import complete!</div>
|
||||
<div class="mt-2">
|
||||
<span class="font-bold">${totalSuccessCount}</span> batch(es) imported successfully.
|
||||
</div>`;
|
||||
if (totalErrorCount > 0) {
|
||||
statusHTML += `<div class="mt-2 text-red-600">
|
||||
<span class="font-bold">${totalErrorCount}</span> errors occurred.
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-gray-600">Error Details: <pre>${errorDetails.join('\n')}</pre></div>`;
|
||||
}
|
||||
statusHTML += `<div class="mt-4 p-2 border border-green-300 rounded bg-green-100">
|
||||
Your materials have been imported. Refreshing list...
|
||||
</div>`;
|
||||
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 = '<span class="inline-block animate-spin mr-1">🔄</span> Refreshing...';
|
||||
button.disabled = true;
|
||||
|
||||
// Add a small delay to make the refresh action more visible
|
||||
setTimeout(function() {
|
||||
loadMaterials();
|
||||
|
||||
// Show success state briefly
|
||||
button.innerHTML = '<span class="inline-block mr-1">✅</span> 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user