Upload files to "/"
This commit is contained in:
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