Upload files to "/"

This commit is contained in:
2025-06-21 14:58:56 -04:00
commit ef551377f8
3 changed files with 1388 additions and 0 deletions

300
firebase-debug.js Normal file
View 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
View 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">&times;</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
View 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);
});
});