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

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);
});
});