405 lines
18 KiB
JavaScript
405 lines
18 KiB
JavaScript
// 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);
|
|
});
|
|
});
|