`;
// Add event listeners for edit and delete
tagElement.querySelector('.edit-tag').addEventListener('click', () => {
editTag(tag);
});
tagElement.querySelector('.delete-tag').addEventListener('click', () => {
deleteTag(tag.id);
});
existingTagsContainer.appendChild(tagElement);
});
}
// Edit a tag
function editTag(tag) {
const tagInfoElement = document.querySelector(`.existing-tag .existing-tag-info:has(+ .existing-tag-actions button[data-id="${tag.id}"])`);
if (!tagInfoElement) {
// Alternative selector for browsers that don't support :has
const tagElement = document.querySelector(`.existing-tag`);
const buttons = tagElement?.querySelectorAll(`.existing-tag-actions button[data-id="${tag.id}"]`);
if (buttons?.length > 0) {
const parent = buttons[0].closest('.existing-tag');
if (parent) {
const infoElement = parent.querySelector('.existing-tag-info');
if (infoElement) {
tagInfoElement = infoElement;
}
}
}
if (!tagInfoElement) return;
}
const originalHTML = tagInfoElement.innerHTML;
tagInfoElement.innerHTML = `
`;
// Add event listeners
tagInfoElement.querySelector('.save-edit').addEventListener('click', () => {
const newName = tagInfoElement.querySelector('.edit-tag-name').value.trim();
const newColor = tagInfoElement.querySelector('.edit-tag-color').value;
if (!newName) {
showToast('Tag name is required', 'error');
return;
}
updateTag(tag.id, newName, newColor);
});
tagInfoElement.querySelector('.cancel-edit').addEventListener('click', () => {
// Restore original HTML
tagInfoElement.innerHTML = originalHTML;
});
}
// Update a tag
function updateTag(id, name, color) {
const token = localStorage.getItem('auth_token');
if (!token) {
console.error('No authentication token found');
return;
}
fetch(`/api/tags/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({
name: name,
color: color
})
})
.then(response => {
if (!response.ok) {
if (response.status === 409) {
throw new Error('A tag with this name already exists');
}
throw new Error('Failed to update tag');
}
return response.json();
})
.then(data => {
// Update tag in allTags array
const index = allTags.findIndex(tag => tag.id === id);
if (index !== -1) {
allTags[index].name = name;
allTags[index].color = color;
}
// Update tag in selectedTags if present
const selectedIndex = selectedTags.findIndex(tag => tag.id === id);
if (selectedIndex !== -1) {
selectedTags[selectedIndex].name = name;
selectedTags[selectedIndex].color = color;
}
// Rerender existing tags and selected tags
renderExistingTags();
renderSelectedTags();
// Update summary if needed
if (document.getElementById('summary-tags')) {
updateSummary();
}
showToast('Tag updated successfully', 'success');
})
.catch(error => {
console.error('Error updating tag:', error);
showToast(error.message || 'Failed to update tag', 'error');
});
}
// Delete a tag
function deleteTag(id) {
if (!confirm('Are you sure you want to delete this tag? It will be removed from all warranties.')) {
return;
}
const token = localStorage.getItem('auth_token');
if (!token) {
console.error('No authentication token found');
showToast('Authentication required', 'error'); // Added toast for better feedback
return;
}
showLoadingSpinner(); // Show loading indicator
fetch(`/api/tags/${id}`, { // Use the correct URL with tag ID
method: 'DELETE', // Use DELETE method
headers: {
'Authorization': 'Bearer ' + token
}
})
.then(response => {
if (!response.ok) {
// Log the status for debugging the 405 error
console.error(`Failed to delete tag. Status: ${response.status} ${response.statusText}`);
// Try to get error message from response body
return response.json().then(errData => {
throw new Error(errData.error || errData.message || 'Failed to delete tag');
}).catch(() => {
// If response body is not JSON or empty
throw new Error(`Failed to delete tag. Status: ${response.status}`);
});
}
return response.json();
})
.then(data => {
// Remove tag from allTags array
allTags = allTags.filter(tag => tag.id !== id);
// Remove tag from selectedTags if present (in both add and edit modes)
selectedTags = selectedTags.filter(tag => tag.id !== id);
editSelectedTags = editSelectedTags.filter(tag => tag.id !== id);
// --- FIX: Re-render UI elements ---
renderExistingTags(); // Update the list in the modal
renderSelectedTags(); // Update selected tags in the add form
renderEditSelectedTags(); // Update selected tags in the edit form
populateTagFilter(); // Update the filter dropdown on the main page
// --- END FIX ---
showToast('Tag deleted successfully', 'success');
})
.catch(error => {
console.error('Error deleting tag:', error);
showToast(error.message || 'Failed to delete tag', 'error'); // Show specific error message
})
.finally(() => {
hideLoadingSpinner(); // Hide loading indicator
});
}
// Set up event listeners for UI controls
function setupUIEventListeners() {
// Initialize edit tabs
initEditTabs();
// Close modals when clicking outside or on close button
document.querySelectorAll('.modal-backdrop, [data-dismiss="modal"]').forEach(element => {
element.addEventListener('click', (e) => {
// Check if the click is on the backdrop itself OR a dismiss button
if (e.target === element || e.target.matches('[data-dismiss="modal"]')) {
// Find the closest modal backdrop to the element clicked
const modalToClose = e.target.closest('.modal-backdrop');
if (modalToClose) {
// *** MODIFIED CHECK ***
// If the click target is the backdrop itself (not a dismiss button)
// AND the modal is the 'addWarrantyModal', then DO NOTHING.
if (modalToClose.id === 'addWarrantyModal' && e.target === modalToClose) {
return; // Ignore backdrop click for addWarrantyModal
}
// *** END MODIFIED CHECK ***
// Otherwise, close the modal (handles other modals' backdrop clicks and all dismiss buttons)
modalToClose.classList.remove('active');
// Reset forms only when closing the respective modal
if (modalToClose.id === 'editModal') {
// Optional: Add any edit form reset logic here if needed
console.log('Edit modal closed, reset logic (if any) can go here.');
} else if (modalToClose.id === 'addWarrantyModal') {
// This reset will now only trigger if closed via dismiss button
resetAddWarrantyWizard();
}
// Add similar reset logic for other modals like deleteModal if needed
// else if (modalToClose.id === 'deleteModal') { ... }
}
}
});
});
// Prevent modal content clicks from closing the modal
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
e.stopPropagation();
});
});
// Filter event listeners
const searchInput = document.getElementById('searchWarranties');
const clearSearchBtn = document.getElementById('clearSearch');
const statusFilter = document.getElementById('statusFilter');
const tagFilter = document.getElementById('tagFilter');
const sortBySelect = document.getElementById('sortBy');
if (searchInput) {
searchInput.addEventListener('input', () => {
currentFilters.search = searchInput.value.toLowerCase();
// Show/hide clear button based on search input
if (clearSearchBtn) {
clearSearchBtn.style.display = searchInput.value ? 'flex' : 'none';
}
// Add visual feedback class to search box when active
if (searchInput.value) {
searchInput.parentElement.classList.add('active-search');
} else {
searchInput.parentElement.classList.remove('active-search');
}
applyFilters();
});
}
if (clearSearchBtn) {
clearSearchBtn.addEventListener('click', () => {
if (searchInput) {
searchInput.value = '';
currentFilters.search = '';
clearSearchBtn.style.display = 'none';
searchInput.parentElement.classList.remove('active-search');
searchInput.focus();
applyFilters();
}
});
}
if (statusFilter) {
statusFilter.addEventListener('change', () => {
currentFilters.status = statusFilter.value;
applyFilters();
});
}
if (tagFilter) {
tagFilter.addEventListener('change', () => {
currentFilters.tag = tagFilter.value;
applyFilters();
});
}
if (sortBySelect) {
sortBySelect.addEventListener('change', () => {
currentFilters.sortBy = sortBySelect.value;
applyFilters();
});
}
// View switcher event listeners
const gridViewBtn = document.getElementById('gridViewBtn');
const listViewBtn = document.getElementById('listViewBtn');
const tableViewBtn = document.getElementById('tableViewBtn');
if (gridViewBtn) gridViewBtn.addEventListener('click', () => switchView('grid'));
if (listViewBtn) listViewBtn.addEventListener('click', () => switchView('list'));
if (tableViewBtn) tableViewBtn.addEventListener('click', () => switchView('table'));
// Export button event listener
const exportBtn = document.getElementById('exportBtn');
if (exportBtn) exportBtn.addEventListener('click', exportWarranties);
// Import button event listener
if (importBtn && csvFileInput) {
importBtn.addEventListener('click', () => {
csvFileInput.click(); // Trigger hidden file input
});
csvFileInput.addEventListener('change', (event) => {
if (event.target.files && event.target.files.length > 0) {
handleImport(event.target.files[0]);
}
});
}
// Refresh button
const refreshBtn = document.getElementById('refreshBtn');
if (refreshBtn) refreshBtn.addEventListener('click', loadWarranties);
// Save warranty changes
const saveWarrantyBtn = document.getElementById('saveWarrantyBtn');
if (saveWarrantyBtn) saveWarrantyBtn.addEventListener('click', saveWarranty);
// Confirm delete button
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
if (confirmDeleteBtn) confirmDeleteBtn.addEventListener('click', deleteWarranty);
// Load saved view preference
// loadViewPreference(); // Disabled: now called after authStateReady
}
// Function to show loading spinner
function showLoadingSpinner() {
if (loadingContainer) {
loadingContainer.style.display = 'flex';
}
}
// Function to hide loading spinner
function hideLoadingSpinner() {
if (loadingContainer) {
loadingContainer.style.display = 'none';
}
}
// Delete warranty function
function deleteWarranty() {
if (!currentWarrantyId) {
showToast('No warranty selected for deletion', 'error');
return;
}
const token = localStorage.getItem('auth_token');
if (!token) {
showToast('Authentication required', 'error');
return;
}
showLoadingSpinner();
fetch(`/api/warranties/${currentWarrantyId}`, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + token
}
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to delete warranty');
}
return response.json();
})
.then(data => {
hideLoadingSpinner();
showToast('Warranty deleted successfully', 'success');
closeModals();
// --- BEGIN FIX: Update UI immediately ---
// Remove the deleted warranty from the global array
const deletedId = currentWarrantyId; // Store ID before resetting
warranties = warranties.filter(warranty => warranty.id !== deletedId);
currentWarrantyId = null; // Reset current ID
// Re-render the list using the updated local array
applyFilters();
// --- END FIX ---
})
.catch(error => {
hideLoadingSpinner();
console.error('Error deleting warranty:', error);
showToast('Failed to delete warranty', 'error');
});
}
// Save warranty updates
function saveWarranty() {
if (!currentWarrantyId) {
showToast('No warranty selected for update', 'error');
return;
}
// --- Get form values ---
const productName = document.getElementById('editProductName').value.trim();
const purchaseDate = document.getElementById('editPurchaseDate').value;
const isLifetime = document.getElementById('editIsLifetime').checked;
const warrantyYears = document.getElementById('editWarrantyYears').value; // Declare only once
// Basic validation
if (!productName) {
showToast('Product name is required', 'error');
return;
}
if (!purchaseDate) {
showToast('Purchase date is required', 'error');
return;
}
// --- Modified Validation ---
if (!isLifetime) {
if (!warrantyYears || parseFloat(warrantyYears) <= 0) {
showToast('Warranty period (years) must be greater than 0 for non-lifetime warranties', 'error');
// Optional: focus the years input again
const yearsInput = document.getElementById('editWarrantyYears');
if (yearsInput) { // Check if element exists
yearsInput.focus();
yearsInput.classList.add('invalid');
}
return;
}
}
// --- End Modified Validation ---
// Create form data
const formData = new FormData();
formData.append('product_name', productName);
formData.append('purchase_date', purchaseDate);
// Optional fields
const productUrl = document.getElementById('editProductUrl').value.trim();
if (productUrl) {
formData.append('product_url', productUrl);
}
const purchasePrice = document.getElementById('editPurchasePrice').value;
if (purchasePrice) {
formData.append('purchase_price', purchasePrice);
}
// Serial numbers
const serialInputs = document.querySelectorAll('#editSerialNumbersContainer input');
serialInputs.forEach(input => {
if (input.value.trim()) {
formData.append('serial_numbers', input.value.trim());
}
});
// Tags - add tag IDs as JSON string
if (editSelectedTags && editSelectedTags.length > 0) {
const tagIds = editSelectedTags.map(tag => tag.id);
formData.append('tag_ids', JSON.stringify(tagIds));
} else {
// Send empty array to clear tags
formData.append('tag_ids', JSON.stringify([]));
}
// Files
const invoiceFile = document.getElementById('editInvoice').files[0];
if (invoiceFile) {
formData.append('invoice', invoiceFile);
}
const manualFile = document.getElementById('editManual').files[0];
if (manualFile) {
formData.append('manual', manualFile);
}
// Document deletion flags
const deleteInvoiceBtn = document.getElementById('deleteInvoiceBtn');
if (deleteInvoiceBtn && deleteInvoiceBtn.dataset.delete === 'true') {
formData.append('delete_invoice', 'true');
}
const deleteManualBtn = document.getElementById('deleteManualBtn');
if (deleteManualBtn && deleteManualBtn.dataset.delete === 'true') {
formData.append('delete_manual', 'true');
}
// --- Append is_lifetime and warranty_years ---
formData.append('is_lifetime', isLifetime.toString());
if (!isLifetime) {
formData.append('warranty_years', warrantyYears);
}
// Add notes
const notes = document.getElementById('editNotes').value;
if (notes && notes.trim() !== '') {
formData.append('notes', notes);
} else {
// Explicitly clear notes if empty
formData.append('notes', '');
}
// Add vendor/retailer to form data
const editVendorInput = document.getElementById('editVendor'); // Use the correct ID
formData.append('vendor', editVendorInput ? editVendorInput.value.trim() : ''); // Use the correct variable
// Get auth token
const token = localStorage.getItem('auth_token');
if (!token) {
showToast('Authentication required', 'error');
return;
}
showLoadingSpinner();
// Send request
fetch(`/api/warranties/${currentWarrantyId}`, {
method: 'PUT',
headers: {
'Authorization': 'Bearer ' + token
},
body: formData
})
.then(response => {
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.error || 'Failed to update warranty');
});
}
return response.json();
})
.then(data => {
hideLoadingSpinner();
showToast('Warranty updated successfully', 'success');
closeModals();
// Instantly reload and re-render the warranties list
loadWarranties().then(() => {
applyFilters();
// Always close the notes modal if open, to ensure UI is in sync
const notesModal = document.getElementById('notesModal');
if (notesModal && notesModal.style.display === 'block') {
notesModal.style.display = 'none';
}
});
})
.catch(error => {
hideLoadingSpinner();
console.error('Error updating warranty:', error);
showToast(error.message || 'Failed to update warranty', 'error');
});
}
// Function to populate tag filter dropdown
function populateTagFilter() {
const tagFilter = document.getElementById('tagFilter');
if (!tagFilter) return;
// Clear existing options (except "All Tags")
while (tagFilter.options.length > 1) {
tagFilter.remove(1);
}
// Create a Set to store unique tag names
const uniqueTags = new Set();
// Collect all unique tags from warranties
warranties.forEach(warranty => {
if (warranty.tags && Array.isArray(warranty.tags)) {
warranty.tags.forEach(tag => {
uniqueTags.add(JSON.stringify({id: tag.id, name: tag.name, color: tag.color}));
});
}
});
// Sort tags alphabetically by name
const sortedTags = Array.from(uniqueTags)
.map(tagJson => JSON.parse(tagJson))
.sort((a, b) => a.name.localeCompare(b.name));
// Add options to the dropdown
sortedTags.forEach(tag => {
const option = document.createElement('option');
option.value = tag.id;
option.textContent = tag.name;
option.style.backgroundColor = tag.color;
tagFilter.appendChild(option);
});
}
// --- Add New Function ---
function handleLifetimeChange(event) {
const checkbox = event ? event.target : isLifetimeCheckbox;
const group = warrantyYearsGroup;
const input = warrantyYearsInput;
if (!checkbox || !group || !input) return;
if (checkbox.checked) {
group.style.display = 'none';
input.required = false;
input.value = '';
} else {
group.style.display = 'block';
input.required = true;
}
}
// --- Add New Function ---
function handleEditLifetimeChange(event) {
const checkbox = event ? event.target : editIsLifetimeCheckbox;
const group = editWarrantyYearsGroup;
const input = editWarrantyYearsInput;
if (!checkbox || !group || !input) return;
if (checkbox.checked) {
group.style.display = 'none';
input.required = false;
input.value = '';
} else {
group.style.display = 'block';
input.required = true;
}
}
// --- Add this function to reset the wizard ---
function resetAddWarrantyWizard() {
console.log('Resetting Add Warranty Wizard...');
// Reset the form fields
if (warrantyForm) {
warrantyForm.reset();
}
// Reset serial numbers container (remove all but the first input structure)
if (serialNumbersContainer) {
serialNumbersContainer.innerHTML = ''; // Clear it
addSerialNumberInput(); // Add the initial input back
}
// Reset file input displays
if (fileName) fileName.textContent = '';
if (manualFileName) fileName.textContent = '';
// Reset selected tags
selectedTags = [];
console.log('Resetting Add Warranty Wizard...');
// Reset the form fields
if (warrantyForm) {
warrantyForm.reset();
}
// Reset serial numbers container (remove all but the first input structure)
if (serialNumbersContainer) {
serialNumbersContainer.innerHTML = ''; // Clear it
addSerialNumberInput(); // Add the initial input back
}
// Reset file input displays
if (fileName) fileName.textContent = '';
if (manualFileName) fileName.textContent = '';
// Reset selected tags
selectedTags = [];
renderSelectedTags(); // Update the display
// Reset tabs to the first one
// Use the globally defined tabContents if available
const tabs = addWarrantyModal?.querySelectorAll('.form-tab');
const contents = addWarrantyModal?.querySelectorAll('.tab-content');
if (tabs && contents && tabs.length > 0 && contents.length > 0) {
currentTabIndex = 0;
switchToTab(0); // Use the existing function to switch
} else {
console.warn("Could not find tabs/contents inside addWarrantyModal to reset.");
}
// Clear any validation states
addWarrantyModal?.querySelectorAll('.invalid').forEach(el => el.classList.remove('invalid'));
addWarrantyModal?.querySelectorAll('.validation-message').forEach(el => el.remove());
// Reset lifetime checkbox state if needed (ensure handler runs)
if (isLifetimeCheckbox) {
isLifetimeCheckbox.checked = false; // Explicitly uncheck
handleLifetimeChange({ target: isLifetimeCheckbox }); // Trigger handler to reset visibility/required state
}
}
// --- Modify setupUIEventListeners or add this within DOMContentLoaded ---
function setupModalTriggers() {
// Show Add Warranty Modal
if (showAddWarrantyBtn && addWarrantyModal) {
showAddWarrantyBtn.addEventListener('click', () => {
resetAddWarrantyWizard(); // Reset before showing
addWarrantyModal.classList.add('active');
initFormTabs(); // Initialize tabs only when modal is shown
switchToTab(0); // Ensure the first tab content is displayed correctly after reset
});
}
// Hide Add Warranty Modal (using existing close logic)
if (addWarrantyModal) {
// Close button inside modal
const closeBtn = addWarrantyModal.querySelector('.close-btn');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
addWarrantyModal.classList.remove('active');
resetAddWarrantyWizard(); // Reset on close
});
}
// REMOVED: Backdrop click listener
/*
addWarrantyModal.addEventListener('click', (e) => {
if (e.target === addWarrantyModal) {
addWarrantyModal.classList.remove('active');
resetAddWarrantyWizard(); // Reset on close
}
});
*/
// Optional: Cancel button in footer if you add one
// ... (cancel button logic remains unchanged)
}
// --- Edit Modal Triggers (Keep existing logic) ---
// Close edit/delete modals when clicking outside or on close button
document.querySelectorAll('#editModal, #deleteModal, [data-dismiss="modal"]').forEach(element => {
element.addEventListener('click', (e) => {
// Check if the click is on the backdrop itself OR a dismiss button
if (e.target === element || e.target.matches('[data-dismiss="modal"]')) {
// Find the closest modal backdrop to the element clicked
const modalToClose = e.target.closest('.modal-backdrop');
if (modalToClose) {
// *** ADD CHECK: Do NOT close addWarrantyModal via this general listener ***
if (modalToClose.id === 'addWarrantyModal') {
return; // Ignore backdrop clicks for the add modal here
}
// *** END ADD CHECK ***
modalToClose.classList.remove('active');
// Reset edit form state if closing edit modal
if (modalToClose.id === 'editModal') {
// Optional: Add any edit form reset logic here if needed
}
}
}
});
});
// Prevent modal content clicks from closing the modal (Keep for all modals)
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
e.stopPropagation();
});
});
}
// --- CSV Import Functionality ---
async function handleImport(file) {
if (!file) {
showToast('No file selected.', 'warning');
return;
}
if (!file.name.toLowerCase().endsWith('.csv')) {
showToast('Invalid file type. Please select a .csv file.', 'error');
return;
}
// Show loading indicator
showLoadingSpinner();
const formData = new FormData();
formData.append('csv_file', file);
try {
// const token = localStorage.getItem('token'); // Incorrect key
const token = localStorage.getItem('auth_token'); // Correct key used elsewhere
if (!token) {
showToast('Authentication error. Please log in again.', 'error');
hideLoadingSpinner();
// Maybe redirect to login: window.location.href = '/login.html';
return;
}
const response = await fetch('/api/warranties/import', {
method: 'POST',
headers: {
// Content-Type is automatically set by browser when using FormData
'Authorization': `Bearer ${token}`
},
body: formData
});
hideLoadingSpinner();
const result = await response.json();
if (response.ok) {
const { success_count, failure_count, errors } = result;
let message = `${success_count} warranties imported successfully.`;
if (failure_count > 0) {
message += ` ${failure_count} rows failed.`;
// Log detailed errors to the console for now
console.warn('Import errors:', errors);
// Consider showing errors in a modal or separate report later
}
showToast(message, 'success');
// ***** FIX: Reload the tags list *****
console.log("Import successful, reloading tags...");
await loadTags(); // Fetch the updated list of all tags
// ***** END FIX *****
// Add a small delay to ensure backend has processed the data
await new Promise(resolve => setTimeout(resolve, 500));
// Await the warranties load to ensure UI is updated
await loadWarranties();
// Force a UI refresh by reapplying filters
applyFilters();
} else {
showToast(`Import failed: ${result.error || 'Unknown error'}`, 'error');
if (result.errors) {
console.error('Detailed import errors:', result.errors);
}
}
} catch (error) {
hideLoadingSpinner();
console.error('Error during file import:', error);
showToast('An error occurred during import. Check console for details.', 'error');
} finally {
// Reset the file input so the user can select the same file again if needed
if (csvFileInput) {
csvFileInput.value = '';
}
}
}
// --- End CSV Import Functionality ---
// --- Add Storage Event Listener for Real-time Sync ---
window.addEventListener('storage', (event) => {
const prefix = getPreferenceKeyPrefix();
const viewKeys = [
`${prefix}defaultView`,
'viewPreference',
`${prefix}warrantyView`,
// Add `${prefix}viewPreference` if still used/relevant
`${prefix}viewPreference`
];
// Check for view preference changes
if (viewKeys.includes(event.key) && event.newValue) {
console.log(`Storage event detected for view preference (${event.key}). New value: ${event.newValue}`);
// Check if the new value is different from the current view to avoid loops
if (event.newValue !== currentView) {
// Ensure view buttons exist before switching (we're on the main page)
if (gridViewBtn || listViewBtn || tableViewBtn) {
switchView(event.newValue);
}
} else {
console.log('Storage event value matches current view, ignoring.');
}
}
// --- Added: Check for date format changes ---
if (event.key === 'dateFormat' && event.newValue) {
console.log(`Storage event detected for dateFormat. New value: ${event.newValue}`);
// Re-apply filters to re-render warranties with the new date format
if (warrantiesList) { // Only apply if the warranty list exists on the page
applyFilters();
showToast('Date format updated.', 'info'); // Optional: Notify user
}
}
// --- End Added Check ---
// --- Added: Check for currency symbol changes ---
if (event.key === `${prefix}currencySymbol` && event.newValue) {
console.log(`Storage event detected for ${prefix}currencySymbol. New value: ${event.newValue}`);
if (warrantiesList) { // Only apply if on the main page
updateCurrencySymbols(); // Update symbols outside cards (e.g., in forms if they exist)
applyFilters(); // Re-render cards to update symbols inside them
showToast('Currency symbol updated.', 'info'); // Optional: Notify user
}
}
// --- End Added Check ---
});
// --- End Storage Event Listener ---
// Add modal HTML to the end of the body if not present
if (!document.getElementById('notesModal')) {
const notesModal = document.createElement('div');
notesModal.id = 'notesModal';
notesModal.className = 'modal-backdrop';
notesModal.innerHTML = `
Warranty Notes
`;
document.body.appendChild(notesModal);
document.getElementById('closeNotesModal').addEventListener('click', () => {
notesModal.classList.remove('active');
});
}
// Add global to track which warranty is being edited in the notes modal
let notesModalWarrantyId = null;
let notesModalWarrantyObj = null;
function showNotesModal(notes, warrantyOrId = null) {
const notesModal = document.getElementById('notesModal');
const notesModalContent = document.getElementById('notesModalContent');
const notesModalTextarea = document.getElementById('notesModalTextarea');
const editBtn = document.getElementById('editNotesBtn');
const saveBtn = document.getElementById('saveNotesBtn');
const cancelBtn = document.getElementById('cancelEditNotesBtn');
// Support both (notes, warrantyObj) and (notes, id) for backward compatibility
if (typeof warrantyOrId === 'object' && warrantyOrId !== null) {
notesModalWarrantyId = warrantyOrId.id;
notesModalWarrantyObj = warrantyOrId;
} else {
notesModalWarrantyId = warrantyOrId;
// Try to find the warranty object from global warranties array
notesModalWarrantyObj = warranties.find(w => w.id === notesModalWarrantyId) || null;
}
// Show note content, hide textarea and edit controls
notesModalContent.style.display = '';
notesModalContent.textContent = notes;
notesModalTextarea.style.display = 'none';
editBtn.style.display = '';
saveBtn.style.display = 'none';
cancelBtn.style.display = 'none';
// Edit button handler
editBtn.onclick = function() {
notesModalContent.style.display = 'none';
notesModalTextarea.style.display = '';
notesModalTextarea.value = notes;
editBtn.style.display = 'none';
saveBtn.style.display = '';
cancelBtn.style.display = '';
notesModalTextarea.focus();
};
// Save button handler
saveBtn.onclick = async function() {
const newNote = notesModalTextarea.value.trim(); // Trim the note
if (!notesModalWarrantyId || !notesModalWarrantyObj) {
showToast('No warranty selected for note update', 'error');
return;
}
// Save note via API, sending all required fields
try {
showLoadingSpinner();
const token = localStorage.getItem('auth_token');
const formData = new FormData();
// --- Populate with existing data to avoid clearing fields ---
formData.append('product_name', notesModalWarrantyObj.product_name);
formData.append('purchase_date', (notesModalWarrantyObj.purchase_date || '').split('T')[0]);
formData.append('is_lifetime', notesModalWarrantyObj.is_lifetime ? 'true' : 'false');
if (!notesModalWarrantyObj.is_lifetime) {
formData.append('warranty_years', notesModalWarrantyObj.warranty_years || ''); // Use empty string if null/undefined
}
if (notesModalWarrantyObj.product_url) {
formData.append('product_url', notesModalWarrantyObj.product_url);
}
if (notesModalWarrantyObj.purchase_price !== null && notesModalWarrantyObj.purchase_price !== undefined) { // Check for null/undefined
formData.append('purchase_price', notesModalWarrantyObj.purchase_price);
}
if (notesModalWarrantyObj.serial_numbers && Array.isArray(notesModalWarrantyObj.serial_numbers)) {
notesModalWarrantyObj.serial_numbers.forEach(sn => {
if (sn && sn.trim() !== '') {
formData.append('serial_numbers', sn);
}
});
}
// Send empty array if no serial numbers exist or are provided
else if (!formData.has('serial_numbers')) {
formData.append('serial_numbers', JSON.stringify([]));
}
if (notesModalWarrantyObj.tags && Array.isArray(notesModalWarrantyObj.tags)) {
const tagIds = notesModalWarrantyObj.tags.map(tag => tag.id);
formData.append('tag_ids', JSON.stringify(tagIds));
}
// Send empty array if no tags exist or are provided
else {
formData.append('tag_ids', JSON.stringify([]));
}
// --- End Populate ---
formData.append('notes', newNote); // Append the potentially empty, trimmed note
// Add vendor/retailer to form data
const editVendorOrRetailer = document.getElementById('editVendorOrRetailer');
formData.append('vendor', editVendorOrRetailer ? editVendorOrRetailer.value.trim() : '');
const response = await fetch(`/api/warranties/${notesModalWarrantyId}`, { // Added await and response handling
method: 'PUT',
headers: {
'Authorization': 'Bearer ' + token
},
body: formData
});
if (!response.ok) { // Check if the API call was successful
const errorData = await response.json().catch(() => ({})); // Try to parse error, default to empty object
throw new Error(errorData.error || `Failed to update note (Status: ${response.status})`);
}
hideLoadingSpinner();
showToast('Note updated', 'success');
// --- Updated UI logic ---
if (newNote === '') {
// If the note is now empty, close the modal
document.getElementById('notesModal').classList.remove('active');
} else {
// If note is not empty, update the view and stay in the modal
notesModalContent.textContent = newNote;
notesModalContent.style.display = '';
notesModalTextarea.style.display = 'none';
editBtn.style.display = '';
saveBtn.style.display = 'none';
cancelBtn.style.display = 'none';
// Update the local warranty object's notes
if (notesModalWarrantyObj) {
notesModalWarrantyObj.notes = newNote;
}
}
// --- End Updated UI logic ---
// Refresh warranties list and THEN update UI
await loadWarranties(); // Wait for data refresh
applyFilters(); // Re-render the list with updated data
} catch (e) {
hideLoadingSpinner();
console.error("Error updating note:", e); // Log the error
showToast(e.message || 'Failed to update note', 'error'); // Show specific error if available
}
};
// Cancel button handler
cancelBtn.onclick = function() {
notesModalContent.style.display = '';
notesModalTextarea.style.display = 'none';
editBtn.style.display = '';
saveBtn.style.display = 'none';
cancelBtn.style.display = 'none';
};
notesModal.classList.add('active');
}
// Utility to get currency symbol from preferences/localStorage
function getCurrencySymbol() {
// Use the global prefix determined after auth ready
let prefix = userPreferencePrefix; // Use let to allow default override
if (!prefix) {
console.warn('[getCurrencySymbol] User preference prefix not set yet, defaulting prefix to user_');
prefix = 'user_'; // Default prefix if called too early
}
console.log(`[getCurrencySymbol] Using determined prefix: ${prefix}`);
let symbol = '$'; // Default value
const rawValue = localStorage.getItem(`${prefix}currencySymbol`);
console.log(`[getCurrencySymbol Debug] Raw value read from localStorage key '${prefix}currencySymbol':`, rawValue);
// +++ END ADDED LOG +++
// --- Priority 1: Load from individual key --- (Saved by settings-new.js)
const individualSymbol = rawValue; // Use the already read value
if (individualSymbol) { // Check uses the already read value
symbol = individualSymbol;
console.log(`[getCurrencySymbol] Loaded symbol from individual key (${prefix}currencySymbol): ${symbol}`);
return symbol;
}
// --- Priority 2: Load from preferences object (Legacy/Fallback) ---
try {
const prefsString = localStorage.getItem(`${prefix}preferences`);
console.log(`[getCurrencySymbol] Read prefsString for ${prefix}preferences:`, prefsString);
if (prefsString) {
const prefs = JSON.parse(prefsString);
if (prefs && prefs.currency_symbol) {
symbol = prefs.currency_symbol;
console.log(`[getCurrencySymbol] Loaded symbol from object key (${prefix}preferences): ${symbol}`);
}
}
} catch (e) {
console.error(`Error reading ${prefix}preferences from localStorage:`, e);
// Keep the default '$' symbol in case of error parsing the object
}
console.log(`[getCurrencySymbol] Returning symbol (default or from object): ${symbol}`);
return symbol;
}
function updateCurrencySymbols() {
const symbol = getCurrencySymbol();
console.log(`Updating currency symbols to: ${symbol}`); // Log the symbol being applied
const elements = document.querySelectorAll('.currency-symbol');
console.log(`Found ${elements.length} elements with class 'currency-symbol'.`); // Log how many elements are found
elements.forEach(el => {
// console.log('Updating element:', el); // Optional: Log each element being updated
el.textContent = symbol;
});
}
// If you want to update currency symbols live when storage changes (e.g. settings page open in another tab):
window.addEventListener('storage', function(e) {
const prefix = getPreferenceKeyPrefix();
// Only update if the main preferences object for the current user type changed
if (e.key === `${prefix}preferences`) {
console.log(`Storage event detected for ${prefix}preferences. Updating currency symbols.`);
updateCurrencySymbols();
}
});
// +++ NEW FUNCTION TO LOAD PREFS AND SAVE TO LOCALSTORAGE +++
async function loadAndApplyUserPreferences() {
// Use the global prefix determined after auth ready
let prefix = userPreferencePrefix; // <<< CHANGED const to let
if (!prefix) {
console.error('[Prefs Loader] Cannot load preferences: User preference prefix not set yet. Defaulting to user_');
// Setting a default might be risky if the user *is* admin but prefix wasn't set in time.
// Consider how authStateReady ensures prefix is set before this runs.
// For now, let's try defaulting, but this might need review.
prefix = 'user_';
}
console.log(`[Prefs Loader] Attempting to load preferences using prefix: ${prefix}`);
if (window.auth && window.auth.isAuthenticated()) {
const token = window.auth.getToken();
if (!token) {
console.error('[Prefs Loader] Cannot load preferences: No auth token found.');
return; // Exit if no token
}
try {
const response = await fetch('/api/auth/preferences', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const apiPrefs = await response.json();
console.log('[Prefs Loader] Preferences loaded from API:', apiPrefs);
// Save relevant prefs to localStorage
if (apiPrefs.currency_symbol) {
localStorage.setItem(`${prefix}currencySymbol`, apiPrefs.currency_symbol);
console.log(`[Prefs Loader] Saved ${prefix}currencySymbol: ${apiPrefs.currency_symbol}`);
}
if (apiPrefs.default_view) {
localStorage.setItem(`${prefix}defaultView`, apiPrefs.default_view);
console.log(`[Prefs Loader] Saved ${prefix}defaultView: ${apiPrefs.default_view}`);
}
if (apiPrefs.expiring_soon_days !== undefined) {
localStorage.setItem(`${prefix}expiringSoonDays`, apiPrefs.expiring_soon_days);
// Also update the global variable used by processWarrantyData
expiringSoonDays = apiPrefs.expiring_soon_days;
console.log(`[Prefs Loader] Saved ${prefix}expiringSoonDays: ${apiPrefs.expiring_soon_days}`);
console.log(`[Prefs Loader] Updated global expiringSoonDays variable to: ${expiringSoonDays}`);
}
if (apiPrefs.date_format) {
localStorage.setItem('dateFormat', apiPrefs.date_format);
console.log(`[Prefs Loader] Saved dateFormat: ${apiPrefs.date_format}`);
}
// Optionally trigger immediate UI updates if needed, although renderWarranties will use these new values
// updateCurrencySymbols();
} else {
const errorData = await response.json().catch(() => ({}));
console.warn(`[Prefs Loader] Failed to load preferences from API: ${response.status}`, errorData.message || '');
// Set defaults in localStorage maybe?
if (!localStorage.getItem('dateFormat')) localStorage.setItem('dateFormat', 'MDY');
if (!localStorage.getItem(`${prefix}currencySymbol`)) localStorage.setItem(`${prefix}currencySymbol`, '$');
// etc.
}
} catch (error) {
console.error('[Prefs Loader] Error fetching/applying preferences from API:', error);
// Set defaults in localStorage on error?
if (!localStorage.getItem('dateFormat')) localStorage.setItem('dateFormat', 'MDY');
if (!localStorage.getItem(`${prefix}currencySymbol`)) localStorage.setItem(`${prefix}currencySymbol`, '$');
// etc.
}
} else {
console.warn('[Prefs Loader] Cannot load preferences: User not authenticated or auth module not available.');
// Apply defaults if not authenticated?
if (!localStorage.getItem('dateFormat')) localStorage.setItem('dateFormat', 'MDY');
if (!localStorage.getItem(`${prefix}currencySymbol`)) localStorage.setItem(`${prefix}currencySymbol`, '$');
// etc.
}
}
// +++ END NEW FUNCTION +++