Files
Warracker/frontend/script.js
sassanix e045aae208 Vendor, Date Format, Added security
Refer to changelogs
2025-05-04 13:02:51 -03:00

3950 lines
154 KiB
JavaScript

// alert('script.js loaded!'); // Remove alert after confirming script loads
console.log('[DEBUG] script.js loaded and running');
// Global variables
let warranties = [];
let currentTabIndex = 0;
let tabContents = []; // Initialize as empty array
let editMode = false;
let currentWarrantyId = null;
let userPreferencePrefix = null; // <<< ADDED GLOBAL PREFIX VARIABLE
let currentFilters = {
status: 'all',
tag: 'all',
search: '',
sortBy: 'expiration'
};
// Tag related variables
let allTags = [];
let selectedTags = []; // Will hold objects with id, name, color
// Global variable for edit mode tags
let editSelectedTags = [];
// DOM Elements
const warrantyForm = document.getElementById('warrantyForm');
const settingsBtn = document.getElementById('settingsBtn');
const settingsMenu = document.getElementById('settingsMenu');
const darkModeToggle = document.getElementById('darkModeToggle');
const warrantiesList = document.getElementById('warrantiesList');
const refreshBtn = document.getElementById('refreshBtn');
const searchInput = document.getElementById('searchWarranties');
const clearSearchBtn = document.getElementById('clearSearch');
const statusFilter = document.getElementById('statusFilter');
const sortBySelect = document.getElementById('sortBy');
const exportBtn = document.getElementById('exportBtn');
const gridViewBtn = document.getElementById('gridViewBtn');
const listViewBtn = document.getElementById('listViewBtn');
const tableViewBtn = document.getElementById('tableViewBtn');
const tableViewHeader = document.querySelector('.table-view-header');
const fileInput = document.getElementById('invoice');
const fileName = document.getElementById('fileName');
const manualInput = document.getElementById('manual');
const manualFileName = document.getElementById('manualFileName');
const editModal = document.getElementById('editModal');
const deleteModal = document.getElementById('deleteModal');
const editWarrantyForm = document.getElementById('editWarrantyForm');
const saveWarrantyBtn = document.getElementById('saveWarrantyBtn');
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
const loadingContainer = document.getElementById('loadingContainer');
const toastContainer = document.getElementById('toastContainer');
// CSV Import Elements
const importBtn = document.getElementById('importBtn');
const csvFileInput = document.getElementById('csvFileInput');
if (importBtn) {
importBtn.classList.remove('import-btn');
importBtn.classList.add('export-btn');
}
// Tag DOM Elements
const selectedTagsContainer = document.getElementById('selectedTags');
const tagSearch = document.getElementById('tagSearch');
const tagsList = document.getElementById('tagsList');
const manageTagsBtn = document.getElementById('manageTagsBtn');
const tagManagementModal = document.getElementById('tagManagementModal');
const newTagForm = document.getElementById('newTagForm');
const existingTagsContainer = document.getElementById('existingTags');
// --- Add near other DOM Element declarations ---
const isLifetimeCheckbox = document.getElementById('isLifetime');
const warrantyYearsGroup = document.getElementById('warrantyYearsGroup');
const warrantyYearsInput = document.getElementById('warrantyYears');
const editIsLifetimeCheckbox = document.getElementById('editIsLifetime');
const editWarrantyYearsGroup = document.getElementById('editWarrantyYearsGroup');
const editWarrantyYearsInput = document.getElementById('editWarrantyYears');
// Add near other DOM Element declarations
const showAddWarrantyBtn = document.getElementById('showAddWarrantyBtn');
const addWarrantyModal = document.getElementById('addWarrantyModal');
/**
* Get current user type (admin or user)
* @returns {string} 'admin' or 'user'
*/
function getUserType() {
try {
const userInfo = JSON.parse(localStorage.getItem('user_info') || '{}');
return userInfo.is_admin === true ? 'admin' : 'user';
} catch (e) {
console.error('Error determining user type:', e);
return 'user'; // Default to user if we can't determine
}
}
/**
* Get the appropriate localStorage key prefix based on user type
* @returns {string} The prefix to use for localStorage keys
*/
function getPreferenceKeyPrefix() {
return getUserType() === 'admin' ? 'admin_' : 'user_';
}
// Theme Management - Simplified
function setTheme(isDark) {
const theme = isDark ? 'dark' : 'light';
console.log('Setting theme to:', theme);
// 1. Apply theme attribute to document root
document.documentElement.setAttribute('data-theme', theme);
// 2. Save the single source of truth to localStorage
localStorage.setItem('darkMode', isDark);
// Update toggle state if the toggle exists on this page (e.g., in the header)
const headerToggle = document.getElementById('darkModeToggle');
if (headerToggle) {
headerToggle.checked = isDark;
}
}
// Initialization logic on DOMContentLoaded
document.addEventListener('DOMContentLoaded', function() {
console.log('[DEBUG] Registering authStateReady event handler');
// ... other initialization ...
// REMOVE call to undefined checkLoginStatus - Handled by auth.js
// checkLoginStatus();
// --- DEFERRED WARRANTY LOADING ---
// Don't load warranties immediately. Wait for authentication.
// if (document.getElementById('warrantiesList')) {
// loadWarranties(); // <<< OLD CALL - REMOVED
// }
// --- END DEFERRED WARRANTY LOADING ---
// Setup form submission (assuming addWarrantyForm exists)
const form = document.getElementById('addWarrantyForm');
if (form) {
form.addEventListener('submit', handleFormSubmit);
// Initialize form tabs if the form exists
initFormTabs();
}
// REMOVED setupSettingsMenu - Handled by auth.js
// setupSettingsMenu();
// Initialize theme toggle state *after* DOM is loaded
// ... (theme toggle init logic) ...
// Setup view switcher (assuming view switcher elements exist)
if (document.getElementById('gridViewBtn')) {
// setupViewSwitcher(); // Removed undefined function
loadViewPreference();
}
// Setup filter controls (assuming filter controls exist)
if (document.getElementById('filterControls')) {
// setupFilterControls(); // Removed: function not defined
populateTagFilter();
}
// Initialize modal interactions
// initializeModals(); // Removed: function not defined, handled by setupModalTriggers
setupModalTriggers();
// Initialize Tag functionality (assuming tag elements exist)
if (document.getElementById('tagSearchInput')) {
initTagFunctionality();
loadTags();
}
// Initialize form-specific lifetime checkbox handler
const lifetimeCheckbox = document.getElementById('isLifetime');
if (lifetimeCheckbox) {
lifetimeCheckbox.addEventListener('change', handleLifetimeChange);
handleLifetimeChange({ target: lifetimeCheckbox }); // Initial check
}
// --- LOAD WARRANTIES AFTER AUTH ---
// Listen for an event from auth.js indicating authentication is complete and user context is ready.
// ** IMPORTANT: Replace 'authStateReady' with the actual event name fired by auth.js **
window.addEventListener('authStateReady', async function handleAuthReady() { // <-- Make handler async
console.log('[DEBUG] authStateReady handler called');
console.log("Auth state ready event received. Preparing preferences and warranties...");
// Ensure this listener runs only once
window.removeEventListener('authStateReady', handleAuthReady);
// Set prefix
userPreferencePrefix = getPreferenceKeyPrefix();
console.log(`[authStateReady] Determined and stored global prefix: ${userPreferencePrefix}`);
// Load preferences
await loadAndApplyUserPreferences();
// Load warranty data (fetches, processes, populates global array)
if (document.getElementById('warrantiesList')) {
console.log("[authStateReady] Loading warranty data...");
await loadWarranties(); // Waits for fetch/process
console.log('[DEBUG] After loadWarranties, warranties array:', warranties);
} else {
console.log("[authStateReady] Warranties list element not found.");
}
// Now that data and preferences are ready, apply view/currency and render via applyFilters
console.log("[authStateReady] Applying preferences and rendering...");
loadViewPreference(); // Sets currentView and UI classes/buttons
updateCurrencySymbols(); // Update symbols
// Apply filters using the loaded data and render the list
if (document.getElementById('warrantiesList')) {
applyFilters();
}
}, { once: true }); // Use { once: true } as a fallback if removeEventListener isn't reliable across scripts
// --- END LOAD WARRANTIES AFTER AUTH ---
// updateCurrencySymbols(); // Call removed, rely on loadWarranties triggering render with correct symbol
});
// Initialize theme based on user preference or system preference
function initializeTheme() {
// Only use the global darkMode key for theme persistence
const savedTheme = localStorage.getItem('darkMode');
if (savedTheme !== null) {
setTheme(savedTheme === 'true');
} else {
setTheme(window.matchMedia('(prefers-color-scheme: dark)').matches);
}
}
// Variables
let currentView = 'grid'; // Default view
let expiringSoonDays = 30; // Default value, will be updated from user preferences
// API URL
const API_URL = '/api/warranties';
// Form tab navigation variables (simplified)
let formTabs = []; // Changed from const to let, initialized as empty
// Removed const formTabsElements = document.querySelectorAll('.form-tab');
// Removed const formTabs = formTabsElements ? Array.from(formTabsElements) : [];
// Removed const tabContentsElements = document.querySelectorAll('.tab-content');
// Removed tabContents assignment here
const nextButton = document.querySelector('.next-tab'); // Keep these if needed globally, otherwise might remove
const prevButton = document.querySelector('.prev-tab'); // Keep these if needed globally, otherwise might remove
// --- Add near other DOM Element declarations ---
// ... existing code ...
// Add save button handler for notes modal (if not already present)
const saveNotesBtn = document.getElementById('saveNotesBtn');
if (saveNotesBtn) {
saveNotesBtn.onclick = async function() {
// Get the warranty ID being edited
const warrantyId = notesModalWarrantyId;
const notesValue = document.getElementById('notesModalTextarea').value;
if (!warrantyId || !notesModalWarrantyObj) return;
// Get auth token
const token = localStorage.getItem('auth_token');
if (!token) {
showToast('Authentication required', 'error');
return;
}
showLoadingSpinner();
try {
// Use FormData and send all required fields, just like the edit modal
const formData = new FormData();
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 || '');
}
if (notesModalWarrantyObj.product_url) {
formData.append('product_url', notesModalWarrantyObj.product_url);
}
if (notesModalWarrantyObj.purchase_price !== null && notesModalWarrantyObj.purchase_price !== 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);
}
});
} 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));
} else {
formData.append('tag_ids', JSON.stringify([]));
}
formData.append('notes', notesValue);
const response = await fetch(`/api/warranties/${warrantyId}`, {
method: 'PUT',
headers: {
'Authorization': 'Bearer ' + token
},
body: formData
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Failed to update notes');
}
hideLoadingSpinner();
showToast('Notes updated successfully', 'success');
// Close the modal
const notesModal = document.getElementById('notesModal');
if (notesModal) notesModal.style.display = 'none';
// Now reload warranties and re-render UI
await loadWarranties();
applyFilters();
} catch (error) {
hideLoadingSpinner();
console.error('Error updating notes:', error);
showToast(error.message || 'Failed to update notes', 'error');
}
};
}
// Initialize form tabs
function initFormTabs() {
console.log('Initializing form tabs...');
// Use the modal context if available, otherwise query document
const modalContext = document.getElementById('addWarrantyModal'); // Assuming this is the context
const context = modalContext && modalContext.classList.contains('active') ? modalContext : document;
const tabsContainer = context.querySelector('.form-tabs');
// Re-query tabContents and formTabs within the correct context and update global variables
const contentsElements = context.querySelectorAll('.tab-content');
tabContents = contentsElements ? Array.from(contentsElements) : []; // Update global variable
const tabsElements = tabsContainer ? tabsContainer.querySelectorAll('.form-tab') : [];
formTabs = tabsElements ? Array.from(tabsElements) : []; // Update global variable
const nextButton = context.querySelector('#nextTabBtn'); // Use context
const prevButton = context.querySelector('#prevTabBtn'); // Use context
const submitButton = context.querySelector('#submitWarrantyBtn'); // Use context
// Use the updated global variables length for checks
if (!tabsContainer || !tabContents.length || !formTabs.length || !nextButton || !prevButton || !submitButton) {
console.warn('Form tab elements not found in the expected context. Skipping tab initialization.');
return; // Don't proceed if elements aren't present
}
// Remove the local 'tabs' and 'contents' variables, use global ones now
// let currentTabIndex = 0; // Already global
// const tabs = tabsContainer.querySelectorAll('.form-tab'); // Use global formTabs
// const contents = document.querySelectorAll('.tab-content'); // Use global tabContents
// Remove the inner switchToTab and updateNavigationButtons functions as they are defined globally
/*
function switchToTab(index) {
// ... removed inner function ...
}
function updateNavigationButtons() {
// ... removed inner function ...
}
*/
// --- CLONE AND REPLACE NAV BUTTONS TO REMOVE OLD LISTENERS ---
// Ensure buttons exist before cloning
let nextButtonCloned = nextButton;
let prevButtonCloned = prevButton;
if (nextButton && prevButton) {
nextButtonCloned = nextButton.cloneNode(true);
prevButtonCloned = prevButton.cloneNode(true);
nextButton.parentNode.replaceChild(nextButtonCloned, nextButton);
prevButton.parentNode.replaceChild(prevButtonCloned, prevButton);
} else {
console.warn("Next/Prev buttons not found for cloning listeners.");
}
// ... (rest of initFormTabs, including event listeners, ensure element checks)
// Make sure event listeners use the correct global functions and variables
formTabs.forEach((tab, index) => { // Use global formTabs
if (tab) { // Check if tab exists
tab.addEventListener('click', () => {
// Allow clicking only on previous tabs if valid, or current
if (index < currentTabIndex) {
let canSwitch = true;
for (let i = 0; i < index; i++) {
// Ensure validateTab uses the correct global tabContents
if (!validateTab(i)) {
canSwitch = false;
break;
}
}
if (canSwitch) switchToTab(index); // Call global function
} else if (index === currentTabIndex) {
// Clicking current tab does nothing
} else {
// Try to navigate forward by clicking tab
// Ensure validateTab uses the correct global tabContents
if (validateTab(currentTabIndex)) {
// Mark current as completed
if(formTabs[currentTabIndex]) formTabs[currentTabIndex].classList.add('completed'); // Use global formTabs
switchToTab(index); // Call global function
} else {
// If current tab is invalid, show errors for it
showValidationErrors(currentTabIndex);
}
}
});
}
});
if (nextButtonCloned) { // Check button exists
nextButtonCloned.addEventListener('click', () => {
// Ensure validateTab uses the correct global tabContents
if (validateTab(currentTabIndex)) {
if (formTabs[currentTabIndex]) formTabs[currentTabIndex].classList.add('completed'); // Use global formTabs
// Use global formTabs length
if (currentTabIndex < formTabs.length - 1) { // <-- Ensure this uses formTabs.length
switchToTab(currentTabIndex + 1); // Call global function
}
} else {
// If current tab is invalid, show errors
showValidationErrors(currentTabIndex);
}
});
} else {
console.warn("Cloned Next button not found, listener not added.");
}
if (prevButtonCloned) { // Check button exists
prevButtonCloned.addEventListener('click', () => {
if (currentTabIndex > 0) {
switchToTab(currentTabIndex - 1);
}
});
}
// Initialize the first tab
switchToTab(0);
}
// Switch to a specific tab
function switchToTab(index) {
console.log(`Switching to tab ${index} from tab ${currentTabIndex}`);
// Ensure index is within bounds
if (index < 0 || index >= formTabs.length) {
console.log(`Invalid tab index: ${index}, not switching`);
return;
}
// Update summary FIRST if switching TO the summary tab
if (index === formTabs.length - 1) {
updateSummary();
}
// Update active tab
formTabs.forEach(tab => tab.classList.remove('active'));
tabContents.forEach(content => content.classList.remove('active'));
formTabs[index].classList.add('active');
tabContents[index].classList.add('active');
// Update current tab index
currentTabIndex = index;
// Update progress indicator
document.querySelector('.form-tabs').setAttribute('data-step', currentTabIndex);
// Update completed tabs
updateCompletedTabs();
// Update navigation buttons
updateNavigationButtons();
}
// Update navigation buttons based on current tab
function updateNavigationButtons() {
const prevButton = document.querySelector('.prev-tab');
const nextButton = document.querySelector('.next-tab');
const submitButton = document.querySelector('button[type="submit"]');
// Hide/show previous button
prevButton.style.display = currentTabIndex === 0 ? 'none' : 'block';
// Hide/show next button and submit button
if (currentTabIndex === formTabs.length - 1) {
nextButton.style.display = 'none';
submitButton.style.display = 'block';
} else {
nextButton.style.display = 'block';
submitButton.style.display = 'none';
}
}
// Update completed tabs
function updateCompletedTabs() {
formTabs.forEach((tab, index) => {
if (index < currentTabIndex) {
tab.classList.add('completed');
} else {
tab.classList.remove('completed');
}
});
}
// Validate a specific tab
function validateTab(tabIndex) {
const tabContent = tabContents[tabIndex];
const requiredInputs = tabContent.querySelectorAll('input[required]');
// If there are no required inputs in this tab, it's automatically valid
if (requiredInputs.length === 0) {
return true;
}
let isValid = true;
requiredInputs.forEach(input => {
if (!input.value.trim()) {
isValid = false;
input.classList.add('invalid');
} else {
input.classList.remove('invalid');
}
});
return isValid;
}
// Show validation errors for a specific tab
function showValidationErrors(tabIndex) {
const tabContent = tabContents[tabIndex];
const requiredInputs = tabContent.querySelectorAll('input[required]');
requiredInputs.forEach(input => {
if (!input.value.trim()) {
input.classList.add('invalid');
// Add validation message if not already present
let validationMessage = input.nextElementSibling;
if (!validationMessage || !validationMessage.classList.contains('validation-message')) {
validationMessage = document.createElement('div');
validationMessage.className = 'validation-message';
validationMessage.textContent = 'This field is required';
input.parentNode.insertBefore(validationMessage, input.nextSibling);
}
}
});
// Show toast message
showToast('Please fill in all required fields', 'error');
}
// Update summary tab with current form values
function updateSummary() {
// Product information
const summaryProductName = document.getElementById('summary-product-name');
if (summaryProductName) {
summaryProductName.textContent =
document.getElementById('productName')?.value || '-';
}
const summaryProductUrl = document.getElementById('summary-product-url');
if (summaryProductUrl) {
summaryProductUrl.textContent =
document.getElementById('productUrl')?.value || '-';
}
// Serial numbers
const serialNumbers = [];
document.querySelectorAll('input[name="serial_numbers[]"]').forEach(input => {
if (input && input.value && input.value.trim()) {
serialNumbers.push(input.value.trim());
}
});
const serialNumbersContainer = document.getElementById('summary-serial-numbers');
if (serialNumbersContainer) {
if (serialNumbers.length > 0) {
serialNumbersContainer.innerHTML = '<ul>' +
serialNumbers.map(sn => `<li>${sn}</li>`).join('') +
'</ul>';
} else {
serialNumbersContainer.textContent = 'None';
}
}
// Warranty details
const purchaseDateStr = document.getElementById('purchaseDate')?.value;
const summaryPurchaseDate = document.getElementById('summary-purchase-date');
if (summaryPurchaseDate) {
if (purchaseDateStr) {
// Use the same logic as formatDate to handle YYYY-MM-DD
const parts = String(purchaseDateStr).split('-');
let formattedDate = '-'; // Default
if (parts.length === 3) {
const year = parseInt(parts[0], 10);
const month = parseInt(parts[1], 10) - 1; // JS months are 0-indexed
const day = parseInt(parts[2], 10);
const dateObj = new Date(Date.UTC(year, month, day));
if (!isNaN(dateObj.getTime())) {
// Format manually (example: Jan 1, 2023)
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
formattedDate = `${monthNames[month]} ${day}, ${year}`;
}
}
summaryPurchaseDate.textContent = formattedDate;
} else {
summaryPurchaseDate.textContent = '-';
}
}
// --- Handle Lifetime in Summary ---
const isLifetime = isLifetimeCheckbox ? isLifetimeCheckbox.checked : false;
const warrantyYears = warrantyYearsInput ? warrantyYearsInput.value : null;
const summaryWarrantyYears = document.getElementById('summary-warranty-years');
if (summaryWarrantyYears) {
if (isLifetime) {
summaryWarrantyYears.textContent = 'Lifetime';
} else if (warrantyYears) {
const yearsNum = parseFloat(warrantyYears);
summaryWarrantyYears.textContent = `${yearsNum} ${yearsNum === 1 ? 'year' : 'years'}`;
} else {
summaryWarrantyYears.textContent = '-';
}
}
// Calculate and display expiration date
const summaryExpirationDate = document.getElementById('summary-expiration-date');
if (summaryExpirationDate && purchaseDateStr && warrantyYears) {
const expirationDate = new Date(Date.UTC(parseInt(purchaseDateStr.split('-')[0]), parseInt(purchaseDateStr.split('-')[1]) - 1, parseInt(purchaseDateStr.split('-')[2])));
const yearsNum = parseFloat(warrantyYears);
if (!isNaN(yearsNum)) {
expirationDate.setFullYear(expirationDate.getFullYear() + Math.floor(yearsNum));
expirationDate.setMonth(expirationDate.getMonth() + Math.round((yearsNum % 1) * 12));
summaryExpirationDate.textContent = expirationDate.toLocaleDateString();
} else {
summaryExpirationDate.textContent = '-';
}
} else if (summaryExpirationDate) {
summaryExpirationDate.textContent = '-';
}
// Purchase price
const purchasePrice = document.getElementById('purchasePrice')?.value;
const summaryPurchasePrice = document.getElementById('summary-purchase-price');
if (summaryPurchasePrice) {
summaryPurchasePrice.textContent = purchasePrice ?
`$${parseFloat(purchasePrice).toFixed(2)}` : 'Not specified';
}
// Documents
const invoiceFile = document.getElementById('invoice')?.files[0];
const summaryInvoice = document.getElementById('summary-invoice');
if (summaryInvoice) {
summaryInvoice.textContent = invoiceFile ?
invoiceFile.name : 'No file selected';
}
const manualFile = document.getElementById('manual')?.files[0];
const summaryManual = document.getElementById('summary-manual');
if (summaryManual) {
summaryManual.textContent = manualFile ?
manualFile.name : 'No file selected';
}
// Tags
const summaryTags = document.getElementById('summary-tags');
if (summaryTags) {
if (selectedTags && selectedTags.length > 0) {
summaryTags.innerHTML = '';
selectedTags.forEach(tag => {
const tagElement = document.createElement('span');
tagElement.className = 'tag';
tagElement.style.backgroundColor = tag.color;
tagElement.style.color = getContrastColor(tag.color);
tagElement.textContent = tag.name;
summaryTags.appendChild(tagElement);
});
} else {
summaryTags.textContent = 'No tags selected';
}
}
// Vendor/Retailer
const vendor = document.getElementById('vendor');
document.getElementById('summary-vendor').textContent = vendor && vendor.value ? vendor.value : '-';
}
// Add input event listeners to remove validation errors when user types
document.addEventListener('input', (e) => {
if (e.target.hasAttribute('required') && e.target.classList.contains('invalid')) {
if (e.target.value.trim()) {
e.target.classList.remove('invalid');
// Remove validation message if exists
const validationMessage = e.target.nextElementSibling;
if (validationMessage && validationMessage.classList.contains('validation-message')) {
validationMessage.remove();
}
}
}
});
// Function to reset the form and initialize serial number inputs
function resetForm() {
// Reset the form
warrantyForm.reset();
// Reset serial numbers container
serialNumbersContainer.innerHTML = '';
// Add the first serial number input
addSerialNumberInput();
// Reset form tabs
currentTabIndex = 0;
switchToTab(0);
// Clear any file input displays
fileName.textContent = '';
manualFileName.textContent = '';
}
async function exportWarranties() {
// Get filtered warranties
let warrantiesToExport = [...warranties];
// Apply current filters
if (currentFilters.search) {
const searchTerm = currentFilters.search.toLowerCase();
warrantiesToExport = warrantiesToExport.filter(warranty => {
// Check if product name contains search term
const productNameMatch = warranty.product_name.toLowerCase().includes(searchTerm);
// Check if any tag name contains search term
const tagMatch = warranty.tags && Array.isArray(warranty.tags) &&
warranty.tags.some(tag => tag.name.toLowerCase().includes(searchTerm));
// Check if vendor name contains search term
const vendorMatch = warranty.vendor && warranty.vendor.toLowerCase().includes(searchTerm);
// Return true if either product name, tag name, or vendor name matches
return productNameMatch || tagMatch || vendorMatch;
});
}
if (currentFilters.status !== 'all') {
warrantiesToExport = warrantiesToExport.filter(warranty =>
warranty.status === currentFilters.status
);
}
// Apply tag filter
if (currentFilters.tag !== 'all') {
const tagId = parseInt(currentFilters.tag);
warrantiesToExport = warrantiesToExport.filter(warranty =>
warranty.tags && Array.isArray(warranty.tags) &&
warranty.tags.some(tag => tag.id === tagId)
);
}
// Create CSV content
let csvContent = "data:text/csv;charset=utf-8,";
// Add headers
csvContent += "Product Name,Purchase Date,Warranty Period,Expiration Date,Status,Serial Numbers,Tags,Vendor\n";
// Add data rows
warrantiesToExport.forEach(warranty => {
// Format serial numbers as comma-separated string
const serialNumbers = Array.isArray(warranty.serial_numbers)
? warranty.serial_numbers.filter(s => s).join(', ')
: '';
// Format tags as comma-separated string
const tags = Array.isArray(warranty.tags)
? warranty.tags.map(tag => tag.name).join(', ')
: '';
// Format row data
const row = [
warranty.product_name || '',
formatDate(new Date(warranty.purchase_date)),
`${warranty.warranty_years || 0} ${warranty.warranty_years === 1 ? 'year' : 'years'}`,
formatDate(new Date(warranty.expiration_date)),
warranty.status || '',
serialNumbers,
tags,
warranty.vendor || ''
];
// Add row to CSV content
csvContent += row.map(field => `"${field.toString().replace(/"/g, '""')}"`).join(',') + '\n';
});
// Create a download link
const encodedUri = encodeURI(csvContent);
const link = document.createElement('a');
link.setAttribute('href', encodedUri);
link.setAttribute('download', `warranties_export_${formatDate(new Date())}.csv`);
document.body.appendChild(link);
// Trigger download
link.click();
// Clean up
document.body.removeChild(link);
// Show success notification
showToast('Warranties exported successfully', 'success');
}
// Switch view of warranties list
async function switchView(viewType) { // Added async
console.log(`Switching to view: ${viewType}`);
currentView = viewType;
const prefix = getPreferenceKeyPrefix();
const viewKey = `${prefix}defaultView`;
const currentStoredValue = localStorage.getItem(viewKey);
// Save to localStorage immediately for responsiveness
if (currentStoredValue !== viewType) {
localStorage.setItem(viewKey, viewType);
// Keep legacy keys for now if needed, but primary is viewKey
localStorage.setItem(`${prefix}warrantyView`, viewType);
localStorage.setItem('viewPreference', viewType);
console.log(`Saved view preference (${viewKey}) to localStorage: ${viewType}`);
} else {
console.log(`View preference (${viewKey}) already set to ${viewType} in localStorage.`);
}
// --- BEGIN ADDED: Save preference to API ---
if (window.auth && window.auth.isAuthenticated()) {
const token = window.auth.getToken();
if (token) {
try {
console.log(`Attempting to save view preference (${viewType}) to API...`);
const response = await fetch('/api/auth/preferences', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ default_view: viewType }) // Send only the changed preference
});
if (response.ok) {
console.log('Successfully saved view preference to API.');
} else {
const errorData = await response.json().catch(() => ({}));
console.warn(`Failed to save view preference to API: ${response.status}`, errorData.message || '');
// Optional: Show a non-intrusive warning toast?
// showToast('Failed to sync view preference with server.', 'warning');
}
} catch (error) {
console.error('Error saving view preference to API:', error);
// Optional: Show a non-intrusive warning toast?
// showToast('Error syncing view preference with server.', 'error');
}
} else {
console.warn('Cannot save view preference to API: No auth token found.');
}
} else {
console.warn('Cannot save view preference to API: User not authenticated or auth module not loaded.');
}
// --- END ADDED: Save preference to API ---
// Make sure warrantiesList exists before modifying classes
if (warrantiesList) {
warrantiesList.classList.remove('grid-view', 'list-view', 'table-view');
warrantiesList.classList.add(`${viewType}-view`);
}
// Make sure view buttons exist
if (gridViewBtn && listViewBtn && tableViewBtn) {
gridViewBtn.classList.remove('active');
listViewBtn.classList.remove('active');
tableViewBtn.classList.remove('active');
// Add active class to the correct button
if (viewType === 'grid') gridViewBtn.classList.add('active');
if (viewType === 'list') listViewBtn.classList.add('active');
if (viewType === 'table') tableViewBtn.classList.add('active');
}
// Show/hide table header only if it exists
if (tableViewHeader) {
tableViewHeader.classList.toggle('visible', viewType === 'table');
}
// Re-render warranties only if warrantiesList exists
if (warrantiesList) {
renderWarranties(filterWarranties()); // Assuming filterWarranties() returns the correct array
}
}
// Load view preference from localStorage
function loadViewPreference() {
// Get the appropriate key prefix based on user type
const prefix = getPreferenceKeyPrefix();
let savedView = null;
// --- BEGIN EDIT: Check keys in priority order ---
const userSpecificView = localStorage.getItem(`${prefix}defaultView`);
const generalView = localStorage.getItem('viewPreference');
const legacyWarrantyView = localStorage.getItem(`${prefix}warrantyView`);
if (userSpecificView) {
savedView = userSpecificView;
console.log(`Loaded view preference from ${prefix}defaultView:`, savedView);
} else if (generalView) {
savedView = generalView;
console.log('Loaded view preference from viewPreference:', savedView);
} else if (legacyWarrantyView) {
savedView = legacyWarrantyView;
console.log(`Loaded view preference from legacy ${prefix}warrantyView:`, savedView);
}
// --- END EDIT ---
// Default to grid view if no preference is saved
savedView = savedView || 'grid';
console.log(`Applying view preference: ${savedView}`);
// Switch view only if view buttons exist (implying it's the main page)
if (gridViewBtn || listViewBtn || tableViewBtn) {
switchView(savedView);
}
}
// Dark mode toggle
if (darkModeToggle) { // Add check for darkModeToggle
darkModeToggle.addEventListener('change', (e) => {
setTheme(e.target.checked);
});
}
// Add event listener for adding new serial number inputs
// Add check for serialNumbersContainer before adding listener
if (serialNumbersContainer) {
serialNumbersContainer.addEventListener('click', (e) => {
if (e.target.closest('.add-serial-number')) {
addSerialNumberInput();
}
});
}
// Add a serial number input field
function addSerialNumberInput(container = serialNumbersContainer) {
// Check if the container exists before proceeding
if (!container) {
console.warn('Serial numbers container not found, cannot add input.');
return;
}
const div = document.createElement('div');
div.className = 'serial-number-input d-flex mb-2';
// Create an input element
const input = document.createElement('input');
input.type = 'text';
input.className = 'form-control';
input.name = 'serial_numbers[]';
input.placeholder = 'Enter serial number';
// Check if this is the first serial number input
const isFirstInput = container.querySelectorAll('.serial-number-input').length === 0;
// Append input to the input group
div.appendChild(input);
// Only add remove button if this is not the first input
if (!isFirstInput) {
// Create a remove button
const removeButton = document.createElement('button');
removeButton.type = 'button';
removeButton.className = 'btn btn-sm btn-danger remove-serial';
removeButton.innerHTML = '<i class="fas fa-times"></i>';
// Add event listener to remove button
removeButton.addEventListener('click', function() {
container.removeChild(div);
});
// Append remove button to the input group
div.appendChild(removeButton);
}
// Insert the new input group before the add button
const addButton = container.querySelector('.add-serial');
if (addButton) {
container.insertBefore(div, addButton);
} else {
container.appendChild(div);
// Create and append an add button if it doesn't exist
const addButton = document.createElement('button');
addButton.type = 'button';
addButton.className = 'btn btn-sm btn-secondary add-serial';
addButton.innerHTML = '<i class="fas fa-plus"></i> Add Serial Number';
addButton.addEventListener('click', function() {
addSerialNumberInput(container);
});
container.appendChild(addButton);
}
}
// Functions
function showLoading() {
loadingContainer.classList.add('active');
}
function hideLoading() {
loadingContainer.classList.remove('active');
}
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.innerHTML = `
${message}
<button class="toast-close">&times;</button>
`;
// Add close event
toast.querySelector('.toast-close').addEventListener('click', () => {
toast.remove();
});
toastContainer.appendChild(toast);
// Auto remove after 3 seconds
setTimeout(() => {
if (toast.parentElement) {
toast.remove();
}
}, 3000);
}
// Update file name display when a file is selected
function updateFileName(event, inputId = 'invoice', outputId = 'fileName') {
const input = event ? event.target : document.getElementById(inputId);
const output = document.getElementById(outputId);
if (!input || !output) return;
if (input.files && input.files[0]) {
output.textContent = input.files[0].name;
} else {
output.textContent = '';
}
}
// Helper function to process warranty data
function processWarrantyData(warranty) {
console.log('Processing warranty data:', warranty);
// Create a copy of the warranty object to avoid modifying the original
const processedWarranty = { ...warranty };
// Ensure product_name exists
if (!processedWarranty.product_name) {
processedWarranty.product_name = 'Unnamed Product';
}
const today = new Date();
today.setHours(0, 0, 0, 0); // Normalize today to midnight for accurate date comparisons
// Parse purchase_date string (YYYY-MM-DD) into a UTC Date object
let purchaseDateObj = null;
if (processedWarranty.purchase_date) {
const parts = String(processedWarranty.purchase_date).split('-');
if (parts.length === 3) {
const year = parseInt(parts[0], 10);
const month = parseInt(parts[1], 10) - 1; // JS months are 0-indexed
const day = parseInt(parts[2], 10);
purchaseDateObj = new Date(Date.UTC(year, month, day));
if (isNaN(purchaseDateObj.getTime())) {
purchaseDateObj = null; // Invalid date parsed
}
} else {
// Fallback for unexpected formats, though backend should send YYYY-MM-DD
purchaseDateObj = new Date(processedWarranty.purchase_date);
if (isNaN(purchaseDateObj.getTime())) {
purchaseDateObj = null;
}
}
}
processedWarranty.purchaseDate = purchaseDateObj;
// Parse expiration_date similarly (assuming it's also YYYY-MM-DD)
let expirationDateObj = null;
if (processedWarranty.expiration_date) {
const parts = String(processedWarranty.expiration_date).split('-');
if (parts.length === 3) {
const year = parseInt(parts[0], 10);
const month = parseInt(parts[1], 10) - 1;
const day = parseInt(parts[2], 10);
expirationDateObj = new Date(Date.UTC(year, month, day));
if (isNaN(expirationDateObj.getTime())) {
expirationDateObj = null;
}
} else {
expirationDateObj = new Date(processedWarranty.expiration_date);
if (isNaN(expirationDateObj.getTime())) {
expirationDateObj = null;
}
}
}
processedWarranty.expirationDate = expirationDateObj;
// --- Lifetime Handling ---
if (processedWarranty.is_lifetime) {
processedWarranty.status = 'active';
processedWarranty.statusText = 'Lifetime';
processedWarranty.daysRemaining = Infinity;
} else if (processedWarranty.expirationDate && !isNaN(processedWarranty.expirationDate.getTime())) {
// Existing logic for dated warranties
const expirationDateOnly = new Date(processedWarranty.expirationDate);
expirationDateOnly.setHours(0,0,0,0);
const timeDiff = expirationDateOnly - today;
const daysRemaining = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
processedWarranty.daysRemaining = daysRemaining;
if (daysRemaining < 0) {
processedWarranty.status = 'expired';
processedWarranty.statusText = 'Expired';
} else if (daysRemaining < expiringSoonDays) {
processedWarranty.status = 'expiring';
processedWarranty.statusText = `Expiring Soon (${daysRemaining} day${daysRemaining !== 1 ? 's' : ''})`;
} else {
processedWarranty.status = 'active';
processedWarranty.statusText = `${daysRemaining} day${daysRemaining !== 1 ? 's' : ''} remaining`;
}
} else {
processedWarranty.status = 'unknown';
processedWarranty.statusText = 'Unknown Status';
processedWarranty.daysRemaining = null;
}
console.log('Processed warranty data result:', processedWarranty);
return processedWarranty;
}
// Function to process all warranties in the array
function processAllWarranties() {
console.log('Processing all warranties in array...');
if (warranties && warranties.length > 0) {
warranties = warranties.map(warranty => processWarrantyData(warranty));
}
console.log('Processed warranties:', warranties);
}
async function loadWarranties() {
// +++ REMOVED: Ensure Preferences are loaded FIRST (Now handled by authStateReady) +++
// await loadAndApplyUserPreferences();
// +++ Preferences Loaded +++
try {
console.log('[DEBUG] Entered loadWarranties');
showLoading();
// Fetch user preferences (including date format) before loading warranties
// --- THIS INNER PREFERENCE FETCH IS NOW REDUNDANT, REMOVE/COMMENT OUT ---
/*
try {
const token = window.auth.getToken(); // Ensure token is retrieved here
if (!token) throw new Error("No auth token found"); // Added error handling
const prefsResponse = await fetch('/api/auth/preferences', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (prefsResponse.ok) {
const prefsData = await prefsResponse.json();
console.log("Preferences fetched in loadWarranties:", prefsData);
// Update expiringSoonDays
if (prefsData && typeof prefsData.expiring_soon_days !== 'undefined') {
const oldValue = expiringSoonDays;
expiringSoonDays = prefsData.expiring_soon_days;
console.log('Updated expiring soon days from preferences:', expiringSoonDays);
// Reprocess logic moved below warranty fetch
}
// --- ADDED: Update dateFormat in localStorage ---
if (prefsData && typeof prefsData.date_format !== 'undefined') {
const oldDateFormat = localStorage.getItem('dateFormat');
localStorage.setItem('dateFormat', prefsData.date_format);
console.log(`Updated dateFormat in localStorage from API: ${prefsData.date_format}`);
// Trigger re-render if format changed and warranties already exist (though unlikely at this stage)
if (warranties && warranties.length > 0 && oldDateFormat !== prefsData.date_format) {
console.log('Date format changed, triggering re-render via applyFilters');
applyFilters(); // Re-render warranties with new format
}
} else {
// If API doesn't return date_format, ensure localStorage has a default
if (!localStorage.getItem('dateFormat')) {
localStorage.setItem('dateFormat', 'MDY');
console.log('API did not return date_format, setting localStorage default to MDY');
}
}
// --- END ADDED SECTION ---
} else {
// Handle failed preference fetch
console.warn('Failed to fetch preferences:', prefsResponse.status);
// Ensure a default date format exists if fetch fails
if (!localStorage.getItem('dateFormat')) {
localStorage.setItem('dateFormat', 'MDY');
console.log('Preferences fetch failed, setting localStorage default date format to MDY');
}
}
} catch (error) {
console.error('Error loading preferences:', error);
// Ensure a default date format exists on error
if (!localStorage.getItem('dateFormat')) {
localStorage.setItem('dateFormat', 'MDY');
console.log('Error fetching preferences, setting localStorage default date format to MDY');
}
// Continue loading warranties even if preferences fail
}
*/
// --- END REDUNDANT PREFERENCE FETCH ---
// Use the full URL to avoid path issues
const apiUrl = window.location.origin + '/api/warranties';
// Check if auth is available and user is authenticated
if (!window.auth || !window.auth.isAuthenticated()) {
console.log('[DEBUG] Early return: User not authenticated');
renderEmptyState('Please log in to view your warranties.');
hideLoading();
return;
}
// Get the auth token
const token = window.auth.getToken();
if (!token) {
console.log('[DEBUG] Early return: No auth token available');
renderEmptyState('Authentication error. Please log in again.');
hideLoading();
return;
}
// Create request with auth header
const options = {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
};
console.log('Fetching warranties with auth token');
const response = await fetch(apiUrl, options);
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: `HTTP error ${response.status}` }));
console.error('Error loading warranties:', response.status, errorData);
throw new Error(`Error loading warranties: ${errorData.message || response.status}`);
}
const data = await response.json();
console.log('[DEBUG] Received warranties from server:', data);
if (!Array.isArray(data)) {
console.error('[DEBUG] API did not return an array! Data:', data);
}
// Process each warranty to calculate status and days remaining
warranties = Array.isArray(data) ? data.map(warranty => {
const processed = processWarrantyData(warranty);
console.log('[DEBUG] Processed warranty:', processed);
return processed;
}) : [];
console.log('[DEBUG] Final warranties array:', warranties);
if (warranties.length === 0) {
console.log('No warranties found, showing empty state');
renderEmptyState('No warranties found. Add your first warranty using the form.');
} else {
console.log('Applying filters to display warranties');
// Populate tag filter dropdown with tags from warranties
populateTagFilter();
// REMOVED: applyFilters(); // Now called from authStateReady after data and prefs are loaded
}
} catch (error) {
console.error('[DEBUG] Error loading warranties:', error);
renderEmptyState('Error loading warranties. Please try again later.');
} finally {
hideLoading();
}
}
function renderEmptyState(message = 'No warranties yet. Add your first warranty to get started.') {
warrantiesList.innerHTML = `
<div class="empty-state">
<i class="fas fa-box-open"></i>
<h3>No warranties found</h3>
<p>${message}</p>
</div>
`;
}
function formatDate(date) {
// Input 'date' should now be a Date object created by processWarrantyData (or null)
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
return 'N/A';
}
// Get the user's preferred format from localStorage, default to MDY
const formatPreference = localStorage.getItem('dateFormat') || 'MDY';
// Manually extract UTC components to avoid timezone discrepancies
const year = date.getUTCFullYear();
const monthIndex = date.getUTCMonth(); // 0-indexed for month names array
const day = date.getUTCDate();
// Padded numeric values
const monthPadded = (monthIndex + 1).toString().padStart(2, '0');
const dayPadded = day.toString().padStart(2, '0');
// Abbreviated month names
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const monthAbbr = monthNames[monthIndex];
switch (formatPreference) {
case 'DMY':
return `${dayPadded}/${monthPadded}/${year}`;
case 'YMD':
return `${year}-${monthPadded}-${dayPadded}`;
case 'MDY_WORDS': // Added
return `${monthAbbr} ${day}, ${year}`;
case 'DMY_WORDS': // Added
return `${day} ${monthAbbr} ${year}`;
case 'YMD_WORDS': // Added
return `${year} ${monthAbbr} ${day}`;
case 'MDY':
default:
return `${monthPadded}/${dayPadded}/${year}`;
}
}
async function renderWarranties(warrantiesToRender) {
console.log('renderWarranties called with:', warrantiesToRender);
if (!warrantiesToRender || warrantiesToRender.length === 0) {
renderEmptyState();
return;
}
const today = new Date();
const symbol = getCurrencySymbol(); // Get the correct symbol HERE
warrantiesList.innerHTML = '';
// Apply sorting based on current sort selection
const sortedWarranties = [...warrantiesToRender].sort((a, b) => {
switch (currentFilters.sortBy) {
case 'name':
return a.product_name.localeCompare(b.product_name);
case 'purchase':
return new Date(b.purchase_date || 0) - new Date(a.purchase_date || 0);
case 'expiration':
default:
const dateA = new Date(a.expiration_date || 0);
const dateB = new Date(b.expiration_date || 0);
const isExpiredA = dateA < today;
const isExpiredB = dateB < today;
if (isExpiredA && !isExpiredB) return 1;
if (!isExpiredA && isExpiredB) return -1;
// Both active or both expired, sort by date
return dateA - dateB;
}
});
console.log('Sorted warranties:', sortedWarranties);
// Update the container class based on current view
warrantiesList.className = `warranties-list ${currentView}-view`;
// Show/hide table header for table view
if (tableViewHeader) {
tableViewHeader.classList.toggle('visible', currentView === 'table');
}
// Update view buttons to reflect current view
if (gridViewBtn && listViewBtn && tableViewBtn) {
gridViewBtn.classList.toggle('active', currentView === 'grid');
listViewBtn.classList.toggle('active', currentView === 'list');
tableViewBtn.classList.toggle('active', currentView === 'table');
}
sortedWarranties.forEach(warranty => {
// --- Use processed data ---
const purchaseDate = warranty.purchaseDate;
const expirationDate = warranty.expirationDate;
const isLifetime = warranty.is_lifetime;
const statusClass = warranty.status || 'unknown';
const statusText = warranty.statusText || 'Unknown Status';
const warrantyYearsText = isLifetime ? 'Lifetime' : (warranty.warranty_years !== undefined ? `${warranty.warranty_years} ${warranty.warranty_years === 1 ? 'year' : 'years'}` : 'N/A');
const expirationDateText = isLifetime ? 'Lifetime' : formatDate(expirationDate);
// Make sure serial numbers array exists and is valid
const validSerialNumbers = Array.isArray(warranty.serial_numbers)
? warranty.serial_numbers.filter(sn => sn && typeof sn === 'string' && sn.trim() !== '')
: [];
// Prepare tags HTML
const tagsHtml = warranty.tags && warranty.tags.length > 0
? `<div class="tags-row">
${warranty.tags.map(tag =>
`<span class="tag" style="background-color: ${tag.color}; color: ${getContrastColor(tag.color)}">
${tag.name}
</span>`
).join('')}
</div>`
: '';
// Add notes display button if present
let notesHtml = '';
const hasNotes = warranty.notes && warranty.notes.trim() !== '';
// Remove the button, and instead prepare a notes link for document-links-row
let notesLinkHtml = '';
if (hasNotes) {
notesLinkHtml = `<a href="#" class="notes-link" data-id="${warranty.id}" title="View Notes"><i class='fas fa-sticky-note'></i> Notes</a>`;
}
const cardElement = document.createElement('div');
cardElement.className = `warranty-card ${statusClass === 'expired' ? 'expired' : statusClass === 'expiring' ? 'expiring-soon' : 'active'}`;
if (currentView === 'grid') {
// Grid view HTML structure
cardElement.innerHTML = `
<div class="product-name-header">
<h3 class="warranty-title">${warranty.product_name || 'Unnamed Product'}</h3>
<div class="warranty-actions">
<button class="action-btn edit-btn" title="Edit" data-id="${warranty.id}">
<i class="fas fa-edit"></i>
</button>
<button class="action-btn delete-btn" title="Delete" data-id="${warranty.id}">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="warranty-content">
<div class="warranty-info">
<div>Purchased: <span>${formatDate(purchaseDate)}</span></div>
<div>Warranty: <span>${warrantyYearsText}</span></div>
<div>Expires: <span>${expirationDateText}</span></div>
${warranty.purchase_price ? `<div><span>Price: </span><span class="currency-symbol">${symbol}</span><span>${parseFloat(warranty.purchase_price).toFixed(2)}</span></div>` : ''}
${warranty.vendor ? `<div>Vendor: <span>${warranty.vendor}</span></div>` : ''}
${validSerialNumbers.length > 0 ? `
<div class="serial-numbers">
<strong>Serial Numbers:</strong>
<ul>
${validSerialNumbers.map(sn => `<li>${sn}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
</div>
<div class="warranty-status-row status-${statusClass}">
<span>${statusText}</span>
</div>
<div class="document-links-row">
<div class="document-links-inner-container">
${warranty.product_url ? `
<a href="${warranty.product_url}" class="product-link" target="_blank">
<i class="fas fa-globe"></i> Product Website
</a>
` : ''}
${warranty.invoice_path && warranty.invoice_path !== 'null' ? `
<a href="#" onclick="openSecureFile('${warranty.invoice_path}'); return false;" class="invoice-link">
<i class="fas fa-file-invoice"></i> Invoice
</a>` : ''}
${warranty.manual_path && warranty.manual_path !== 'null' ? `
<a href="#" onclick="openSecureFile('${warranty.manual_path}'); return false;" class="manual-link">
<i class="fas fa-book"></i> Manual
</a>` : ''}
${notesLinkHtml}
</div>
</div>
${tagsHtml}
`;
} else if (currentView === 'list') {
// List view HTML structure
cardElement.innerHTML = `
<div class="product-name-header">
<h3 class="warranty-title">${warranty.product_name || 'Unnamed Product'}</h3>
<div class="warranty-actions">
<button class="action-btn edit-btn" title="Edit" data-id="${warranty.id}">
<i class="fas fa-edit"></i>
</button>
<button class="action-btn delete-btn" title="Delete" data-id="${warranty.id}">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="warranty-content">
<div class="warranty-info">
<div>Purchased: <span>${formatDate(purchaseDate)}</span></div>
<div>Warranty: <span>${warrantyYearsText}</span></div>
<div>Expires: <span>${expirationDateText}</span></div>
${warranty.purchase_price ? `<div><span>Price: </span><span class="currency-symbol">${symbol}</span><span>${parseFloat(warranty.purchase_price).toFixed(2)}</span></div>` : ''}
${warranty.vendor ? `<div>Vendor: <span>${warranty.vendor}</span></div>` : ''}
${validSerialNumbers.length > 0 ? `
<div class="serial-numbers">
<strong>Serial Numbers:</strong>
<ul>
${validSerialNumbers.map(sn => `<li>${sn}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
</div>
<div class="warranty-status-row status-${statusClass}">
<span>${statusText}</span>
</div>
<div class="document-links-row">
<div class="document-links-inner-container">
${warranty.product_url ? `
<a href="${warranty.product_url}" class="product-link" target="_blank">
<i class="fas fa-globe"></i> Product Website
</a>
` : ''}
${warranty.invoice_path && warranty.invoice_path !== 'null' ? `
<a href="#" onclick="openSecureFile('${warranty.invoice_path}'); return false;" class="invoice-link">
<i class="fas fa-file-invoice"></i> Invoice
</a>` : ''}
${warranty.manual_path && warranty.manual_path !== 'null' ? `
<a href="#" onclick="openSecureFile('${warranty.manual_path}'); return false;" class="manual-link">
<i class="fas fa-book"></i> Manual
</a>` : ''}
${notesLinkHtml}
</div>
</div>
${tagsHtml}
`;
} else if (currentView === 'table') {
// Table view HTML structure
cardElement.innerHTML = `
<div class="product-name-header">
<h3 class="warranty-title">${warranty.product_name || 'Unnamed Product'}</h3>
<div class="warranty-actions">
<button class="action-btn edit-btn" title="Edit" data-id="${warranty.id}">
<i class="fas fa-edit"></i>
</button>
<button class="action-btn delete-btn" title="Delete" data-id="${warranty.id}">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="warranty-content">
<div class="warranty-info">
<div>Purchased: <span>${formatDate(purchaseDate)}</span></div>
<div>Expires: <span>${expirationDateText}</span></div>
</div>
</div>
<div class="warranty-status-row status-${statusClass}">
<span>${statusText}</span>
</div>
<div class="document-links-row">
<div class="document-links-inner-container">
${warranty.product_url ? `
<a href="${warranty.product_url}" class="product-link" target="_blank">
<i class="fas fa-globe"></i> Product Website
</a>
` : ''}
${warranty.invoice_path && warranty.invoice_path !== 'null' ? `
<a href="#" onclick="openSecureFile('${warranty.invoice_path}'); return false;" class="invoice-link">
<i class="fas fa-file-invoice"></i> Invoice
</a>` : ''}
${warranty.manual_path && warranty.manual_path !== 'null' ? `
<a href="#" onclick="openSecureFile('${warranty.manual_path}'); return false;" class="manual-link">
<i class="fas fa-book"></i> Manual
</a>` : ''}
${notesLinkHtml}
</div>
</div>
${tagsHtml}
`;
}
// Add event listeners
warrantiesList.appendChild(cardElement);
// Edit button event listener
cardElement.querySelector('.edit-btn').addEventListener('click', () => {
openEditModal(warranty);
});
// Delete button event listener
cardElement.querySelector('.delete-btn').addEventListener('click', () => {
openDeleteModal(warranty.id, warranty.product_name);
});
// View notes button event listener
const notesLink = cardElement.querySelector('.notes-link');
if (notesLink) {
notesLink.addEventListener('click', (e) => {
e.preventDefault();
showNotesModal(warranty.notes, warranty);
});
}
});
}
function filterWarranties() {
const searchTerm = searchInput ? searchInput.value.toLowerCase() : ''; // Add null check for searchInput
// Show or hide the clear search button if it exists
if (clearSearchBtn) {
clearSearchBtn.style.display = searchTerm ? 'flex' : 'none';
}
if (!searchTerm) {
return warranties; // Return the full list if no search term
// REMOVED: renderWarranties();
// REMOVED: return;
}
const filtered = warranties.filter(warranty => {
// Check product name
if (warranty.product_name && warranty.product_name.toLowerCase().includes(searchTerm)) { // Add null check
return true;
}
// Check tags
if (warranty.tags && Array.isArray(warranty.tags)) {
if (warranty.tags.some(tag => tag.name && tag.name.toLowerCase().includes(searchTerm))) {
return true;
}
}
// Check notes
if (warranty.notes && warranty.notes.toLowerCase().includes(searchTerm)) {
return true;
}
// Check vendor
if (warranty.vendor && warranty.vendor.toLowerCase().includes(searchTerm)) {
return true;
}
// Check if any serial number contains search term
if (warranty.serial_numbers && Array.isArray(warranty.serial_numbers)) {
if (warranty.serial_numbers.some(sn => sn && sn.toLowerCase().includes(searchTerm))) {
return true;
}
}
return false;
});
// REMOVED: Add visual feedback if no results found
// REMOVED: if (filtered.length === 0) {
// REMOVED: renderEmptyState(`No matches found for "${searchTerm}". Try a different search term.`);
// REMOVED: } else {
// REMOVED: renderWarranties(filtered);
// REMOVED: }
return filtered; // Return the filtered list
}
function applyFilters() {
console.log('Applying filters with:', currentFilters);
// Filter warranties based on currentFilters
const filtered = warranties.filter(warranty => {
// Status filter
if (currentFilters.status !== 'all' && warranty.status !== currentFilters.status) {
return false;
}
// Tag filter
if (currentFilters.tag !== 'all') {
const tagId = parseInt(currentFilters.tag);
const hasTag = warranty.tags && Array.isArray(warranty.tags) &&
warranty.tags.some(tag => tag.id === tagId);
if (!hasTag) {
return false;
}
}
// Search filter
if (currentFilters.search) {
const searchTerm = currentFilters.search.toLowerCase();
// Check if product name contains search term
const productNameMatch = warranty.product_name.toLowerCase().includes(searchTerm);
// Check if any tag name contains search term
const tagMatch = warranty.tags && Array.isArray(warranty.tags) &&
warranty.tags.some(tag => tag.name.toLowerCase().includes(searchTerm));
// Check if notes contains search term
const notesMatch = warranty.notes && warranty.notes.toLowerCase().includes(searchTerm);
// Check if vendor contains search term
const vendorMatch = warranty.vendor && warranty.vendor.toLowerCase().includes(searchTerm);
// Check if any serial number contains search term
const serialNumberMatch = warranty.serial_numbers && Array.isArray(warranty.serial_numbers) &&
warranty.serial_numbers.some(sn => sn && sn.toLowerCase().includes(searchTerm));
// Return true if any match
if (!productNameMatch && !tagMatch && !notesMatch && !vendorMatch && !serialNumberMatch) {
return false;
}
}
return true;
});
console.log('Filtered warranties:', filtered);
// Render the filtered warranties
renderWarranties(filtered);
}
function openEditModal(warranty) {
currentWarrantyId = warranty.id;
// Populate form fields
document.getElementById('editProductName').value = warranty.product_name;
document.getElementById('editProductUrl').value = warranty.product_url || '';
document.getElementById('editPurchaseDate').value = warranty.purchase_date.split('T')[0];
document.getElementById('editWarrantyYears').value = warranty.warranty_years;
document.getElementById('editPurchasePrice').value = warranty.purchase_price || '';
document.getElementById('editVendor').value = warranty.vendor || '';
// Clear existing serial number inputs
const editSerialNumbersContainer = document.getElementById('editSerialNumbersContainer');
editSerialNumbersContainer.innerHTML = '';
// Add event listener for adding new serial number inputs in edit modal
editSerialNumbersContainer.addEventListener('click', (e) => {
if (e.target.closest('.add-serial-number')) {
addSerialNumberInput(editSerialNumbersContainer);
}
});
const validSerialNumbers = Array.isArray(warranty.serial_numbers)
? warranty.serial_numbers.filter(sn => sn && typeof sn === 'string' && sn.trim() !== '')
: [];
if (validSerialNumbers.length === 0) {
// Add a single empty input if there are no serial numbers
addSerialNumberInput(editSerialNumbersContainer);
} else {
// Add the first serial number with an "Add Another" button only (no remove button)
const firstInput = document.createElement('div');
firstInput.className = 'serial-number-input';
firstInput.innerHTML = `
<input type="text" name="serial_numbers[]" class="form-control" placeholder="Enter serial number" value="${validSerialNumbers[0]}">
<button type="button" class="btn btn-sm btn-primary add-serial-number">
<i class="fas fa-plus"></i> Add Another
</button>
`;
// Add event listener for the Add button
firstInput.querySelector('.add-serial-number').addEventListener('click', function(e) {
e.stopPropagation(); // Stop event from bubbling up
addSerialNumberInput(editSerialNumbersContainer);
});
editSerialNumbersContainer.appendChild(firstInput);
// Add the rest of the serial numbers with "Remove" buttons
for (let i = 1; i < validSerialNumbers.length; i++) {
const newInput = document.createElement('div');
newInput.className = 'serial-number-input';
newInput.innerHTML = `
<input type="text" name="serial_numbers[]" class="form-control" placeholder="Enter serial number" value="${validSerialNumbers[i]}">
<button type="button" class="btn btn-sm btn-danger remove-serial-number">
<i class="fas fa-minus"></i> Remove
</button>
`;
// Add remove button functionality
newInput.querySelector('.remove-serial-number').addEventListener('click', function() {
this.parentElement.remove();
});
editSerialNumbersContainer.appendChild(newInput);
}
}
// Show current invoice if exists
const currentInvoiceElement = document.getElementById('currentInvoice');
const deleteInvoiceBtn = document.getElementById('deleteInvoiceBtn');
if (currentInvoiceElement && deleteInvoiceBtn) {
if (warranty.invoice_path && warranty.invoice_path !== 'null') {
currentInvoiceElement.innerHTML = `
<span class="text-success">
<i class="fas fa-check-circle"></i> Current invoice:
<a href="#" onclick="openSecureFile('${warranty.invoice_path}'); return false;">View</a>
(Upload a new file to replace)
</span>
`;
deleteInvoiceBtn.style.display = '';
} else {
currentInvoiceElement.innerHTML = '<span>No invoice uploaded</span>';
deleteInvoiceBtn.style.display = 'none';
}
// Reset delete state
deleteInvoiceBtn.dataset.delete = 'false';
deleteInvoiceBtn.onclick = function() {
deleteInvoiceBtn.dataset.delete = 'true';
currentInvoiceElement.innerHTML = '<span class="text-danger">Invoice will be deleted on save</span>';
deleteInvoiceBtn.style.display = 'none';
};
}
// Show current manual if exists
const currentManualElement = document.getElementById('currentManual');
const deleteManualBtn = document.getElementById('deleteManualBtn');
if (currentManualElement && deleteManualBtn) {
if (warranty.manual_path && warranty.manual_path !== 'null') {
currentManualElement.innerHTML = `
<span class="text-success">
<i class="fas fa-check-circle"></i> Current manual:
<a href="#" onclick="openSecureFile('${warranty.manual_path}'); return false;">View</a>
(Upload a new file to replace)
</span>
`;
deleteManualBtn.style.display = '';
} else {
currentManualElement.innerHTML = '<span>No manual uploaded</span>';
deleteManualBtn.style.display = 'none';
}
// Reset delete state
deleteManualBtn.dataset.delete = 'false';
deleteManualBtn.onclick = function() {
deleteManualBtn.dataset.delete = 'true';
currentManualElement.innerHTML = '<span class="text-danger">Manual will be deleted on save</span>';
deleteManualBtn.style.display = 'none';
};
}
// Reset file inputs
document.getElementById('editInvoice').value = '';
document.getElementById('editManual').value = '';
document.getElementById('editFileName').textContent = '';
document.getElementById('editManualFileName').textContent = '';
// Initialize file input event listeners
const editInvoiceInput = document.getElementById('editInvoice');
if (editInvoiceInput) {
editInvoiceInput.addEventListener('change', function(event) {
updateFileName(event, 'editInvoice', 'editFileName');
});
}
const editManualInput = document.getElementById('editManual');
if (editManualInput) {
editManualInput.addEventListener('change', function(event) {
updateFileName(event, 'editManual', 'editManualFileName');
});
}
// Show edit modal
const modalBackdrop = document.getElementById('editModal');
if (modalBackdrop) {
modalBackdrop.classList.add('active'); // Add active class to display as flex
}
// Reset tabs to first tab
const editTabBtns = document.querySelectorAll('.edit-tab-btn');
editTabBtns.forEach(btn => btn.classList.remove('active'));
document.querySelector('.edit-tab-btn[data-tab="edit-product-info"]').classList.add('active');
// Reset tab content
document.querySelectorAll('.edit-tab-content').forEach(content => content.classList.remove('active'));
document.getElementById('edit-product-info').classList.add('active');
// Initialize edit mode tags
editSelectedTags = [];
// If warranty has tags, populate editSelectedTags
if (warranty.tags && Array.isArray(warranty.tags)) {
editSelectedTags = warranty.tags.map(tag => ({
id: tag.id,
name: tag.name,
color: tag.color
}));
}
// Render selected tags using the helper function
renderEditSelectedTags();
// Set up tag search in edit mode
const editTagSearch = document.getElementById('editTagSearch');
const editTagsList = document.getElementById('editTagsList');
if (editTagSearch && editTagsList) {
// Add event listeners for tag search
editTagSearch.addEventListener('focus', () => {
renderEditTagsList();
editTagsList.classList.add('show');
});
editTagSearch.addEventListener('input', () => {
renderEditTagsList(editTagSearch.value);
});
// Add event listener to close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!editTagSearch.contains(e.target) && !editTagsList.contains(e.target)) {
editTagsList.classList.remove('show');
}
});
}
// Set up manage tags button in edit mode
const editManageTagsBtn = document.getElementById('editManageTagsBtn');
if (editManageTagsBtn) {
editManageTagsBtn.addEventListener('click', (e) => {
e.preventDefault();
openTagManagementModal();
});
}
// Validate all tabs to update completion indicators
validateEditTab('edit-product-info');
validateEditTab('edit-warranty-details');
validateEditTab('edit-documents');
validateEditTab('edit-tags');
// Add input event listeners to update validation status
document.querySelectorAll('#editWarrantyForm input').forEach(input => {
input.addEventListener('input', function() {
// Find the tab this input belongs to
const tabContent = this.closest('.edit-tab-content');
if (tabContent) {
validateEditTab(tabContent.id);
}
});
});
// --- Set Lifetime Checkbox and Toggle Years Input ---
if (editIsLifetimeCheckbox && editWarrantyYearsGroup && editWarrantyYearsInput) {
editIsLifetimeCheckbox.checked = warranty.is_lifetime || false;
handleEditLifetimeChange(); // Call handler to set initial state
// Remove previous listener if exists
editIsLifetimeCheckbox.removeEventListener('change', handleEditLifetimeChange);
// Add new listener
editIsLifetimeCheckbox.addEventListener('change', handleEditLifetimeChange);
// Set years value only if NOT lifetime
editWarrantyYearsInput.value = warranty.is_lifetime ? '' : (warranty.warranty_years || '');
} else {
console.error("Lifetime warranty elements not found in edit form");
}
// Set notes
const notesInput = document.getElementById('editNotes');
if (notesInput) {
notesInput.value = warranty.notes || '';
}
}
function openDeleteModal(warrantyId, productName) {
currentWarrantyId = warrantyId;
const deleteProductNameElement = document.getElementById('deleteProductName');
if (deleteProductNameElement) {
deleteProductNameElement.textContent = productName || '';
}
const deleteModal = document.getElementById('deleteModal');
if (deleteModal) {
deleteModal.classList.add('active');
}
}
// Function to close all modals
function closeModals() {
document.querySelectorAll('.modal-backdrop').forEach(modal => {
modal.classList.remove('active');
});
}
// Validate file size before upload
function validateFileSize(formData, maxSizeMB = 32) {
let totalSize = 0;
// Check file sizes
if (formData.has('invoice') && formData.get('invoice').size > 0) {
totalSize += formData.get('invoice').size;
}
if (formData.has('manual') && formData.get('manual').size > 0) {
totalSize += formData.get('manual').size;
}
// Convert bytes to MB for comparison and display
const totalSizeMB = totalSize / (1024 * 1024);
console.log(`Total upload size: ${totalSizeMB.toFixed(2)} MB`);
// Check if total size exceeds limit
if (totalSizeMB > maxSizeMB) {
return {
valid: false,
message: `Total file size (${totalSizeMB.toFixed(2)} MB) exceeds the maximum allowed size of ${maxSizeMB} MB. Please reduce file sizes.`
};
}
return {
valid: true
};
}
// Submit form function - event handler for form submit
function submitForm(event) {
event.preventDefault();
// --- Add Lifetime Check ---
if (!isLifetimeCheckbox.checked && (!warrantyYearsInput.value || parseFloat(warrantyYearsInput.value) <= 0)) {
showToast('Warranty period (years) is required and must be greater than 0 unless it\'s a lifetime warranty', 'error');
switchToTab(1); // Switch to warranty details tab
warrantyYearsInput.focus();
warrantyYearsInput.classList.add('invalid');
return;
}
// Validate all tabs
for (let i = 0; i < tabContents.length; i++) {
if (!validateTab(i)) {
// Switch to the first invalid tab
switchToTab(i);
return;
}
}
// Create form data object
const formData = new FormData(warrantyForm);
// Add serial numbers to form data
const serialInputs = document.querySelectorAll('#serialNumbersContainer input');
serialInputs.forEach(input => {
if (input.value.trim()) {
formData.append('serial_numbers', input.value.trim());
}
});
// Add tag IDs to form data as JSON string
if (selectedTags && selectedTags.length > 0) {
const tagIds = selectedTags.map(tag => tag.id);
formData.append('tag_ids', JSON.stringify(tagIds));
}
// --- Ensure is_lifetime is correctly added ---
if (!isLifetimeCheckbox.checked) {
formData.append('is_lifetime', 'false');
}
// Show loading spinner
showLoadingSpinner();
// Send the form data to the server
fetch('/api/warranties', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('auth_token')
},
body: formData
})
.then(response => {
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.error || 'Failed to add warranty');
});
}
return response.json();
})
.then(data => {
hideLoadingSpinner();
showToast('Warranty added successfully', 'success');
// --- Close and reset the modal on success ---
if (addWarrantyModal) {
addWarrantyModal.classList.remove('active');
}
resetAddWarrantyWizard(); // Reset the wizard form
// --- End modification ---
loadWarranties().then(() => {
applyFilters();
}); // Reload the list and update UI
})
.catch(error => {
hideLoadingSpinner();
console.error('Error adding warranty:', error);
showToast(error.message || 'Failed to add warranty', 'error');
});
}
// Initialize page
document.addEventListener('DOMContentLoaded', function() {
// Initialize the warranty form *only* if the form element exists
if (warrantyForm) {
initWarrantyForm();
}
// Load warranties (might need checks if warrantiesList doesn't always exist)
if (warrantiesList) {
// REMOVED: loadWarranties(); // Now called after authStateReady
// REMOVED: loadViewPreference(); // Now called after authStateReady
loadTags(); // Load tags for the form
initTagFunctionality(); // Initialize tag search/selection
}
// Initialize theme (should be safe on all pages)
initializeTheme();
// Set up event listeners for other UI controls (should contain checks)
setupUIEventListeners();
setupModalTriggers(); // Add the new modal listeners
// Check if user is logged in and update UI
// checkLoginStatus(); // Removed undefined function
// Setup form submission
const form = document.getElementById('addWarrantyForm');
if (form) {
form.addEventListener('submit', handleFormSubmit);
}
// Setup settings menu toggle
// setupSettingsMenu(); // Removed: function not defined, handled by auth.js
// Initialize theme toggle state *after* DOM is loaded
// Find the header toggle (assuming ID 'darkModeToggle')
const headerToggle = document.getElementById('darkModeToggle');
if (headerToggle) {
// Set initial state based on theme applied by theme-loader.js
const currentTheme = document.documentElement.getAttribute('data-theme');
headerToggle.checked = currentTheme === 'dark';
// Add listener to update theme when toggled
headerToggle.addEventListener('change', function() {
setTheme(this.checked);
});
}
// REMOVE any direct calls to initializeTheme() from here or globally
// initializeTheme();
// Setup view switcher
// setupViewSwitcher(); // Removed undefined function
// Setup filter controls
// setupFilterControls(); // Removed: function not defined
// Setup form tabs and navigation
// initFormTabs(); // <-- Remove this line from DOMContentLoaded
// Initialize modal interactions
// initializeModals(); // Removed: function not defined, handled by setupModalTriggers
// Load preferences (if needed for things other than theme)
// loadPreferences(); // Consider if needed
// REMOVED: updateCurrencySymbols(); // Now called after authStateReady
});
// Add this function to handle edit tab functionality
function initEditTabs() {
const editTabBtns = document.querySelectorAll('.edit-tab-btn');
editTabBtns.forEach(btn => {
btn.addEventListener('click', () => {
// Remove active class from all tabs
editTabBtns.forEach(b => b.classList.remove('active'));
// Add active class to clicked tab
btn.classList.add('active');
// Hide all tab content
document.querySelectorAll('.edit-tab-content').forEach(content => {
content.classList.remove('active');
});
// Show the selected tab content
const tabId = btn.getAttribute('data-tab');
document.getElementById(tabId).classList.add('active');
});
});
}
// Update validateEditTabs function
function validateEditTab(tabId) {
const tab = document.getElementById(tabId);
let isValid = true;
// Get all required inputs in this tab
const requiredInputs = tab.querySelectorAll('input[required]');
// Check if all required fields are filled
requiredInputs.forEach(input => {
if (!input.value) {
isValid = false;
input.classList.add('invalid');
} else {
input.classList.remove('invalid');
}
});
// Update the tab button to show completion status
const tabBtn = document.querySelector(`.edit-tab-btn[data-tab="${tabId}"]`);
if (isValid) {
tabBtn.classList.add('completed');
} else {
tabBtn.classList.remove('completed');
}
return isValid;
}
// Add this function for secure file access
function openSecureFile(filePath) {
if (!filePath || filePath === 'null') {
console.error('Invalid file path:', filePath);
showToast('Invalid file path', 'error');
return false;
}
console.log('Opening secure file:', filePath);
// Get the file name from the path
const fileName = filePath.split('/').pop();
// Get auth token
const token = window.auth.getToken();
if (!token) {
showToast('Authentication error. Please log in again.', 'error');
return false;
}
// Use fetch with proper authorization header
fetch(`/api/secure-file/${fileName}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(response => {
if (!response.ok) {
throw new Error(`Error: ${response.status} ${response.statusText}`);
}
return response.blob();
})
.then(blob => {
// Create a URL for the blob
const blobUrl = window.URL.createObjectURL(blob);
// Open in new tab
window.open(blobUrl, '_blank');
})
.catch(error => {
console.error('Error fetching file:', error);
showToast('Error opening file: ' + error.message, 'error');
});
return false;
}
// Initialize the warranty form and all its components
function initWarrantyForm() {
// Initialize form tabs
if (formTabs && tabContents) {
initFormTabs();
}
// Initialize serial number inputs
addSerialNumberInput();
// Initialize file input display
if (document.getElementById('invoice')) {
document.getElementById('invoice').addEventListener('change', function(event) {
updateFileName(event, 'invoice', 'fileName');
});
}
if (document.getElementById('manual')) {
document.getElementById('manual').addEventListener('change', function(event) {
updateFileName(event, 'manual', 'manualFileName');
});
}
// Initialize tag functionality
initTagFunctionality();
// Form submission
if (warrantyForm) {
warrantyForm.addEventListener('submit', submitForm);
}
// Initialize lifetime checkbox listener
if (isLifetimeCheckbox && warrantyYearsGroup && warrantyYearsInput) {
isLifetimeCheckbox.addEventListener('change', handleLifetimeChange);
handleLifetimeChange(); // Initial check
} else {
console.error("Lifetime warranty elements not found in add form");
}
}
// Initialize tag functionality
function initTagFunctionality() {
// Skip if tag elements don't exist
if (!tagSearch || !tagsList || !manageTagsBtn || !selectedTagsContainer) {
console.log('Tag elements not found, skipping tag initialization');
return;
}
// Load tags from API if not already loaded
if (allTags.length === 0) {
loadTags();
}
// Tag search input
tagSearch.addEventListener('focus', () => {
renderTagsList();
tagsList.classList.add('show');
});
tagSearch.addEventListener('input', () => {
renderTagsList(tagSearch.value);
});
document.addEventListener('click', (e) => {
if (!tagSearch.contains(e.target) && !tagsList.contains(e.target)) {
tagsList.classList.remove('show');
}
});
// Manage tags button
manageTagsBtn.addEventListener('click', (e) => {
e.preventDefault();
openTagManagementModal();
});
// Tag management form
if (newTagForm) {
newTagForm.addEventListener('submit', (e) => {
e.preventDefault();
createNewTag();
});
}
// Close modal buttons
if (tagManagementModal) {
const closeButtons = tagManagementModal.querySelectorAll('[data-dismiss="modal"]');
closeButtons.forEach(button => {
button.addEventListener('click', () => {
tagManagementModal.style.display = 'none';
});
});
}
}
// Function to load all tags
async function loadTags() {
try {
const token = localStorage.getItem('auth_token');
if (!token) {
console.error('No auth token found');
return;
}
showLoadingSpinner();
const response = await fetch('/api/tags', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error(`Failed to load tags: ${response.status}`);
}
const data = await response.json();
console.log('Loaded tags:', data);
// Store tags globally
allTags = data;
// Populate the tag filter
populateTagFilter();
// Render selected tags if any
if (selectedTagsContainer) {
renderSelectedTags();
}
hideLoadingSpinner();
return data;
} catch (error) {
console.error('Error loading tags:', error);
hideLoadingSpinner();
return [];
}
}
// Render the tags dropdown list
function renderTagsList(searchTerm = '') {
if (!tagsList) return;
tagsList.innerHTML = '';
// Filter tags based on search term
const filteredTags = allTags.filter(tag =>
!searchTerm || tag.name.toLowerCase().includes(searchTerm.toLowerCase())
);
// Add option to create new tag if search term is provided and not in list
if (searchTerm && !filteredTags.some(tag => tag.name.toLowerCase() === searchTerm.toLowerCase())) {
const createOption = document.createElement('div');
createOption.className = 'tag-option create-tag';
createOption.innerHTML = `<i class="fas fa-plus"></i> Create "${searchTerm}"`;
createOption.addEventListener('click', () => {
createTag(searchTerm).then(newTag => {
// Add the new tag to selectedTags
selectedTags.push(newTag);
renderSelectedTags();
renderTagsList(''); // Clear search and refresh list
});
tagsList.classList.remove('show');
});
tagsList.appendChild(createOption);
}
// Add existing tags to dropdown
filteredTags.forEach(tag => {
const option = document.createElement('div');
option.className = 'tag-option';
// Check if tag is already selected
const isSelected = selectedTags.some(selected => selected.id === tag.id);
option.innerHTML = `
<span class="tag-color" style="background-color: ${tag.color}"></span>
${tag.name}
<span class="tag-status">${isSelected ? '<i class="fas fa-check"></i>' : ''}</span>
`;
option.addEventListener('click', () => {
if (isSelected) {
// Remove tag if already selected
selectedTags = selectedTags.filter(selected => selected.id !== tag.id);
} else {
// Add tag if not selected
selectedTags.push({
id: tag.id,
name: tag.name,
color: tag.color
});
}
renderSelectedTags();
renderTagsList(searchTerm);
});
tagsList.appendChild(option);
});
// Show the dropdown
tagsList.classList.add('show');
}
// Update renderEditTagsList to add new tag to editSelectedTags after creation
function renderEditTagsList(searchTerm = '') {
const editTagsList = document.getElementById('editTagsList');
if (!editTagsList) return;
editTagsList.innerHTML = '';
// Filter tags based on search term
const filteredTags = allTags.filter(tag =>
!searchTerm || tag.name.toLowerCase().includes(searchTerm.toLowerCase())
);
// Add option to create new tag if search term is provided and not in list
if (searchTerm && !filteredTags.some(tag => tag.name.toLowerCase() === searchTerm.toLowerCase())) {
const createOption = document.createElement('div');
createOption.className = 'tag-option create-tag';
createOption.innerHTML = `<i class="fas fa-plus"></i> Create "${searchTerm}"`;
createOption.addEventListener('click', () => {
createTag(searchTerm).then(newTag => {
// Add the new tag to editSelectedTags
editSelectedTags.push(newTag);
renderEditSelectedTags();
renderEditTagsList(''); // Clear search and refresh list
});
editTagsList.classList.remove('show');
});
editTagsList.appendChild(createOption);
}
// Add existing tags to dropdown
filteredTags.forEach(tag => {
const option = document.createElement('div');
option.className = 'tag-option';
// Check if tag is already selected
const isSelected = editSelectedTags.some(selected => selected.id === tag.id);
option.innerHTML = `
<span class="tag-color" style="background-color: ${tag.color}"></span>
${tag.name}
<span class="tag-status">${isSelected ? '<i class="fas fa-check"></i>' : ''}</span>
`;
option.addEventListener('click', () => {
if (isSelected) {
// Remove tag if already selected
editSelectedTags = editSelectedTags.filter(selected => selected.id !== tag.id);
} else {
// Add tag if not selected
editSelectedTags.push({
id: tag.id,
name: tag.name,
color: tag.color
});
}
// Use our helper function to render selected tags
renderEditSelectedTags();
renderEditTagsList(searchTerm);
});
editTagsList.appendChild(option);
});
// Show the dropdown
editTagsList.classList.add('show');
}
// Render the selected tags
function renderSelectedTags() {
if (!selectedTagsContainer) return;
selectedTagsContainer.innerHTML = '';
if (selectedTags.length === 0) {
const placeholder = document.createElement('span');
placeholder.className = 'no-tags-selected';
placeholder.textContent = 'No tags selected';
selectedTagsContainer.appendChild(placeholder);
return;
}
selectedTags.forEach(tag => {
const tagElement = document.createElement('span');
tagElement.className = 'tag';
tagElement.style.backgroundColor = tag.color;
tagElement.style.color = getContrastColor(tag.color);
tagElement.innerHTML = `
${tag.name}
<span class="remove-tag" data-id="${tag.id}">&times;</span>
`;
// Add event listener for removing tag
tagElement.querySelector('.remove-tag').addEventListener('click', (e) => {
e.stopPropagation();
selectedTags = selectedTags.filter(t => t.id !== tag.id);
renderSelectedTags();
// Update summary if needed
if (document.getElementById('summary-tags')) {
updateSummary();
}
});
selectedTagsContainer.appendChild(tagElement);
});
}
// Helper function to render the edit selected tags
function renderEditSelectedTags() {
const editSelectedTagsContainer = document.getElementById('editSelectedTags');
if (!editSelectedTagsContainer) return;
editSelectedTagsContainer.innerHTML = '';
if (editSelectedTags.length > 0) {
editSelectedTags.forEach(tag => {
const tagElement = document.createElement('span');
tagElement.className = 'tag';
tagElement.style.backgroundColor = tag.color;
tagElement.style.color = getContrastColor(tag.color);
tagElement.innerHTML = `
${tag.name}
<span class="remove-tag" data-id="${tag.id}">&times;</span>
`;
// Add event listener for removing tag
const removeButton = tagElement.querySelector('.remove-tag');
removeButton.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault(); // Add this to prevent default action
// Prevent the event from bubbling up to parent elements
if (e.cancelBubble !== undefined) {
e.cancelBubble = true;
}
editSelectedTags = editSelectedTags.filter(t => t.id !== tag.id);
// Re-render just the tags
renderEditSelectedTags();
return false; // Add return false for older browsers
});
editSelectedTagsContainer.appendChild(tagElement);
});
} else {
const placeholder = document.createElement('span');
placeholder.className = 'no-tags-selected';
placeholder.textContent = 'No tags selected';
editSelectedTagsContainer.appendChild(placeholder);
}
}
// Update createTag to return a Promise
function createTag(name) {
return new Promise((resolve, reject) => {
const token = localStorage.getItem('auth_token');
if (!token) {
console.error('No authentication token found');
reject(new Error('No authentication token found'));
return;
}
// Generate a random color for the tag
const color = '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
fetch('/api/tags', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({
name: name,
color: color
})
})
.then(response => {
if (!response.ok) {
if (response.status === 409) {
reject(new Error('A tag with this name already exists'));
return;
}
reject(new Error('Failed to create tag'));
return;
}
return response.json();
})
.then(data => {
if (!data) return;
const newTag = {
id: data.id,
name: data.name,
color: data.color
};
allTags.push(newTag);
renderExistingTags();
populateTagFilter();
showToast('Tag created successfully', 'success');
resolve(newTag);
})
.catch(error => {
console.error('Error creating tag:', error);
showToast(error.message || 'Failed to create tag', 'error');
reject(error);
});
});
}
// Helper function to determine text color based on background color
function getContrastColor(hexColor) {
// Convert hex to RGB
const r = parseInt(hexColor.substr(1, 2), 16);
const g = parseInt(hexColor.substr(3, 2), 16);
const b = parseInt(hexColor.substr(5, 2), 16);
// Calculate luminance
const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
// Return black or white depending on luminance
return (yiq >= 128) ? '#000000' : '#ffffff';
}
// Open tag management modal
function openTagManagementModal() {
if (!tagManagementModal) return;
// Populate existing tags
renderExistingTags();
// Show modal
tagManagementModal.style.display = 'block';
}
// Render existing tags in the management modal
function renderExistingTags() {
if (!existingTagsContainer) return;
existingTagsContainer.innerHTML = '';
if (allTags.length === 0) {
existingTagsContainer.innerHTML = '<div class="no-tags">No tags created yet</div>';
return;
}
allTags.forEach(tag => {
const tagElement = document.createElement('div');
tagElement.className = 'existing-tag';
tagElement.innerHTML = `
<div class="existing-tag-info">
<div class="existing-tag-color" style="background-color: ${tag.color}"></div>
<div class="existing-tag-name">${tag.name}</div>
</div>
<div class="existing-tag-actions">
<button class="btn btn-sm btn-secondary edit-tag" data-id="${tag.id}">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-danger delete-tag" data-id="${tag.id}">
<i class="fas fa-trash"></i>
</button>
</div>
`;
// 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 = `
<input type="text" class="form-control edit-tag-name" value="${tag.name}" style="width: 60%;">
<input type="color" class="edit-tag-color" value="${tag.color}" style="width: 40px; height: 38px;">
<button class="btn btn-sm btn-primary save-edit" data-id="${tag.id}">Save</button>
<button class="btn btn-sm btn-secondary cancel-edit">Cancel</button>
`;
// 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 = `
<div class="modal" style="max-width: 500px;">
<div class="modal-header">
<h3 class="modal-title">Warranty Notes</h3>
<button class="close-btn" id="closeNotesModal">&times;</button>
</div>
<div class="modal-body">
<div id="notesModalContent" style="white-space: pre-line;"></div>
<textarea id="notesModalTextarea" style="display:none;width:100%;min-height:100px;"></textarea>
</div>
<div class="modal-footer" id="notesModalFooter">
<button class="btn btn-secondary" id="editNotesBtn">Edit</button>
<button class="btn btn-primary" id="saveNotesBtn" style="display:none;">Save</button>
<button class="btn btn-danger" id="cancelEditNotesBtn" style="display:none;">Cancel</button>
</div>
</div>
`;
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 +++