// Global variables let warranties = []; let currentTabIndex = 0; let tabContents; let editMode = false; let currentWarrantyId = null; 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'); // 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'); /** * 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 function setTheme(isDark) { // Get the appropriate key prefix based on user type const prefix = getPreferenceKeyPrefix(); console.log(`Setting theme with prefix: ${prefix}`); document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light'); // Update darkMode settings localStorage.setItem(`${prefix}darkMode`, isDark); localStorage.setItem('darkMode', isDark); // Keep for backward compatibility // Update DOM if (isDark) { document.body.classList.add('dark-mode'); } else { document.body.classList.remove('dark-mode'); } // Set toggle state if (darkModeToggle) { darkModeToggle.checked = isDark; } // Also update preferences in localStorage for consistency try { let userPrefs = {}; const storedPrefs = localStorage.getItem(`${prefix}preferences`); if (storedPrefs) { userPrefs = JSON.parse(storedPrefs); } userPrefs.theme = isDark ? 'dark' : 'light'; localStorage.setItem(`${prefix}preferences`, JSON.stringify(userPrefs)); } catch (e) { console.error(`Error updating theme in ${prefix}preferences:`, e); } } // Initialize theme based on user preference or system preference function initializeTheme() { // Get the appropriate key prefix based on user type const prefix = getPreferenceKeyPrefix(); console.log(`Initializing theme with prefix: ${prefix}`); // First check user-specific setting const userDarkMode = localStorage.getItem(`${prefix}darkMode`); if (userDarkMode !== null) { console.log(`Found user-specific dark mode setting: ${userDarkMode}`); setTheme(userDarkMode === 'true'); return; } // Then check global setting for backward compatibility const globalDarkMode = localStorage.getItem('darkMode'); if (globalDarkMode !== null) { console.log(`Found global dark mode setting: ${globalDarkMode}`); setTheme(globalDarkMode === 'true'); return; } // Check for system preference if no stored preference const prefersDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; console.log(`No saved preference, using system preference: ${prefersDarkMode}`); setTheme(prefersDarkMode); } // Initialize theme when page loads initializeTheme(); // 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 const formTabs = Array.from(document.querySelectorAll('.form-tab')); const tabContentsArray = document.querySelectorAll('.tab-content'); tabContents = Array.from(tabContentsArray); // Convert NodeList to Array const nextButton = document.querySelector('.next-tab'); const prevButton = document.querySelector('.prev-tab'); // Initialize form tabs function initFormTabs() { console.log('Initializing form tabs...'); // Hide the submit button initially const submitButton = document.querySelector('button[type="submit"]'); if (submitButton) { submitButton.style.display = 'none'; } // Show/hide navigation buttons based on current tab updateNavigationButtons(); // Remove any existing event listeners before adding new ones if (nextButton && prevButton) { const nextTabClone = nextButton.cloneNode(true); const prevTabClone = prevButton.cloneNode(true); nextButton.parentNode.replaceChild(nextTabClone, nextButton); prevButton.parentNode.replaceChild(prevTabClone, prevButton); // Add event listeners for tab navigation document.querySelector('.next-tab').addEventListener('click', () => { console.log('Next button clicked, current tab:', currentTabIndex); if (validateTab(currentTabIndex)) { switchToTab(currentTabIndex + 1); } else { showValidationErrors(currentTabIndex); } }); document.querySelector('.prev-tab').addEventListener('click', () => { console.log('Previous button clicked, current tab:', currentTabIndex); switchToTab(currentTabIndex - 1); }); } // Add click event for tab headers formTabs.forEach((tab, index) => { tab.addEventListener('click', () => { // Only allow clicking on previous tabs or current tab if (index <= currentTabIndex) { switchToTab(index); } }); }); } // 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 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 summary if on summary tab if (index === formTabs.length - 1) { updateSummary(); } } // 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 = ''; } else { serialNumbersContainer.textContent = 'None'; } } // Warranty details const purchaseDate = document.getElementById('purchaseDate')?.value; const summaryPurchaseDate = document.getElementById('summary-purchase-date'); if (summaryPurchaseDate) { summaryPurchaseDate.textContent = purchaseDate ? new Date(purchaseDate).toLocaleDateString() : '-'; } const warrantyYears = document.getElementById('warrantyYears')?.value; const summaryWarrantyYears = document.getElementById('summary-warranty-years'); if (summaryWarrantyYears) { summaryWarrantyYears.textContent = warrantyYears ? `${warrantyYears} ${warrantyYears > 1 ? 'years' : 'year'}` : '-'; } // Calculate and display expiration date const summaryExpirationDate = document.getElementById('summary-expiration-date'); if (summaryExpirationDate && purchaseDate && warrantyYears) { const expirationDate = new Date(purchaseDate); expirationDate.setFullYear(expirationDate.getFullYear() + parseInt(warrantyYears)); summaryExpirationDate.textContent = expirationDate.toLocaleDateString(); } 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'; } } } // 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)); // Return true if either product name or tag name matches return productNameMatch || tagMatch; }); } 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\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 ]; // 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 function switchView(viewType) { console.log(`Switching to ${viewType} view...`); // Update currentView currentView = viewType; // Remove active class from all view buttons gridViewBtn.classList.remove('active'); listViewBtn.classList.remove('active'); tableViewBtn.classList.remove('active'); // Update the view warrantiesList.className = `warranties-list ${viewType}-view`; // Hide table header if not in table view if (tableViewHeader) { tableViewHeader.style.display = viewType === 'table' ? 'flex' : 'none'; } // Add active class to the selected view button if (viewType === 'grid') { gridViewBtn.classList.add('active'); } else if (viewType === 'list') { listViewBtn.classList.add('active'); } else if (viewType === 'table') { tableViewBtn.classList.add('active'); } // Re-render warranties with the new view console.log('Applying filters after switching view...'); applyFilters(); // Get prefix for user-specific preferences const prefix = getPreferenceKeyPrefix(); // Save view preference to localStorage with the appropriate prefix localStorage.setItem(`${prefix}warrantyView`, viewType); localStorage.setItem('warrantyView', viewType); // Keep global setting for backward compatibility } // Load saved view preference function loadViewPreference() { // Get prefix for user-specific preferences const prefix = getPreferenceKeyPrefix(); // First check for user-specific warrantyView const userSavedView = localStorage.getItem(`${prefix}warrantyView`); if (userSavedView) { console.log(`Found user-specific view preference: ${userSavedView}`); switchView(userSavedView); return; } // If not found, check for user-specific defaultView const userDefaultView = localStorage.getItem(`${prefix}defaultView`); if (userDefaultView) { console.log(`Found user-specific default view: ${userDefaultView}`); switchView(userDefaultView); return; } // If no user-specific preferences found, check global preferences for backward compatibility const globalSavedView = localStorage.getItem('warrantyView'); if (globalSavedView) { console.log(`Found global view preference: ${globalSavedView}`); switchView(globalSavedView); return; } const globalDefaultView = localStorage.getItem('defaultView'); if (globalDefaultView) { console.log(`Found global default view: ${globalDefaultView}`); switchView(globalDefaultView); return; } // Default to grid view if no preferences found console.log('No view preference found, defaulting to grid view'); switchView('grid'); } // Dark mode toggle darkModeToggle.addEventListener('change', (e) => { setTheme(e.target.checked); }); const serialNumbersContainer = document.getElementById('serialNumbersContainer'); // Add event listener for adding new serial number inputs serialNumbersContainer.addEventListener('click', (e) => { if (e.target.closest('.add-serial-number')) { addSerialNumberInput(); } }); // Add a serial number input field function addSerialNumberInput(container = serialNumbersContainer) { if (!container) return; // Create a new input group const inputGroup = document.createElement('div'); inputGroup.className = 'serial-number-input'; // 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 inputGroup.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 = ''; // Add event listener to remove button removeButton.addEventListener('click', function() { container.removeChild(inputGroup); }); // Append remove button to the input group inputGroup.appendChild(removeButton); } // Insert the new input group before the add button const addButton = container.querySelector('.add-serial'); if (addButton) { container.insertBefore(inputGroup, addButton); } else { container.appendChild(inputGroup); // 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 = ' 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} `; // 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(); // Handle purchase date let purchaseDate = null; if (processedWarranty.purchase_date) { purchaseDate = new Date(processedWarranty.purchase_date); // Check if date is valid if (isNaN(purchaseDate.getTime())) { purchaseDate = null; } } processedWarranty.purchaseDate = purchaseDate; // Handle expiration date let expirationDate = null; if (processedWarranty.expiration_date) { expirationDate = new Date(processedWarranty.expiration_date); // Check if date is valid if (isNaN(expirationDate.getTime())) { expirationDate = null; } } processedWarranty.expirationDate = expirationDate; // Calculate days remaining only if expiration date is valid let daysRemaining = null; if (expirationDate && !isNaN(expirationDate.getTime())) { daysRemaining = Math.floor((expirationDate - today) / (1000 * 60 * 60 * 24)); } let statusClass = 'active'; let statusText = 'Active'; if (daysRemaining === null) { statusClass = 'unknown'; statusText = 'Unknown status'; } else if (daysRemaining < 0) { statusClass = 'expired'; statusText = 'Expired'; } else if (daysRemaining < expiringSoonDays) { console.log(`Using expiringSoonDays: ${expiringSoonDays} for warranty: ${processedWarranty.product_name}`); statusClass = 'expiring'; statusText = `Expiring Soon (${daysRemaining} days)`; } else { statusText = `${daysRemaining} days remaining`; } // Add status to warranty object processedWarranty.status = statusClass; processedWarranty.daysRemaining = daysRemaining; processedWarranty.statusText = statusText; 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() { try { console.log('Loading warranties...'); showLoading(); // Get expiring soon days from user preferences if available try { const prefsResponse = await fetch('/api/auth/preferences', { headers: { 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` } }); if (prefsResponse.ok) { const data = await prefsResponse.json(); if (data && data.expiring_soon_days) { const oldValue = expiringSoonDays; expiringSoonDays = data.expiring_soon_days; console.log('Updated expiring soon days from preferences:', expiringSoonDays); // If we already have warranties loaded and the value changed, reprocess them if (warranties && warranties.length > 0 && oldValue !== expiringSoonDays) { console.log('Reprocessing warranties with new expiringSoonDays value'); warranties = warranties.map(warranty => processWarrantyData(warranty)); renderWarrantiesTable(warranties); } } } } catch (error) { console.error('Error loading preferences:', error); // Continue with default value } // 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('User not authenticated, showing empty state'); renderEmptyState('Please log in to view your warranties.'); hideLoading(); return; } // Get the auth token const token = window.auth.getToken(); if (!token) { console.log('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('Received warranties from server:', data); // Process each warranty to calculate status and days remaining warranties = data.map(warranty => { return processWarrantyData(warranty); }); console.log('Processed warranties:', 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(); applyFilters(); } } catch (error) { console.error('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 = `

No warranties found

${message}

`; } function formatDate(date) { if (!date) return 'N/A'; // If date is already a Date object, use it directly const dateObj = date instanceof Date ? date : new Date(date); // Check if date is valid if (isNaN(dateObj.getTime())) { return 'N/A'; } return dateObj.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); } async function renderWarranties(warrantiesToRender) { console.log('renderWarranties called with:', warrantiesToRender); if (!warrantiesToRender || warrantiesToRender.length === 0) { renderEmptyState(); return; } const today = new Date(); 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 the pre-processed dates from the warranty object const purchaseDate = warranty.purchaseDate; const expirationDate = warranty.expirationDate; // Use the pre-calculated status and days remaining from the warranty object const statusClass = warranty.status || 'unknown'; const statusText = warranty.statusText || 'Unknown status'; // Debug file paths console.log(`Warranty ID ${warranty.id} - Product: ${warranty.product_name}`); console.log(`- Invoice path: ${warranty.invoice_path}`); console.log(`- Manual path: ${warranty.manual_path}`); // 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 ? `
${warranty.tags.map(tag => ` ${tag.name} ` ).join('')}
` : ''; 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 = `

${warranty.product_name || 'Unnamed Product'}

Purchased: ${formatDate(purchaseDate)}
Warranty: ${warranty.warranty_years !== undefined ? `${warranty.warranty_years} ${warranty.warranty_years === 1 ? 'year' : 'years'}` : 'N/A'}
Expires: ${formatDate(expirationDate)}
${warranty.purchase_price ? `
Price: $${parseFloat(warranty.purchase_price).toFixed(2)}
` : ''} ${validSerialNumbers.length > 0 ? `
Serial Numbers:
    ${validSerialNumbers.map(sn => `
  • ${sn}
  • `).join('')}
` : ''}
${statusText}
${tagsHtml} `; } else if (currentView === 'list') { // List view HTML structure cardElement.innerHTML = `

${warranty.product_name || 'Unnamed Product'}

Purchased: ${formatDate(purchaseDate)}
Warranty: ${warranty.warranty_years !== undefined ? `${warranty.warranty_years} ${warranty.warranty_years === 1 ? 'year' : 'years'}` : 'N/A'}
Expires: ${formatDate(expirationDate)}
${warranty.purchase_price ? `
Price: $${parseFloat(warranty.purchase_price).toFixed(2)}
` : ''} ${validSerialNumbers.length > 0 ? `
Serial Numbers:
    ${validSerialNumbers.map(sn => `
  • ${sn}
  • `).join('')}
` : ''}
${statusText}
${tagsHtml} `; } else if (currentView === 'table') { // Table view HTML structure cardElement.innerHTML = `

${warranty.product_name || 'Unnamed Product'}

Purchased: ${formatDate(purchaseDate)}
Expires: ${formatDate(expirationDate)}
${statusText}
${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); }); }); } function filterWarranties() { const searchTerm = searchInput.value.toLowerCase(); // Show or hide the clear search button clearSearchBtn.style.display = searchTerm ? 'flex' : 'none'; if (!searchTerm) { renderWarranties(); return; } const filtered = warranties.filter(warranty => { // Check product name if (warranty.product_name.toLowerCase().includes(searchTerm)) { return true; } // Check tags if (warranty.tags && Array.isArray(warranty.tags)) { return warranty.tags.some(tag => tag.name.toLowerCase().includes(searchTerm)); } return false; }); // Add visual feedback if no results found if (filtered.length === 0) { renderEmptyState(`No matches found for "${searchTerm}". Try a different search term.`); } else { renderWarranties(filtered); } } 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)); // Return true if either product name or tag name matches if (!productNameMatch && !tagMatch) { 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 || ''; // 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 = ` `; // 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 = ` `; // 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'); if (currentInvoiceElement) { if (warranty.invoice_path && warranty.invoice_path !== 'null') { currentInvoiceElement.innerHTML = ` Current invoice: View (Upload a new file to replace) `; } else { currentInvoiceElement.innerHTML = 'No invoice uploaded'; } } // Show current manual if exists const currentManualElement = document.getElementById('currentManual'); if (currentManualElement) { if (warranty.manual_path && warranty.manual_path !== 'null') { currentManualElement.innerHTML = ` Current manual: View (Upload a new file to replace) `; } else { currentManualElement.innerHTML = 'No manual uploaded'; } } // 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); } }); }); } 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(); // 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)); } // 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'); // Reset form and reload warranties resetForm(); // Reset selected tags selectedTags = []; if (selectedTagsContainer) { selectedTagsContainer.innerHTML = ''; } loadWarranties(); }) .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 initWarrantyForm(); // Load warranties loadWarranties(); // Initialize theme initializeTheme(); // Set up event listeners for other UI controls setupUIEventListeners(); }); // 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 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 = ` Create "${searchTerm}"`; createOption.addEventListener('click', () => { createTag(searchTerm); 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 = ` ${tag.name} ${isSelected ? '' : ''} `; 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'); } // 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} × `; // 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); }); } // Create a new tag function createTag(name) { const token = localStorage.getItem('auth_token'); if (!token) { console.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) { throw new Error('Failed to create tag'); } return response.json(); }) .then(data => { // Add new tag to allTags array allTags.push({ id: data.id, name: data.name, color: data.color }); // Select the new tag selectedTags.push({ id: data.id, name: data.name, color: data.color }); // Clear search and rerender if (tagSearch) tagSearch.value = ''; renderSelectedTags(); showToast('Tag created successfully', 'success'); }) .catch(error => { console.error('Error creating tag:', error); showToast(error.message || 'Failed to create tag', '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 = '
No tags created yet
'; return; } allTags.forEach(tag => { const tagElement = document.createElement('div'); tagElement.className = 'existing-tag'; tagElement.innerHTML = `
${tag.name}
`; // Add event listeners for edit and delete tagElement.querySelector('.edit-tag').addEventListener('click', () => { editTag(tag); }); tagElement.querySelector('.delete-tag').addEventListener('click', () => { deleteTag(tag.id); }); existingTagsContainer.appendChild(tagElement); }); } // Edit a tag function editTag(tag) { const tagInfoElement = document.querySelector(`.existing-tag .existing-tag-info:has(+ .existing-tag-actions button[data-id="${tag.id}"])`); if (!tagInfoElement) { // Alternative selector for browsers that don't support :has const tagElement = document.querySelector(`.existing-tag`); const buttons = tagElement?.querySelectorAll(`.existing-tag-actions button[data-id="${tag.id}"]`); if (buttons?.length > 0) { const parent = buttons[0].closest('.existing-tag'); if (parent) { const infoElement = parent.querySelector('.existing-tag-info'); if (infoElement) { tagInfoElement = infoElement; } } } if (!tagInfoElement) return; } const originalHTML = tagInfoElement.innerHTML; tagInfoElement.innerHTML = ` `; // Add event listeners tagInfoElement.querySelector('.save-edit').addEventListener('click', () => { const newName = tagInfoElement.querySelector('.edit-tag-name').value.trim(); const newColor = tagInfoElement.querySelector('.edit-tag-color').value; if (!newName) { showToast('Tag name is required', 'error'); return; } updateTag(tag.id, newName, newColor); }); tagInfoElement.querySelector('.cancel-edit').addEventListener('click', () => { // Restore original HTML tagInfoElement.innerHTML = originalHTML; }); } // Update a tag function updateTag(id, name, color) { const token = localStorage.getItem('auth_token'); if (!token) { console.error('No authentication token found'); return; } fetch(`/api/tags/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ name: name, color: color }) }) .then(response => { if (!response.ok) { if (response.status === 409) { throw new Error('A tag with this name already exists'); } throw new Error('Failed to update tag'); } return response.json(); }) .then(data => { // Update tag in allTags array const index = allTags.findIndex(tag => tag.id === id); if (index !== -1) { allTags[index].name = name; allTags[index].color = color; } // Update tag in selectedTags if present const selectedIndex = selectedTags.findIndex(tag => tag.id === id); if (selectedIndex !== -1) { selectedTags[selectedIndex].name = name; selectedTags[selectedIndex].color = color; } // Rerender existing tags and selected tags renderExistingTags(); renderSelectedTags(); // Update summary if needed if (document.getElementById('summary-tags')) { updateSummary(); } showToast('Tag updated successfully', 'success'); }) .catch(error => { console.error('Error updating tag:', error); showToast(error.message || 'Failed to update tag', 'error'); }); } // Delete a tag function deleteTag(id) { if (!confirm('Are you sure you want to delete this tag? It will be removed from all warranties.')) { return; } const token = localStorage.getItem('auth_token'); if (!token) { console.error('No authentication token found'); return; } fetch(`/api/tags/${id}`, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } }) .then(response => { if (!response.ok) { throw new Error('Failed to delete tag'); } return response.json(); }) .then(data => { // Remove tag from allTags array allTags = allTags.filter(tag => tag.id !== id); // Remove tag from selectedTags if present selectedTags = selectedTags.filter(tag => tag.id !== id); // Rerender existing tags and selected tags renderExistingTags(); renderSelectedTags(); // Update summary if needed if (document.getElementById('summary-tags')) { updateSummary(); } showToast('Tag deleted successfully', 'success'); }) .catch(error => { console.error('Error deleting tag:', error); showToast('Failed to delete tag', 'error'); }); } // Set up event listeners for UI controls function setupUIEventListeners() { // Initialize settings button const settingsBtn = document.querySelector('.settings-btn'); const settingsMenu = document.querySelector('.settings-menu'); if (settingsBtn && settingsMenu) { settingsBtn.addEventListener('click', (e) => { e.stopPropagation(); settingsMenu.classList.toggle('active'); }); // Close settings menu when clicking outside document.addEventListener('click', (e) => { if (settingsMenu.classList.contains('active') && !settingsMenu.contains(e.target) && !settingsBtn.contains(e.target)) { settingsMenu.classList.remove('active'); } }); } // 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) => { if (e.target === element) { closeModals(); } }); }); // 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); // 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(); } // 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(); loadWarranties(); }) .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; } const productName = document.getElementById('editProductName').value.trim(); const purchaseDate = document.getElementById('editPurchaseDate').value; const warrantyYears = document.getElementById('editWarrantyYears').value; // Basic validation if (!productName) { showToast('Product name is required', 'error'); return; } if (!purchaseDate) { showToast('Purchase date is required', 'error'); return; } if (!warrantyYears || warrantyYears <= 0) { showToast('Warranty period must be greater than 0', 'error'); return; } // Create form data const formData = new FormData(); formData.append('product_name', productName); formData.append('purchase_date', purchaseDate); formData.append('warranty_years', warrantyYears); // 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); } // 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(); loadWarranties(); }) .catch(error => { hideLoadingSpinner(); console.error('Error updating warranty:', error); showToast(error.message || 'Failed to update warranty', 'error'); }); } // Render the tags dropdown list for edit mode 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 = ` Create "${searchTerm}"`; createOption.addEventListener('click', () => { createTag(searchTerm); 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 = ` ${tag.name} ${isSelected ? '' : ''} `; 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'); } // 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); }); } // 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} × `; // 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); } }