// ===== MOBILE HAMBURGER MENU (kept for other pages) =====
(function() {
function buildMobileMenuContent(panel, header) {
if (!panel || !header) return;
// Build only once
if (panel.getAttribute('data-built') === 'true') return;
panel.innerHTML = '';
const createSection = (titleText) => {
const title = document.createElement('div');
title.className = 'section-title';
title.textContent = titleText;
panel.appendChild(title);
};
const appendLinks = (links) => {
if (!links || links.length === 0) return;
const list = document.createElement('div');
list.className = 'menu-list';
links.forEach(a => {
try {
const cloned = a.cloneNode(true);
// Remove ids to avoid duplicates
if (cloned.id) cloned.id = '';
list.appendChild(cloned);
} catch (e) {
// Skip problematic nodes
}
});
panel.appendChild(list);
};
// Resolve auth state and display name
const isAuthenticated = !!(function(){ try { return localStorage.getItem('auth_token'); } catch(_) { return null; } })();
let displayName = '';
const headerDisplayNameEl = header.querySelector('#userDisplayName');
if (headerDisplayNameEl && headerDisplayNameEl.textContent) {
displayName = headerDisplayNameEl.textContent.trim();
}
if (!displayName) {
try {
const userInfo = JSON.parse(localStorage.getItem('user_info') || 'null');
if (userInfo) displayName = userInfo.first_name || userInfo.username || 'Account';
} catch(_) {}
}
if (!displayName) displayName = 'Account';
// Nav links
const navLinksContainer = header.querySelector('.nav-links');
const navAnchors = navLinksContainer ? Array.from(navLinksContainer.querySelectorAll('a')) : [];
if (navAnchors.length) {
createSection('Navigation');
appendLinks(navAnchors);
}
// Auth buttons if present (only when NOT authenticated)
const authContainer = header.querySelector('#authContainer');
const authAnchors = (!isAuthenticated && authContainer) ? Array.from(authContainer.querySelectorAll('a')) : [];
if (!isAuthenticated && authAnchors.length) {
createSection('Account');
appendLinks(authAnchors);
}
// User menu dropdown items if present
const userDropdown = header.querySelector('#userMenuDropdown');
const userMenuAnchors = userDropdown ? Array.from(userDropdown.querySelectorAll('a')) : [];
if (userMenuAnchors.length) {
// Use username as section title when available
createSection(displayName || 'Menu');
appendLinks(userMenuAnchors);
// Add Logout if available
const logoutSource = header.querySelector('#logoutMenuItem');
if (logoutSource) {
const logoutLink = document.createElement('a');
logoutLink.href = '#';
logoutLink.id = 'mobileLogoutLink';
logoutLink.innerHTML = ' Logout ';
logoutLink.addEventListener('click', async function(e) {
e.preventDefault();
try {
if (window.auth && typeof window.auth.logout === 'function') {
await window.auth.logout();
} else {
localStorage.removeItem('auth_token');
localStorage.removeItem('user_info');
window.location.href = 'login.html';
}
} catch (_) {
localStorage.removeItem('auth_token');
localStorage.removeItem('user_info');
window.location.href = 'login.html';
}
});
const list = document.createElement('div');
list.className = 'menu-list';
list.appendChild(logoutLink);
panel.appendChild(list);
}
}
panel.setAttribute('data-built', 'true');
}
function initializeMobileMenu() {
const header = document.querySelector('header');
if (!header) return;
const toggleBtn = header.querySelector('.mobile-menu-toggle');
const panel = document.getElementById('mobileMenuPanel');
const overlay = document.getElementById('mobileMenuOverlay');
if (!toggleBtn || !panel || !overlay) return;
// Prevent double-binding
if (toggleBtn.getAttribute('data-mm-bound') === '1') return;
toggleBtn.setAttribute('data-mm-bound', '1');
const openMenu = () => {
buildMobileMenuContent(panel, header);
panel.classList.add('is-open');
overlay.classList.add('is-open');
document.body.classList.add('mobile-menu-active');
};
const closeMenu = () => {
panel.classList.remove('is-open');
overlay.classList.remove('is-open');
document.body.classList.remove('mobile-menu-active');
};
toggleBtn.addEventListener('click', function(e) {
e.preventDefault();
const isOpen = panel.classList.contains('is-open');
if (isOpen) closeMenu(); else openMenu();
});
overlay.addEventListener('click', function() {
closeMenu();
});
panel.addEventListener('click', function(e) {
const link = e.target.closest('a');
if (link && link.href) {
closeMenu();
}
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && panel.classList.contains('is-open')) {
closeMenu();
}
});
}
// Expose and initialize
window.initializeMobileMenu = initializeMobileMenu;
document.addEventListener('DOMContentLoaded', initializeMobileMenu);
})();
console.log('[SCRIPT VERSION] 20250529-005 - Added CSS cache busting for consistent styling across domains');
console.log('[DEBUG] script.js loaded and running');
// alert('script.js loaded!'); // Remove alert after confirming script loads
// Global variables
let warranties = [];
let warrantiesLoaded = false; // Track if warranties have been loaded from API
let lastLoadedArchived = false; // Track if the current warranties array came from archived endpoint
let lastLoadedIncludesArchived = false; // Track if the current warranties list includes archived items
let currentTabIndex = 0;
let tabContents = []; // Initialize as empty array
let editMode = false;
let currentWarrantyId = null;
let userPreferencePrefix = null; // <<< ADDED GLOBAL PREFIX VARIABLE
let isGlobalView = false; // Track if admin is viewing all users' warranties
let currentFilters = {
status: 'all',
tag: 'all',
search: '',
sortBy: 'expiration',
vendor: 'all', // Added vendor filter
warranty_type: 'all' // Added warranty type filter
};
// 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 vendorFilter = document.getElementById('vendorFilter'); // Added vendor filter select
const warrantyTypeFilter = document.getElementById('warrantyTypeFilter'); // Added warranty type filter select
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');
// Admin view controls
const adminViewSwitcher = document.getElementById('adminViewSwitcher');
const personalViewBtn = document.getElementById('personalViewBtn');
const globalViewBtn = document.getElementById('globalViewBtn');
const warrantiesPanelTitle = document.getElementById('warrantiesPanelTitle');
const fileInput = document.getElementById('invoice');
const fileName = document.getElementById('fileName');
const manualInput = document.getElementById('manual');
const manualFileName = document.getElementById('manualFileName');
const otherDocumentInput = document.getElementById('otherDocument');
const otherDocumentFileName = document.getElementById('otherDocumentFileName');
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');
// Claims modal elements
const claimsModal = document.getElementById('claimsModal');
const claimFormModal = document.getElementById('claimFormModal');
const claimsListBody = document.getElementById('claimsListBody');
const addClaimBtn = document.getElementById('addClaimBtn');
const claimForm = document.getElementById('claimForm');
const saveClaimBtn = document.getElementById('saveClaimBtn');
const editClaimId = document.getElementById('editClaimId');
const claimFormTitle = document.getElementById('claimFormTitle');
const warrantyClaimInfo = document.getElementById('warrantyClaimInfo');
// --- Add near other DOM Element declarations ---
const isLifetimeCheckbox = document.getElementById('isLifetime');
const warrantyDurationFields = document.getElementById('warrantyDurationFields'); // New container
const warrantyDurationYearsInput = document.getElementById('warrantyDurationYears');
const warrantyDurationMonthsInput = document.getElementById('warrantyDurationMonths');
const warrantyDurationDaysInput = document.getElementById('warrantyDurationDays');
const editIsLifetimeCheckbox = document.getElementById('editIsLifetime');
const editWarrantyDurationFields = document.getElementById('editWarrantyDurationFields'); // New container
const editWarrantyDurationYearsInput = document.getElementById('editWarrantyDurationYears');
const editWarrantyDurationMonthsInput = document.getElementById('editWarrantyDurationMonths');
const editWarrantyDurationDaysInput = document.getElementById('editWarrantyDurationDays');
// Warranty Type DOM Elements
const warrantyTypeInput = document.getElementById('warrantyType');
const warrantyTypeCustomInput = document.getElementById('warrantyTypeCustom');
const editWarrantyTypeInput = document.getElementById('editWarrantyType');
const editWarrantyTypeCustomInput = document.getElementById('editWarrantyTypeCustom');
// Warranty method selection elements
const durationMethodRadio = document.getElementById('durationMethod');
const exactDateMethodRadio = document.getElementById('exactDateMethod');
const exactExpirationField = document.getElementById('exactExpirationField');
const exactExpirationDateInput = document.getElementById('exactExpirationDate');
const editDurationMethodRadio = document.getElementById('editDurationMethod');
const editExactDateMethodRadio = document.getElementById('editExactDateMethod');
const editExactExpirationField = document.getElementById('editExactExpirationField');
const editExactExpirationDateInput = document.getElementById('editExactExpirationDate');
// Add near other DOM Element declarations
const showAddWarrantyBtn = document.getElementById('showAddWarrantyBtn');
const addWarrantyModal = document.getElementById('addWarrantyModal');
// Currency dropdown elements
const currencySelect = document.getElementById('currency');
const editCurrencySelect = document.getElementById('editCurrency');
const serialNumbersContainer = document.getElementById('serialNumbersContainer'); // Ensure this is defined
/**
* 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
}
}
/**
* Initialize view controls for all authenticated users
*/
async function initViewControls() {
// Check if global view is enabled
try {
const response = await fetch('/api/settings/global-view-status', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + (window.auth ? window.auth.getToken() : localStorage.getItem('auth_token')),
'Content-Type': 'application/json'
}
});
if (response.ok) {
const result = await response.json();
const scopeDropdown = document.getElementById('scopeDropdown');
// Keep bottom admin switcher hidden; control only the header scope dropdown
if (adminViewSwitcher) adminViewSwitcher.style.display = 'none';
if (result.enabled) {
if (scopeDropdown) scopeDropdown.style.display = 'block';
// Apply saved scope (update title and state)
const savedScope = loadViewScopePreference();
if (savedScope === 'global') {
isGlobalView = true;
updateWarrantiesPanelTitle(true);
} else {
isGlobalView = false;
updateWarrantiesPanelTitle(false);
}
} else {
if (scopeDropdown) scopeDropdown.style.display = 'none';
if (isGlobalView) {
isGlobalView = false;
updateWarrantiesPanelTitle(false);
await loadWarranties(true);
applyFilters();
}
}
} else {
console.error('Failed to check global view status');
// Hide bottom switcher even on failure; show header dropdown for fallback
if (adminViewSwitcher) adminViewSwitcher.style.display = 'none';
const scopeDropdown = document.getElementById('scopeDropdown');
if (scopeDropdown) scopeDropdown.style.display = 'block';
}
} catch (error) {
console.error('Error checking global view status:', error);
// Keep bottom switcher hidden; show header dropdown as fallback
if (adminViewSwitcher) adminViewSwitcher.style.display = 'none';
const scopeDropdown = document.getElementById('scopeDropdown');
if (scopeDropdown) scopeDropdown.style.display = 'block';
}
}
/**
* Switch to personal view (user's own warranties)
*/
async function switchToPersonalView() {
if (!personalViewBtn || !globalViewBtn) return;
isGlobalView = false;
personalViewBtn.classList.add('active');
globalViewBtn.classList.remove('active');
// Save view preference
saveViewScopePreference('personal');
// Update title
updateWarrantiesPanelTitle(false);
// Update header scope label
try {
const currentScopeIcon = document.getElementById('currentScopeIcon');
if (currentScopeIcon) {
currentScopeIcon.className = 'fas fa-user';
currentScopeIcon.setAttribute('aria-label', 'Personal');
}
} catch (e) { /* no-op */ }
// Reload warranties
try {
const token = window.auth.getToken();
if (token) {
await loadWarranties(true);
applyFilters();
}
} catch (error) {
console.error('Error switching to personal view:', error);
showToast(window.t('messages.error_loading_personal_warranties'), 'error');
}
}
/**
* Switch to global view (all users' warranties) - available to all users
*/
async function switchToGlobalView() {
if (!personalViewBtn || !globalViewBtn) return;
// Check if global view is still enabled
try {
const response = await fetch('/api/settings/global-view-status', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + (window.auth ? window.auth.getToken() : localStorage.getItem('auth_token')),
'Content-Type': 'application/json'
}
});
if (response.ok) {
const result = await response.json();
if (!result.enabled) {
showToast(window.t('messages.global_view_disabled'), 'error');
return;
}
}
} catch (error) {
console.error('Error checking global view status:', error);
}
isGlobalView = true;
personalViewBtn.classList.remove('active');
globalViewBtn.classList.add('active');
// Save view preference
saveViewScopePreference('global');
// Update title
updateWarrantiesPanelTitle(true);
// Update header scope label
try {
const currentScopeIcon = document.getElementById('currentScopeIcon');
if (currentScopeIcon) {
currentScopeIcon.className = 'fas fa-globe';
currentScopeIcon.setAttribute('aria-label', 'Global');
}
} catch (e) { /* no-op */ }
// Reload warranties
try {
const token = window.auth.getToken();
if (token) {
await loadWarranties(true);
applyFilters();
}
} catch (error) {
console.error('Error switching to global view:', error);
showToast(window.t('messages.error_loading_global_warranties'), 'error');
}
}
/**
* Update warranties panel title with proper translation
* @param {boolean} isGlobal - Whether to show global or personal title
*/
function updateWarrantiesPanelTitle(isGlobal = false) {
if (warrantiesPanelTitle) {
if (window.i18next && window.i18next.t) {
warrantiesPanelTitle.textContent = isGlobal ?
window.i18next.t('warranties.title_global') :
window.i18next.t('warranties.title');
} else {
warrantiesPanelTitle.textContent = isGlobal ? 'All Users\' Warranties' : 'Your Warranties';
}
}
}
/**
* 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_';
}
/**
* Save view scope preference to localStorage
* @param {string} scope - 'personal' or 'global'
*/
function saveViewScopePreference(scope) {
try {
const prefix = getPreferenceKeyPrefix();
localStorage.setItem(`${prefix}viewScope`, scope);
console.log(`Saved view scope preference: ${scope} with prefix: ${prefix}`);
} catch (error) {
console.error('Error saving view scope preference:', error);
}
}
/**
* Load view scope preference from localStorage
* @returns {string} The saved preference ('personal', 'global', or 'personal' as default)
*/
function loadViewScopePreference() {
try {
const prefix = getPreferenceKeyPrefix();
const savedScope = localStorage.getItem(`${prefix}viewScope`);
console.log(`Loaded view scope preference: ${savedScope} with prefix: ${prefix}`);
return savedScope || 'personal'; // Default to personal view
} catch (error) {
console.error('Error loading view scope preference:', error);
return 'personal'; // Default to personal view on error
}
}
// 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;
}
}
// Persist theme to API similar to view/filters
async function saveThemePreference(isDark, saveToApi = true) {
try {
// Always persist locally first
setTheme(isDark);
if (saveToApi && window.auth && window.auth.isAuthenticated && window.auth.isAuthenticated()) {
const token = window.auth.getToken();
if (token) {
try {
console.log('[Theme] Saving theme preference to API:', isDark ? 'dark' : 'light');
const response = await fetch('/api/auth/preferences', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ theme: isDark ? 'dark' : 'light' })
});
if (!response.ok) {
console.warn('[Theme] Failed to save theme to API:', response.status);
}
} catch (err) {
console.error('[Theme] Error saving theme to API:', err);
}
}
}
} catch (e) {
console.warn('Failed to save theme preference', e);
}
}
// Initialization logic on DOMContentLoaded
document.addEventListener('DOMContentLoaded', function() {
// Register Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('./sw.js')
.then(registration => {
console.log('Service Worker registered with scope:', registration.scope);
})
.catch(error => {
console.error('Service Worker registration failed:', error);
});
});
}
// --- Search button click triggers search ---
const searchBtn = document.getElementById('searchBtn');
const searchInput = document.getElementById('searchWarranties');
if (searchBtn && searchInput) {
searchBtn.addEventListener('click', function(e) {
e.preventDefault();
currentFilters.search = searchInput.value.toLowerCase();
applyFilters();
});
}
// --- Ensure globalManageTagsBtn triggers modal and tag form is always initialized ---
// (Redundant with setupUIEventListeners, but ensures modal is always ready)
const globalManageTagsBtn = document.getElementById('globalManageTagsBtn');
if (globalManageTagsBtn) {
globalManageTagsBtn.addEventListener('click', async () => {
if (!allTags || allTags.length === 0) {
showLoadingSpinner();
try {
await loadTags();
} catch (error) {
console.error("Failed to load tags before opening modal:", error);
showToast(window.t('messages.could_not_load_tags'), "error");
hideLoadingSpinner();
return;
}
hideLoadingSpinner();
}
openTagManagementModal();
});
}
console.log('[DEBUG] Registering authStateReady event handler');
// ... other initialization ...
// --- BEGIN REFACTORED TAG MODAL AND MAIN FORM TAG UI SETUP ---
const globalTagManagementModal = document.getElementById('tagManagementModal');
const globalNewTagForm = document.getElementById('newTagForm'); // Inside tagManagementModal
const mainTagSearchInput = document.getElementById('tagSearch'); // For the main "Add Warranty" form's tag search
const warrantyFormElement = document.getElementById('warrantyForm'); // The main add warranty form
// 1. ALWAYS Initialize listeners for the global Tag Management Modal IF IT EXISTS
if (globalTagManagementModal) {
if (globalNewTagForm) {
// Ensure the event listener is attached only once, or manage it if DOMContentLoaded could fire multiple times (not typical)
// For simplicity, assuming DOMContentLoaded runs once per page load.
globalNewTagForm.addEventListener('submit', (e) => {
e.preventDefault();
// Inline implementation for creating a new tag from the modal
const tagNameInput = document.getElementById('newTagName');
const tagColorInput = document.getElementById('newTagColor');
const name = tagNameInput ? tagNameInput.value.trim() : '';
const color = tagColorInput ? tagColorInput.value : '#808080';
if (!name) {
showToast(window.t('messages.tag_name_required'), 'error');
return;
}
// Use the existing createTag function if available
if (typeof createTag === 'function') {
createTag(name, color)
.then(() => {
if (tagNameInput) tagNameInput.value = '';
if (tagColorInput) tagColorInput.value = '#808080';
renderExistingTags && renderExistingTags();
})
.catch((err) => {
showToast((err && err.message) || window.t('messages.failed_to_create_tag'), 'error');
});
} else {
showToast(window.t('messages.tag_creation_function_not_found'), 'error');
}
});
}
const closeButtons = globalTagManagementModal.querySelectorAll('[data-dismiss="modal"]');
closeButtons.forEach(button => {
button.addEventListener('click', (event) => {
globalTagManagementModal.style.display = 'none';
event.stopPropagation(); // This was the fix from before, ensuring it's applied
});
});
console.log('Global Tag Management Modal listeners initialized directly in DOMContentLoaded.');
}
// 2. Initialize Tag functionality FOR THE MAIN ADD WARRANTY FORM (if its specific tag search input exists)
// initTagFunctionality is now refactored to be specific to the main form's tag UI.
if (mainTagSearchInput) {
initTagFunctionality(); // Sets up main form tag search, its manage button, etc.
// Also calls loadTags() if needed for the main form.
}
// --- END REFACTORED TAG MODAL AND MAIN FORM TAG UI SETUP ---
// Setup form submission (assuming addWarrantyForm exists - this is 'warrantyFormElement')
// const form = document.getElementById('addWarrantyForm'); // Old selector
if (warrantyFormElement) { // Use the variable defined above
warrantyFormElement.addEventListener('submit', handleFormSubmit);
// Initialize form tabs if the form exists
// initFormTabs(); // This should be called when the ADD MODAL is SHOWN, not globally here.
// It's correctly in setupModalTriggers for the addWarrantyModal.
}
// Initialize theme toggle state *after* DOM is loaded
// ... (theme toggle init logic remains) ...
// Setup view switcher (assuming view switcher elements exist)
if (document.getElementById('gridViewBtn')) {
// setupViewSwitcher(); // Removed undefined function
loadViewPreference(); // This is fine here, loads initial view preference.
}
// Setup filter controls (assuming filter controls exist)
if (document.getElementById('filterControls')) {
// setupFilterControls(); // Removed: function not defined
// populateTagFilter(); // This should be called AFTER warranties (and their tags) are loaded.
// It's called in loadWarranties -> processAllWarranties or similar flow.
}
// Initialize modal interactions (general modal triggers like close buttons, backdrop)
setupModalTriggers(); // This sets up general modal behaviors and specific triggers for add/edit.
// Initialize form-specific lifetime checkbox handler FOR THE MAIN ADD FORM
const lifetimeCheckbox = document.getElementById('isLifetime'); // Main form's checkbox
if (lifetimeCheckbox) {
lifetimeCheckbox.addEventListener('change', handleLifetimeChange);
handleLifetimeChange({ target: lifetimeCheckbox }); // Initial check
}
// Initialize warranty method selection handlers
if (durationMethodRadio && exactDateMethodRadio) {
durationMethodRadio.addEventListener('change', handleWarrantyMethodChange);
exactDateMethodRadio.addEventListener('change', handleWarrantyMethodChange);
handleWarrantyMethodChange(); // Initial setup
}
if (editDurationMethodRadio && editExactDateMethodRadio) {
editDurationMethodRadio.addEventListener('change', handleEditWarrantyMethodChange);
editExactDateMethodRadio.addEventListener('change', handleEditWarrantyMethodChange);
handleEditWarrantyMethodChange(); // Initial setup for edit form
}
// --- LOAD WARRANTIES AFTER AUTH ---
let authStateHandled = false;
async function runAuthenticatedTasks(isAuthenticated) { // Added isAuthenticated parameter
if (!isAuthenticated) {
console.log('[DEBUG] runAuthenticatedTasks: Called with isAuthenticated = false. Not running tasks yet.');
// Do not set authStateHandled = true here, allow a subsequent call with true.
return;
}
// If we reach here, isAuthenticated is true.
if (authStateHandled) {
console.log('[DEBUG] runAuthenticatedTasks: Tasks already handled (or in progress by another call).');
return;
}
authStateHandled = true; // Set flag only when tasks are actually starting with isAuthenticated = true.
console.log('[DEBUG] runAuthenticatedTasks: Executing with isAuthenticated = true.');
// Set prefix
userPreferencePrefix = getPreferenceKeyPrefix();
console.log(`[runAuthenticatedTasks] Determined and stored global prefix: ${userPreferencePrefix}`);
// Re-check auth status just before critical data loads
const currentAuthStatus = window.auth && window.auth.isAuthenticated();
console.log(`[runAuthenticatedTasks] Current auth status before loading prefs/warranties: ${currentAuthStatus}`);
if (currentAuthStatus) {
await loadAndApplyUserPreferences(true); // Pass true, as we've confirmed auth
await loadTags(); // Ensure all available tags are loaded
await loadCurrencies(); // Load currencies for dropdowns
// Initialize Paperless-ngx integration
await initPaperlessNgxIntegration();
// Initialize view controls for all users
initViewControls();
if (document.getElementById('warrantiesList')) {
console.log("[runAuthenticatedTasks] Loading warranty data...");
await loadWarranties(true); // Pass true
console.log('[DEBUG] After loadWarranties, warranties array:', warranties);
// After warranties are loaded and filter dropdowns populated, load filter/sort prefs and apply
loadFilterAndSortPreferences();
// Reflect saved filters in UI selects if present
const statusFilterEl = document.getElementById('statusFilter');
const tagFilterEl = document.getElementById('tagFilter');
const vendorFilterEl = document.getElementById('vendorFilter');
const warrantyTypeFilterEl = document.getElementById('warrantyTypeFilter');
const sortByEl = document.getElementById('sortBy');
if (statusFilterEl && currentFilters.status) statusFilterEl.value = currentFilters.status;
if (tagFilterEl && currentFilters.tag) tagFilterEl.value = currentFilters.tag;
if (vendorFilterEl && currentFilters.vendor) vendorFilterEl.value = currentFilters.vendor;
if (warrantyTypeFilterEl && currentFilters.warranty_type) warrantyTypeFilterEl.value = currentFilters.warranty_type;
if (sortByEl && currentFilters.sortBy) sortByEl.value = currentFilters.sortBy;
// Update the filter indicator if available
if (typeof window.updateFilterIndicator === 'function') {
window.updateFilterIndicator();
}
} else {
console.log("[runAuthenticatedTasks] Warranties list element not found.");
}
} else {
console.warn("[runAuthenticatedTasks] Auth status became false before loading data. Aborting data load.");
// Optionally, reset authStateHandled if we want to allow another attempt
// authStateHandled = false;
}
// Now that data and preferences are ready, apply view/currency and render via applyFilters
console.log("[runAuthenticatedTasks] 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();
}
}
// Listener for the 'authStateReady' event
window.addEventListener('authStateReady', async function handleAuthEvent(event) {
console.log('[DEBUG] authStateReady event received in script.js. Detail:', event.detail);
// Pass the isAuthenticated status from the event detail to runAuthenticatedTasks
await runAuthenticatedTasks(event.detail && event.detail.isAuthenticated);
}); // Removed { once: true } to allow re-evaluation if auth state changes
// Proactive check after a brief delay to allow auth.js to initialize
setTimeout(async () => {
console.log('[DEBUG] Proactive auth check in script.js (after timeout).');
if (window.auth) {
// Pass the current authentication status to runAuthenticatedTasks
await runAuthenticatedTasks(window.auth.isAuthenticated());
} else {
console.log('[DEBUG] Proactive check: window.auth not available. Event listener should handle it.');
// Call with false if auth module isn't ready, to avoid tasks running prematurely.
await runAuthenticatedTasks(false);
}
}, 500); // Delay
// --- 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';
async function toggleArchiveStatus(warrantyId, shouldArchive) {
try {
const token = window.auth.getToken();
if (!token) throw new Error('No auth token');
const response = await fetch(`/api/warranties/${warrantyId}/archive`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ archived: !!shouldArchive })
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.error || `HTTP ${response.status}`);
}
// Reload appropriate list
// Always reload current view to ensure accurate list source
await loadWarranties(true);
showToast(shouldArchive ? (window.t ? window.t('messages.archived_success') : 'Archived') : (window.t ? window.t('messages.unarchived_success') : 'Unarchived'), 'success');
} catch (e) {
console.error('Failed to toggle archive status', e);
showToast(window.t ? window.t('messages.error_updating_archive_status') : 'Failed to update archive status', 'error');
}
}
// Utility function to escape HTML
function escapeHtml(text) {
if (typeof text !== 'string') return text;
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
// 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(window.t('messages.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) {
// Append duration components instead of warranty_years
formData.append('warranty_duration_years', notesModalWarrantyObj.warranty_duration_years || 0);
formData.append('warranty_duration_months', notesModalWarrantyObj.warranty_duration_months || 0);
formData.append('warranty_duration_days', notesModalWarrantyObj.warranty_duration_days || 0);
// If all duration fields are 0 but we have an expiration date, this was created with exact date method
const isExactDateWarranty = (notesModalWarrantyObj.warranty_duration_years || 0) === 0 &&
(notesModalWarrantyObj.warranty_duration_months || 0) === 0 &&
(notesModalWarrantyObj.warranty_duration_days || 0) === 0 &&
notesModalWarrantyObj.expiration_date;
if (isExactDateWarranty) {
// For exact date warranties, send the expiration date as exact_expiration_date
formData.append('exact_expiration_date', notesModalWarrantyObj.expiration_date.split('T')[0]);
}
}
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.vendor) {
formData.append('vendor', notesModalWarrantyObj.vendor);
}
if (notesModalWarrantyObj.warranty_type) {
formData.append('warranty_type', notesModalWarrantyObj.warranty_type);
}
if (typeof notesModalWarrantyObj.model_number !== 'undefined' && notesModalWarrantyObj.model_number !== null) {
formData.append('model_number', notesModalWarrantyObj.model_number);
}
if (notesModalWarrantyObj.serial_numbers && Array.isArray(notesModalWarrantyObj.serial_numbers)) {
notesModalWarrantyObj.serial_numbers.forEach(sn => {
if (sn && sn.trim() !== '') {
formData.append('serial_numbers[]', sn); // Use [] for arrays
}
});
} else if (!formData.has('serial_numbers[]')) {
// Send empty array if none exist
// formData.append('serial_numbers[]', ''); // Sending empty string might not work as expected, better to not send if empty
}
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);
// Also include model number value from edit input if present on DOM
const editModelNumberInput = document.getElementById('editModelNumber');
if (editModelNumberInput && editModelNumberInput.value.trim() !== '') {
formData.set('model_number', editModelNumberInput.value.trim());
}
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(window.t('messages.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 || window.t('messages.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 controls = tabContent.querySelectorAll('input, textarea, select');
let isTabValid = true;
controls.forEach(control => {
// Clear previous validation state
control.classList.remove('invalid');
let validationMessageElement = control.nextElementSibling;
if (validationMessageElement && validationMessageElement.classList.contains('validation-message')) {
validationMessageElement.remove();
}
// Manual validation for required fields
if (control.hasAttribute('required') && control.value.trim() === '') {
isTabValid = false;
control.classList.add('invalid');
// Mark as invalid, message will be added by showValidationErrors
} else if (!control.validity.valid) { // For other HTML5 validation issues (e.g., type mismatch)
isTabValid = false;
control.classList.add('invalid');
}
});
return isTabValid;
}
// Show validation errors for a specific tab
function showValidationErrors(tabIndex) {
const tabContent = tabContents[tabIndex];
const controls = tabContent.querySelectorAll('input, textarea, select');
let firstInvalidControl = null;
let validationToast = document.querySelector('.validation-toast'); // Check for existing validation toast
controls.forEach(control => {
if (!control.validity.valid) {
if (!firstInvalidControl) firstInvalidControl = control;
control.classList.add('invalid');
// Add or update validation message
let validationMessageElement = control.nextElementSibling;
if (!validationMessageElement || !validationMessageElement.classList.contains('validation-message')) {
validationMessageElement = document.createElement('div');
validationMessageElement.className = 'validation-message';
control.parentNode.insertBefore(validationMessageElement, control.nextSibling);
}
if (control.hasAttribute('required') && control.value.trim() === '') {
validationMessageElement.textContent = window.i18next ? window.i18next.t('messages.please_fill_out_this_field') : 'Please fill out this field.';
} else {
validationMessageElement.textContent = control.validationMessage || (window.i18next ? window.i18next.t('messages.field_is_invalid') : 'This field is invalid.');
}
} else {
// Ensure invalid class is removed if somehow missed by validateTab (shouldn't happen)
control.classList.remove('invalid');
// Remove validation message if control is now valid
let validationMessageElement = control.nextElementSibling;
if (validationMessageElement && validationMessageElement.classList.contains('validation-message')) {
validationMessageElement.remove();
}
}
});
// The browser will attempt to focus the first invalid field when form submission is prevented.
// Switching to the tab containing the error (done by handleFormSubmit) is key.
// Manage a single validation toast
if (!validationToast) {
validationToast = showToast(window.t('messages.correct_errors_in_tab'), 'error', 0); // 0 duration = persistent
validationToast.classList.add('validation-toast'); // Add a class to identify it
} else {
// Update existing toast message if needed (optional)
validationToast.querySelector('span').textContent = window.t('messages.correct_errors_in_tab');
}
}
// 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 = '
' +
serialNumbers.map(sn => `${sn} `).join('') +
' ';
} 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 summaryWarrantyDuration = document.getElementById('summary-warranty-duration'); // Use new ID
if (summaryWarrantyDuration) {
if (isLifetime) {
summaryWarrantyDuration.textContent = window.i18next ? window.i18next.t('warranties.lifetime') : 'Lifetime';
} else {
const years = parseInt(warrantyDurationYearsInput?.value || 0);
const months = parseInt(warrantyDurationMonthsInput?.value || 0);
const days = parseInt(warrantyDurationDaysInput?.value || 0);
let durationParts = [];
if (years > 0) {
const yearText = window.i18next ? window.i18next.t('warranties.year', {count: years}) : `year${years !== 1 ? 's' : ''}`;
durationParts.push(`${years} ${yearText}`);
}
if (months > 0) {
const monthText = window.i18next ? window.i18next.t('warranties.month', {count: months}) : `month${months !== 1 ? 's' : ''}`;
durationParts.push(`${months} ${monthText}`);
}
if (days > 0) {
const dayText = window.i18next ? window.i18next.t('warranties.day', {count: days}) : `day${days !== 1 ? 's' : ''}`;
durationParts.push(`${days} ${dayText}`);
}
summaryWarrantyDuration.textContent = durationParts.length > 0 ? durationParts.join(', ') : '-';
}
}
// Warranty type - handle dropdown and custom input
const warrantyTypeSelect = document.getElementById('warrantyType');
const warrantyTypeCustom = document.getElementById('warrantyTypeCustom');
const summaryWarrantyType = document.getElementById('summary-warranty-type');
if (summaryWarrantyType) {
let warrantyTypeText = 'Not specified';
if (warrantyTypeSelect && warrantyTypeSelect.value) {
if (warrantyTypeSelect.value === 'other' && warrantyTypeCustom && warrantyTypeCustom.value.trim()) {
warrantyTypeText = warrantyTypeCustom.value.trim();
} else if (warrantyTypeSelect.value !== 'other') {
warrantyTypeText = warrantyTypeSelect.value;
}
}
summaryWarrantyType.textContent = warrantyTypeText;
}
// Purchase price
const purchasePrice = document.getElementById('purchasePrice')?.value;
const currency = document.getElementById('currency')?.value;
const summaryPurchasePrice = document.getElementById('summary-purchase-price');
if (summaryPurchasePrice) {
if (purchasePrice) {
const symbol = getCurrencySymbol();
const position = getCurrencyPosition();
const amount = parseFloat(purchasePrice).toFixed(2);
summaryPurchasePrice.innerHTML = formatCurrencyHTML(amount, symbol, position);
} else {
summaryPurchasePrice.textContent = 'Not specified';
}
}
// Documents
const productPhotoFile = document.getElementById('productPhoto')?.files[0];
const summaryProductPhoto = document.getElementById('summary-product-photo');
if (summaryProductPhoto) {
summaryProductPhoto.textContent = productPhotoFile ?
productPhotoFile.name : 'No photo selected';
}
const invoiceFile = document.getElementById('invoice')?.files[0];
const invoiceUrlField = document.getElementById('invoiceUrl');
const invoiceUrl = invoiceUrlField ? invoiceUrlField.value : '';
const summaryInvoice = document.getElementById('summary-invoice');
if (summaryInvoice) {
if (invoiceFile) {
summaryInvoice.textContent = invoiceFile.name;
} else if (invoiceUrl) {
summaryInvoice.textContent = 'URL: ' + invoiceUrl;
} else {
summaryInvoice.textContent = 'Not specified';
}
}
const manualFile = document.getElementById('manual')?.files[0];
const manualUrlField = document.getElementById('manualUrl');
const manualUrl = manualUrlField ? manualUrlField.value : '';
const summaryManual = document.getElementById('summary-manual');
if (summaryManual) {
if (manualFile) {
summaryManual.textContent = manualFile.name;
} else if (manualUrl) {
summaryManual.textContent = 'URL: ' + manualUrl;
} else {
summaryManual.textContent = 'Not specified';
}
}
const otherDocumentFile = document.getElementById('otherDocument')?.files[0];
const otherDocumentUrlField = document.getElementById('otherDocumentUrl');
const otherDocumentUrl = otherDocumentUrlField ? otherDocumentUrlField.value : '';
const summaryOtherDocument = document.getElementById('summary-other-document');
if (summaryOtherDocument) {
if (otherDocumentFile) {
summaryOtherDocument.textContent = otherDocumentFile.name;
} else if (otherDocumentUrl) {
summaryOtherDocument.textContent = 'URL: ' + otherDocumentUrl;
} else {
summaryOtherDocument.textContent = 'Not specified';
}
}
// 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
const productPhotoFileName = document.getElementById('productPhotoFileName');
if (productPhotoFileName) productPhotoFileName.textContent = '';
fileName.textContent = '';
manualFileName.textContent = '';
if (otherDocumentFileName) otherDocumentFileName.textContent = '';
// Reset photo preview
const productPhotoPreview = document.getElementById('productPhotoPreview');
if (productPhotoPreview) {
productPhotoPreview.style.display = 'none';
}
}
async function exportWarranties() {
console.log('[EXPORT DEBUG] Starting export process');
console.log('[EXPORT DEBUG] Total warranties in memory:', warranties.length);
console.log('[EXPORT DEBUG] Current filters:', currentFilters);
// Get filtered warranties
let warrantiesToExport = [...warranties];
console.log('[EXPORT DEBUG] Initial warranties to export:', warrantiesToExport.length);
// Apply current filters
if (currentFilters.search) {
const searchTerm = currentFilters.search.toLowerCase();
console.log('[EXPORT DEBUG] Applying search filter:', searchTerm);
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;
});
console.log('[EXPORT DEBUG] After search filter:', warrantiesToExport.length);
}
if (currentFilters.status !== 'all') {
console.log('[EXPORT DEBUG] Applying status filter:', currentFilters.status);
warrantiesToExport = warrantiesToExport.filter(warranty =>
warranty.status === currentFilters.status
);
console.log('[EXPORT DEBUG] After status filter:', warrantiesToExport.length);
}
// Apply tag filter
if (currentFilters.tag !== 'all') {
const tagId = parseInt(currentFilters.tag);
console.log('[EXPORT DEBUG] Applying tag filter:', tagId);
warrantiesToExport = warrantiesToExport.filter(warranty =>
warranty.tags && Array.isArray(warranty.tags) &&
warranty.tags.some(tag => tag.id === tagId)
);
console.log('[EXPORT DEBUG] After tag filter:', warrantiesToExport.length);
}
// Apply vendor filter
if (currentFilters.vendor !== 'all') {
console.log('[EXPORT DEBUG] Applying vendor filter:', currentFilters.vendor);
warrantiesToExport = warrantiesToExport.filter(warranty =>
(warranty.vendor || '').toLowerCase() === currentFilters.vendor.toLowerCase()
);
console.log('[EXPORT DEBUG] After vendor filter:', warrantiesToExport.length);
}
// Apply warranty type filter
if (currentFilters.warranty_type !== 'all') {
console.log('[EXPORT DEBUG] Applying warranty type filter:', currentFilters.warranty_type);
warrantiesToExport = warrantiesToExport.filter(warranty =>
(warranty.warranty_type || '').toLowerCase() === currentFilters.warranty_type.toLowerCase()
);
console.log('[EXPORT DEBUG] After warranty type filter:', warrantiesToExport.length);
}
console.log('[EXPORT DEBUG] Final warranties to export:', warrantiesToExport.length);
console.log('[EXPORT DEBUG] Warranty IDs being exported:', warrantiesToExport.map(w => w.id));
// Create CSV content
let csvContent = "data:text/csv;charset=utf-8,";
// Add headers - Updated for duration components
csvContent += "ProductName,PurchaseDate,IsLifetime,WarrantyDurationYears,WarrantyDurationMonths,WarrantyDurationDays,ExpirationDate,Status,PurchasePrice,SerialNumber,ProductURL,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 - Updated for duration components
const row = [
warranty.product_name || '',
formatDateYYYYMMDD(new Date(warranty.purchase_date)),
warranty.is_lifetime ? 'TRUE' : 'FALSE',
warranty.warranty_duration_years || 0,
warranty.warranty_duration_months || 0,
warranty.warranty_duration_days || 0,
warranty.is_lifetime ? '' : formatDateYYYYMMDD(new Date(warranty.expiration_date)), // Expiration date empty for lifetime
warranty.status || '',
warranty.purchase_price || '',
serialNumbers,
warranty.product_url || '',
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(window.t('messages.exported_warranties_successfully', {count: warrantiesToExport.length}), 'success');
}
// Switch view of warranties list
async function switchView(viewType, saveToApi = true) { // Added saveToApi parameter with default true
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.`);
}
// --- MODIFIED: Only save preference to API if saveToApi is true ---
if (saveToApi && 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 if (!saveToApi) {
console.log('Skipping API save as saveToApi is false (likely called from loadViewPreference).');
} else {
console.warn('Cannot save view preference to API: User not authenticated or auth module not loaded.');
}
// --- END MODIFIED: 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-apply current filters after view change to preserve user's selection
try {
// Persist current filters before render (defensive no-op if unchanged)
// Skip API save on initial load to mirror view settings behavior
saveFilterPreferences(false);
} catch (_) {}
if (warrantiesList && warrantiesLoaded) {
// Reapply full filter set (status, tag, vendor, type, search, sort)
applyFilters();
}
// Update header dropdown label if present
try {
const currentViewIcon = document.getElementById('currentViewIcon');
if (currentViewIcon) {
currentViewIcon.className = 'fas';
if (viewType === 'list') {
currentViewIcon.classList.add('fa-list');
currentViewIcon.setAttribute('aria-label', 'List');
} else if (viewType === 'table') {
currentViewIcon.classList.add('fa-table');
currentViewIcon.setAttribute('aria-label', 'Table');
} else {
currentViewIcon.classList.add('fa-th-large');
currentViewIcon.setAttribute('aria-label', 'Grid');
}
}
} catch (e) { /* no-op */ }
}
// 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 from loadViewPreference: ${savedView}`);
// Switch view only if view buttons exist (implying it's the main page)
if (gridViewBtn || listViewBtn || tableViewBtn) {
switchView(savedView, false); // Pass false to prevent API save on initial load
}
}
// Dark mode toggle
if (darkModeToggle) { // Add check for darkModeToggle
darkModeToggle.addEventListener('change', (e) => {
saveThemePreference(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 = window.i18next ? window.i18next.t('warranties.enter_serial_number') : 'Enter serial number';
console.log('i18next available for serial number placeholder:', !!window.i18next);
if (window.i18next) {
console.log('Translation for warranties.enter_serial_number:', window.i18next.t('warranties.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 = ' ';
// 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 = ' ' + (window.i18next ? window.i18next.t('warranties.add_serial_number') : 'Add Serial Number');
console.log('i18next available in addSerialNumberInput:', !!window.i18next);
if (window.i18next) {
console.log('Translation for warranties.add_serial_number:', window.i18next.t('warranties.add_serial_number'));
}
addButton.addEventListener('click', function() {
addSerialNumberInput(container);
});
container.appendChild(addButton);
}
}
// Functions
function showLoading() {
let localLoadingContainer = window.loadingContainer || document.getElementById('loadingContainer');
if (localLoadingContainer) {
localLoadingContainer.classList.add('active');
window.loadingContainer = localLoadingContainer; // Update global reference if found
} else {
console.error('WarrackerDebug: loadingContainer element not found by showLoading(). Ensure it exists in the HTML and script.js is loaded after it.');
}
}
function hideLoading() {
let localLoadingContainer = window.loadingContainer || document.getElementById('loadingContainer');
if (localLoadingContainer) {
localLoadingContainer.classList.remove('active');
window.loadingContainer = localLoadingContainer; // Update global reference if found
} else {
console.error('WarrackerDebug: loadingContainer element not found by hideLoading().');
}
}
function showToast(message, type = 'info', duration = 5000) {
// Check if a toast with the same message and type already exists
const existingToasts = document.querySelectorAll(`.toast.toast-${type}`);
for (let i = 0; i < existingToasts.length; i++) {
const span = existingToasts[i].querySelector('span');
if (span && span.textContent === message) {
return existingToasts[i]; // Don't create a new one
}
}
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
const icon = document.createElement('i');
switch (type) {
case 'success':
icon.className = 'fas fa-check-circle';
break;
case 'error':
icon.className = 'fas fa-exclamation-circle';
break;
case 'warning':
icon.className = 'fas fa-exclamation-triangle';
break;
default:
icon.className = 'fas fa-info-circle';
}
const messageSpan = document.createElement('span');
messageSpan.textContent = message;
toast.appendChild(icon);
toast.appendChild(messageSpan);
toastContainer.appendChild(toast);
// Add a method to remove the toast
toast.remove = function() {
toast.classList.add('toast-fade-out');
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
};
// Auto-hide toast after specified duration (if not 0)
if (duration > 0) {
setTimeout(() => {
toast.remove();
}, duration);
}
return toast;
}
// Update file name display when a file is selected
function updateFileName(event, inputId = 'invoice', outputId = 'fileName') {
const file = event.target.files[0];
const output = document.getElementById(outputId);
if (file && output) {
output.textContent = file.name;
} else if (output) {
output.textContent = '';
}
// Handle photo preview for product photo
if (inputId === 'productPhoto' || inputId === 'editProductPhoto') {
const previewId = inputId === 'productPhoto' ? 'productPhotoPreview' : 'editProductPhotoPreview';
const imgId = inputId === 'productPhoto' ? 'productPhotoImg' : 'editProductPhotoImg';
const preview = document.getElementById(previewId);
const img = document.getElementById(imgId);
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
img.src = e.target.result;
preview.style.display = 'block';
};
reader.readAsDataURL(file);
} else {
preview.style.display = 'none';
}
}
}
// 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 };
// Flag archived items when merged into All view
processedWarranty.is_archived = !!(warranty.__isArchived || warranty.is_archived);
// 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 = window.i18next ? window.i18next.t('warranties.lifetime') : 'Lifetime';
processedWarranty.daysRemaining = Infinity;
// Ensure duration components are 0 for lifetime
processedWarranty.warranty_duration_years = 0;
processedWarranty.warranty_duration_months = 0;
processedWarranty.warranty_duration_days = 0;
} 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 = window.i18next ? window.i18next.t('warranties.expired') : 'Expired';
} else if (daysRemaining < expiringSoonDays) {
processedWarranty.status = 'expiring';
const dayText = window.i18next ?
window.i18next.t('warranties.day', {count: daysRemaining}) :
`day${daysRemaining !== 1 ? 's' : ''}`;
processedWarranty.statusText = window.i18next ?
window.i18next.t('warranties.days_remaining', {days: daysRemaining, dayText: dayText}) :
`${daysRemaining} day${daysRemaining !== 1 ? 's' : ''} remaining`;
} else {
processedWarranty.status = 'active';
const dayText = window.i18next ?
window.i18next.t('warranties.day', {count: daysRemaining}) :
`day${daysRemaining !== 1 ? 's' : ''}`;
processedWarranty.statusText = window.i18next ?
window.i18next.t('warranties.days_remaining', {days: daysRemaining, dayText: dayText}) :
`${daysRemaining} day${daysRemaining !== 1 ? 's' : ''} remaining`;
}
// Preserve original duration values to detect input method
const originalYears = processedWarranty.warranty_duration_years || 0;
const originalMonths = processedWarranty.warranty_duration_months || 0;
const originalDays = processedWarranty.warranty_duration_days || 0;
// Track the original input method based on duration values
const wasExactDateMethod = originalYears === 0 && originalMonths === 0 && originalDays === 0;
processedWarranty.original_input_method = wasExactDateMethod ? 'exact_date' : 'duration';
// Calculate duration from dates if all duration components are 0 (exact date method was used)
const hasNoDuration = originalYears === 0 && originalMonths === 0 && originalDays === 0;
if (hasNoDuration && purchaseDateObj && processedWarranty.expirationDate) {
console.log('[DEBUG] Calculating duration from dates for exact date warranty');
const calculatedDuration = calculateDurationFromDates(
purchaseDateObj.toISOString().split('T')[0],
processedWarranty.expirationDate.toISOString().split('T')[0]
);
if (calculatedDuration) {
// Store calculated duration for display purposes
processedWarranty.display_duration_years = calculatedDuration.years;
processedWarranty.display_duration_months = calculatedDuration.months;
processedWarranty.display_duration_days = calculatedDuration.days;
console.log('[DEBUG] Calculated duration:', calculatedDuration);
// Keep original values at 0 to preserve input method detection
processedWarranty.warranty_duration_years = 0;
processedWarranty.warranty_duration_months = 0;
processedWarranty.warranty_duration_days = 0;
}
} else {
// Use original duration values for display
processedWarranty.display_duration_years = originalYears;
processedWarranty.display_duration_months = originalMonths;
processedWarranty.display_duration_days = originalDays;
}
} else {
processedWarranty.status = 'unknown';
processedWarranty.statusText = window.i18next ? window.i18next.t('warranties.unknown_status') : '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(isAuthenticated) { // Added isAuthenticated parameter
// +++ REMOVED: Ensure Preferences are loaded FIRST (Now handled by authStateReady) +++
// await loadAndApplyUserPreferences();
// +++ Preferences Loaded +++
try {
console.log('[DEBUG] Entered loadWarranties, isAuthenticated:', isAuthenticated);
// Reset the flag when starting to load warranties
warrantiesLoaded = false;
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 ---
// Check saved view scope preference to determine which API endpoint to use
const savedScope = loadViewScopePreference();
const shouldUseGlobalView = savedScope === 'global';
// Use the appropriate API endpoint based on saved preference
const baseUrl = window.location.origin;
// If status is 'archived', use archived endpoint (support global vs personal)
const isArchivedView = currentFilters && currentFilters.status === 'archived';
const apiUrl = isArchivedView
? (shouldUseGlobalView ? `${baseUrl}/api/warranties/global/archived` : `${baseUrl}/api/warranties/archived`)
: (shouldUseGlobalView ? `${baseUrl}/api/warranties/global` : `${baseUrl}/api/warranties`);
console.log(`[DEBUG] Using API endpoint based on saved preference '${savedScope}', archivedView=${isArchivedView}: ${apiUrl}`);
// Check if auth is available and user is authenticated using the passed parameter
if (!isAuthenticated) {
console.log('[DEBUG] loadWarranties: Early return - User not authenticated based on passed parameter.');
renderEmptyState(window.t('messages.login_to_view_warranties'));
hideLoading();
return;
}
// Get the auth token
const token = window.auth.getToken();
if (!token) {
console.log('[DEBUG] Early return: No auth token available');
renderEmptyState(window.t('messages.authentication_error_login_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);
}
// Update isGlobalView to match the loaded data
isGlobalView = shouldUseGlobalView;
console.log(`[DEBUG] Set isGlobalView to: ${isGlobalView}`);
// Optionally merge archived items into the "All" view (only in personal scope)
let combinedData = Array.isArray(data) ? data : [];
lastLoadedIncludesArchived = false;
if (!isArchivedView && currentFilters && currentFilters.status === 'all') {
try {
const archivedUrl = shouldUseGlobalView ? `${baseUrl}/api/warranties/global/archived` : `${baseUrl}/api/warranties/archived`;
const archivedResp = await fetch(archivedUrl, options);
if (archivedResp.ok) {
const archivedData = await archivedResp.json();
const archivedMarked = Array.isArray(archivedData)
? archivedData.map(w => ({ ...w, __isArchived: true }))
: [];
combinedData = combinedData.concat(archivedMarked);
lastLoadedIncludesArchived = true;
console.log(`[DEBUG] Merged ${archivedMarked.length} archived warranties into All view`);
} else {
// Log but do not block rendering of non-archived warranties
let errInfo = '';
try {
const errJson = await archivedResp.json();
errInfo = JSON.stringify(errJson);
} catch (_) {}
console.warn('[DEBUG] Failed to load archived warranties for All view:', archivedResp.status, errInfo);
}
} catch (mergeErr) {
console.warn('[DEBUG] Error while merging archived into All:', mergeErr);
}
}
// Process each warranty to calculate status and days remaining
warranties = Array.isArray(combinedData) ? combinedData.map(warranty => processWarrantyData(warranty)) : [];
lastLoadedArchived = isArchivedView;
console.log('[DEBUG] Final warranties array:', warranties);
console.log('[DEBUG] Total warranties loaded:', warranties.length);
console.log('[DEBUG] Warranty IDs loaded:', warranties.map(w => w.id));
// Set flag to indicate warranties have been loaded from API
warrantiesLoaded = true;
if (warranties.length === 0) {
console.log('No warranties found, showing empty state');
renderEmptyState(window.t('messages.no_warranties_found_add_first'));
} else {
console.log('Applying filters to display warranties');
// Populate tag filter dropdown with tags from warranties
populateTagFilter();
populateVendorFilter(); // Added call to populate vendor filter
populateWarrantyTypeFilter(); // Added call to populate warranty type filter
// Ensure the UI reflects the freshly loaded data
applyFilters();
}
} catch (error) {
console.error('[DEBUG] Error loading warranties:', error);
warrantiesLoaded = false; // Reset flag on error
renderEmptyState(window.t('messages.error_loading_warranties_try_again'));
} finally {
hideLoading();
}
}
function renderEmptyState(message = 'No warranties yet. Add your first warranty to get started.') {
warrantiesList.innerHTML = `
${window.t ? window.t('messages.no_warranties_found') : 'No warranties found'}
${message || (window.t ? window.t('messages.no_warranties_found_add_first') : 'No warranties yet. Add your first warranty to get started.')}
`;
}
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}`;
}
}
function formatDateYYYYMMDD(date) {
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
return 'N/A';
}
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const day = String(date.getUTCDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* Calculate the age of a product from purchase date to now
* @param {string|Date} purchaseDate - The purchase date
* @returns {string} - Formatted age string (e.g., "2 years, 3 months", "6 months", "15 days")
*/
function calculateProductAge(purchaseDate) {
if (!purchaseDate) return 'Unknown';
const purchase = new Date(purchaseDate);
const now = new Date();
if (isNaN(purchase.getTime()) || purchase > now) {
return 'Unknown';
}
// Calculate the difference
let years = now.getFullYear() - purchase.getFullYear();
let months = now.getMonth() - purchase.getMonth();
let days = now.getDate() - purchase.getDate();
// Adjust for negative days
if (days < 0) {
months--;
const lastMonth = new Date(now.getFullYear(), now.getMonth(), 0);
days += lastMonth.getDate();
}
// Adjust for negative months
if (months < 0) {
years--;
months += 12;
}
// Format the result
const parts = [];
if (years > 0) {
const yearText = window.i18next ? window.i18next.t('warranties.year', {count: years}) : `year${years !== 1 ? 's' : ''}`;
parts.push(`${years} ${yearText}`);
}
if (months > 0) {
const monthText = window.i18next ? window.i18next.t('warranties.month', {count: months}) : `month${months !== 1 ? 's' : ''}`;
parts.push(`${months} ${monthText}`);
}
if (days > 0 && years === 0) { // Only show days if less than a year old
const dayText = window.i18next ? window.i18next.t('warranties.day', {count: days}) : `day${days !== 1 ? 's' : ''}`;
parts.push(`${days} ${dayText}`);
}
if (parts.length === 0) {
return 'Today'; // Purchased today
}
return parts.join(', ');
}
/**
* Calculate the age of a product in days for sorting purposes
* @param {string|Date} purchaseDate - The purchase date
* @returns {number} - Age in days (0 if invalid date)
*/
function calculateProductAgeInDays(purchaseDate) {
if (!purchaseDate) return 0;
const purchase = new Date(purchaseDate);
const now = new Date();
if (isNaN(purchase.getTime()) || purchase > now) {
return 0;
}
// Calculate difference in milliseconds and convert to days
const diffTime = now.getTime() - purchase.getTime();
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
return diffDays;
}
async function renderWarranties(warrantiesToRender) {
console.log('renderWarranties called with:', warrantiesToRender);
// Guard clause: If the main warrantiesList element doesn't exist on the current page, exit.
// This can happen if saveWarranty -> applyFilters -> renderWarranties is called from a page
// that doesn't have the main list view (e.g., the status page).
if (!warrantiesList) {
console.warn('renderWarranties: warrantiesList element not found. Aborting render. This might be normal if not on the main warranties page.');
return;
}
const isArchivedView = currentFilters && currentFilters.status === 'archived';
if (!warrantiesToRender || warrantiesToRender.length === 0) {
renderEmptyState(isArchivedView ? (window.t ? window.t('messages.no_archived_warranties') : 'No archived warranties.') : undefined); // renderEmptyState should also check for warrantiesList or its specific container
return;
}
const today = new Date();
const globalSymbol = getCurrencySymbol(); // Get the global symbol as fallback
warrantiesList.innerHTML = '';
// Apply sorting based on current sort selection
const sortedWarranties = [...warrantiesToRender].sort((a, b) => {
switch (currentFilters.sortBy) {
case 'name':
return (a.product_name || '').toLowerCase().localeCompare((b.product_name || '').toLowerCase());
case 'purchase':
return new Date(b.purchase_date || 0) - new Date(a.purchase_date || 0);
case 'age': // Added age sorting
return calculateProductAgeInDays(b.purchase_date) - calculateProductAgeInDays(a.purchase_date); // Oldest first
case 'vendor': // Added vendor sorting
return (a.vendor || '').toLowerCase().localeCompare((b.vendor || '').toLowerCase());
case 'warranty_type': // Added warranty type sorting
return (a.warranty_type || '').toLowerCase().localeCompare((b.warranty_type || '').toLowerCase());
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 ${isArchivedView ? 'archived-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;
let statusClass = warranty.status || 'unknown';
let statusText = warranty.statusText || 'Unknown Status';
// If showing archived-only view or item itself is archived within All view
if (isArchivedView || warranty.is_archived) {
const archivedTextRaw = window.i18next ? window.i18next.t('warranties.archived') : 'Archived';
const archivedLabel = archivedTextRaw && archivedTextRaw !== 'warranties.archived' ? archivedTextRaw : 'Archived';
statusClass = 'archived';
statusText = archivedLabel;
}
// Format warranty duration text
let warrantyDurationText = window.i18next ? window.i18next.t('warranties.na') : 'N/A';
if (isLifetime) {
warrantyDurationText = window.i18next ? window.i18next.t('warranties.lifetime') : 'Lifetime';
} else {
// Use display_duration values if available, otherwise fall back to warranty_duration values
const years = warranty.display_duration_years !== undefined ? warranty.display_duration_years : (warranty.warranty_duration_years || 0);
const months = warranty.display_duration_months !== undefined ? warranty.display_duration_months : (warranty.warranty_duration_months || 0);
const days = warranty.display_duration_days !== undefined ? warranty.display_duration_days : (warranty.warranty_duration_days || 0);
// If all duration fields are 0 but we have expiration date, calculate from dates
if (years === 0 && months === 0 && days === 0 && warranty.expiration_date && warranty.purchase_date) {
const calculatedDuration = calculateDurationFromDates(warranty.purchase_date, warranty.expiration_date);
if (calculatedDuration) {
let parts = [];
if (calculatedDuration.years > 0) {
const yearText = window.i18next ? window.i18next.t('warranties.year', {count: calculatedDuration.years}) : `year${calculatedDuration.years !== 1 ? 's' : ''}`;
parts.push(`${calculatedDuration.years} ${yearText}`);
}
if (calculatedDuration.months > 0) {
const monthText = window.i18next ? window.i18next.t('warranties.month', {count: calculatedDuration.months}) : `month${calculatedDuration.months !== 1 ? 's' : ''}`;
parts.push(`${calculatedDuration.months} ${monthText}`);
}
if (calculatedDuration.days > 0) {
const dayText = window.i18next ? window.i18next.t('warranties.day', {count: calculatedDuration.days}) : `day${calculatedDuration.days !== 1 ? 's' : ''}`;
parts.push(`${calculatedDuration.days} ${dayText}`);
}
if (parts.length > 0) {
warrantyDurationText = parts.join(', ');
}
}
} else {
// Use the stored/calculated duration fields
let parts = [];
if (years > 0) {
const yearText = window.i18next ? window.i18next.t('warranties.year', {count: years}) : `year${years !== 1 ? 's' : ''}`;
parts.push(`${years} ${yearText}`);
}
if (months > 0) {
const monthText = window.i18next ? window.i18next.t('warranties.month', {count: months}) : `month${months !== 1 ? 's' : ''}`;
parts.push(`${months} ${monthText}`);
}
if (days > 0) {
const dayText = window.i18next ? window.i18next.t('warranties.day', {count: days}) : `day${days !== 1 ? 's' : ''}`;
parts.push(`${days} ${dayText}`);
}
if (parts.length > 0) {
warrantyDurationText = parts.join(', ');
}
}
}
const expirationDateText = isLifetime ? (window.i18next ? window.i18next.t('warranties.lifetime') : 'Lifetime') : formatDate(expirationDate);
// Calculate product age
const productAge = calculateProductAge(warranty.purchase_date);
// 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 user info HTML for global view
let userInfoHtml = '';
if (isGlobalView && warranty.user_display_name) {
const ownerLabel = window.i18next ? window.i18next.t('warranties.owner') : 'Owner';
userInfoHtml = `${ownerLabel}: ${warranty.user_display_name}
`;
}
// Prepare tags HTML
const tagsHtml = warranty.tags && warranty.tags.length > 0
? `
${warranty.tags.map(tag =>
`
${tag.name}
`
).join('')}
`
: '';
// 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) {
const notesLabel = window.i18next ? window.i18next.t('warranties.notes') : 'Notes';
notesLinkHtml = ` ${notesLabel} `;
}
const hasDocuments = Boolean(
warranty.product_url ||
warranty.invoice_path || warranty.invoice_url ||
warranty.manual_path || warranty.manual_url ||
warranty.other_document_path || warranty.other_document_url ||
warranty.paperless_invoice_id || warranty.paperless_manual_id ||
warranty.paperless_photo_id || warranty.paperless_other_id ||
hasNotes
);
// Get current user ID to check warranty ownership
const currentUserId = (() => {
try {
const userInfo = JSON.parse(localStorage.getItem('user_info') || '{}');
return userInfo.id;
} catch (e) {
return null;
}
})();
// Check if current user can edit/delete this warranty
// Allow if: not in global view, user owns the warranty, or user is admin
const isAdmin = getUserType() === 'admin';
const canEdit = !isGlobalView || (warranty.user_id === currentUserId) || isAdmin;
// Determine claims button class and title based on claim status
let claimsButtonClass = 'action-btn claims-link';
let claimsTitle = 'Claims';
if (warranty.claim_status_summary === 'OPEN') {
claimsButtonClass += ' claims-open';
claimsTitle = 'Claims (Open)';
} else if (warranty.claim_status_summary === 'FINISHED') {
claimsButtonClass += ' claims-finished';
claimsTitle = 'Claims (Finished)';
}
// Generate action buttons HTML based on permissions
const actionButtonsHtml = canEdit ? `
${ (isArchivedView || warranty.is_archived) ? `
` : `
`}
` : `
`;
const cardElement = document.createElement('div');
const isArchivedItem = isArchivedView || warranty.is_archived;
cardElement.className = `warranty-card ${isArchivedItem ? 'archived' : (statusClass === 'expired' ? 'expired' : statusClass === 'expiring' ? 'expiring-soon' : 'active')}`;
// Claims button styling will be handled in the action buttons HTML generation
if (currentView === 'grid') {
// Grid view HTML structure
const photoThumbnailHtml = warranty.product_photo_path && warranty.product_photo_path !== 'null' ? `
` : '';
cardElement.innerHTML = `
${photoThumbnailHtml}
${userInfoHtml}
${window.i18next ? window.i18next.t('warranties.age') : 'Age'}: ${productAge}
${window.i18next ? window.i18next.t('warranties.warranty') : 'Warranty'}: ${warrantyDurationText}
${window.i18next ? window.i18next.t('warranties.warranty_ends') : 'Warranty Ends'}: ${expirationDateText}
${warranty.purchase_price ? `
${window.i18next ? window.i18next.t('warranties.price') : 'Price'}: ${formatCurrencyHTML(warranty.purchase_price, warranty.currency ? getCurrencySymbolByCode(warranty.currency) : getCurrencySymbol(), getCurrencyPosition())}
` : ''}
${validSerialNumbers.length > 0 ? `
${window.i18next ? window.i18next.t('warranties.serial_number') : 'Serial Number'}: ${validSerialNumbers[0]}
${validSerialNumbers.length > 1 ? `
${validSerialNumbers.slice(1).map(sn => `${sn} `).join('')}
` : ''}
` : ''}
${warranty.model_number ? `
${window.i18next ? window.i18next.t('warranties.model_number') : 'Model Number'}: ${warranty.model_number}
` : ''}
${warranty.vendor ? `
${window.i18next ? window.i18next.t('warranties.vendor') : 'Vendor'}: ${warranty.vendor}
` : ''}
${warranty.warranty_type ? `
${window.i18next ? window.i18next.t('warranties.type') : 'Type'}: ${warranty.warranty_type}
` : ''}
${hasDocuments ? `
` : ''}
${tagsHtml}
${statusText}
`;
} else if (currentView === 'list') {
// List view HTML structure
const photoThumbnailHtml = warranty.product_photo_path && warranty.product_photo_path !== 'null' ? `
` : '';
cardElement.innerHTML = `
${photoThumbnailHtml}
${userInfoHtml}
${window.i18next ? window.i18next.t('warranties.age') : 'Age'}: ${productAge}
${window.i18next ? window.i18next.t('warranties.warranty') : 'Warranty'}: ${warrantyDurationText}
${window.i18next ? window.i18next.t('warranties.warranty_ends') : 'Warranty Ends'}: ${expirationDateText}
${warranty.purchase_price ? `
${window.i18next ? window.i18next.t('warranties.price') : 'Price'}: ${formatCurrencyHTML(warranty.purchase_price, warranty.currency ? getCurrencySymbolByCode(warranty.currency) : getCurrencySymbol(), getCurrencyPosition())}
` : ''}
${validSerialNumbers.length > 0 ? `
${window.i18next ? window.i18next.t('warranties.serial_number') : 'Serial Number'}: ${validSerialNumbers[0]}
${validSerialNumbers.length > 1 ? `
${validSerialNumbers.slice(1).map(sn => `${sn} `).join('')}
` : ''}
` : ''}
${warranty.model_number ? `
${window.i18next ? window.i18next.t('warranties.model_number') : 'Model Number'}: ${warranty.model_number}
` : ''}
${warranty.vendor ? `
${window.i18next ? window.i18next.t('warranties.vendor') : 'Vendor'}: ${warranty.vendor}
` : ''}
${warranty.warranty_type ? `
${window.i18next ? window.i18next.t('warranties.type') : 'Type'}: ${warranty.warranty_type}
` : ''}
${hasDocuments ? `
` : ''}
${tagsHtml}
${statusText}
`;
} else if (currentView === 'table') {
// Table view HTML structure
const photoThumbnailHtml = warranty.product_photo_path && warranty.product_photo_path !== 'null' ? `
` : '';
cardElement.innerHTML = `
${photoThumbnailHtml}
${userInfoHtml}
${window.i18next ? window.i18next.t('warranties.age') : 'Age'}: ${productAge}
${window.i18next ? window.i18next.t('warranties.warranty') : 'Warranty'}: ${warrantyDurationText}
${window.i18next ? window.i18next.t('warranties.warranty_ends') : 'Warranty Ends'}: ${expirationDateText}
${warranty.purchase_price ? `
${window.i18next ? window.i18next.t('warranties.price') : 'Price'}: ${formatCurrencyHTML(warranty.purchase_price, warranty.currency ? getCurrencySymbolByCode(warranty.currency) : getCurrencySymbol(), getCurrencyPosition())}
` : ''}
${validSerialNumbers.length > 0 ? `
${window.i18next ? window.i18next.t('warranties.serial_number') : 'Serial Number'}: ${validSerialNumbers[0]}
${validSerialNumbers.length > 1 ? `
${validSerialNumbers.slice(1).map(sn => `${sn} `).join('')}
` : ''}
` : ''}
${warranty.model_number ? `
${window.i18next ? window.i18next.t('warranties.model_number') : 'Model Number'}: ${warranty.model_number}
` : ''}
${warranty.vendor ? `
${window.i18next ? window.i18next.t('warranties.vendor') : 'Vendor'}: ${warranty.vendor}
` : ''}
${warranty.warranty_type ? `
${window.i18next ? window.i18next.t('warranties.type') : 'Type'}: ${warranty.warranty_type}
` : ''}
${statusText}
${hasDocuments ? `
` : ''}
${tagsHtml}
`;
}
// Add event listeners
warrantiesList.appendChild(cardElement);
// Add event listeners only if user can edit (buttons exist)
if (canEdit) {
// Edit button event listener
const editBtn = cardElement.querySelector('.edit-btn');
if (editBtn && !editBtn.disabled) {
editBtn.addEventListener('click', async () => {
console.log('[DEBUG] Edit button clicked for warranty ID:', warranty.id);
// Find the current warranty data instead of using the potentially stale warranty object
const currentWarranty = warranties.find(w => w.id === warranty.id);
console.log('[DEBUG] Found current warranty:', currentWarranty ? 'Yes' : 'No', currentWarranty?.notes);
if (currentWarranty) {
await openEditModal(currentWarranty);
} else {
showToast(window.t('messages.warranty_not_found_refresh'), 'error');
}
});
}
// Delete button event listener
const deleteBtn = cardElement.querySelector('.delete-btn');
if (deleteBtn) {
deleteBtn.addEventListener('click', () => {
openDeleteModal(warranty.id, warranty.product_name);
});
}
// Archive/Unarchive button event listeners
const archiveBtn = cardElement.querySelector('.archive-btn');
if (archiveBtn) {
archiveBtn.addEventListener('click', () => {
openArchiveModal(warranty.id, warranty.product_name);
});
}
const unarchiveBtn = cardElement.querySelector('.unarchive-btn');
if (unarchiveBtn) {
unarchiveBtn.addEventListener('click', async () => {
await toggleArchiveStatus(warranty.id, false);
});
}
}
// View notes button event listener
const notesLink = cardElement.querySelector('.notes-link');
if (notesLink) {
notesLink.addEventListener('click', (e) => {
e.preventDefault();
// Find the current warranty data instead of using the potentially stale warranty object
const currentWarranty = warranties.find(w => w.id === warranty.id);
if (currentWarranty) {
showNotesModal(currentWarranty.notes, currentWarranty);
} else {
showToast(window.t('messages.warranty_not_found_refresh'), 'error');
}
});
}
});
// Load secure images with authentication after rendering
loadSecureImages();
// Improved: Align card heights after all images have loaded
if (currentView === 'grid') {
const cards = warrantiesList.querySelectorAll('.warranty-card');
if (cards.length > 0) {
const images = warrantiesList.querySelectorAll('.secure-image');
let loadedCount = 0;
const totalImages = images.length;
const alignHeights = () => {
let maxHeight = 0;
cards.forEach(card => {
card.style.minHeight = ''; // Reset
const height = card.getBoundingClientRect().height;
if (height > maxHeight) maxHeight = height;
});
cards.forEach(card => {
card.style.minHeight = `${maxHeight}px`;
});
};
if (totalImages === 0) {
alignHeights(); // No images, align immediately
} else {
images.forEach(img => {
if (img.complete) {
loadedCount++;
if (loadedCount === totalImages) alignHeights();
} else {
img.addEventListener('load', () => {
loadedCount++;
if (loadedCount === totalImages) alignHeights();
});
img.addEventListener('error', () => {
loadedCount++;
if (loadedCount === totalImages) alignHeights();
});
}
});
}
}
}
// Update the timeline chart if on the status page or appropriate
if (typeof updateTimelineChart === 'function') {
updateTimelineChart();
}
console.log('Warranties rendered successfully');
}
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 model number
if (warranty.model_number && warranty.model_number.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('[FILTER DEBUG] Applying filters with:', currentFilters);
console.log('[FILTER DEBUG] Total warranties before filtering:', warranties.length);
// If user selected archived status, reload data from archived endpoint and return
if (currentFilters.status === 'archived') {
if (!lastLoadedArchived) {
loadWarranties(true);
return;
}
} else if (lastLoadedArchived) {
// If leaving archived view, reload normal source
loadWarranties(true);
return;
} else if (currentFilters.status === 'all' && !lastLoadedIncludesArchived) {
// Ensure All view re-merges archived items after switching away and back
loadWarranties(true);
return;
}
// Filter warranties based on currentFilters
const filtered = warranties.filter(warranty => {
// Exclude archived items from specific status views (only show in 'all' or 'archived')
if (warranty.is_archived && currentFilters.status !== 'all' && currentFilters.status !== 'archived') {
return false;
}
// Status filter: allow archived items to pass in All view
if (currentFilters.status !== 'all' && currentFilters.status !== 'archived' && 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;
}
}
// Vendor filter
if (currentFilters.vendor !== 'all' && (warranty.vendor || '').toLowerCase() !== currentFilters.vendor.toLowerCase()) {
return false;
}
// Warranty type filter
if (currentFilters.warranty_type !== 'all' && (warranty.warranty_type || '').toLowerCase() !== currentFilters.warranty_type.toLowerCase()) {
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 model number contains search term
const modelNumberMatch = warranty.model_number && warranty.model_number.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 && !modelNumberMatch && !serialNumberMatch) {
return false;
}
}
return true;
});
console.log('[FILTER DEBUG] Filtered warranties:', filtered.length);
console.log('[FILTER DEBUG] Filtered warranty IDs:', filtered.map(w => w.id));
// Render the filtered warranties
renderWarranties(filtered);
}
// --- Persist and restore filters/sort ---
async function saveFilterPreferences(saveToApi = true) {
try {
const prefix = getPreferenceKeyPrefix();
const filtersToSave = {
status: currentFilters.status || 'all',
tag: currentFilters.tag || 'all',
vendor: currentFilters.vendor || 'all',
warranty_type: currentFilters.warranty_type || 'all',
search: currentFilters.search || '',
sortBy: currentFilters.sortBy || 'expiration'
};
localStorage.setItem(`${prefix}warrantyFilters`, JSON.stringify(filtersToSave));
// Save to API if authenticated and saveToApi is true
if (saveToApi && window.auth && window.auth.isAuthenticated()) {
const token = window.auth.getToken();
if (token) {
try {
console.log('[Filters] Saving filter preferences to API:', filtersToSave);
const response = await fetch('/api/auth/preferences', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ saved_filters: filtersToSave })
});
if (response.ok) {
console.log('[Filters] Successfully saved filter preferences to API');
} else {
console.warn('[Filters] Failed to save filter preferences to API:', response.status);
}
} catch (error) {
console.error('[Filters] Error saving filter preferences to API:', error);
}
}
}
} catch (e) {
console.warn('Failed to save filter preferences', e);
}
}
// Deprecated: Sort preference is now saved as part of saveFilterPreferences
function loadFilterAndSortPreferences() {
try {
const prefix = getPreferenceKeyPrefix();
const savedFiltersRaw = localStorage.getItem(`${prefix}warrantyFilters`);
if (savedFiltersRaw) {
const savedFilters = JSON.parse(savedFiltersRaw);
if (savedFilters && typeof savedFilters === 'object') {
console.log('[Filters] Loading saved filters from localStorage:', savedFilters);
currentFilters.status = savedFilters.status || currentFilters.status;
currentFilters.tag = savedFilters.tag || currentFilters.tag;
currentFilters.vendor = savedFilters.vendor || currentFilters.vendor;
currentFilters.warranty_type = savedFilters.warranty_type || currentFilters.warranty_type;
currentFilters.search = savedFilters.search || '';
currentFilters.sortBy = savedFilters.sortBy || currentFilters.sortBy;
// Restore search input UI state
const searchInput = document.getElementById('searchWarranties');
const clearSearchBtn = document.getElementById('clearSearch');
if (searchInput && savedFilters.search) {
searchInput.value = savedFilters.search;
// Show clear button if search has value
if (clearSearchBtn) {
clearSearchBtn.style.display = 'flex';
}
// Add active search class
searchInput.parentElement.classList.add('active-search');
}
}
}
} catch (e) {
console.warn('Failed to load filter/sort preferences', e);
}
}
async function openEditModal(warranty) {
// Close any existing modals first
closeModals();
currentWarrantyId = warranty.id;
// Load currencies for the dropdown and wait for it to complete
await loadCurrencies();
console.log('[DEBUG] Opening edit modal for warranty:', warranty.id, 'with notes:', warranty.notes);
// Populate form fields
document.getElementById('editProductName').value = warranty.product_name;
document.getElementById('editProductUrl').value = warranty.product_url || '';
const editModelNumberInput = document.getElementById('editModelNumber');
if (editModelNumberInput) {
editModelNumberInput.value = warranty.model_number || '';
}
document.getElementById('editPurchaseDate').value = warranty.purchase_date.split('T')[0];
// Populate new duration fields
document.getElementById('editWarrantyDurationYears').value = warranty.warranty_duration_years || 0;
document.getElementById('editWarrantyDurationMonths').value = warranty.warranty_duration_months || 0;
document.getElementById('editWarrantyDurationDays').value = warranty.warranty_duration_days || 0;
document.getElementById('editPurchasePrice').value = warranty.purchase_price || '';
// Set currency dropdown
const editCurrencySelect = document.getElementById('editCurrency');
if (editCurrencySelect && warranty.currency) {
editCurrencySelect.value = warranty.currency;
}
document.getElementById('editVendor').value = warranty.vendor || '';
// Populate URL fields for documents (with null checks)
const editInvoiceUrl = document.getElementById('editInvoiceUrl');
if (editInvoiceUrl) editInvoiceUrl.value = warranty.invoice_url || '';
const editManualUrl = document.getElementById('editManualUrl');
if (editManualUrl) editManualUrl.value = warranty.manual_url || '';
const editOtherDocumentUrl = document.getElementById('editOtherDocumentUrl');
if (editOtherDocumentUrl) editOtherDocumentUrl.value = warranty.other_document_url || '';
// Handle warranty type - check if it's a predefined option or custom
const editWarrantyTypeSelect = document.getElementById('editWarrantyType');
const editWarrantyTypeCustom = document.getElementById('editWarrantyTypeCustom');
if (editWarrantyTypeSelect && warranty.warranty_type) {
// Check if the warranty type exists as an option in the dropdown
const options = Array.from(editWarrantyTypeSelect.options);
const matchingOption = options.find(option => option.value === warranty.warranty_type);
if (matchingOption) {
// It's a predefined option
editWarrantyTypeSelect.value = warranty.warranty_type;
if (editWarrantyTypeCustom) editWarrantyTypeCustom.style.display = 'none';
} else {
// It's a custom value
editWarrantyTypeSelect.value = 'other';
if (editWarrantyTypeCustom) {
editWarrantyTypeCustom.style.display = 'block';
editWarrantyTypeCustom.value = warranty.warranty_type;
}
}
} else if (editWarrantyTypeSelect) {
editWarrantyTypeSelect.value = '';
if (editWarrantyTypeCustom) editWarrantyTypeCustom.style.display = 'none';
}
// Clear existing serial number inputs
const editSerialNumbersContainer = document.getElementById('editSerialNumbersContainer');
editSerialNumbersContainer.innerHTML = '';
// Normalize serial_numbers to array of strings if needed
if (Array.isArray(warranty.serial_numbers) && warranty.serial_numbers.length > 0 && typeof warranty.serial_numbers[0] === 'object') {
warranty.serial_numbers = warranty.serial_numbers
.map(snObj => snObj && snObj.serial_number)
.filter(sn => typeof sn === 'string' && sn.trim() !== '');
}
// 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 = `
${i18next.t('warranties.add_serial_number')}
`;
// 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 = `
${i18next.t('actions.delete')}
`;
// 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) {
const hasLocalInvoice = warranty.invoice_path && warranty.invoice_path !== 'null';
const hasPaperlessInvoice = warranty.paperless_invoice_id && warranty.paperless_invoice_id !== null;
if (hasLocalInvoice) {
currentInvoiceElement.innerHTML = `
${i18next.t('warranties.current_invoice')}:
View
(${i18next.t('warranties.upload_new_file_replace')})
`;
deleteInvoiceBtn.style.display = '';
} else if (hasPaperlessInvoice) {
currentInvoiceElement.innerHTML = `
${i18next.t('warranties.current_invoice')}:
View
(${i18next.t('warranties.upload_new_file_replace')})
`;
deleteInvoiceBtn.style.display = '';
} else {
currentInvoiceElement.innerHTML = `${i18next.t('warranties.no_invoice_uploaded')} `;
deleteInvoiceBtn.style.display = 'none';
}
// Reset delete state
deleteInvoiceBtn.dataset.delete = 'false';
deleteInvoiceBtn.onclick = function() {
deleteInvoiceBtn.dataset.delete = 'true';
currentInvoiceElement.innerHTML = `${i18next.t('warranties.invoice_will_be_deleted')} `;
deleteInvoiceBtn.style.display = 'none';
};
}
// Show current manual if exists
const currentManualElement = document.getElementById('currentManual');
const deleteManualBtn = document.getElementById('deleteManualBtn');
if (currentManualElement && deleteManualBtn) {
const hasLocalManual = warranty.manual_path && warranty.manual_path !== 'null';
const hasPaperlessManual = warranty.paperless_manual_id && warranty.paperless_manual_id !== null;
if (hasLocalManual) {
currentManualElement.innerHTML = `
${i18next.t('warranties.current_manual')}:
View
(${i18next.t('warranties.upload_new_file_replace')})
`;
deleteManualBtn.style.display = '';
} else if (hasPaperlessManual) {
currentManualElement.innerHTML = `
${i18next.t('warranties.current_manual')}:
View
(${i18next.t('warranties.upload_new_file_replace')})
`;
deleteManualBtn.style.display = '';
} else {
currentManualElement.innerHTML = `${i18next.t('warranties.no_manual_uploaded')} `;
deleteManualBtn.style.display = 'none';
}
// Reset delete state
deleteManualBtn.dataset.delete = 'false';
deleteManualBtn.onclick = function() {
deleteManualBtn.dataset.delete = 'true';
currentManualElement.innerHTML = `${i18next.t('warranties.manual_will_be_deleted')} `;
deleteManualBtn.style.display = 'none';
};
}
// Show current product photo if exists
const currentProductPhotoElement = document.getElementById('currentProductPhoto');
const deleteProductPhotoBtn = document.getElementById('deleteProductPhotoBtn');
if (currentProductPhotoElement && deleteProductPhotoBtn) {
const hasLocalPhoto = warranty.product_photo_path && warranty.product_photo_path !== 'null';
const hasPaperlessPhoto = warranty.paperless_photo_id && warranty.paperless_photo_id !== null;
if (hasLocalPhoto) {
currentProductPhotoElement.innerHTML = `
${i18next.t('warranties.current_photo')}:
(${i18next.t('warranties.upload_new_photo_replace')})
`;
deleteProductPhotoBtn.style.display = '';
} else if (hasPaperlessPhoto) {
currentProductPhotoElement.innerHTML = `
${i18next.t('warranties.current_photo')}:
View
(${i18next.t('warranties.upload_new_photo_replace')})
`;
deleteProductPhotoBtn.style.display = '';
} else {
currentProductPhotoElement.innerHTML = `${i18next.t('warranties.no_photo_uploaded')} `;
deleteProductPhotoBtn.style.display = 'none';
}
// Reset delete state
deleteProductPhotoBtn.dataset.delete = 'false';
deleteProductPhotoBtn.onclick = function() {
deleteProductPhotoBtn.dataset.delete = 'true';
currentProductPhotoElement.innerHTML = `${i18next.t('warranties.photo_will_be_deleted')} `;
deleteProductPhotoBtn.style.display = 'none';
};
}
// Show current other document if exists
const currentOtherDocumentElement = document.getElementById('currentOtherDocument');
const deleteOtherDocumentBtn = document.getElementById('deleteOtherDocumentBtn');
if (currentOtherDocumentElement && deleteOtherDocumentBtn) {
const hasLocalOther = warranty.other_document_path && warranty.other_document_path !== 'null';
const hasPaperlessOther = warranty.paperless_other_id && warranty.paperless_other_id !== null;
if (hasLocalOther) {
currentOtherDocumentElement.innerHTML = `
${i18next.t('warranties.current_other_document')}:
View
(${i18next.t('warranties.upload_new_file_replace')})
`;
deleteOtherDocumentBtn.style.display = '';
} else if (hasPaperlessOther) {
currentOtherDocumentElement.innerHTML = `
${i18next.t('warranties.current_other_document')}:
View
(${i18next.t('warranties.upload_new_file_replace')})
`;
deleteOtherDocumentBtn.style.display = '';
} else {
currentOtherDocumentElement.innerHTML = `${i18next.t('warranties.no_other_document_uploaded')} `;
deleteOtherDocumentBtn.style.display = 'none';
}
// Reset delete state
deleteOtherDocumentBtn.dataset.delete = 'false';
deleteOtherDocumentBtn.onclick = function() {
deleteOtherDocumentBtn.dataset.delete = 'true';
currentOtherDocumentElement.innerHTML = `${i18next.t('warranties.other_document_will_be_deleted')} `;
deleteOtherDocumentBtn.style.display = 'none';
};
}
// Reset file inputs
document.getElementById('editProductPhoto').value = '';
document.getElementById('editInvoice').value = '';
document.getElementById('editManual').value = '';
document.getElementById('editOtherDocument').value = '';
document.getElementById('editProductPhotoFileName').textContent = '';
document.getElementById('editFileName').textContent = '';
document.getElementById('editManualFileName').textContent = '';
document.getElementById('editOtherDocumentFileName').textContent = '';
// Reset photo preview
const editPhotoPreview = document.getElementById('editProductPhotoPreview');
if (editPhotoPreview) {
editPhotoPreview.style.display = 'none';
}
// Set storage options based on current document storage
if (paperlessNgxEnabled) {
// Set product photo storage option
const editProductPhotoStorageRadios = document.getElementsByName('editProductPhotoStorage');
if (editProductPhotoStorageRadios.length > 0) {
const isPaperlessPhoto = warranty.paperless_photo_id && warranty.paperless_photo_id !== null;
editProductPhotoStorageRadios.forEach(radio => {
radio.checked = isPaperlessPhoto ? (radio.value === 'paperless') : (radio.value === 'local');
});
}
// Set invoice storage option
const editInvoiceStorageRadios = document.getElementsByName('editInvoiceStorage');
if (editInvoiceStorageRadios.length > 0) {
const isPaperlessInvoice = warranty.paperless_invoice_id && warranty.paperless_invoice_id !== null;
editInvoiceStorageRadios.forEach(radio => {
radio.checked = isPaperlessInvoice ? (radio.value === 'paperless') : (radio.value === 'local');
});
}
// Set manual storage option
const editManualStorageRadios = document.getElementsByName('editManualStorage');
if (editManualStorageRadios.length > 0) {
const isPaperlessManual = warranty.paperless_manual_id && warranty.paperless_manual_id !== null;
editManualStorageRadios.forEach(radio => {
radio.checked = isPaperlessManual ? (radio.value === 'paperless') : (radio.value === 'local');
});
}
// Set other document storage option
const editOtherDocumentStorageRadios = document.getElementsByName('editOtherDocumentStorage');
if (editOtherDocumentStorageRadios.length > 0) {
const isPaperlessOther = warranty.paperless_other_id && warranty.paperless_other_id !== null;
editOtherDocumentStorageRadios.forEach(radio => {
radio.checked = isPaperlessOther ? (radio.value === 'paperless') : (radio.value === 'local');
});
}
console.log('[Edit Modal] Storage options set based on current document storage:', {
photo: warranty.paperless_photo_id ? 'paperless' : 'local',
invoice: warranty.paperless_invoice_id ? 'paperless' : 'local',
manual: warranty.paperless_manual_id ? 'paperless' : 'local',
other: warranty.paperless_other_id ? 'paperless' : 'local'
});
}
// Initialize file input event listeners
const editProductPhotoInput = document.getElementById('editProductPhoto');
if (editProductPhotoInput) {
editProductPhotoInput.addEventListener('change', function(event) {
updateFileName(event, 'editProductPhoto', 'editProductPhotoFileName');
});
}
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');
});
}
const editOtherDocumentInput = document.getElementById('editOtherDocument');
if (editOtherDocumentInput) {
editOtherDocumentInput.addEventListener('change', function(event) {
updateFileName(event, 'editOtherDocument', 'editOtherDocumentFileName');
});
}
// 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 Duration Fields ---
if (editIsLifetimeCheckbox && editWarrantyDurationFields) {
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 duration values only if NOT lifetime
if (!warranty.is_lifetime) {
document.getElementById('editWarrantyDurationYears').value = warranty.warranty_duration_years || 0;
document.getElementById('editWarrantyDurationMonths').value = warranty.warranty_duration_months || 0;
document.getElementById('editWarrantyDurationDays').value = warranty.warranty_duration_days || 0;
} else {
document.getElementById('editWarrantyDurationYears').value = '';
document.getElementById('editWarrantyDurationMonths').value = '';
document.getElementById('editWarrantyDurationDays').value = '';
}
} else {
console.error("Lifetime warranty elements or duration fields not found in edit form");
}
// --- Set Warranty Method Selection ---
if (editDurationMethodRadio && editExactDateMethodRadio && editExactExpirationDateInput) {
console.log('[DEBUG Edit Modal] Warranty method detection:', {
originalInputMethod: warranty.original_input_method,
isLifetime: warranty.is_lifetime,
expirationDate: warranty.expiration_date,
warrantyDurationYears: warranty.warranty_duration_years,
warrantyDurationMonths: warranty.warranty_duration_months,
warrantyDurationDays: warranty.warranty_duration_days
});
// Use the original input method if available, otherwise fall back to previous logic
if (!warranty.is_lifetime) {
if (warranty.original_input_method === 'exact_date') {
// Use exact date method
editExactDateMethodRadio.checked = true;
editDurationMethodRadio.checked = false;
editExactExpirationDateInput.value = warranty.expiration_date.split('T')[0];
console.log('[DEBUG Edit Modal] Selected exact date method based on original_input_method');
} else {
// Use duration method (either explicitly set or fallback)
editDurationMethodRadio.checked = true;
editExactDateMethodRadio.checked = false;
editExactExpirationDateInput.value = '';
console.log('[DEBUG Edit Modal] Selected duration method based on original_input_method or fallback');
}
}
// Set up event listeners for warranty method change
editDurationMethodRadio.removeEventListener('change', handleEditWarrantyMethodChange);
editExactDateMethodRadio.removeEventListener('change', handleEditWarrantyMethodChange);
editDurationMethodRadio.addEventListener('change', handleEditWarrantyMethodChange);
editExactDateMethodRadio.addEventListener('change', handleEditWarrantyMethodChange);
console.log('[DEBUG Edit Modal] Event listeners attached to warranty method radio buttons');
console.log('[DEBUG Edit Modal] Initial radio states:', {
durationChecked: editDurationMethodRadio.checked,
exactDateChecked: editExactDateMethodRadio.checked
});
// Call handler to set initial state
handleEditWarrantyMethodChange();
}
// Set notes
const notesInput = document.getElementById('editNotes');
if (notesInput) {
notesInput.value = warranty.notes || '';
}
// Update currency symbols and positioning for the edit form
const symbol = getCurrencySymbol();
const position = getCurrencyPosition();
updateFormCurrencyPosition(symbol, position);
// Trigger currency positioning after modal is visible
setTimeout(() => {
if (position === 'right') {
const editPriceInput = document.getElementById('editPurchasePrice');
const editCurrencySymbol = document.getElementById('editCurrencySymbol');
if (editPriceInput && editCurrencySymbol) {
// Force update the currency position now that modal is visible
const wrapper = editPriceInput.closest('.price-input-wrapper');
if (wrapper && wrapper.classList.contains('currency-right')) {
const updateEvent = new Event('focus');
editPriceInput.dispatchEvent(updateEvent);
const blurEvent = new Event('blur');
editPriceInput.dispatchEvent(blurEvent);
}
}
}
}, 200);
// Load secure images with authentication for the edit modal
setTimeout(() => loadSecureImages(), 100); // Small delay to ensure DOM is updated
}
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');
}
}
// Open Archive confirmation modal
function openArchiveModal(warrantyId, productName) {
currentWarrantyId = warrantyId;
const archiveProductNameElement = document.getElementById('archiveProductName');
if (archiveProductNameElement) {
archiveProductNameElement.textContent = productName || '';
}
const archiveModal = document.getElementById('archiveModal');
if (archiveModal) {
archiveModal.classList.add('active');
}
}
// Confirm archive handler
async function confirmArchive() {
if (!currentWarrantyId) {
showToast('No warranty selected', 'error');
return;
}
try {
await toggleArchiveStatus(currentWarrantyId, true);
closeModals();
// Guidance toast for where to find archived items
const guidance = 'You can find archived warranties by selecting "Archived" in Filters.';
showToast(guidance, 'info');
currentWarrantyId = null;
} catch (e) {
// toggleArchiveStatus already toasts on failure
console.error('Archive failed', e);
}
}
// 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;
}
if (formData.has('other_document') && formData.get('other_document').size > 0) {
totalSize += formData.get('other_document').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
async function handleFormSubmit(event) { // Made async to properly await paperless uploads
event.preventDefault();
const isLifetime = isLifetimeCheckbox.checked;
const isDurationMethod = durationMethodRadio && durationMethodRadio.checked;
const years = parseInt(warrantyDurationYearsInput.value || 0);
const months = parseInt(warrantyDurationMonthsInput.value || 0);
const days = parseInt(warrantyDurationDaysInput.value || 0);
const exactDate = exactExpirationDateInput ? exactExpirationDateInput.value : '';
// --- Updated Lifetime and Method Check ---
if (!isLifetime) {
if (isDurationMethod) {
// Validate duration fields
if (years === 0 && months === 0 && days === 0) {
showValidationErrors(1);
switchToTab(1); // Switch to warranty details tab
// Optionally focus the first duration input
if (warrantyDurationYearsInput) warrantyDurationYearsInput.focus();
// Add invalid class to the container or individual inputs if needed
if (warrantyDurationFields) warrantyDurationFields.classList.add('invalid-duration'); // Example
return;
}
} else {
// Validate exact expiration date
if (!exactDate) {
showValidationErrors(1);
switchToTab(1); // Switch to warranty details tab
if (exactExpirationDateInput) exactExpirationDateInput.focus();
return;
}
// Validate that expiration date is in the future relative to purchase date
const purchaseDate = document.getElementById('purchaseDate').value;
if (purchaseDate && exactDate <= purchaseDate) {
showToast(window.t('messages.expiration_date_after_purchase_date'), 'error');
switchToTab(1);
if (exactExpirationDateInput) exactExpirationDateInput.focus();
return;
}
}
}
// Remove invalid duration class if validation passes
if (warrantyDurationFields) warrantyDurationFields.classList.remove('invalid-duration');
// Validate all tabs
for (let i = 0; i < tabContents.length; i++) {
if (!validateTab(i)) {
// Switch to the first invalid tab
switchToTab(i);
showValidationErrors(i);
return;
}
}
// Create form data object
const formData = new FormData(warrantyForm);
// Ensure model_number is included if present
const modelNumberInput = document.getElementById('modelNumber');
if (modelNumberInput && modelNumberInput.value.trim() !== '') {
formData.set('model_number', modelNumberInput.value.trim());
}
// Handle warranty type - use custom value if "other" is selected
const warrantyTypeSelect = document.getElementById('warrantyType');
const warrantyTypeCustom = document.getElementById('warrantyTypeCustom');
if (warrantyTypeSelect && warrantyTypeSelect.value === 'other' && warrantyTypeCustom && warrantyTypeCustom.value.trim()) {
formData.set('warranty_type', warrantyTypeCustom.value.trim());
}
// Debug: Log all form data entries
console.log('=== DEBUG: Form Data Contents ===');
for (let [key, value] of formData.entries()) {
console.log(`${key}: ${value}`);
}
console.log('=== END DEBUG ===');
// Product URL handling
let productUrlValue = formData.get('product_url');
if (productUrlValue && typeof productUrlValue === 'string') {
productUrlValue = productUrlValue.trim();
if (productUrlValue && !productUrlValue.startsWith('http://') && !productUrlValue.startsWith('https://')) {
formData.set('product_url', 'https://' + productUrlValue);
} else if (productUrlValue) {
// Ensure trimmed value is set back if it was already valid
formData.set('product_url', productUrlValue);
}
}
// Remove old warranty_years if it exists in formData (it shouldn't if HTML is correct)
formData.delete('warranty_years');
// Append new duration fields (already handled by FormData constructor if names match)
// formData.append('warranty_duration_years', years);
// formData.append('warranty_duration_months', months);
// formData.append('warranty_duration_days', days);
// Add serial numbers to form data (using correct name 'serial_numbers[]')
const serialInputs = document.querySelectorAll('#serialNumbersContainer input[name="serial_numbers[]"]');
// Clear existing serial_numbers[] from formData before appending new ones
formData.delete('serial_numbers[]');
serialInputs.forEach(input => {
if (input.value.trim()) {
formData.append('serial_numbers[]', input.value.trim()); // Use [] for arrays
}
});
// 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));
} else {
formData.append('tag_ids', JSON.stringify([])); // Send empty array if no tags
}
// Add URL fields for documents (with null checks)
const invoiceUrlField = document.getElementById('invoiceUrl');
formData.append('invoice_url', invoiceUrlField ? invoiceUrlField.value || '' : '');
const manualUrlField = document.getElementById('manualUrl');
formData.append('manual_url', manualUrlField ? manualUrlField.value || '' : '');
const otherDocumentUrlField = document.getElementById('otherDocumentUrl');
formData.append('other_document_url', otherDocumentUrlField ? otherDocumentUrlField.value || '' : '');
// --- Ensure is_lifetime is correctly added ---
// FormData already includes it if the checkbox is checked. If not checked, it's omitted.
// We need to explicitly add 'false' if it's not checked.
if (!isLifetimeCheckbox.checked) {
formData.append('is_lifetime', 'false');
// Add warranty method and exact expiration date if using exact date method
if (!isDurationMethod && exactDate) {
formData.append('exact_expiration_date', exactDate);
// Ensure duration fields are 0 when using exact date
formData.set('warranty_duration_years', '0');
formData.set('warranty_duration_months', '0');
formData.set('warranty_duration_days', '0');
}
} else {
// Ensure duration fields are 0 if lifetime is checked
formData.set('warranty_duration_years', '0');
formData.set('warranty_duration_months', '0');
formData.set('warranty_duration_days', '0');
}
// Add other_document file (always, as no storage selection for this)
const otherDocumentFile = document.getElementById('otherDocument').files[0];
if (otherDocumentFile) {
formData.append('other_document', otherDocumentFile);
}
// --- Only append invoice/manual files to FormData if storage is 'local' ---
const invoiceFile = document.getElementById('invoice')?.files[0];
const manualFile = document.getElementById('manual')?.files[0];
let invoiceStorage = 'local';
let manualStorage = 'local';
const invoiceStorageRadio = document.querySelector('input[name="invoiceStorage"]:checked');
const manualStorageRadio = document.querySelector('input[name="manualStorage"]:checked');
if (invoiceStorageRadio) invoiceStorage = invoiceStorageRadio.value;
if (manualStorageRadio) manualStorage = manualStorageRadio.value;
formData.set('invoiceStorage', invoiceStorage);
formData.set('manualStorage', manualStorage);
console.log('[DEBUG] Invoice storage:', invoiceStorage, 'Manual storage:', manualStorage);
console.log('[DEBUG] Invoice file:', invoiceFile);
console.log('[DEBUG] Manual file:', manualFile);
if (invoiceStorage === 'local' && invoiceFile) {
console.log('[DEBUG] Appending invoice file to FormData (local storage)');
formData.append('invoice', invoiceFile);
}
if (manualStorage === 'local' && manualFile) {
console.log('[DEBUG] Appending manual file to FormData (local storage)');
formData.append('manual', manualFile);
}
if (invoiceStorage === 'paperless') {
console.log('[DEBUG] Invoice should be uploaded to Paperless-ngx');
}
if (manualStorage === 'paperless') {
console.log('[DEBUG] Manual should be uploaded to Paperless-ngx');
}
// Add selected Paperless documents (for linking existing docs, not uploads)
const selectedPaperlessProductPhoto = document.getElementById('selectedPaperlessProductPhoto');
const selectedPaperlessInvoice = document.getElementById('selectedPaperlessInvoice');
const selectedPaperlessManual = document.getElementById('selectedPaperlessManual');
const selectedPaperlessOtherDocument = document.getElementById('selectedPaperlessOtherDocument');
if (selectedPaperlessProductPhoto && selectedPaperlessProductPhoto.value) {
formData.append('paperless_photo_id', selectedPaperlessProductPhoto.value);
}
if (selectedPaperlessInvoice && selectedPaperlessInvoice.value) {
formData.append('paperless_invoice_id', selectedPaperlessInvoice.value);
}
if (selectedPaperlessManual && selectedPaperlessManual.value) {
formData.append('paperless_manual_id', selectedPaperlessManual.value);
}
if (selectedPaperlessOtherDocument && selectedPaperlessOtherDocument.value) {
formData.append('paperless_other_id', selectedPaperlessOtherDocument.value);
}
// Show loading spinner
showLoadingSpinner();
try {
// Process Paperless-ngx uploads if enabled
const paperlessUploads = await processPaperlessNgxUploads(formData);
// Add Paperless-ngx document IDs to form data
Object.keys(paperlessUploads).forEach(key => {
formData.append(key, paperlessUploads[key]);
});
// Send the form data to the server
const response = await fetch('/api/warranties', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('auth_token')
},
body: formData
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to add warranty');
}
const data = await response.json();
hideLoadingSpinner();
showToast(window.t('messages.warranty_added_successfully'), 'success');
// Store the new warranty ID for auto-linking
const newWarrantyId = data.id;
// --- Store file info and storage type before upload for auto-link logic ---
const invoiceFileInput = document.getElementById('invoice');
const manualFileInput = document.getElementById('manual');
const invoiceFilePre = invoiceFileInput?.files[0];
const manualFilePre = manualFileInput?.files[0];
const invoiceStoragePre = formData.get('invoiceStorage');
const manualStoragePre = formData.get('manualStorage');
// Auto-link any documents that were uploaded to Paperless-ngx (match edit modal behavior)
const autoLinkTypes = [];
const fileInfo = {};
if (invoiceStoragePre === 'paperless' && invoiceFilePre) {
autoLinkTypes.push('invoice');
fileInfo.invoice = invoiceFilePre.name;
}
if (manualStoragePre === 'paperless' && manualFilePre) {
autoLinkTypes.push('manual');
fileInfo.manual = manualFilePre.name;
}
// Other document does not have a storage option, so skip unless you add support
// If you want to support auto-linking for 'other', add logic here
console.log('[Auto-Link DEBUG] newWarrantyId:', newWarrantyId, 'autoLinkTypes:', autoLinkTypes, 'fileInfo:', fileInfo, 'invoiceStorage:', invoiceStoragePre, 'manualStorage:', manualStoragePre);
if (autoLinkTypes.length > 0 && newWarrantyId) {
console.log('[Auto-Link] Starting automatic document linking after warranty creation (Paperless-ngx uploads only)', autoLinkTypes, fileInfo);
setTimeout(() => {
console.log('[Auto-Link DEBUG] Calling autoLinkRecentDocuments with:', newWarrantyId, autoLinkTypes, fileInfo);
autoLinkRecentDocuments(newWarrantyId, autoLinkTypes, 10, 10000, fileInfo);
}, 3000); // Wait 3 seconds for Paperless-ngx to process the documents
}
// Close and reset the modal on success
if (addWarrantyModal) {
addWarrantyModal.classList.remove('active');
}
resetAddWarrantyWizard(); // Reset the wizard form
try {
await loadWarranties(true);
console.log('Warranties reloaded after adding new warranty');
applyFilters();
// Load secure images for the new cards
setTimeout(() => {
console.log('Loading secure images for new warranty cards');
loadSecureImages();
}, 200);
} catch (error) {
console.error('Error reloading warranties after adding:', error);
}
} catch (error) {
hideLoadingSpinner();
console.error('Error adding warranty:', error);
showToast(error.message || window.t('messages.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); // Use renamed handler
}
// 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
// Initialize claims functionality
initClaimsEventListeners();
});
// ====== WARRANTY CLAIMS FUNCTIONALITY ======
// Global variables for claims
let currentClaimsWarrantyId = null;
let currentClaims = [];
let currentClaimsCanEdit = false; // Track if current user can edit the warranty being viewed
/**
* Initialize claims event listeners
*/
function initClaimsEventListeners() {
// Event delegation for claims links
if (warrantiesList) {
warrantiesList.addEventListener('click', (e) => {
if (e.target.closest('.claims-link')) {
e.preventDefault();
const warrantyId = parseInt(e.target.closest('.claims-link').dataset.id);
openClaimsModal(warrantyId);
return;
}
// Archive button
if (e.target.closest('.archive-btn')) {
e.preventDefault();
const el = e.target.closest('.archive-btn');
const warrantyId = parseInt(el.dataset.id);
const card = el.closest('.warranty-card');
const titleEl = card ? card.querySelector('.product-name-header .warranty-title') : null;
const productName = titleEl ? titleEl.textContent.trim() : '';
openArchiveModal(warrantyId, productName);
return;
}
// Unarchive button
if (e.target.closest('.unarchive-btn')) {
e.preventDefault();
const warrantyId = parseInt(e.target.closest('.unarchive-btn').dataset.id);
toggleArchiveStatus(warrantyId, false);
return;
}
});
}
// Add claim button
if (addClaimBtn) {
addClaimBtn.addEventListener('click', () => {
openClaimFormModal();
});
}
// Claim form submission
if (claimForm) {
claimForm.addEventListener('submit', handleClaimFormSubmit);
}
// Modal close handling - Removed backdrop click listeners to match warranty modal behavior
// Claims modals now only close via explicit close buttons with [data-dismiss="modal"]
// The general modal click prevention (e.stopPropagation) handles preventing backdrop clicks
// Add event listeners for claims modal close buttons
if (claimsModal) {
claimsModal.querySelectorAll('[data-dismiss="modal"]').forEach(closeBtn => {
closeBtn.addEventListener('click', () => {
closeClaimsModal();
});
});
}
if (claimFormModal) {
claimFormModal.querySelectorAll('[data-dismiss="modal"]').forEach(closeBtn => {
closeBtn.addEventListener('click', () => {
closeClaimFormModal();
});
});
}
}
/**
* Open the claims modal for a specific warranty
*/
async function openClaimsModal(warrantyId) {
try {
currentClaimsWarrantyId = warrantyId;
// Find warranty info
const warranty = warranties.find(w => w.id === warrantyId);
if (!warranty) {
showToast(window.i18next ? window.i18next.t('claims.warranty_not_found') : 'Warranty not found', 'error');
return;
}
// Determine if current user can edit this warranty
const currentUserId = (() => {
try {
const userInfo = JSON.parse(localStorage.getItem('user_info') || '{}');
return userInfo.id;
} catch (e) {
return null;
}
})();
const isAdmin = getUserType() === 'admin';
currentClaimsCanEdit = !isGlobalView || (warranty.user_id === currentUserId) || isAdmin;
// Show/hide the Add New Claim button based on edit permissions
if (addClaimBtn) {
addClaimBtn.style.display = currentClaimsCanEdit ? 'inline-block' : 'none';
}
// Update warranty info in modal
if (warrantyClaimInfo) {
warrantyClaimInfo.innerHTML = `
${warranty.product_name || 'Unnamed Product'}
${warranty.vendor || 'Unknown Vendor'}
Expires: ${formatDate(warranty.expiration_date ? new Date(warranty.expiration_date) : null)}
${warranty.statusText || 'Unknown Status'}
`;
}
// Show modal
claimsModal.classList.add('active');
// Load claims
await loadClaims(warrantyId);
} catch (error) {
console.error('Error opening claims modal:', error);
showToast('Failed to open claims modal', 'error');
}
}
/**
* Load claims for a warranty
*/
async function loadClaims(warrantyId) {
try {
// Show loading
if (claimsListBody) {
claimsListBody.innerHTML = `
Loading claims...
`;
}
// Fetch claims
const response = await fetch(`/api/warranties/${warrantyId}/claims`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + (window.auth ? window.auth.getToken() : localStorage.getItem('auth_token')),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to load claims');
}
currentClaims = await response.json();
renderClaims();
} catch (error) {
console.error('Error loading claims:', error);
if (claimsListBody) {
claimsListBody.innerHTML = `
Failed to load claims
`;
}
}
}
/**
* Render claims list
*/
function renderClaims() {
if (!claimsListBody) return;
if (currentClaims.length === 0) {
const noClaimsMessage = currentClaimsCanEdit
? (window.i18next ? window.i18next.t('claims.no_claims_message') : 'Click "Add New Claim" to get started')
: 'No claims have been filed for this warranty';
claimsListBody.innerHTML = `
${window.i18next ? window.i18next.t('claims.no_claims_yet') : 'No Claims Yet'}
${noClaimsMessage}
`;
return;
}
let claimsHtml = '';
currentClaims.forEach(claim => {
claimsHtml += `
${claim.description ? `
${escapeHtml(claim.description)}
` : ''}
${claim.resolution ? `
Resolution: ${escapeHtml(claim.resolution)}
` : ''}
`;
});
claimsListBody.innerHTML = claimsHtml;
// Add event listeners for edit and delete buttons (only if user can edit)
if (currentClaimsCanEdit) {
claimsListBody.addEventListener('click', (e) => {
if (e.target.closest('.edit-claim-btn')) {
const claimId = parseInt(e.target.closest('.edit-claim-btn').dataset.claimId);
const claim = currentClaims.find(c => c.id === claimId);
if (claim) {
openClaimFormModal(claim);
}
}
if (e.target.closest('.delete-claim-btn')) {
const claimId = parseInt(e.target.closest('.delete-claim-btn').dataset.claimId);
if (confirm(window.i18next ? window.i18next.t('claims.confirm_delete_claim') : 'Are you sure you want to delete this claim?')) {
deleteClaim(claimId);
}
}
});
}
}
/**
* Open claim form modal for adding or editing
*/
function openClaimFormModal(claim = null) {
if (!claimFormModal) return;
// Update title
if (claimFormTitle) {
claimFormTitle.textContent = claim ? (window.i18next ? window.i18next.t('claims.edit_claim') : 'Edit Claim') : (window.i18next ? window.i18next.t('claims.add_new_claim') : 'Add New Claim');
}
// Reset form
if (claimForm) {
claimForm.reset();
}
if (claim) {
// Populate form with claim data
if (editClaimId) editClaimId.value = claim.id;
if (document.getElementById('claimDate')) document.getElementById('claimDate').value = claim.claim_date || '';
if (document.getElementById('claimStatus')) document.getElementById('claimStatus').value = claim.status;
if (document.getElementById('claimNumber')) document.getElementById('claimNumber').value = claim.claim_number || '';
if (document.getElementById('claimDescription')) document.getElementById('claimDescription').value = claim.description || '';
if (document.getElementById('claimResolution')) document.getElementById('claimResolution').value = claim.resolution || '';
if (document.getElementById('resolutionDate')) document.getElementById('resolutionDate').value = claim.resolution_date || '';
} else {
// Set default values for new claim
if (editClaimId) editClaimId.value = '';
if (document.getElementById('claimDate')) document.getElementById('claimDate').value = new Date().toISOString().split('T')[0];
if (document.getElementById('claimStatus')) document.getElementById('claimStatus').value = 'Submitted';
}
// Show modal
claimFormModal.classList.add('active');
}
/**
* Handle claim form submission
*/
async function handleClaimFormSubmit(event) {
event.preventDefault();
try {
const formData = new FormData(claimForm);
const claimId = editClaimId.value;
const claimData = {
claim_date: formData.get('claim_date'),
status: formData.get('status'),
claim_number: formData.get('claim_number'),
description: formData.get('description'),
resolution: formData.get('resolution'),
resolution_date: formData.get('resolution_date')
};
// Remove empty strings
Object.keys(claimData).forEach(key => {
if (claimData[key] === '') {
claimData[key] = null;
}
});
const isEdit = claimId && claimId !== '';
const url = isEdit
? `/api/warranties/${currentClaimsWarrantyId}/claims/${claimId}`
: `/api/warranties/${currentClaimsWarrantyId}/claims`;
const method = isEdit ? 'PUT' : 'POST';
const response = await fetch(url, {
method: method,
headers: {
'Authorization': 'Bearer ' + (window.auth ? window.auth.getToken() : localStorage.getItem('auth_token')),
'Content-Type': 'application/json'
},
body: JSON.stringify(claimData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to save claim');
}
showToast(isEdit ? (window.i18next ? window.i18next.t('claims.claim_updated_successfully') : 'Claim updated successfully') : (window.i18next ? window.i18next.t('claims.claim_created_successfully') : 'Claim created successfully'), 'success');
closeClaimFormModal();
// Reload claims
await loadClaims(currentClaimsWarrantyId);
} catch (error) {
console.error('Error saving claim:', error);
showToast(error.message || 'Failed to save claim', 'error');
}
}
/**
* Delete a claim
*/
async function deleteClaim(claimId) {
try {
const response = await fetch(`/api/warranties/${currentClaimsWarrantyId}/claims/${claimId}`, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + (window.auth ? window.auth.getToken() : localStorage.getItem('auth_token')),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to delete claim');
}
showToast(window.i18next ? window.i18next.t('claims.claim_deleted_successfully') : 'Claim deleted successfully', 'success');
// Reload claims
await loadClaims(currentClaimsWarrantyId);
} catch (error) {
console.error('Error deleting claim:', error);
showToast(error.message || 'Failed to delete claim', 'error');
}
}
/**
* Close claims modal
*/
function closeClaimsModal() {
if (claimsModal) {
claimsModal.classList.remove('active');
}
currentClaimsWarrantyId = null;
currentClaims = [];
currentClaimsCanEdit = false;
}
/**
* Close claim form modal
*/
function closeClaimFormModal() {
if (claimFormModal) {
claimFormModal.classList.remove('active');
}
}
/**
* Escape HTML to prevent XSS
*/
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 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);
if (!tab) {
console.warn('validateEditTab: Could not find tab with ID:', tabId);
return false; // Or true, depending on desired behavior for missing tabs
}
let isTabValid = true;
// Get all relevant form controls within the tab
const controls = tab.querySelectorAll('input, textarea, select');
controls.forEach(control => {
// Check the native HTML5 validity state
if (!control.validity.valid) {
isTabValid = false;
control.classList.add('invalid');
// Optionally, you could add logic here to display specific messages
// or rely on browser default behavior if the form is submitted.
} else {
control.classList.remove('invalid');
}
});
// Update the tab button to show completion status
const tabBtn = document.querySelector(`.edit-tab-btn[data-tab="${tabId}"]`);
if (tabBtn) {
if (isTabValid) {
tabBtn.classList.add('completed');
} else {
tabBtn.classList.remove('completed');
}
}
return isTabValid;
}
// Add this function for secure file access
function openSecureFile(filePath) {
console.log(`[openSecureFile] Opening file: ${filePath}`);
// Get the file name from the path, handling both uploads/ prefix and direct filenames
let fileName = filePath;
if (filePath.startsWith('uploads/')) {
fileName = filePath.substring(8); // Remove 'uploads/' prefix
} else if (filePath.startsWith('/uploads/')) {
fileName = filePath.substring(9); // Remove '/uploads/' prefix
}
console.log(`[openSecureFile] Processed filename: ${fileName}`);
const token = auth.getToken();
if (!token) {
showToast(window.t('messages.login_to_access_files'), 'error');
return false;
}
// Enhanced fetch with retry logic and better error handling
const fetchWithRetry = async (url, options, retries = 2) => {
for (let i = 0; i <= retries; i++) {
try {
console.log(`[openSecureFile] Attempt ${i + 1} to fetch: ${url}`);
const response = await fetch(url, options);
if (!response.ok) {
if (response.status === 401) {
throw new Error('Authentication error. Please log in again.');
} else if (response.status === 403) {
throw new Error('You are not authorized to access this file.');
} else if (response.status === 404) {
throw new Error('File not found. It may have been deleted.');
} else {
throw new Error(`Server error: ${response.status} ${response.statusText}`);
}
}
// Check if response has content-length header
const contentLength = response.headers.get('content-length');
console.log(`[openSecureFile] Response Content-Length: ${contentLength}`);
// Convert to blob with error handling
const blob = await response.blob();
console.log(`[openSecureFile] Blob size: ${blob.size} bytes`);
// Verify blob size matches content-length if available
if (contentLength && parseInt(contentLength) !== blob.size) {
console.warn(`[openSecureFile] Content-Length mismatch: header=${contentLength}, blob=${blob.size}`);
if (i < retries) {
console.log(`[openSecureFile] Retrying due to content-length mismatch...`);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second before retry
continue;
} else {
console.error(`[openSecureFile] Final attempt failed with content-length mismatch`);
}
}
return blob;
} catch (error) {
console.error(`[openSecureFile] Attempt ${i + 1} failed:`, error);
// If this is a content-length mismatch or network error, retry
if (i < retries && (
error.message.includes('content-length') ||
error.message.includes('Failed to fetch') ||
error.name === 'TypeError'
)) {
console.log(`[openSecureFile] Retrying after error: ${error.message}`);
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); // Exponential backoff
continue;
}
throw error;
}
}
};
fetchWithRetry(`/api/secure-file/${fileName}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache'
}
})
.then(blob => {
console.log(`[openSecureFile] Successfully received blob of size: ${blob.size}`);
// Create a URL for the blob
const blobUrl = window.URL.createObjectURL(blob);
// Open in new tab
const newWindow = window.open(blobUrl, '_blank');
// Clean up the blob URL after a delay to prevent memory leaks
setTimeout(() => {
window.URL.revokeObjectURL(blobUrl);
}, 10000); // Clean up after 10 seconds
// Check if window was blocked by popup blocker
if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') {
showToast(window.t('messages.popup_blocked'), 'warning');
window.URL.revokeObjectURL(blobUrl); // Clean up immediately if blocked
}
})
.catch(error => {
console.error('Error fetching file:', error);
// Provide more specific error messages
let errorMessage = 'Error opening file';
if (error.message.includes('Authentication')) {
errorMessage = 'Authentication error. Please refresh and try again.';
} else if (error.message.includes('authorized')) {
errorMessage = 'You are not authorized to access this file.';
} else if (error.message.includes('not found')) {
errorMessage = 'File not found. It may have been deleted.';
} else if (error.message.includes('Failed to fetch') || error.name === 'TypeError') {
errorMessage = 'Network error. Please check your connection and try again.';
} else {
errorMessage = `Error opening file: ${error.message}`;
}
showToast(errorMessage, 'error');
});
return false;
}
/**
* Open a Paperless-ngx document by ID
*/
/**
* Generate document link HTML for both local and Paperless-ngx documents
*/
function generateDocumentLink(warranty, docType) {
const docConfig = {
invoice: {
localPath: warranty.invoice_path,
paperlessId: warranty.paperless_invoice_id,
url: warranty.invoice_url,
icon: 'fas fa-file-invoice',
label: 'Invoice',
className: 'invoice-link'
},
manual: {
localPath: warranty.manual_path,
paperlessId: warranty.paperless_manual_id,
url: warranty.manual_url,
icon: 'fas fa-book',
label: 'Manual',
className: 'manual-link'
},
other: {
localPath: warranty.other_document_path,
paperlessId: warranty.paperless_other_id,
url: warranty.other_document_url,
icon: 'fas fa-file-alt',
label: 'Files',
className: 'other-document-link'
},
photo: {
localPath: warranty.product_photo_path,
paperlessId: warranty.paperless_photo_id,
url: null, // Photos don't have URLs
icon: 'fas fa-image',
label: 'Photo',
className: 'photo-link'
}
};
const config = docConfig[docType];
if (!config) return '';
// Check Global View permissions for "other" documents
if (docType === 'other' && isGlobalView) {
// Get current user ID to check warranty ownership
const currentUserId = (() => {
try {
const userInfo = JSON.parse(localStorage.getItem('user_info') || '{}');
return userInfo.id;
} catch (e) {
return null;
}
})();
const isAdmin = getUserType() === 'admin';
const canViewOtherDocs = !isGlobalView || (warranty.user_id === currentUserId) || isAdmin;
// Hide "other" documents in Global View unless user owns the warranty or is admin
if (!canViewOtherDocs) {
return '';
}
}
const hasLocal = config.localPath && config.localPath !== 'null';
const hasPaperless = config.paperlessId && config.paperlessId !== null;
const hasUrl = config.url && config.url.trim() !== '';
let linksHtml = '';
if (hasLocal) {
linksHtml += `
${config.label}
`;
} else if (hasPaperless) {
const warrantyContextJson = JSON.stringify({user_id: warranty.user_id, id: warranty.id}).replace(/"/g, '"');
linksHtml += `
${config.label}
`;
}
// Add URL link if available
if (hasUrl) {
if (linksHtml) linksHtml += ' ';
linksHtml += `
${config.label} Link
`;
}
return linksHtml;
}
// 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('productPhoto')) {
document.getElementById('productPhoto').addEventListener('change', function(event) {
updateFileName(event, 'productPhoto', 'productPhotoFileName');
});
}
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');
});
}
if (document.getElementById('otherDocument')) {
document.getElementById('otherDocument').addEventListener('change', function(event) {
updateFileName(event, 'otherDocument', 'otherDocumentFileName');
});
}
// Initialize tag functionality
initTagFunctionality();
// Form submission
if (warrantyForm) {
warrantyForm.addEventListener('submit', handleFormSubmit); // Use renamed handler
}
// Initialize lifetime checkbox listener
if (isLifetimeCheckbox && warrantyDurationFields) { // Check for new container
isLifetimeCheckbox.addEventListener('change', handleLifetimeChange);
handleLifetimeChange(); // Initial check
} else {
console.error("Lifetime warranty elements or duration fields not found in add form");
}
}
// Initialize tag functionality
function initTagFunctionality() {
// This function now ONLY sets up listeners for the main "Add Warranty" form's tag interface.
// Assumes globalTagManagementModal listeners (new tag form, close buttons) are set up separately if the modal exists.
// Get main form tag elements
const mainFormTagSearch = document.getElementById('tagSearch');
const mainFormTagsList = document.getElementById('tagsList'); // Dropdown for search in main form
const mainFormManageTagsBtn = document.getElementById('manageTagsBtn'); // "Manage Tags" button in main form
const mainFormSelectedTagsContainer = document.getElementById('selectedTags'); // Container for selected tags in main form
// Skip if main form specific tag elements don't exist
if (!mainFormTagSearch || !mainFormTagsList || !mainFormManageTagsBtn || !mainFormSelectedTagsContainer) {
console.log('Main form tag UI elements (tagSearch, tagsList, manageTagsBtn, or selectedTagsContainer) not found, skipping main form tag UI initialization.');
return;
}
console.log('Initializing main form tag UI functionality (search, selection, manage button).');
// Load allTags if not already loaded (needed for search suggestions in the main form)
if (allTags.length === 0) {
loadTags(); // loadTags is async
}
mainFormTagSearch.addEventListener('focus', () => {
renderTagsList(); // Renders suggestions into mainFormTagsList based on allTags
mainFormTagsList.classList.add('show');
});
mainFormTagSearch.addEventListener('input', () => {
renderTagsList(mainFormTagSearch.value); // Filters suggestions
});
// Hide main form's tag suggestion dropdown when clicking outside
document.addEventListener('click', (e) => {
// Check if mainFormTagSearch and mainFormTagsList are still valid (e.g. not removed from DOM)
if (mainFormTagSearch && mainFormTagsList &&
!mainFormTagSearch.contains(e.target) &&
!mainFormTagsList.contains(e.target)) {
mainFormTagsList.classList.remove('show');
}
});
// "Manage Tags" button in the main form opens the global tagManagementModal
mainFormManageTagsBtn.addEventListener('click', (e) => {
e.preventDefault();
openTagManagementModal(); // This function shows the global modal
});
// Initial rendering of selected tags for the main form (if any are pre-selected or loaded)
renderSelectedTags(); // Renders into mainFormSelectedTagsContainer
}
// Function to load all tags
async function loadTags(force = false) {
console.log('[script.js] loadTags() called. Current page:', window.location.pathname);
// Check if tags are already loaded and reasonably populated
if (!force && allTags && allTags.length > 0) {
console.log('[script.js] Tags already loaded in allTags global. Skipping fetch. Count:', allTags.length);
// Optionally, re-dispatch the event if other components might need it on subsequent (though now less likely) calls
// document.dispatchEvent(new CustomEvent('allTagsLoaded', { detail: allTags }));
return;
}
try {
const token = auth.getToken();
if (!token) {
console.warn('[script.js] No token available for loadTags. User might not be authenticated yet.');
allTags = []; // Ensure allTags is empty if we can't load
return;
}
const response = await fetch('/api/tags', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) {
const errorText = await response.text();
console.error('[script.js] Failed to load tags:', response.status, errorText);
allTags = []; // Default to empty on error
return;
}
const fetchedTags = await response.json();
// Assuming fetchedTags is an array of {id, name, color, ...} as expected by other functions
allTags = fetchedTags;
console.log('[script.js] All tags loaded into global allTags variable:', allTags.length, 'tags. Sample:', allTags.slice(0,2));
// Dispatch event for any components that might be waiting for tags (e.g., Tagify instances)
document.dispatchEvent(new CustomEvent('allTagsLoaded', { detail: allTags }));
} catch (error) {
console.error('[script.js] Error in loadTags():', error);
allTags = []; // Default to empty on critical error
}
}
// 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).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 = `
${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');
}
// 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 = ` 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 = `
${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');
}
// 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);
});
}
// 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);
}
}
// Update createTag to return a Promise
function createTag(name) {
return new Promise((resolve, reject) => {
// Enhanced auth manager availability check
if (!window.auth) {
console.error('[createTag] Auth manager not available');
reject(new Error('Authentication system not ready. Please try again.'));
return;
}
// Use auth manager's getToken method instead of directly accessing localStorage
const token = window.auth.getToken();
console.log('[createTag] Debug info:', {
hasToken: !!token,
tokenLength: token ? token.length : 0,
hasUserInfo: !!localStorage.getItem('user_info'),
authManagerAvailable: !!window.auth,
isAuthenticated: window.auth.isAuthenticated(),
tokenSource: 'auth.getToken()'
});
if (!token) {
console.error('[createTag] No authentication token found');
reject(new Error('No authentication token found. Please try logging in again.'));
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) {
// Enhanced error handling to capture specific error details
return response.json().then(errorData => {
console.error('[createTag] API Error Response:', {
status: response.status,
statusText: response.statusText,
errorData: errorData
});
if (response.status === 409) {
reject(new Error(window.t('messages.tag_already_exists')));
return;
}
if (response.status === 401) {
reject(new Error('Authentication failed. Please try logging in again.'));
return;
}
if (response.status === 403) {
reject(new Error('Permission denied. You may not have access to create tags.'));
return;
}
const errorMsg = errorData?.error || errorData?.message || 'Failed to create tag';
reject(new Error(errorMsg));
}).catch(() => {
// If response body is not JSON or is empty
console.error('[createTag] Non-JSON error response:', response.status, response.statusText);
reject(new Error(`Failed to create tag (${response.status})`));
});
}
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(window.t('messages.tag_created_successfully'), 'success');
resolve(newTag);
})
.catch(error => {
console.error('Error creating tag:', error);
showToast(error.message || window.t('messages.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();
addTagManagementEventListeners();
// Show modal
tagManagementModal.classList.add('active');
}
// Render existing tags in the management modal
function renderExistingTags() {
if (!existingTagsContainer) return;
existingTagsContainer.innerHTML = '';
const wrapper = document.createElement('div');
// Ensure horizontal scroll on small screens
wrapper.className = 'table-responsive';
const table = document.createElement('table');
table.className = 'tag-management-table';
// Header
const thead = document.createElement('thead');
thead.innerHTML = `
Name
`;
table.appendChild(thead);
const tbody = document.createElement('tbody');
if (!allTags || allTags.length === 0) {
const emptyRow = document.createElement('tr');
emptyRow.innerHTML = `${window.t ? window.t('messages.no_tags_created_yet') : 'No tags created yet'}
`;
tbody.appendChild(emptyRow);
} else {
allTags.forEach(tag => {
const row = document.createElement('tr');
row.className = 'existing-tag-row';
row.dataset.id = String(tag.id);
row.innerHTML = `
${escapeHtml(tag.name)}
`;
tbody.appendChild(row);
});
}
table.appendChild(tbody);
wrapper.appendChild(table);
existingTagsContainer.appendChild(wrapper);
// Ensure delegation is attached once
addTagManagementEventListeners();
}
// Edit a tag
function editTag(row) {
// Switch row into edit mode (toggle inputs/buttons)
const nameDisplay = row.querySelector('.tag-name-display');
const nameInput = row.querySelector('.tag-name-input');
const viewActions = row.querySelector('.view-actions');
const editActions = row.querySelector('.edit-actions');
if (!nameDisplay || !nameInput || !viewActions || !editActions) return;
nameDisplay.style.display = 'none';
nameInput.style.display = 'block';
viewActions.style.display = 'none';
editActions.style.display = 'flex';
row.classList.add('editing');
}
// Update a tag
function updateTag(id, name, color) {
const token = window.auth ? window.auth.getToken() : 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(window.t('messages.failed_to_update_tag'));
}
return response.json();
})
.then(data => {
// Refresh lists to ensure consistency and exit edit mode
return loadTags(true).then(() => {
// Update local selections and warranties to reflect new tag data immediately
try {
if (data && typeof data.id !== 'undefined') {
// Update selected tags in Add Warranty flow
if (Array.isArray(selectedTags) && selectedTags.length > 0) {
selectedTags = selectedTags.map(t => t.id === data.id ? { id: data.id, name: data.name, color: data.color } : t);
}
// Update selected tags in Edit Warranty flow
if (Array.isArray(editSelectedTags) && editSelectedTags.length > 0) {
editSelectedTags = editSelectedTags.map(t => t.id === data.id ? { id: data.id, name: data.name, color: data.color } : t);
}
// Update tags inside existing warranties so cards reflect changes
if (Array.isArray(warranties) && warranties.length > 0) {
warranties.forEach(w => {
if (Array.isArray(w.tags)) {
w.tags.forEach(tag => {
if (tag.id === data.id) {
tag.name = data.name;
tag.color = data.color;
}
});
}
});
}
}
} catch (e) {
console.warn('[script.js] Non-fatal error while updating local tag state after update:', e);
}
renderExistingTags();
renderSelectedTags();
renderEditSelectedTags();
populateTagFilter();
renderWarranties(warranties);
if (document.getElementById('summary-tags')) {
updateSummary();
}
showToast(window.t('messages.tag_updated_successfully'), 'success');
});
})
.catch(error => {
console.error('Error updating tag:', error);
showToast(error.message || window.t('messages.failed_to_update_tag'), 'error');
});
}
// Centralized event delegation for tag management table
function addTagManagementEventListeners() {
if (!tagManagementModal) return;
const container = existingTagsContainer;
if (!container) return;
// Avoid attaching multiple times
if (container.dataset.tmListenersAttached === 'true') return;
container.dataset.tmListenersAttached = 'true';
container.addEventListener('click', (e) => {
const row = e.target.closest('tr.existing-tag-row');
if (!row) return;
const id = parseInt(row.dataset.id, 10);
if (Number.isNaN(id)) return;
if (e.target.closest('.edit-tag')) {
editTag(row);
return;
}
if (e.target.closest('.cancel-edit')) {
// Exit edit mode: revert inputs/buttons visibility
const nameDisplay = row.querySelector('.tag-name-display');
const nameInput = row.querySelector('.tag-name-input');
const viewActions = row.querySelector('.view-actions');
const editActions = row.querySelector('.edit-actions');
if (nameDisplay && nameInput && viewActions && editActions) {
nameDisplay.style.display = '';
nameInput.style.display = 'none';
viewActions.style.display = '';
editActions.style.display = 'none';
}
row.classList.remove('editing');
return;
}
if (e.target.closest('.save-tag')) {
const nameInput = row.querySelector('.tag-name-input');
const colorInput = row.querySelector('.tag-color-input-hidden');
const newName = (nameInput && nameInput.value ? nameInput.value.trim() : '');
const newColor = colorInput ? colorInput.value : undefined;
if (!newName) {
showToast(window.t('messages.tag_name_required'), 'error');
return;
}
updateTag(id, newName, newColor);
row.classList.remove('editing');
return;
}
if (e.target.closest('.delete-tag')) {
deleteTag(id);
return;
}
});
// Color picker change via delegation
container.addEventListener('input', (e) => {
if (e.target.matches('.tag-color-input-hidden')) {
const row = e.target.closest('tr.existing-tag-row');
const swatch = row ? row.querySelector('.tag-color-swatch') : null;
if (swatch) swatch.style.backgroundColor = e.target.value;
}
});
// Auto-save color changes without requiring Edit/Save click
container.addEventListener('change', (e) => {
if (e.target.matches('.tag-color-input-hidden')) {
const row = e.target.closest('tr.existing-tag-row');
if (!row) return;
const id = parseInt(row.dataset.id, 10);
if (Number.isNaN(id)) return;
const nameInput = row.querySelector('.tag-name-input');
const nameDisplay = row.querySelector('.tag-name-display');
const currentName = (nameInput && nameInput.style.display !== 'none' ? nameInput.value : (nameDisplay ? nameDisplay.textContent : '')).trim();
const newColor = e.target.value;
if (!currentName) {
showToast(window.t ? window.t('messages.tag_name_required') : 'Tag name is required', 'error');
return;
}
updateTag(id, currentName, newColor);
}
});
}
// 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(window.t('messages.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);
// Remove tag from warranties array
warranties.forEach(warranty => {
if (warranty.tags && Array.isArray(warranty.tags)) {
warranty.tags = warranty.tags.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
renderWarranties(warranties); // Update warranty cards to remove deleted tag
// --- END FIX ---
showToast(window.t('messages.tag_deleted_successfully'), 'success');
})
.catch(error => {
console.error('Error deleting tag:', error);
showToast(error.message || window.t('messages.failed_to_delete_tag'), 'error'); // Show specific error message
})
.finally(() => {
hideLoadingSpinner(); // Hide loading indicator
});
}
// Set up event listeners for UI controls
function setupUIEventListeners() {
// --- Global Manage Tags Button ---
const globalManageTagsBtn = document.getElementById('globalManageTagsBtn');
if (globalManageTagsBtn) {
globalManageTagsBtn.addEventListener('click', async () => {
// Ensure allTags are loaded before opening the modal
if (!allTags || allTags.length === 0) {
showLoadingSpinner();
try {
await loadTags();
} catch (error) {
console.error("Failed to load tags before opening modal:", error);
showToast(window.t('messages.could_not_load_tags'), "error");
hideLoadingSpinner();
return;
}
hideLoadingSpinner();
}
openTagManagementModal();
});
}
// 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 one of the protected modals, then DO NOTHING.
if ((modalToClose.id === 'addWarrantyModal' || modalToClose.id === 'editModal' ||
modalToClose.id === 'claimsModal' || modalToClose.id === 'claimFormModal') && e.target === modalToClose) {
return; // Ignore backdrop click for protected modals
}
// *** 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');
const vendorFilter = document.getElementById('vendorFilter'); // Added vendor filter select
const warrantyTypeFilter = document.getElementById('warrantyTypeFilter'); // Added warranty type filter select
if (searchInput) {
// Debounce logic: only apply filters after user stops typing for 300ms
let searchDebounceTimeout;
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');
}
// Debounce applyFilters
if (searchDebounceTimeout) clearTimeout(searchDebounceTimeout);
searchDebounceTimeout = setTimeout(() => {
applyFilters();
saveFilterPreferences();
}, 300);
});
}
if (clearSearchBtn) {
clearSearchBtn.addEventListener('click', () => {
if (searchInput) {
searchInput.value = '';
currentFilters.search = '';
clearSearchBtn.style.display = 'none';
searchInput.parentElement.classList.remove('active-search');
searchInput.focus();
applyFilters();
saveFilterPreferences();
}
});
}
if (statusFilter) {
statusFilter.addEventListener('change', () => {
currentFilters.status = statusFilter.value;
applyFilters();
saveFilterPreferences();
});
}
if (tagFilter) {
tagFilter.addEventListener('change', () => {
currentFilters.tag = tagFilter.value;
applyFilters();
saveFilterPreferences();
});
}
if (vendorFilter) { // Added event listener for vendor filter
vendorFilter.addEventListener('change', () => {
currentFilters.vendor = vendorFilter.value;
applyFilters();
saveFilterPreferences();
});
}
if (warrantyTypeFilter) { // Added event listener for warranty type filter
warrantyTypeFilter.addEventListener('change', () => {
currentFilters.warranty_type = warrantyTypeFilter.value;
applyFilters();
saveFilterPreferences();
});
}
if (sortBySelect) {
sortBySelect.addEventListener('change', () => {
currentFilters.sortBy = sortBySelect.value;
applyFilters();
saveFilterPreferences();
});
}
// Note: Clear Filters button handler is in index.html inline script to close popover
// 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);
// Warranty Type dropdown handlers for custom option
if (warrantyTypeInput && warrantyTypeCustomInput) {
warrantyTypeInput.addEventListener('change', () => {
if (warrantyTypeInput.value === 'other') {
warrantyTypeCustomInput.style.display = 'block';
warrantyTypeCustomInput.focus();
} else {
warrantyTypeCustomInput.style.display = 'none';
warrantyTypeCustomInput.value = '';
}
updateSummary(); // Update summary when warranty type changes
});
// Also update summary when custom warranty type changes
warrantyTypeCustomInput.addEventListener('input', updateSummary);
}
if (editWarrantyTypeInput && editWarrantyTypeCustomInput) {
editWarrantyTypeInput.addEventListener('change', () => {
if (editWarrantyTypeInput.value === 'other') {
editWarrantyTypeCustomInput.style.display = 'block';
editWarrantyTypeCustomInput.focus();
} else {
editWarrantyTypeCustomInput.style.display = 'none';
editWarrantyTypeCustomInput.value = '';
}
});
}
// Save warranty changes
const saveWarrantyBtn = document.getElementById('saveWarrantyBtn');
if (saveWarrantyBtn) {
let functionToAttachOnClick = saveWarranty; // Default to the original saveWarranty from script.js
// Check if the observer setup function from status.js is available
if (typeof window.setupSaveWarrantyObserver === 'function') {
console.log('[script.js] window.setupSaveWarrantyObserver (from status.js) was FOUND. Attempting to wrap local saveWarranty function.');
try {
// Call the observer setup function, passing it the original saveWarranty from this script.
// The observer setup function is expected to return a new function that wraps the original.
functionToAttachOnClick = window.setupSaveWarrantyObserver(saveWarranty);
// Optional: A flag to let status.js know that script.js has handled the wrapping.
// This can be useful if status.js has any fallback/polling logic to prevent double-wrapping.
window.saveWarrantyObserverAttachedByScriptJS = true;
console.log('[script.js] Local saveWarranty function has been successfully WRAPPED by the observer from status.js.');
} catch (e) {
console.error('[script.js] An error occurred while trying to wrap saveWarranty with the observer from status.js:', e);
// If an error occurs during wrapping, functionToAttachOnClick will remain the original saveWarranty.
}
} else {
console.log('[script.js] window.setupSaveWarrantyObserver (from status.js) was NOT FOUND. Using the original saveWarranty function for the button.');
}
// Add the event listener using the (potentially) wrapped function.
saveWarrantyBtn.addEventListener('click', () => {
console.log('[script.js] Save button (saveWarrantyBtn) clicked. Invoking the determined save function (functionToAttachOnClick).');
if (typeof functionToAttachOnClick === 'function') {
functionToAttachOnClick(); // Execute the determined save function
} else {
console.error('[script.js] CRITICAL: functionToAttachOnClick is not a function when save button was clicked!');
}
});
} else {
console.warn('[script.js] saveWarrantyBtn DOM element not found. Cannot attach click listener.');
}
// Confirm delete button
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
if (confirmDeleteBtn) confirmDeleteBtn.addEventListener('click', deleteWarranty);
// Confirm archive button
const confirmArchiveBtn = document.getElementById('confirmArchiveBtn');
if (confirmArchiveBtn) confirmArchiveBtn.addEventListener('click', confirmArchive);
// 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';
}
}
// Paperless upload loading functions
function showPaperlessUploadLoading(documentType) {
// Create or show the Paperless upload overlay
let overlay = document.getElementById('paperless-upload-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'paperless-upload-overlay';
overlay.innerHTML = `
Uploading to Paperless-ngx
Uploading document...
`;
document.body.appendChild(overlay);
// Add CSS styles
const style = document.createElement('style');
style.textContent = `
#paperless-upload-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
backdrop-filter: blur(2px);
}
.paperless-upload-modal {
background: var(--card-bg, #fff);
border-radius: 12px;
padding: 2rem;
max-width: 400px;
width: 90%;
text-align: center;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
border: 1px solid var(--border-color, #ddd);
}
.paperless-upload-content h3 {
margin: 1rem 0 0.5rem 0;
color: var(--text-color, #333);
font-size: 1.2rem;
}
.paperless-upload-content p {
margin: 0.5rem 0 1.5rem 0;
color: var(--text-secondary, #666);
font-size: 0.9rem;
}
.paperless-upload-spinner {
width: 40px;
height: 40px;
border: 4px solid var(--border-color, #ddd);
border-top: 4px solid var(--primary-color, #007bff);
border-radius: 50%;
animation: paperless-spin 1s linear infinite;
margin: 0 auto 1rem auto;
}
@keyframes paperless-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.paperless-upload-progress {
width: 100%;
height: 6px;
background: var(--border-color, #ddd);
border-radius: 3px;
overflow: hidden;
margin-top: 1rem;
}
.paperless-upload-progress-bar {
height: 100%;
background: linear-gradient(90deg, var(--primary-color, #007bff), var(--success-color, #28a745));
border-radius: 3px;
width: 0%;
transition: width 0.3s ease;
animation: paperless-progress-pulse 2s ease-in-out infinite;
}
@keyframes paperless-progress-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
`;
document.head.appendChild(style);
}
overlay.style.display = 'flex';
// Update status text
const statusEl = document.getElementById('paperless-upload-status');
if (statusEl) {
statusEl.textContent = `Uploading ${documentType} to Paperless-ngx...`;
}
// Animate progress bar
const progressBar = document.getElementById('paperless-progress-bar');
if (progressBar) {
progressBar.style.width = '30%';
setTimeout(() => {
progressBar.style.width = '60%';
}, 1000);
setTimeout(() => {
progressBar.style.width = '80%';
}, 2000);
}
}
function updatePaperlessUploadStatus(message, isProcessing = false) {
const statusEl = document.getElementById('paperless-upload-status');
const progressBar = document.getElementById('paperless-progress-bar');
if (statusEl) {
statusEl.textContent = message;
}
if (isProcessing && progressBar) {
progressBar.style.width = '90%';
}
}
function hidePaperlessUploadLoading() {
const overlay = document.getElementById('paperless-upload-overlay');
if (overlay) {
// Complete the progress bar first
const progressBar = document.getElementById('paperless-progress-bar');
if (progressBar) {
progressBar.style.width = '100%';
}
// Hide after a short delay to show completion
setTimeout(() => {
overlay.style.display = 'none';
// Reset progress bar for next use
if (progressBar) {
progressBar.style.width = '0%';
}
}, 500);
}
}
// Delete warranty function
function deleteWarranty() {
if (!currentWarrantyId) {
showToast(window.t('messages.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() {
console.log("[script.js] CORE saveWarranty (original from script.js) EXECUTING.");
if (!currentWarrantyId) {
showToast(window.t('messages.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 isDurationMethod = editDurationMethodRadio && editDurationMethodRadio.checked;
// Get new duration values
const years = parseInt(document.getElementById('editWarrantyDurationYears').value || 0);
const months = parseInt(document.getElementById('editWarrantyDurationMonths').value || 0);
const days = parseInt(document.getElementById('editWarrantyDurationDays').value || 0);
const exactDate = editExactExpirationDateInput ? editExactExpirationDateInput.value : '';
// Basic validation
if (!productName) {
showToast(window.t('messages.product_name_required'), 'error');
return;
}
if (!purchaseDate) {
showToast(window.t('messages.purchase_date_required'), 'error');
return;
}
// --- Updated Validation ---
if (!isLifetime) {
if (isDurationMethod) {
// Validate duration fields
if (years === 0 && months === 0 && days === 0) {
showToast(window.t('messages.warranty_duration_required'), 'error');
// Optional: focus the years input again
const yearsInput = document.getElementById('editWarrantyDurationYears');
if (yearsInput) { // Check if element exists
yearsInput.focus();
// Add invalid class to container or inputs
if (editWarrantyDurationFields) editWarrantyDurationFields.classList.add('invalid-duration');
}
return;
}
} else {
// Validate exact expiration date
if (!exactDate) {
showToast(window.t('messages.exact_expiration_date_required'), 'error');
if (editExactExpirationDateInput) editExactExpirationDateInput.focus();
return;
}
// Validate that expiration date is in the future relative to purchase date
if (purchaseDate && exactDate <= purchaseDate) {
showToast(window.t('messages.expiration_date_after_purchase_date'), 'error');
if (editExactExpirationDateInput) editExactExpirationDateInput.focus();
return;
}
}
}
// Remove invalid duration class if validation passes
if (editWarrantyDurationFields) editWarrantyDurationFields.classList.remove('invalid-duration');
// --- End Updated Validation ---
// Create form data
const formData = new FormData();
formData.append('product_name', productName);
formData.append('purchase_date', purchaseDate);
// Optional fields
let productUrl = document.getElementById('editProductUrl').value.trim();
if (productUrl) {
if (!productUrl.startsWith('http://') && !productUrl.startsWith('https://')) {
productUrl = 'https://' + productUrl;
}
formData.append('product_url', productUrl);
}
const purchasePrice = document.getElementById('editPurchasePrice').value;
const currency = document.getElementById('editCurrency').value;
if (purchasePrice) {
formData.append('purchase_price', purchasePrice);
}
if (currency) {
formData.append('currency', currency);
}
// Serial numbers (use correct name 'serial_numbers[]')
const serialInputs = document.querySelectorAll('#editSerialNumbersContainer input[name="serial_numbers[]"]');
// Clear existing before appending
formData.delete('serial_numbers[]');
serialInputs.forEach(input => {
if (input.value.trim()) {
formData.append('serial_numbers[]', input.value.trim()); // Use []
}
});
// 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([]));
}
// Add URL fields for documents (with null checks)
const editInvoiceUrlField = document.getElementById('editInvoiceUrl');
formData.append('invoice_url', editInvoiceUrlField ? editInvoiceUrlField.value || '' : '');
const editManualUrlField = document.getElementById('editManualUrl');
formData.append('manual_url', editManualUrlField ? editManualUrlField.value || '' : '');
const editOtherDocumentUrlField = document.getElementById('editOtherDocumentUrl');
formData.append('other_document_url', editOtherDocumentUrlField ? editOtherDocumentUrlField.value || '' : '');
// 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);
}
const otherDocumentFile = document.getElementById('editOtherDocument').files[0];
if (otherDocumentFile) {
formData.append('other_document', otherDocumentFile);
}
// Product photo
const productPhotoFile = document.getElementById('editProductPhoto').files[0];
if (productPhotoFile) {
formData.append('product_photo', productPhotoFile);
}
// 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');
}
const deleteOtherDocumentBtn = document.getElementById('deleteOtherDocumentBtn');
if (deleteOtherDocumentBtn && deleteOtherDocumentBtn.dataset.delete === 'true') {
formData.append('delete_other_document', 'true');
}
const deleteProductPhotoBtn = document.getElementById('deleteProductPhotoBtn');
if (deleteProductPhotoBtn && deleteProductPhotoBtn.dataset.delete === 'true') {
formData.append('delete_product_photo', 'true');
}
// --- Append is_lifetime and duration components ---
formData.append('is_lifetime', isLifetime.toString());
if (!isLifetime) {
if (isDurationMethod) {
formData.append('warranty_duration_years', years);
formData.append('warranty_duration_months', months);
formData.append('warranty_duration_days', days);
} else {
// Using exact date method
formData.append('exact_expiration_date', exactDate);
// Ensure duration fields are 0 when using exact date
formData.append('warranty_duration_years', 0);
formData.append('warranty_duration_months', 0);
formData.append('warranty_duration_days', 0);
}
} else {
// Ensure duration is 0 if lifetime
formData.append('warranty_duration_years', 0);
formData.append('warranty_duration_months', 0);
formData.append('warranty_duration_days', 0);
}
// 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 model number to form data (optional)
const editModelNumber = document.getElementById('editModelNumber');
if (editModelNumber && editModelNumber.value.trim() !== '') {
formData.append('model_number', editModelNumber.value.trim());
} else {
// Explicitly clear if empty
formData.append('model_number', '');
}
// 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
// Add warranty type to form data - handle custom type
const editWarrantyTypeInput = document.getElementById('editWarrantyType');
const editWarrantyTypeCustomInput = document.getElementById('editWarrantyTypeCustom');
let warrantyTypeValue = '';
if (editWarrantyTypeInput) {
if (editWarrantyTypeInput.value === 'other' && editWarrantyTypeCustomInput && editWarrantyTypeCustomInput.value.trim()) {
warrantyTypeValue = editWarrantyTypeCustomInput.value.trim();
} else {
warrantyTypeValue = editWarrantyTypeInput.value.trim();
}
}
formData.append('warranty_type', warrantyTypeValue);
// Add selected Paperless documents for edit form
const selectedEditPaperlessProductPhoto = document.getElementById('selectedEditPaperlessProductPhoto');
const selectedEditPaperlessInvoice = document.getElementById('selectedEditPaperlessInvoice');
const selectedEditPaperlessManual = document.getElementById('selectedEditPaperlessManual');
const selectedEditPaperlessOtherDocument = document.getElementById('selectedEditPaperlessOtherDocument');
if (selectedEditPaperlessProductPhoto && selectedEditPaperlessProductPhoto.value) {
formData.append('paperless_photo_id', selectedEditPaperlessProductPhoto.value);
}
if (selectedEditPaperlessInvoice && selectedEditPaperlessInvoice.value) {
formData.append('paperless_invoice_id', selectedEditPaperlessInvoice.value);
}
if (selectedEditPaperlessManual && selectedEditPaperlessManual.value) {
formData.append('paperless_manual_id', selectedEditPaperlessManual.value);
}
if (selectedEditPaperlessOtherDocument && selectedEditPaperlessOtherDocument.value) {
formData.append('paperless_other_id', selectedEditPaperlessOtherDocument.value);
}
// DEBUG: Log what we're sending to the backend
console.log('[DEBUG saveWarranty] Form data being sent:');
console.log('[DEBUG saveWarranty] isLifetime:', isLifetime);
console.log('[DEBUG saveWarranty] isDurationMethod:', isDurationMethod);
console.log('[DEBUG saveWarranty] exactDate:', exactDate);
console.log('[DEBUG saveWarranty] years/months/days:', years, months, days);
// Log all form data entries
for (let [key, value] of formData.entries()) {
console.log(`[DEBUG saveWarranty] FormData: ${key} = ${value}`);
}
// Get auth token
const token = localStorage.getItem('auth_token');
if (!token) {
showToast('Authentication required', 'error');
return;
}
showLoadingSpinner();
// Process Paperless-ngx uploads if enabled
processEditPaperlessNgxUploads(formData)
.then(paperlessUploads => {
// Add Paperless-ngx document IDs to form data
Object.keys(paperlessUploads).forEach(key => {
formData.append(key, paperlessUploads[key]);
});
// Send request
return 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();
// Always reload from server to ensure we get the latest data including product photo paths
console.log('Reloading warranties after edit to ensure latest data including product photos');
loadWarranties(true).then(() => {
console.log('Warranties reloaded after editing warranty');
applyFilters();
// Load secure images for the updated cards - additional call to ensure they load
setTimeout(() => {
console.log('Loading secure images for updated warranty cards');
loadSecureImages();
}, 200); // Slightly longer delay to ensure everything is rendered
// 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';
}
console.log('Warranty updated and reloaded from server');
// Auto-link any documents that were uploaded to Paperless-ngx
if ((invoiceFile || manualFile || otherDocumentFile) && currentWarrantyId) {
console.log('[Auto-Link] Starting automatic document linking after warranty update');
// Collect filename information for intelligent searching
const fileInfo = {};
if (invoiceFile) fileInfo.invoice = invoiceFile.name;
if (manualFile) fileInfo.manual = manualFile.name;
if (otherDocumentFile) fileInfo.other = otherDocumentFile.name;
setTimeout(() => {
autoLinkRecentDocuments(currentWarrantyId, ['invoice', 'manual', 'other'], 10, 10000, fileInfo);
}, 3000); // Wait 3 seconds for Paperless-ngx to process the documents
}
}).catch(error => {
console.error('Error reloading warranties after edit:', error);
});
})
.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
// Add options to the dropdown
sortedTags.forEach(tag => {
const option = document.createElement('option');
option.value = tag.id;
option.textContent = tag.name; // Reverted to textContent
// Apply background color directly for now, acknowledging potential contrast issues
// option.style.backgroundColor = tag.color; // Removed to prevent individual option background colors
tagFilter.appendChild(option);
});
}
// Function to populate vendor filter dropdown
function populateVendorFilter() {
const vendorFilterElement = document.getElementById('vendorFilter');
if (!vendorFilterElement) return;
// Clear existing options (except "All Vendors")
while (vendorFilterElement.options.length > 1) {
vendorFilterElement.remove(1);
}
// Create a Set to store unique vendor names (case-insensitive)
const uniqueVendors = new Set();
// Collect all unique, non-empty vendors from warranties
warranties.forEach(warranty => {
if (warranty.vendor && warranty.vendor.trim() !== '') {
uniqueVendors.add(warranty.vendor.trim().toLowerCase());
}
});
// Sort vendors alphabetically (after converting back to original case for display if needed, or just use lowercase)
// For simplicity, we'll sort the lowercase versions and display them as is.
// If original casing is important, a map could be used to store original values.
const sortedVendors = Array.from(uniqueVendors).sort((a, b) => a.localeCompare(b));
// Add options to the dropdown
sortedVendors.forEach(vendor => {
const option = document.createElement('option');
option.value = vendor; // Use lowercase for value consistency
// Capitalize first letter for display
option.textContent = vendor.charAt(0).toUpperCase() + vendor.slice(1);
vendorFilterElement.appendChild(option);
});
}
// Function to populate warranty type filter dropdown
function populateWarrantyTypeFilter() {
const warrantyTypeFilterElement = document.getElementById('warrantyTypeFilter');
if (!warrantyTypeFilterElement) return;
// Clear existing options (except "All Types")
while (warrantyTypeFilterElement.options.length > 1) {
warrantyTypeFilterElement.remove(1);
}
// Create a Set to store unique warranty types (case-insensitive)
const uniqueWarrantyTypes = new Set();
// Collect all unique, non-empty warranty types from warranties
warranties.forEach(warranty => {
if (warranty.warranty_type && warranty.warranty_type.trim() !== '') {
uniqueWarrantyTypes.add(warranty.warranty_type.trim().toLowerCase());
}
});
// Sort warranty types alphabetically
const sortedWarrantyTypes = Array.from(uniqueWarrantyTypes).sort((a, b) => a.localeCompare(b));
// Add options to the dropdown
sortedWarrantyTypes.forEach(warrantyType => {
const option = document.createElement('option');
option.value = warrantyType; // Use lowercase for value consistency
// Capitalize first letter for display
option.textContent = warrantyType.charAt(0).toUpperCase() + warrantyType.slice(1);
warrantyTypeFilterElement.appendChild(option);
});
}
// --- Updated Function ---
function handleLifetimeChange(event) {
const checkbox = event ? event.target : isLifetimeCheckbox;
const durationFields = warrantyDurationFields; // Use new container ID
const yearsInput = warrantyDurationYearsInput;
const monthsInput = warrantyDurationMonthsInput;
const daysInput = warrantyDurationDaysInput;
const warrantyEntryMethod = document.getElementById('warrantyEntryMethod');
if (!checkbox || !durationFields || !yearsInput || !monthsInput || !daysInput) {
console.error("Lifetime or duration elements not found in add form");
return;
}
if (checkbox.checked) {
// Hide warranty method selection and both input methods
if (warrantyEntryMethod) warrantyEntryMethod.style.display = 'none';
durationFields.style.display = 'none';
if (exactExpirationField) exactExpirationField.style.display = 'none';
// Clear and make fields not required
yearsInput.required = false;
monthsInput.required = false;
daysInput.required = false;
yearsInput.value = '';
monthsInput.value = '';
daysInput.value = '';
if (exactExpirationDateInput) exactExpirationDateInput.value = '';
} else {
// Show warranty method selection
if (warrantyEntryMethod) warrantyEntryMethod.style.display = 'block';
// Call method change handler to show appropriate fields
handleWarrantyMethodChange();
}
}
// --- Updated Function ---
function handleEditLifetimeChange(event) {
const checkbox = event ? event.target : editIsLifetimeCheckbox;
const durationFields = editWarrantyDurationFields; // Use new container ID
const yearsInput = editWarrantyDurationYearsInput;
const monthsInput = editWarrantyDurationMonthsInput;
const daysInput = editWarrantyDurationDaysInput;
const editWarrantyEntryMethod = document.getElementById('editWarrantyEntryMethod');
if (!checkbox || !durationFields || !yearsInput || !monthsInput || !daysInput) {
console.error("Lifetime or duration elements not found in edit form");
return;
}
if (checkbox.checked) {
// Hide warranty method selection and both input methods
if (editWarrantyEntryMethod) editWarrantyEntryMethod.style.display = 'none';
durationFields.style.display = 'none';
if (editExactExpirationField) editExactExpirationField.style.display = 'none';
// Clear and make fields not required
yearsInput.required = false;
monthsInput.required = false;
daysInput.required = false;
yearsInput.value = '';
monthsInput.value = '';
daysInput.value = '';
if (editExactExpirationDateInput) editExactExpirationDateInput.value = '';
} else {
// Show warranty method selection
if (editWarrantyEntryMethod) editWarrantyEntryMethod.style.display = 'block';
// Call method change handler to show appropriate fields
handleEditWarrantyMethodChange();
}
}
// --- Add this function to reset the wizard ---
function resetAddWarrantyWizard() {
console.log('Resetting Add Warranty Wizard...');
// Reset the form fields
if (warrantyForm) {
warrantyForm.reset();
// Explicitly set storage options to 'local'
const storageTypes = ['invoice', 'manual'];
storageTypes.forEach(type => {
const localRadio = document.querySelector(`input[name="${type}Storage"][value="local"]`);
if (localRadio) {
localRadio.checked = true;
}
});
}
// 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) manualFileName.textContent = '';
if (otherDocumentFileName) otherDocumentFileName.textContent = '';
// Clear Paperless document selections (only for invoice and manual)
clearPaperlessSelection('invoice');
clearPaperlessSelection('manual');
// Reset selected tags
selectedTags = [];
console.log('Resetting Add Warranty Wizard...');
// No need to reset the form again as we already did it above
// 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) manualFileName.textContent = '';
if (otherDocumentFileName) otherDocumentFileName.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
// Set currency dropdown to user's preferred currency after form reset
const preferredCurrencyCode = getCurrencyCode();
if (currencySelect && preferredCurrencyCode) {
currencySelect.value = preferredCurrencyCode;
console.log(`[Modal Open] Set currency dropdown to user preference: ${preferredCurrencyCode}`);
}
// Update currency symbols and positioning for the add form
const symbol = getCurrencySymbol();
const position = getCurrencyPosition();
updateFormCurrencyPosition(symbol, position);
// Trigger currency positioning after modal is visible
setTimeout(() => {
if (position === 'right') {
const addPriceInput = document.getElementById('purchasePrice');
const addCurrencySymbol = document.getElementById('addCurrencySymbol');
if (addPriceInput && addCurrencySymbol) {
// Force update the currency position now that modal is visible
const wrapper = addPriceInput.closest('.price-input-wrapper');
if (wrapper && wrapper.classList.contains('currency-right')) {
const updateEvent = new Event('focus');
addPriceInput.dispatchEvent(updateEvent);
const blurEvent = new Event('blur');
addPriceInput.dispatchEvent(blurEvent);
}
}
}
}, 200);
});
}
// 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, #archiveModal, [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 or editModal via this general listener for backdrop clicks ***
if ((modalToClose.id === 'addWarrantyModal' || modalToClose.id === 'editModal') && e.target === modalToClose) {
return; // Ignore backdrop clicks for the add and edit modals 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(true); // 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(true);
// 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 currentPrefix = getPreferenceKeyPrefix(); // Re-calculate prefix
const viewKeysToWatch = [
`${currentPrefix}defaultView`,
'viewPreference',
`${currentPrefix}warrantyView`,
// Add `${currentPrefix}viewPreference` if still used/relevant
`${currentPrefix}viewPreference`
];
// Check for view preference changes
if (viewKeysToWatch.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, false); // Apply change, don't re-save to API
}
} 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 === `${currentPrefix}currencySymbol` && event.newValue) {
console.log(`Storage event detected for ${currentPrefix}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 = `
`;
document.body.appendChild(notesModal);
document.getElementById('closeNotesModal').addEventListener('click', () => {
notesModal.classList.remove('active');
});
// Add event listener for Edit Warranty button
document.getElementById('editWarrantyBtn').addEventListener('click', async () => {
// Find the current warranty data from the global array
const currentWarranty = warranties.find(w => w.id === notesModalWarrantyId);
if (currentWarranty) {
console.log('[DEBUG] Edit Warranty button clicked, opening edit modal with warranty:', currentWarranty.id, 'notes:', currentWarranty.notes);
// Close the notes modal first
notesModal.classList.remove('active');
// Open the edit modal with current data
await openEditModal(currentWarranty);
} else {
showToast(window.t('messages.warranty_not_found_refresh'), 'error');
}
});
}
// 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 = '';
// Use the current content from the modal display instead of the stale notes parameter
notesModalTextarea.value = notesModalContent.textContent;
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;
}
// Frontend check for invalid duration before attempting to save notes
if (!notesModalWarrantyObj.is_lifetime &&
(parseInt(notesModalWarrantyObj.warranty_duration_years) || 0) === 0 &&
(parseInt(notesModalWarrantyObj.warranty_duration_months) || 0) === 0 &&
(parseInt(notesModalWarrantyObj.warranty_duration_days) || 0) === 0 &&
!notesModalWarrantyObj.expiration_date) {
showToast('Cannot save notes: The warranty has an invalid duration. Please edit the full warranty details to set a valid duration first.', 'error', 7000); // Longer toast duration
return; // Prevent API call
}
// 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) {
// Append duration components instead of warranty_years
formData.append('warranty_duration_years', notesModalWarrantyObj.warranty_duration_years || 0);
formData.append('warranty_duration_months', notesModalWarrantyObj.warranty_duration_months || 0);
formData.append('warranty_duration_days', notesModalWarrantyObj.warranty_duration_days || 0);
// If all duration fields are 0 but we have an expiration date, this was created with exact date method
const isExactDateWarranty = (notesModalWarrantyObj.warranty_duration_years || 0) === 0 &&
(notesModalWarrantyObj.warranty_duration_months || 0) === 0 &&
(notesModalWarrantyObj.warranty_duration_days || 0) === 0 &&
notesModalWarrantyObj.expiration_date;
if (isExactDateWarranty) {
// For exact date warranties, send the expiration date as exact_expiration_date
formData.append('exact_expiration_date', notesModalWarrantyObj.expiration_date.split('T')[0]);
}
}
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);
}
// Correctly append serial numbers
if (notesModalWarrantyObj.serial_numbers && Array.isArray(notesModalWarrantyObj.serial_numbers)) {
notesModalWarrantyObj.serial_numbers.forEach(sn => {
// Ensure sn is treated as a string before trim, and append with [] for array
if (sn && String(sn).trim() !== '') {
formData.append('serial_numbers[]', String(sn).trim());
}
});
}
// If notesModalWarrantyObj.serial_numbers is empty or not an array,
// no 'serial_numbers[]' fields will be appended, which is typically interpreted as an empty list by backends.
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');
// Update the warranty in the global warranties array immediately
const warrantyIndex = warranties.findIndex(w => w.id === notesModalWarrantyId);
if (warrantyIndex !== -1) {
warranties[warrantyIndex].notes = newNote;
}
// --- 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(true); // 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 to get user's preferred currency code
function getCurrencyCode() {
// Use the global prefix determined after auth ready
let prefix = userPreferencePrefix;
if (!prefix) {
console.warn('[getCurrencyCode] User preference prefix not set yet, defaulting prefix to user_');
prefix = 'user_';
}
console.log(`[getCurrencyCode] Using determined prefix: ${prefix}`);
// Default to USD
let currencyCode = 'USD';
// Try to get currency code from localStorage
const rawValue = localStorage.getItem(`${prefix}currencyCode`);
console.log(`[getCurrencyCode Debug] Raw value read from localStorage key '${prefix}currencyCode':`, rawValue);
if (rawValue) {
currencyCode = rawValue;
console.log(`[getCurrencyCode] Loaded currency code from individual key (${prefix}currencyCode): ${currencyCode}`);
return currencyCode;
}
// Fallback: Try to derive currency code from symbol
const symbol = getCurrencySymbol();
const symbolToCurrencyMap = {
'$': 'USD', 'ā¬': 'EUR', 'Ā£': 'GBP', 'Ā„': 'JPY', 'ā¹': 'INR', 'ā©': 'KRW',
'CHF': 'CHF', 'C$': 'CAD', 'A$': 'AUD', 'kr': 'SEK', 'zÅ': 'PLN',
'KÄ': 'CZK', 'Ft': 'HUF', 'ā½': 'RUB', 'R$': 'BRL', 'ā¦': 'NGN',
'āŖ': 'ILS', 'āŗ': 'TRY', 'āØ': 'PKR', 'ą§³': 'BDT', 'ąøæ': 'THB',
'ā«': 'VND', 'RM': 'MYR', 'S$': 'SGD', 'Rp': 'IDR', 'ā±': 'PHP',
'NT$': 'TWD', 'HK$': 'HKD', 'ā®': 'MNT', 'āø': 'KZT', 'ā¼': 'AZN',
'ā¾': 'GEL', 'ā“': 'UAH', 'NZ$': 'NZD'
};
if (symbolToCurrencyMap[symbol]) {
currencyCode = symbolToCurrencyMap[symbol];
console.log(`[getCurrencyCode] Derived currency code from symbol '${symbol}': ${currencyCode}`);
} else {
console.log(`[getCurrencyCode] Could not derive currency code from symbol '${symbol}', using default: ${currencyCode}`);
}
return currencyCode;
}
// Function to load currencies from API and populate dropdowns
async function loadCurrencies() {
try {
const token = window.auth ? window.auth.getToken() : localStorage.getItem('auth_token');
const response = await fetch('/api/currencies', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch currencies');
}
const currencies = await response.json();
// Get user's preferred currency code for default selection
const preferredCurrencyCode = getCurrencyCode();
// Populate add warranty currency dropdown
if (currencySelect) {
currencySelect.innerHTML = '';
currencies.forEach(currency => {
const option = document.createElement('option');
option.value = currency.code;
option.textContent = `${currency.code} - ${currency.name} (${currency.symbol})`;
currencySelect.appendChild(option);
});
// Set default selection to user's preferred currency
console.log(`[loadCurrencies] Preferred currency code: ${preferredCurrencyCode}`);
console.log(`[loadCurrencies] Available currency options:`, Array.from(currencySelect.options).map(opt => opt.value));
if (preferredCurrencyCode) {
// Use setTimeout to ensure DOM is fully updated
setTimeout(() => {
currencySelect.value = preferredCurrencyCode;
console.log(`[loadCurrencies] Set add warranty currency default to: ${preferredCurrencyCode}`);
console.log(`[loadCurrencies] Current selected value: ${currencySelect.value}`);
// Trigger change event to update any dependent UI
const changeEvent = new Event('change', { bubbles: true });
currencySelect.dispatchEvent(changeEvent);
}, 10);
} else {
console.log(`[loadCurrencies] No preferred currency code found, keeping default USD`);
}
}
// Populate edit warranty currency dropdown
if (editCurrencySelect) {
editCurrencySelect.innerHTML = '';
currencies.forEach(currency => {
const option = document.createElement('option');
option.value = currency.code;
option.textContent = `${currency.code} - ${currency.name} (${currency.symbol})`;
editCurrencySelect.appendChild(option);
});
}
console.log('Currencies loaded successfully');
} catch (error) {
console.error('Error loading currencies:', error);
// Fallback to USD if loading fails
if (currencySelect) {
currencySelect.innerHTML = 'USD - US Dollar ($) ';
}
if (editCurrencySelect) {
editCurrencySelect.innerHTML = 'USD - US Dollar ($) ';
}
}
}
function getCurrencySymbolByCode(currencyCode) {
const currencyMap = {
'USD': '$', 'EUR': 'ā¬', 'GBP': 'Ā£', 'JPY': 'Ā„', 'CNY': 'Ā„', 'INR': 'ā¹', 'KRW': 'ā©',
'CHF': 'CHF', 'CAD': 'C$', 'AUD': 'A$', 'SEK': 'kr', 'NOK': 'kr', 'DKK': 'kr',
'PLN': 'zÅ', 'CZK': 'KÄ', 'HUF': 'Ft', 'BGN': 'лв', 'RON': 'lei', 'HRK': 'kn',
'RUB': 'ā½', 'BRL': 'R$', 'MXN': '$', 'ARS': '$', 'CLP': '$', 'COP': '$',
'PEN': 'S/', 'VES': 'Bs', 'ZAR': 'R', 'EGP': 'Ā£', 'NGN': 'ā¦', 'KES': 'KSh',
'GHS': 'āµ', 'MAD': 'DH', 'TND': 'DT', 'AED': 'AED', 'SAR': 'SR', 'QAR': 'QR',
'KWD': 'KD', 'BHD': 'BD', 'OMR': 'OR', 'JOD': 'JD', 'LBP': 'LL', 'ILS': 'āŖ',
'TRY': 'āŗ', 'IRR': 'ļ·¼', 'PKR': 'āØ', 'BDT': 'ą§³', 'LKR': 'Rs', 'NPR': 'Rs',
'BTN': 'Nu', 'MMK': 'K', 'THB': 'ąøæ', 'VND': 'ā«', 'LAK': 'ā', 'KHR': 'į',
'MYR': 'RM', 'SGD': 'S$', 'IDR': 'Rp', 'PHP': 'ā±', 'TWD': 'NT$', 'HKD': 'HK$',
'MOP': 'MOP', 'KPW': 'ā©', 'MNT': 'ā®', 'KZT': 'āø', 'UZS': 'soŹ»m', 'TJS': 'SM',
'KGS': 'Ń', 'TMT': 'T', 'AFN': 'Ų', 'AMD': 'Ö', 'AZN': 'ā¼', 'GEL': 'ā¾',
'MDL': 'L', 'UAH': 'ā“', 'BYN': 'Br', 'RSD': 'Гин', 'MKD': 'Ген', 'ALL': 'L',
'BAM': 'KM', 'ISK': 'kr', 'FJD': 'FJ$', 'PGK': 'K', 'SBD': 'SI$', 'TOP': 'T$',
'VUV': 'VT', 'WST': 'WS$', 'XPF': 'ā£', 'NZD': 'NZ$'
};
return currencyMap[currencyCode] || currencyCode;
}
function getCurrencyPosition() {
let prefix = userPreferencePrefix;
if (!prefix) {
console.warn('[getCurrencyPosition] User preference prefix not set yet, defaulting prefix to user_');
prefix = 'user_';
}
let position = 'left'; // Default position
const rawValue = localStorage.getItem(`${prefix}currencyPosition`);
console.log(`[getCurrencyPosition] Raw value from localStorage (${prefix}currencyPosition):`, rawValue);
if (rawValue) {
position = rawValue;
console.log(`[getCurrencyPosition] Loaded position from localStorage: ${position}`);
} else {
console.log(`[getCurrencyPosition] No position found, using default: ${position}`);
}
return position;
}
function formatCurrencyHTML(amount, symbol, position) {
const formattedAmount = parseFloat(amount).toFixed(2);
if (position === 'right') {
return `${formattedAmount} ${symbol} `;
} else {
return `${symbol} ${formattedAmount} `;
}
}
function updateCurrencySymbols() {
const symbol = getCurrencySymbol();
const position = getCurrencyPosition();
console.log(`Updating currency symbols to: ${symbol}, position: ${position}`);
// Update all currency symbols
const elements = document.querySelectorAll('.currency-symbol');
console.log(`Found ${elements.length} elements with class 'currency-symbol'.`);
elements.forEach(el => {
el.textContent = symbol;
});
// Update form currency positioning
updateFormCurrencyPosition(symbol, position);
}
function updateFormCurrencyPosition(symbol, position) {
// Handle add warranty form
const addPriceWrapper = document.getElementById('addPriceInputWrapper');
const addCurrencySymbol = document.getElementById('addCurrencySymbol');
const addPriceInput = document.getElementById('purchasePrice');
if (addPriceWrapper && addCurrencySymbol) {
addCurrencySymbol.textContent = symbol;
if (position === 'right') {
addPriceWrapper.classList.add('currency-right');
// Set up dynamic positioning for right-aligned currency
if (addPriceInput) {
setupDynamicCurrencyPosition(addPriceInput, addCurrencySymbol);
}
} else {
addPriceWrapper.classList.remove('currency-right');
// Reset any dynamic positioning
if (addCurrencySymbol) {
addCurrencySymbol.style.right = '';
}
}
console.log(`Updated add form currency position: ${position}`);
}
// Handle edit warranty form
const editPriceWrapper = document.getElementById('editPriceInputWrapper');
const editCurrencySymbol = document.getElementById('editCurrencySymbol');
const editPriceInput = document.getElementById('editPurchasePrice');
if (editPriceWrapper && editCurrencySymbol) {
editCurrencySymbol.textContent = symbol;
if (position === 'right') {
editPriceWrapper.classList.add('currency-right');
// Set up dynamic positioning for right-aligned currency
if (editPriceInput) {
setupDynamicCurrencyPosition(editPriceInput, editCurrencySymbol);
}
} else {
editPriceWrapper.classList.remove('currency-right');
// Reset any dynamic positioning
if (editCurrencySymbol) {
editCurrencySymbol.style.right = '';
}
}
console.log(`Updated edit form currency position: ${position}`);
}
}
function setupDynamicCurrencyPosition(input, currencySymbol) {
if (!input || !currencySymbol) return;
function updatePosition() {
const wrapper = input.closest('.price-input-wrapper');
if (!wrapper || !wrapper.classList.contains('currency-right')) return;
// Wait for elements to be fully rendered
if (wrapper.offsetWidth === 0) {
setTimeout(updatePosition, 50);
return;
}
// Get the input value or placeholder
const text = input.value || input.placeholder || '0.00';
// Create a temporary element to measure text width
const tempSpan = document.createElement('span');
tempSpan.style.visibility = 'hidden';
tempSpan.style.position = 'absolute';
tempSpan.style.fontSize = window.getComputedStyle(input).fontSize;
tempSpan.style.fontFamily = window.getComputedStyle(input).fontFamily;
tempSpan.style.fontWeight = window.getComputedStyle(input).fontWeight;
tempSpan.style.letterSpacing = window.getComputedStyle(input).letterSpacing;
tempSpan.textContent = text;
document.body.appendChild(tempSpan);
const textWidth = tempSpan.offsetWidth;
document.body.removeChild(tempSpan);
// Calculate position: input padding + text width + small gap
const inputPaddingLeft = parseInt(window.getComputedStyle(input).paddingLeft) || 12;
const gap = 4; // Small gap between text and currency symbol
const wrapperWidth = wrapper.offsetWidth;
const rightPosition = Math.max(8, wrapperWidth - inputPaddingLeft - textWidth - gap - 20);
currencySymbol.style.right = rightPosition + 'px';
console.log(`[Dynamic Currency] Positioned currency symbol at ${rightPosition}px from right for text: "${text}"`);
}
// Update position on various events
input.addEventListener('input', updatePosition);
input.addEventListener('focus', updatePosition);
input.addEventListener('blur', updatePosition);
// Initial positioning with better timing
// Use requestAnimationFrame to ensure DOM is ready
requestAnimationFrame(() => {
updatePosition();
// Also set up additional fallback timers
setTimeout(updatePosition, 100);
setTimeout(updatePosition, 300);
});
}
// 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();
}
// Also update when currency position changes
if (e.key === `${prefix}currencyPosition`) {
console.log(`Storage event detected for ${prefix}currencyPosition. Re-rendering warranties to update currency position.`);
// Update forms immediately
const symbol = getCurrencySymbol();
const position = getCurrencyPosition();
updateFormCurrencyPosition(symbol, position);
// Re-render warranties to apply new currency position
if (typeof processAllWarranties === 'function') {
processAllWarranties();
}
}
});
// +++ NEW FUNCTION TO LOAD PREFS AND SAVE TO LOCALSTORAGE +++
async function loadAndApplyUserPreferences(isAuthenticated) { // Added isAuthenticated parameter
// 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}, isAuthenticated: ${isAuthenticated}`);
if (isAuthenticated && window.auth) { // Use passed isAuthenticated and check if window.auth exists
const token = window.auth.getToken(); // Still need token for the API call
if (!token) {
console.error('[Prefs Loader] Cannot load preferences: No auth token found, even though isAuthenticated was true.');
return; // Exit if no token
}
try {
console.log('[Prefs Loader] Fetching /api/auth/preferences with token.');
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.currency_position) {
localStorage.setItem(`${prefix}currencyPosition`, apiPrefs.currency_position);
console.log(`[Prefs Loader] Saved ${prefix}currencyPosition: ${apiPrefs.currency_position}`);
}
if (apiPrefs.default_view) {
localStorage.setItem(`${prefix}defaultView`, apiPrefs.default_view);
console.log(`[Prefs Loader] Saved ${prefix}defaultView: ${apiPrefs.default_view}`);
}
// Apply theme from API and sync to localStorage
if (apiPrefs.theme) {
const isDark = apiPrefs.theme === 'dark';
// Apply without triggering API save to avoid loops
await saveThemePreference(isDark, false);
console.log(`[Prefs Loader] Applied theme from API: ${apiPrefs.theme}`);
}
// Save filter preferences from API to localStorage
if (apiPrefs.saved_filters) {
localStorage.setItem(`${prefix}warrantyFilters`, JSON.stringify(apiPrefs.saved_filters));
console.log(`[Prefs Loader] Saved ${prefix}warrantyFilters:`, apiPrefs.saved_filters);
}
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 +++
// Warranty method change handlers
function handleWarrantyMethodChange() {
console.log('[DEBUG] handleWarrantyMethodChange called');
const isLifetime = isLifetimeCheckbox && isLifetimeCheckbox.checked;
const isDurationMethod = durationMethodRadio && durationMethodRadio.checked;
console.log('[DEBUG] isLifetime:', isLifetime, 'isDurationMethod:', isDurationMethod);
console.log('[DEBUG] Elements found:', {
warrantyDurationFields: !!warrantyDurationFields,
exactExpirationField: !!exactExpirationField,
exactExpirationDateInput: !!exactExpirationDateInput
});
if (isLifetime) {
// Hide both methods when lifetime is selected
console.log('[DEBUG] Lifetime selected, hiding both methods');
if (warrantyDurationFields) warrantyDurationFields.style.display = 'none';
if (exactExpirationField) exactExpirationField.style.display = 'none';
return;
}
if (isDurationMethod) {
console.log('[DEBUG] Duration method selected');
if (warrantyDurationFields) warrantyDurationFields.style.display = 'block';
if (exactExpirationField) exactExpirationField.style.display = 'none';
// Clear exact date when switching to duration
if (exactExpirationDateInput) exactExpirationDateInput.value = '';
} else {
console.log('[DEBUG] Exact date method selected');
if (warrantyDurationFields) warrantyDurationFields.style.display = 'none';
if (exactExpirationField) exactExpirationField.style.display = 'block';
// Clear duration fields when switching to exact date
if (warrantyDurationYearsInput) warrantyDurationYearsInput.value = '';
if (warrantyDurationMonthsInput) warrantyDurationMonthsInput.value = '';
if (warrantyDurationDaysInput) warrantyDurationDaysInput.value = '';
}
}
function handleEditWarrantyMethodChange() {
console.log('[DEBUG] handleEditWarrantyMethodChange called');
const isLifetime = editIsLifetimeCheckbox && editIsLifetimeCheckbox.checked;
const isDurationMethod = editDurationMethodRadio && editDurationMethodRadio.checked;
console.log('[DEBUG Edit] isLifetime:', isLifetime, 'isDurationMethod:', isDurationMethod);
console.log('[DEBUG Edit] Radio button states:', {
editDurationMethodRadio: editDurationMethodRadio ? editDurationMethodRadio.checked : 'element not found',
editExactDateMethodRadio: editExactDateMethodRadio ? editExactDateMethodRadio.checked : 'element not found'
});
console.log('[DEBUG Edit] Elements found:', {
editWarrantyDurationFields: !!editWarrantyDurationFields,
editExactExpirationField: !!editExactExpirationField,
editExactExpirationDateInput: !!editExactExpirationDateInput
});
if (isLifetime) {
// Hide both methods when lifetime is selected
console.log('[DEBUG Edit] Lifetime selected, hiding both methods');
if (editWarrantyDurationFields) editWarrantyDurationFields.style.display = 'none';
if (editExactExpirationField) editExactExpirationField.style.display = 'none';
return;
}
if (isDurationMethod) {
console.log('[DEBUG Edit] Duration method selected');
if (editWarrantyDurationFields) {
editWarrantyDurationFields.style.display = 'block';
console.log('[DEBUG Edit] Set duration fields to block');
}
if (editExactExpirationField) {
editExactExpirationField.style.display = 'none';
console.log('[DEBUG Edit] Set exact date field to none');
}
// Clear exact date when switching to duration
if (editExactExpirationDateInput) editExactExpirationDateInput.value = '';
} else {
console.log('[DEBUG Edit] Exact date method selected');
if (editWarrantyDurationFields) {
editWarrantyDurationFields.style.display = 'none';
console.log('[DEBUG Edit] Set duration fields to none');
}
if (editExactExpirationField) {
editExactExpirationField.style.display = 'block';
console.log('[DEBUG Edit] Set exact date field to block');
}
// Clear duration fields when switching to exact date
if (editWarrantyDurationYearsInput) editWarrantyDurationYearsInput.value = '';
if (editWarrantyDurationMonthsInput) editWarrantyDurationMonthsInput.value = '';
if (editWarrantyDurationDaysInput) editWarrantyDurationDaysInput.value = '';
}
}
// Function to calculate duration between two dates
function calculateDurationFromDates(startDate, endDate) {
if (!startDate || !endDate) return null;
try {
const start = new Date(startDate);
const end = new Date(endDate);
if (isNaN(start.getTime()) || isNaN(end.getTime())) return null;
let years = end.getFullYear() - start.getFullYear();
let months = end.getMonth() - start.getMonth();
let days = end.getDate() - start.getDate();
// Adjust for negative days
if (days < 0) {
months--;
const prevMonth = new Date(end.getFullYear(), end.getMonth(), 0);
days += prevMonth.getDate();
}
// Adjust for negative months
if (months < 0) {
years--;
months += 12;
}
return { years, months, days };
} catch (error) {
console.error('Error calculating duration:', error);
return null;
}
}
/**
* Load secure images with authentication
*/
async function loadSecureImages() {
const token = localStorage.getItem('auth_token');
if (!token) {
console.log('[DEBUG] No auth token available for secure image loading');
return;
}
// Also find images that may already have src but need to be refreshed
const secureImages = document.querySelectorAll('img.secure-image[data-secure-src]');
console.log(`[DEBUG] Found ${secureImages.length} secure images to load/refresh`);
for (const img of secureImages) {
try {
const secureUrl = img.getAttribute('data-secure-src');
console.log(`[DEBUG] Loading secure image: ${secureUrl}`);
// Clean up existing blob URL if present
const existingBlobUrl = img.getAttribute('data-blob-url');
if (existingBlobUrl) {
URL.revokeObjectURL(existingBlobUrl);
img.removeAttribute('data-blob-url');
}
const response = await fetch(secureUrl, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
img.src = blobUrl;
// Clean up blob URL when image is removed from DOM
img.addEventListener('load', () => {
console.log(`[DEBUG] Secure image loaded successfully: ${secureUrl}`);
}, { once: true });
// Store blob URL for cleanup
img.setAttribute('data-blob-url', blobUrl);
} else {
console.error(`[DEBUG] Failed to load secure image: ${secureUrl}, status: ${response.status}`);
img.style.display = 'none';
}
} catch (error) {
console.error(`[DEBUG] Error loading secure image:`, error);
img.style.display = 'none';
}
}
}
// ============================================================================
// Paperless-ngx Integration Functions
// ============================================================================
// Global variable to store Paperless-ngx enabled state
let paperlessNgxEnabled = false;
/**
* Check if Paperless-ngx integration is enabled
*/
async function checkPaperlessNgxStatus() {
try {
const token = localStorage.getItem('auth_token');
if (!token) return false;
const response = await fetch('/api/admin/settings', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const settings = await response.json();
paperlessNgxEnabled = settings.paperless_enabled === 'true';
window.paperlessNgxEnabled = paperlessNgxEnabled; // Set global variable
console.log('[Paperless-ngx] Integration status:', paperlessNgxEnabled);
return paperlessNgxEnabled;
}
} catch (error) {
console.error('[Paperless-ngx] Error checking status:', error);
}
return false;
}
/**
* Initialize Paperless-ngx integration UI
*/
async function initPaperlessNgxIntegration() {
// Check if Paperless-ngx is enabled
const isEnabled = await checkPaperlessNgxStatus();
if (isEnabled) {
// Show the info alert
const infoAlert = document.getElementById('paperlessInfoAlert');
if (infoAlert) {
infoAlert.style.display = 'block';
}
// Show storage selection options for add modal (only invoice and manual)
const storageSelections = [
'invoiceStorageSelection',
'manualStorageSelection'
];
storageSelections.forEach(id => {
const element = document.getElementById(id);
if (element) {
element.style.display = 'block';
}
});
// Show storage selection options for edit modal (only invoice and manual)
const editStorageSelections = [
'editInvoiceStorageSelection',
'editManualStorageSelection'
];
editStorageSelections.forEach(id => {
const element = document.getElementById(id);
if (element) {
element.style.display = 'block';
}
});
// Show Paperless browse sections
console.log('[Paperless-ngx] Calling togglePaperlessBrowseSections...');
togglePaperlessBrowseSections();
console.log('[Paperless-ngx] UI elements initialized and shown');
} else {
console.log('[Paperless-ngx] Integration disabled, hiding UI elements');
// Hide Paperless browse sections
console.log('[Paperless-ngx] Calling togglePaperlessBrowseSections (disabled)...');
togglePaperlessBrowseSections();
}
}
/**
* Get selected storage option for a document type
* @param {string} documentType - The document type (productPhoto, invoice, manual, otherDocument)
* @param {boolean} isEdit - Whether this is for the edit modal
* @returns {string} - 'local' or 'paperless'
*/
function getStorageOption(documentType, isEdit = false) {
// Only allow Paperless-ngx storage for invoices and manuals
const paperlessAllowedTypes = ['invoice', 'manual'];
if (!paperlessAllowedTypes.includes(documentType)) {
return 'local'; // Force local storage for productPhoto and otherDocument
}
const prefix = isEdit ? 'edit' : '';
const capitalizedType = documentType.charAt(0).toUpperCase() + documentType.slice(1);
const name = `${prefix}${capitalizedType}Storage`;
const radio = document.querySelector(`input[name="${name}"]:checked`);
return radio ? radio.value : 'local';
}
/**
* Upload file to Paperless-ngx
* @param {File} file - The file to upload
* @param {string} documentType - The type of document for tagging
* @returns {Promise} - Upload result with document ID
*/
async function uploadToPaperlessNgx(file, documentType) {
try {
const token = localStorage.getItem('auth_token');
if (!token) {
throw new Error('Authentication token not available');
}
// Show upload loading screen
showPaperlessUploadLoading(documentType);
const formData = new FormData();
formData.append('file', file);
formData.append('document_type', documentType);
formData.append('title', `Warracker ${documentType} - ${file.name}`);
// Add tags for organization
const tags = ['warracker', documentType];
formData.append('tags', tags.join(','));
console.log('[Paperless-ngx] Upload FormData contents:');
console.log(' - file:', file.name, '(' + file.size + ' bytes, ' + file.type + ')');
console.log(' - document_type:', documentType);
console.log(' - title:', `Warracker ${documentType} - ${file.name}`);
console.log(' - tags:', tags.join(','));
updatePaperlessUploadStatus('Uploading file to Paperless-ngx...');
const response = await fetch('/api/paperless/upload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
});
if (!response.ok) {
let errorMessage = 'Failed to upload to Paperless-ngx';
try {
const errorData = await response.json();
errorMessage = errorData.error || errorData.message || errorMessage;
console.error('[Paperless-ngx] Server error details:', errorData);
} catch (parseError) {
console.error('[Paperless-ngx] Could not parse error response:', parseError);
errorMessage = `HTTP ${response.status}: ${response.statusText}`;
}
hidePaperlessUploadLoading();
throw new Error(errorMessage);
}
const result = await response.json();
console.log('[Paperless-ngx] Upload successful:', result);
// Update status based on result
if (result.document_id) {
updatePaperlessUploadStatus('Document uploaded and ready!');
} else {
updatePaperlessUploadStatus('Document uploaded, processing...', true);
}
return {
success: true,
document_id: result.document_id,
message: result.message,
error: result.error // Add this
};
} catch (error) {
console.error('[Paperless-ngx] Upload error:', error);
hidePaperlessUploadLoading();
return {
success: false,
error: error.message
};
}
}
/**
* Handle warranty form submission with Paperless-ngx integration
* This extends the existing saveWarranty function
*/
async function processPaperlessNgxUploads(formData) {
if (!paperlessNgxEnabled) {
return {}; // Return empty object if not enabled
}
const uploads = {};
// Only process invoice and manual for Paperless-ngx uploads
const documentTypes = ['invoice', 'manual'];
for (const docType of documentTypes) {
// Use storage option from formData, not DOM
const storageKey = docType + 'Storage';
const storageOption = formData.get(storageKey) || 'local';
const fileInput = document.getElementById(docType);
const file = fileInput?.files[0];
console.log(`[DEBUG][processPaperlessNgxUploads] docType:`, docType, '| storageOption (from formData):', storageOption, '| file:', file);
if (storageOption === 'paperless') {
if (file) {
console.log(`[Paperless-ngx] Uploading ${docType} to Paperless-ngx`);
// Upload to Paperless-ngx
const uploadResult = await uploadToPaperlessNgx(file, docType);
console.log(`[DEBUG][processPaperlessNgxUploads] uploadResult for ${docType}:`, uploadResult);
if (uploadResult.success || (uploadResult.error && uploadResult.error.includes("duplicate") && uploadResult.document_id)) {
// Map frontend document types to database column names
const fieldMapping = {
'productPhoto': 'paperless_photo_id',
'invoice': 'paperless_invoice_id',
'manual': 'paperless_manual_id',
'otherDocument': 'paperless_other_id'
};
const dbField = fieldMapping[docType];
if (dbField && uploadResult.document_id) {
uploads[dbField] = uploadResult.document_id;
console.log(`[Paperless-ngx] ${docType} uploaded/linked successfully, ID: ${uploadResult.document_id}, stored as: ${dbField}`);
// Hide loading screen immediately for direct uploads
hidePaperlessUploadLoading();
if (uploadResult.error && uploadResult.error.includes("duplicate")) {
showToast("Duplicate document detected in Paperless-ngx. Linked to existing document.", 'info');
}
} else if (dbField && !uploadResult.document_id) {
console.log(`[Paperless-ngx] ${docType} uploaded successfully but no document ID received (async processing). Not storing reference.`);
// Don't hide loading screen yet - auto-link will handle it
updatePaperlessUploadStatus('Document processing, searching for link...', true);
}
// ALWAYS remove the file from FormData since it's been uploaded to Paperless-ngx
// This prevents the backend from also saving it locally
if (formData.has(docType)) {
formData.delete(docType);
console.log(`[Paperless-ngx] Removed ${docType} from FormData to prevent local storage`);
}
} else {
console.error(`[Paperless-ngx] Failed to upload ${docType} to Paperless-ngx:`, uploadResult.error);
throw new Error(`Failed to upload ${docType} to Paperless-ngx: ${uploadResult.error}`);
}
} else {
console.log(`[DEBUG][processPaperlessNgxUploads] No file found for ${docType} with paperless storage option.`);
}
} else {
console.log(`[DEBUG][processPaperlessNgxUploads] Skipping ${docType}, storageOption is not paperless.`);
}
}
return uploads;
}
/**
* Handle warranty edit form submission with Paperless-ngx integration
* This extends the existing edit warranty functionality
*/
async function processEditPaperlessNgxUploads(formData) {
if (!paperlessNgxEnabled) {
return {}; // Return empty object if not enabled
}
const uploads = {};
// Only process invoice and manual for Paperless-ngx uploads
const documentTypes = ['invoice', 'manual'];
for (const docType of documentTypes) {
const storageOption = getStorageOption(docType, true); // true for edit modal
if (storageOption === 'paperless') {
const fileInput = document.getElementById(`edit${docType.charAt(0).toUpperCase() + docType.slice(1)}`);
const file = fileInput?.files[0];
if (file) {
console.log(`[Paperless-ngx] Uploading ${docType} to Paperless-ngx (edit mode)`);
// Upload to Paperless-ngx
const uploadResult = await uploadToPaperlessNgx(file, docType);
if (uploadResult.success || (uploadResult.error && uploadResult.error.includes("duplicate") && uploadResult.document_id)) {
// Map frontend document types to database column names
const fieldMapping = {
'productPhoto': 'paperless_photo_id',
'invoice': 'paperless_invoice_id',
'manual': 'paperless_manual_id',
'otherDocument': 'paperless_other_id'
};
const dbField = fieldMapping[docType];
if (dbField && uploadResult.document_id) {
uploads[dbField] = uploadResult.document_id;
console.log(`[Paperless-ngx] ${docType} uploaded/linked successfully (edit), ID: ${uploadResult.document_id}, stored as: ${dbField}`);
// Hide loading screen immediately for direct uploads
hidePaperlessUploadLoading();
if (uploadResult.error && uploadResult.error.includes("duplicate")) {
showToast("Duplicate document detected in Paperless-ngx. Linked to existing document.", 'info');
}
} else if (dbField && !uploadResult.document_id) {
console.log(`[Paperless-ngx] ${docType} uploaded successfully (edit) but no document ID received (async processing). Not storing reference.`);
// Don't hide loading screen yet - auto-link will handle it
updatePaperlessUploadStatus('Document processing, searching for link...', true);
}
// ALWAYS remove the file from FormData since it's been uploaded to Paperless-ngx
// This prevents the backend from also saving it locally
// Note: In edit mode, the form field names don't have 'edit' prefix in FormData
if (formData.has(docType)) {
formData.delete(docType);
console.log(`[Paperless-ngx] Removed ${docType} from FormData to prevent local storage`);
}
} else {
throw new Error(`Failed to upload ${docType} to Paperless-ngx: ${uploadResult.error}`);
}
}
}
}
return uploads;
}
// Initialize Paperless-ngx integration when the page loads
document.addEventListener('DOMContentLoaded', function() {
// Initialize after a short delay to ensure other components are loaded
setTimeout(() => {
initPaperlessNgxIntegration();
}, 1000);
});
/**
* Debug Paperless-ngx configuration
*/
async function debugPaperlessConfiguration() {
try {
const token = localStorage.getItem('auth_token');
if (!token) {
console.error('[Paperless Debug] No auth token found');
return null;
}
console.log('[Paperless Debug] Checking configuration...');
const response = await fetch('/api/paperless/debug', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
console.error('[Paperless Debug] Debug endpoint failed:', response.status, response.statusText);
const errorText = await response.text();
console.error('[Paperless Debug] Error response:', errorText);
return null;
}
const result = await response.json();
console.log('[Paperless Debug] Configuration:', result);
return result;
} catch (error) {
console.error('[Paperless Debug] Error:', error);
return null;
}
}
/**
* Open a Paperless-ngx document either in Warracker interface or in Paperless-ngx directly
*/
async function openPaperlessDocument(paperlessId, warrantyContext = null) {
console.log(`[openPaperlessDocument] Opening Paperless document: ${paperlessId}`, warrantyContext);
// First, debug the Paperless configuration
const debugInfo = await debugPaperlessConfiguration();
if (debugInfo) {
console.log('[openPaperlessDocument] Debug info:', debugInfo);
if (!debugInfo.paperless_enabled || debugInfo.paperless_enabled === 'false') {
showToast('Paperless-ngx integration is not enabled', 'error');
return;
}
if (!debugInfo.paperless_handler_available) {
showToast('Paperless-ngx is not properly configured. Please check the settings.', 'error');
console.error('[openPaperlessDocument] Paperless handler not available');
if (debugInfo.paperless_handler_error) {
console.error('[openPaperlessDocument] Handler error:', debugInfo.paperless_handler_error);
}
return;
}
if (debugInfo.test_connection_result && !debugInfo.test_connection_result.success) {
showToast(`Paperless-ngx connection failed: ${debugInfo.test_connection_result.message || debugInfo.test_connection_result.error}`, 'error');
return;
}
}
const token = localStorage.getItem('auth_token');
if (!token) {
console.error('[openPaperlessDocument] No auth token available');
showToast('Authentication required', 'error');
return;
}
// Check user preference for viewing documents, considering Global View context
const viewInApp = await getUserPaperlessViewPreference(warrantyContext);
console.log(`[openPaperlessDocument] User preference view in app: ${viewInApp}`);
if (viewInApp) {
// Open document in Warracker interface
console.log(`[openPaperlessDocument] Opening document ${paperlessId} in Warracker interface`);
const documentUrl = `/api/paperless-file/${paperlessId}?token=${encodeURIComponent(token)}`;
const newTab = window.open(documentUrl, '_blank');
if (!newTab) {
showToast('Please allow popups to view documents', 'warning');
} else {
showToast('Opening document in Warracker...', 'info');
}
return;
}
// Default behavior: open in Paperless-ngx interface
try {
// Get the Paperless-ngx base URL
const response = await fetch('/api/paperless/url', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorText = await response.text();
console.error('[openPaperlessDocument] URL endpoint failed:', response.status, errorText);
throw new Error(`Failed to get Paperless-ngx URL: ${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.message || 'Failed to get Paperless-ngx URL');
}
// Construct the direct link to the document in Paperless-ngx
const paperlessUrl = result.url.replace(/\/$/, ''); // Remove trailing slash
const documentUrl = `${paperlessUrl}/documents/${paperlessId}/details`;
console.log(`[openPaperlessDocument] Opening Paperless-ngx document at: ${documentUrl}`);
// Open the document directly in Paperless-ngx interface
const newTab = window.open(documentUrl, '_blank');
if (!newTab) {
showToast('Please allow popups to view documents in Paperless-ngx', 'warning');
} else {
showToast('Opening document in Paperless-ngx...', 'info');
}
} catch (error) {
console.error('Error opening Paperless document:', error);
showToast(`Error opening document: ${error.message}`, 'error');
// Try to determine the base URL from debug info for fallback
if (debugInfo && debugInfo.paperless_url) {
const fallbackUrl = `${debugInfo.paperless_url.replace(/\/$/, '')}/documents/${paperlessId}/details`;
console.log(`[openPaperlessDocument] Trying fallback URL: ${fallbackUrl}`);
const fallbackTab = window.open(fallbackUrl, '_blank');
if (fallbackTab) {
showToast('Opened with fallback URL - please check if Paperless-ngx is accessible', 'warning');
}
} else {
// Last resort fallback
const genericFallbackUrl = `${window.location.protocol}//${window.location.hostname}:8000/documents/${paperlessId}/details`;
console.log(`[openPaperlessDocument] Trying generic fallback URL: ${genericFallbackUrl}`);
const genericTab = window.open(genericFallbackUrl, '_blank');
if (genericTab) {
showToast('Opened with generic fallback URL', 'warning');
}
}
}
}
/**
* Get user preference for viewing Paperless documents in app
*/
async function getUserPaperlessViewPreference(warrantyContext = null) {
// Special handling for Global View: default to view in app for other users' documents
if (warrantyContext && isGlobalView) {
const currentUserId = (() => {
try {
const userInfo = JSON.parse(localStorage.getItem('user_info') || '{}');
return userInfo.id;
} catch (e) {
return null;
}
})();
// If viewing another user's document in Global View, default to view in app
if (currentUserId && warrantyContext.user_id && warrantyContext.user_id !== currentUserId) {
console.log(`[getUserPaperlessViewPreference] Global View: Defaulting to view in app for other user's document (warranty user: ${warrantyContext.user_id}, current user: ${currentUserId})`);
return true;
}
}
// First check localStorage
const prefix = getPreferenceKeyPrefix();
const localPreference = localStorage.getItem(`${prefix}paperlessViewInApp`);
if (localPreference !== null) {
return localPreference === 'true';
}
// If not in localStorage, check API
if (window.auth && window.auth.isAuthenticated && window.auth.isAuthenticated()) {
try {
const response = await fetch('/api/auth/preferences', {
method: 'GET',
headers: {
'Authorization': `Bearer ${window.auth.getToken()}`
}
});
if (response.ok) {
const prefs = await response.json();
return prefs.paperless_view_in_app || false;
}
} catch (e) {
console.warn('Failed to load preferences from API:', e);
}
}
// Default to false (open in Paperless-ngx)
return false;
}
/**
* Debug function to test Paperless document status
*/
async function debugPaperlessDocument(paperlessId) {
console.log(`[debugPaperlessDocument] Debugging Paperless document: ${paperlessId}`);
const token = auth.getToken();
if (!token) {
console.error('[debugPaperlessDocument] No auth token available');
return;
}
try {
const response = await fetch(`/api/paperless/debug-document/${paperlessId}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorText = await response.text();
console.error(`[debugPaperlessDocument] HTTP ${response.status}: ${errorText}`);
return;
}
const debugInfo = await response.json();
console.log(`[debugPaperlessDocument] Debug info for document ${paperlessId}:`, debugInfo);
// Show debug info in a more readable format
let debugMessage = `Debug info for Paperless document ${paperlessId}:\n\n`;
debugMessage += `Document exists: ${debugInfo.document_exists}\n`;
debugMessage += `Database references: ${debugInfo.database_references?.length || 0}\n\n`;
debugMessage += 'Endpoint test results:\n';
for (const [endpoint, result] of Object.entries(debugInfo.endpoints_tested || {})) {
debugMessage += `- ${endpoint}: ${result.success ? 'SUCCESS' : 'FAILED'} (${result.status_code || result.error})\n`;
}
if (debugInfo.recent_documents && Array.isArray(debugInfo.recent_documents)) {
debugMessage += `\nDocument in recent list: ${debugInfo.document_in_recent}\n`;
debugMessage += `Recent documents: ${debugInfo.recent_documents.map(d => `${d.id}: ${d.title}`).join(', ')}\n`;
}
alert(debugMessage);
} catch (error) {
console.error('Error debugging Paperless document:', error);
alert(`Debug failed: ${error.message}`);
}
}
/**
* Clean up invalid Paperless-ngx document references
*/
async function cleanupInvalidPaperlessDocuments() {
console.log('[cleanupInvalidPaperlessDocuments] Starting cleanup...');
const token = auth.getToken();
if (!token) {
console.error('[cleanupInvalidPaperlessDocuments] No auth token available');
return;
}
try {
const response = await fetch('/api/paperless/cleanup-invalid', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorText = await response.text();
console.error(`[cleanupInvalidPaperlessDocuments] HTTP ${response.status}: ${errorText}`);
return;
}
const result = await response.json();
console.log('[cleanupInvalidPaperlessDocuments] Cleanup result:', result);
// Show result to user
let message = result.message || 'Cleanup completed';
if (result.details) {
message += `\n\nDetails:\n`;
message += `- Documents checked: ${result.details.checked}\n`;
message += `- Invalid documents found: ${result.details.invalid_found}\n`;
message += `- References cleaned up: ${result.details.cleaned_up}\n`;
if (result.details.errors && result.details.errors.length > 0) {
message += `\nErrors:\n${result.details.errors.join('\n')}`;
}
}
alert(message);
// Reload warranties to reflect changes
if (result.details && result.details.cleaned_up > 0) {
console.log('[cleanupInvalidPaperlessDocuments] Reloading warranties after cleanup...');
await loadWarranties(true);
}
} catch (error) {
console.error('Error cleaning up Paperless documents:', error);
alert(`Cleanup failed: ${error.message}`);
}
}
/**
* Search for and link a Paperless document by title
* Used when documents were uploaded with async processing and we lost the document ID
*/
async function searchAndLinkPaperlessDocument(warrantyId, documentType, searchTitle) {
try {
console.log(`[Paperless-ngx] Searching for document: ${searchTitle}`);
const response = await fetch('/api/paperless-search-and-link', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify({
warranty_id: warrantyId,
document_type: documentType,
search_title: searchTitle
})
});
const result = await response.json();
if (result.success) {
console.log(`[Paperless-ngx] Document linked successfully: ID ${result.document_id}`);
showToast('Document linked successfully! Refreshing...', 'success');
// Reload warranties to show the updated document links
setTimeout(async () => {
console.log('š [Search&Link] Reloading warranties to show updated document links...');
await loadWarranties(true); // Pass isAuthenticated parameter
// Force re-render of the warranty cards
applyFilters();
// Also reload secure images to update cloud icons
await loadSecureImages();
console.log('ā
[Search&Link] Warranties reloaded and UI updated');
}, 1000);
return { success: true, document_id: result.document_id };
} else {
console.error(`[Paperless-ngx] Failed to link document: ${result.message}`);
showToast(`Failed to link document: ${result.message}`, 'error');
return { success: false, message: result.message };
}
} catch (error) {
console.error(`[Paperless-ngx] Error searching for document:`, error);
showToast('Error searching for document', 'error');
return { success: false, message: error.message };
}
}
/**
* Automatically search for and link recently uploaded documents
* This handles the case where Paperless-ngx async processing returns task_id instead of document_id
*/
async function autoLinkRecentDocuments(warrantyId, documentTypes = ['invoice', 'manual'], maxRetries = 10, retryDelay = 10000, fileInfo = {}) {
console.log(`[Auto-Link] Starting automatic document linking for warranty ${warrantyId}`);
const token = auth.getToken();
if (!token) {
console.error('[Auto-Link] No auth token available');
return;
}
let attempt = 0;
let linkedDocuments = [];
const tryLinking = async () => {
attempt++;
console.log(`[Auto-Link] Attempt ${attempt}/${maxRetries} for warranty ${warrantyId}`);
try {
// First check if Paperless-ngx is properly configured
const debugInfo = await debugPaperlessConfiguration();
if (!debugInfo) {
console.error('[Auto-Link] Could not get Paperless debug info');
return;
}
if (!debugInfo.paperless_enabled || debugInfo.paperless_enabled === 'false') {
console.log('[Auto-Link] Paperless-ngx integration is not enabled, skipping auto-link');
return;
}
if (!debugInfo.paperless_handler_available) {
console.error('[Auto-Link] Paperless handler not available:', debugInfo.paperless_handler_error || 'Unknown error');
return;
}
if (debugInfo.test_connection_result && !debugInfo.test_connection_result.success) {
console.error('[Auto-Link] Paperless connection test failed:', debugInfo.test_connection_result.message || debugInfo.test_connection_result.error);
return;
}
console.log(`[Auto-Link] Using intelligent filename-based search. File info:`, fileInfo);
// Strategy 1: Search by exact filename (most reliable)
let candidateDocuments = [];
for (const [docType, filename] of Object.entries(fileInfo)) {
if (!documentTypes.includes(docType)) continue;
console.log(`[Auto-Link] Searching for ${docType} with filename: "${filename}"`);
// Remove file extension for searching
const baseFilename = filename.replace(/\.[^/.]+$/, '');
// Try multiple search strategies
const searchQueries = [
filename, // Exact filename with extension
baseFilename, // Filename without extension
`"${filename}"`, // Quoted exact match
`"${baseFilename}"`, // Quoted base filename
`Warracker ${docType} - ${baseFilename}` // Warracker format
];
for (const query of searchQueries) {
try {
console.log(`[Auto-Link] Trying search query: "${query}"`);
const response = await fetch(`/api/paperless/search?ordering=-created&query=${encodeURIComponent(query)}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const result = await response.json();
const docs = result.results || [];
console.log(`[Auto-Link] Query "${query}" found ${docs.length} documents`);
if (docs.length > 0) {
// Filter for recent documents (last 24 hours)
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
const recentDocs = docs.filter(doc => {
try {
const docDate = new Date(doc.created);
return docDate > oneDayAgo;
} catch {
return true; // Include if we can't parse the date
}
});
if (recentDocs.length > 0) {
console.log(`[Auto-Link] Found ${recentDocs.length} recent documents for ${docType}`);
candidateDocuments.push({
docType,
filename,
documents: recentDocs,
searchQuery: query
});
break; // Found documents, no need to try other queries for this file
} else if (docs.length > 0) {
// If no recent docs but we found some documents, include them anyway
console.log(`[Auto-Link] Found ${docs.length} older documents for ${docType}, including them anyway`);
candidateDocuments.push({
docType,
filename,
documents: docs.slice(0, 3), // Take up to 3 most recent
searchQuery: query
});
break;
}
}
}
} catch (error) {
console.error(`[Auto-Link] Error searching with query "${query}":`, error);
}
}
}
// Strategy 2: Fallback to Warracker tag search if filename search fails
if (candidateDocuments.length === 0) {
console.log('[Auto-Link] No documents found by filename, trying Warracker tag search...');
const response = await fetch('/api/paperless/search?ordering=-created&created__gte=' +
new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const searchResult = await response.json();
let recentDocs = searchResult.results || [];
// Filter for Warracker documents
const warrackerDocs = recentDocs.filter(doc =>
doc.title && doc.title.includes('Warracker')
);
console.log(`[Auto-Link] Found ${warrackerDocs.length} Warracker documents from last 2 hours`);
// Group by document type
for (const docType of documentTypes) {
const typeDocs = warrackerDocs.filter(doc =>
doc.title && doc.title.includes(docType)
);
if (typeDocs.length > 0) {
candidateDocuments.push({
docType,
filename: fileInfo[docType] || `${docType} document`,
documents: typeDocs,
searchQuery: `Warracker ${docType}`
});
}
}
}
}
// Debug: Show what candidate documents we found
if (candidateDocuments.length > 0) {
console.log('[Auto-Link] Candidate documents found:');
candidateDocuments.forEach(candidate => {
console.log(` ${candidate.docType}: ${candidate.documents.length} documents found with query "${candidate.searchQuery}"`);
candidate.documents.forEach((doc, i) => {
console.log(` ${i+1}. ID: ${doc.id}, Title: "${doc.title}", Created: ${doc.created}`);
});
});
}
// Try to link the best candidate for each document type
for (const candidate of candidateDocuments) {
// Use the most recent document (first in the ordered list)
const doc = candidate.documents[0];
console.log(`[Auto-Link] Attempting to link ${candidate.docType}: ${doc.title} (ID: ${doc.id})`);
try {
const linkResponse = await fetch('/api/paperless-search-and-link', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify({
warranty_id: warrantyId,
document_type: candidate.docType,
search_title: doc.title.replace('Warracker ' + candidate.docType + ' - ', '')
})
});
const linkResult = await linkResponse.json();
if (linkResult.success) {
console.log(`[Auto-Link] Successfully linked ${candidate.docType}: ${doc.title}`);
linkedDocuments.push({
type: candidate.docType,
title: doc.title,
id: doc.id,
filename: candidate.filename
});
} else {
console.log(`[Auto-Link] Failed to link ${candidate.docType}: ${linkResult.message}`);
}
} catch (error) {
console.error(`[Auto-Link] Error linking ${candidate.docType}:`, error);
}
}
// If we found and linked documents, we're done
if (linkedDocuments.length > 0) {
console.log(`[Auto-Link] Successfully linked ${linkedDocuments.length} documents:`, linkedDocuments);
// Update loading screen to show success
updatePaperlessUploadStatus('Documents linked successfully!');
// Show success message with filenames
const docInfo = linkedDocuments.map(d => `${d.type} (${d.filename || d.title})`).join(', ');
showToast(`Automatically linked ${linkedDocuments.length} document(s): ${docInfo}`, 'success');
// Reload warranties to show the updated document links
setTimeout(async () => {
console.log('š [Auto-Link] Reloading warranties to show updated document links...');
await loadWarranties(true); // Pass isAuthenticated parameter
// Force re-render of the warranty cards
applyFilters();
// Also reload secure images to update cloud icons
await loadSecureImages();
console.log('ā
[Auto-Link] Warranties reloaded and UI updated');
// Hide loading screen after successful completion
hidePaperlessUploadLoading();
}, 1000);
return true;
}
// If no documents found and we have retries left, try again
if (attempt < maxRetries) {
console.log(`[Auto-Link] No documents found, retrying in ${retryDelay}ms...`);
updatePaperlessUploadStatus(`Searching for documents (attempt ${attempt + 1}/${maxRetries})...`, true);
setTimeout(tryLinking, retryDelay);
} else {
console.log(`[Auto-Link] No documents found after ${maxRetries} attempts`);
updatePaperlessUploadStatus('Document uploaded but could not auto-link');
showToast('Document uploaded to Paperless-ngx but could not be automatically linked. You can manually link it later.', 'warning');
// Hide loading screen after failed auto-link
setTimeout(() => {
hidePaperlessUploadLoading();
}, 2000);
}
} catch (error) {
console.error(`[Auto-Link] Error in attempt ${attempt}:`, error);
if (attempt < maxRetries) {
console.log(`[Auto-Link] Retrying in ${retryDelay}ms...`);
updatePaperlessUploadStatus(`Error occurred, retrying (${attempt + 1}/${maxRetries})...`, true);
setTimeout(tryLinking, retryDelay);
} else {
updatePaperlessUploadStatus('Upload completed with errors');
showToast('Document uploaded but auto-linking failed due to errors. You can manually link it later.', 'warning');
// Hide loading screen after final error
setTimeout(() => {
hidePaperlessUploadLoading();
}, 2000);
}
}
};
// Start the linking process
tryLinking();
}
// Make debug and cleanup functions available globally for console testing
window.debugPaperlessDocument = debugPaperlessDocument;
window.cleanupInvalidPaperlessDocuments = cleanupInvalidPaperlessDocuments;
window.searchAndLinkPaperlessDocument = searchAndLinkPaperlessDocument;
window.autoLinkRecentDocuments = autoLinkRecentDocuments;
// Helper function to manually link a specific document by title
window.manualLinkDocument = async function(warrantyId, documentType, titleSearchTerm) {
console.log(`š Manually linking document for warranty ${warrantyId}`);
console.log(` Document type: ${documentType}`);
console.log(` Searching for title containing: "${titleSearchTerm}"`);
const token = auth.getToken();
if (!token) {
console.error('ā No auth token available');
return;
}
try {
// Search for documents containing the search term
const response = await fetch(`/api/paperless/search?ordering=-created&query=${encodeURIComponent(titleSearchTerm)}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
console.error(`ā Search failed: ${response.status}`);
return;
}
const result = await response.json();
const docs = result.results || [];
console.log(`š Found ${docs.length} documents matching "${titleSearchTerm}"`);
if (docs.length === 0) {
console.log('ā No documents found. The document might still be processing in Paperless-ngx.');
return;
}
// Show all matching documents
docs.forEach((doc, index) => {
console.log(` ${index + 1}. ID: ${doc.id}, Title: "${doc.title}", Created: ${doc.created}`);
});
// Try to link the first matching document
const docToLink = docs[0];
console.log(`š Attempting to link document ID ${docToLink.id}: "${docToLink.title}"`);
const linkResponse = await fetch('/api/paperless-search-and-link', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
warranty_id: warrantyId,
document_type: documentType,
search_title: docToLink.title.replace('Warracker ' + documentType + ' - ', '')
})
});
const linkResult = await linkResponse.json();
if (linkResult.success) {
console.log(`ā
Successfully linked ${documentType}: ${docToLink.title}`);
showToast(`Document linked successfully: ${documentType}`, 'success');
// Reload warranties to show the updated document links
setTimeout(async () => {
console.log('š Reloading warranties to show updated document links...');
await loadWarranties(true); // Pass isAuthenticated parameter
// Force re-render of the warranty cards
applyFilters();
// Also reload secure images to update cloud icons
await loadSecureImages();
console.log('ā
Warranties reloaded and UI updated');
}, 1000);
} else {
console.error(`ā Failed to link ${documentType}: ${linkResult.message}`);
}
return docToLink;
} catch (error) {
console.error('ā Error in manual linking:', error);
}
};
// Helper function to search for documents in Paperless-ngx (debug function)
window.debugSearchPaperlessDocuments = async function(searchTerm = 'Warracker', limit = 10) {
console.log(`š Searching for documents containing: "${searchTerm}"`);
const token = auth.getToken();
if (!token) {
console.error('ā No auth token available');
return;
}
try {
const response = await fetch(`/api/paperless/search?ordering=-created&query=${encodeURIComponent(searchTerm)}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
console.error(`ā Search failed: ${response.status}`);
return;
}
const result = await response.json();
const docs = result.results || [];
console.log(`š Found ${docs.length} documents:`);
docs.slice(0, limit).forEach((doc, index) => {
console.log(` ${index + 1}. ID: ${doc.id}, Title: "${doc.title}", Created: ${doc.created}`);
});
if (docs.length > limit) {
console.log(` ... and ${docs.length - limit} more documents`);
}
return docs;
} catch (error) {
console.error('ā Error searching documents:', error);
}
};
// Helper function for users to debug Paperless-ngx configuration
window.debugPaperlessSetup = async function() {
console.log('š Debugging Paperless-ngx setup...');
const debugInfo = await debugPaperlessConfiguration();
if (!debugInfo) {
console.error('ā Could not get debug information');
return;
}
console.log('š Paperless-ngx Configuration:');
console.log(` Enabled: ${debugInfo.paperless_enabled}`);
console.log(` URL: ${debugInfo.paperless_url || 'Not set'}`);
console.log(` API Token Set: ${debugInfo.paperless_api_token_set}`);
console.log(` Handler Available: ${debugInfo.paperless_handler_available}`);
if (debugInfo.paperless_handler_error) {
console.error(` Handler Error: ${debugInfo.paperless_handler_error}`);
}
if (debugInfo.test_connection_result) {
console.log(` Connection Test: ${debugInfo.test_connection_result.success ? 'ā
Success' : 'ā Failed'}`);
console.log(` Message: ${debugInfo.test_connection_result.message || debugInfo.test_connection_result.error}`);
}
// Provide recommendations
console.log('\nš” Recommendations:');
if (!debugInfo.paperless_enabled || debugInfo.paperless_enabled === 'false') {
console.log(' - Enable Paperless-ngx integration in Settings');
}
if (!debugInfo.paperless_url) {
console.log(' - Set Paperless-ngx URL in Settings (e.g., http://paperless:8000)');
}
if (!debugInfo.paperless_api_token_set) {
console.log(' - Set API token in Settings (generate from Paperless-ngx ā Settings ā API Tokens)');
}
if (debugInfo.test_connection_result && !debugInfo.test_connection_result.success) {
console.log(' - Check if Paperless-ngx is running and accessible');
console.log(' - Verify URL and API token are correct');
console.log(' - Check network connectivity between Warracker and Paperless-ngx');
}
return debugInfo;
};
// ===== PAPERLESS DOCUMENT BROWSER FUNCTIONALITY =====
// Global variables for paperless browser
let currentPaperlessDocuments = [];
let selectedPaperlessDocument = null;
let currentPaperlessPage = 1;
let totalPaperlessPages = 1;
let currentDocumentType = '';
let paperlessSearchQuery = '';
/**
* Open the Paperless document browser modal
* @param {string} documentType - Type of document being selected (invoice, manual, product_photo, other_document)
*/
function openPaperlessBrowser(documentType) {
currentDocumentType = documentType;
selectedPaperlessDocument = null;
// Reset pagination state
currentPaperlessPage = 1;
totalPaperlessPages = 1;
paperlessSearchQuery = '';
// Show the modal
const modal = document.getElementById('paperlessBrowserModal');
modal.classList.add('active');
// Reset search and filters
document.getElementById('paperlessSearchInput').value = '';
document.getElementById('paperlessTypeFilter').value = '';
document.getElementById('paperlessTagFilter').value = '';
// Load documents
loadAllPaperlessDocuments();
// Load tags for filter
loadPaperlessTags();
// Hide select button initially
const selectBtn = document.getElementById('selectPaperlessDocBtn');
if (selectBtn) {
selectBtn.style.display = 'none';
}
}
/**
* Load all Paperless documents
*/
async function loadAllPaperlessDocuments() {
try {
showPaperlessLoading();
const params = new URLSearchParams();
const offset = (currentPaperlessPage - 1) * 25;
params.append('limit', '25');
params.append('offset', offset.toString());
const response = await fetch(`/api/paperless/search?${params.toString()}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
currentPaperlessDocuments = data.results || [];
totalPaperlessPages = Math.ceil(data.count / 25) || 1;
renderPaperlessDocuments();
updatePaperlessPagination();
} catch (error) {
console.error('Error loading Paperless documents:', error);
showPaperlessError('Failed to load documents from Paperless-ngx');
}
}
/**
* Search Paperless documents
*/
async function searchPaperlessDocuments() {
const searchInput = document.getElementById('paperlessSearchInput');
const typeFilter = document.getElementById('paperlessTypeFilter');
const tagFilter = document.getElementById('paperlessTagFilter');
paperlessSearchQuery = searchInput.value.trim();
try {
showPaperlessLoading();
const params = new URLSearchParams();
if (paperlessSearchQuery) {
params.append('query', paperlessSearchQuery);
}
if (typeFilter.value) {
params.append('document_type', typeFilter.value);
}
if (tagFilter.value) {
params.append('tags__id__in', tagFilter.value);
}
// Add pagination
const offset = (currentPaperlessPage - 1) * 25;
params.append('limit', '25');
params.append('offset', offset.toString());
const response = await fetch(`/api/paperless/search?${params.toString()}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
currentPaperlessDocuments = data.results || [];
totalPaperlessPages = Math.ceil(data.count / 25) || 1;
renderPaperlessDocuments();
updatePaperlessPagination();
} catch (error) {
console.error('Error searching Paperless documents:', error);
showPaperlessError('Failed to search documents');
}
}
/**
* Load Paperless tags for filter dropdown
*/
async function loadPaperlessTags() {
try {
const response = await fetch('/api/paperless/tags', {
method: 'GET',
headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
const tagFilter = document.getElementById('paperlessTagFilter');
// Clear existing options except the first one
tagFilter.innerHTML = 'All Tags ';
// Add tag options
if (data.results) {
data.results.forEach(tag => {
const option = document.createElement('option');
option.value = tag.id;
option.textContent = tag.name;
tagFilter.appendChild(option);
});
}
}
} catch (error) {
console.error('Error loading Paperless tags:', error);
}
}
/**
* Render the list of Paperless documents
*/
function renderPaperlessDocuments() {
const container = document.getElementById('paperlessDocumentsList');
if (currentPaperlessDocuments.length === 0) {
container.innerHTML = `
No documents found
Try adjusting your search terms or filters.
`;
return;
}
const documentsHtml = currentPaperlessDocuments.map(doc => {
const createdDate = new Date(doc.created).toLocaleDateString();
const fileType = doc.mime_type || 'Unknown';
const tags = doc.tags || [];
return `
${escapeHtml(doc.title)}
${createdDate}
${fileType}
${doc.correspondent ? ` ${escapeHtml(doc.correspondent)} ` : ''}
${tags.length > 0 ? `
${tags.map(tag => `${escapeHtml(tag)} `).join('')}
` : ''}
`;
}).join('');
container.innerHTML = documentsHtml;
}
/**
* Select a Paperless document
* @param {number} documentId - ID of the document to select
*/
function selectPaperlessDocument(documentId) {
// Remove previous selection
document.querySelectorAll('.paperless-document-item').forEach(item => {
item.classList.remove('selected');
});
// Add selection to clicked item
const selectedItem = document.querySelector(`[data-id="${documentId}"]`);
if (selectedItem) {
selectedItem.classList.add('selected');
selectedPaperlessDocument = currentPaperlessDocuments.find(doc => doc.id === documentId);
// Show select button
const selectBtn = document.getElementById('selectPaperlessDocBtn');
selectBtn.style.display = 'inline-block';
selectBtn.onclick = () => confirmPaperlessSelection();
}
}
/**
* Confirm the selection of a Paperless document
*/
function confirmPaperlessSelection() {
if (!selectedPaperlessDocument) return;
// Update the UI to show the selected document
updatePaperlessSelectionUI();
// Close the modal
closePaperlessBrowser();
}
/**
* Update the UI to show the selected Paperless document
*/
function updatePaperlessSelectionUI() {
if (!selectedPaperlessDocument || !currentDocumentType) return;
const docName = selectedPaperlessDocument.title;
const docId = selectedPaperlessDocument.id;
// Map document types to their UI elements (only for invoice and manual)
const typeMapping = {
'invoice': {
selectedDiv: 'selectedInvoiceFromPaperless',
hiddenInput: 'selectedPaperlessInvoice'
},
'manual': {
selectedDiv: 'selectedManualFromPaperless',
hiddenInput: 'selectedPaperlessManual'
},
// Edit modal versions
'edit_invoice': {
selectedDiv: 'selectedEditInvoiceFromPaperless',
hiddenInput: 'selectedEditPaperlessInvoice'
},
'edit_manual': {
selectedDiv: 'selectedEditManualFromPaperless',
hiddenInput: 'selectedEditPaperlessManual'
}
};
const mapping = typeMapping[currentDocumentType];
if (!mapping) return;
// Show the selected document
const selectedDiv = document.getElementById(mapping.selectedDiv);
if (selectedDiv) {
selectedDiv.style.display = 'flex';
selectedDiv.querySelector('.selected-doc-name').textContent = docName;
}
// Create or update hidden input to store the document ID
let hiddenInput = document.getElementById(mapping.hiddenInput);
if (!hiddenInput) {
hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.id = mapping.hiddenInput;
hiddenInput.name = mapping.hiddenInput;
document.body.appendChild(hiddenInput);
}
hiddenInput.value = docId;
}
/**
* Clear the Paperless document selection
* @param {string} documentType - Type of document to clear
*/
function clearPaperlessSelection(documentType) {
const typeMapping = {
'invoice': {
selectedDiv: 'selectedInvoiceFromPaperless',
hiddenInput: 'selectedPaperlessInvoice'
},
'manual': {
selectedDiv: 'selectedManualFromPaperless',
hiddenInput: 'selectedPaperlessManual'
},
// Edit modal versions
'edit_invoice': {
selectedDiv: 'selectedEditInvoiceFromPaperless',
hiddenInput: 'selectedEditPaperlessInvoice'
},
'edit_manual': {
selectedDiv: 'selectedEditManualFromPaperless',
hiddenInput: 'selectedEditPaperlessManual'
}
};
const mapping = typeMapping[documentType];
if (!mapping) return;
// Hide the selected document display
const selectedDiv = document.getElementById(mapping.selectedDiv);
if (selectedDiv) {
selectedDiv.style.display = 'none';
}
// Clear the hidden input
const hiddenInput = document.getElementById(mapping.hiddenInput);
if (hiddenInput) {
hiddenInput.value = '';
}
}
/**
* Close the Paperless browser modal
*/
function closePaperlessBrowser() {
const modal = document.getElementById('paperlessBrowserModal');
modal.classList.remove('active');
// Reset state
selectedPaperlessDocument = null;
currentDocumentType = '';
// Hide select button
const selectBtn = document.getElementById('selectPaperlessDocBtn');
selectBtn.style.display = 'none';
}
/**
* Change page in Paperless document browser
* @param {number} direction - Direction to change page (-1 for previous, 1 for next)
*/
function changePage(direction) {
const newPage = currentPaperlessPage + direction;
if (newPage < 1 || newPage > totalPaperlessPages) return;
currentPaperlessPage = newPage;
// Check if we have any active filters
const searchInput = document.getElementById('paperlessSearchInput');
const typeFilter = document.getElementById('paperlessTypeFilter');
const tagFilter = document.getElementById('paperlessTagFilter');
const hasFilters = (searchInput && searchInput.value.trim()) ||
(typeFilter && typeFilter.value) ||
(tagFilter && tagFilter.value);
if (hasFilters) {
searchPaperlessDocuments();
} else {
loadAllPaperlessDocuments();
}
}
/**
* Update pagination controls
*/
function updatePaperlessPagination() {
const paginationDiv = document.getElementById('paperlessPagination');
const prevBtn = document.getElementById('prevPageBtn');
const nextBtn = document.getElementById('nextPageBtn');
const pageInfo = document.getElementById('pageInfo');
if (totalPaperlessPages <= 1) {
paginationDiv.style.display = 'none';
return;
}
paginationDiv.style.display = 'flex';
prevBtn.disabled = currentPaperlessPage <= 1;
nextBtn.disabled = currentPaperlessPage >= totalPaperlessPages;
pageInfo.textContent = `Page ${currentPaperlessPage} of ${totalPaperlessPages}`;
}
/**
* Show loading state in Paperless browser
*/
function showPaperlessLoading() {
const container = document.getElementById('paperlessDocumentsList');
container.innerHTML = `
Loading documents...
`;
}
/**
* Show error message in Paperless browser
* @param {string} message - Error message to display
*/
function showPaperlessError(message) {
const container = document.getElementById('paperlessDocumentsList');
container.innerHTML = `
Error
${escapeHtml(message)}
`;
}
/**
* Show/hide Paperless browse sections based on Paperless-ngx availability
*/
function togglePaperlessBrowseSections() {
const paperlessEnabled = window.paperlessNgxEnabled || false;
console.log('[togglePaperlessBrowseSections] Paperless enabled:', paperlessEnabled);
// List of paperless browse section IDs (only for invoice and manual)
const browseSectionIds = [
'invoicePaperlessBrowse',
'manualPaperlessBrowse',
'editInvoicePaperlessBrowse',
'editManualPaperlessBrowse'
];
let foundSections = 0;
browseSectionIds.forEach(id => {
const section = document.getElementById(id);
if (section) {
foundSections++;
section.style.display = paperlessEnabled ? 'block' : 'none';
console.log(`[togglePaperlessBrowseSections] ${id}: ${paperlessEnabled ? 'shown' : 'hidden'}`);
} else {
console.warn(`[togglePaperlessBrowseSections] Section not found: ${id}`);
}
});
console.log(`[togglePaperlessBrowseSections] Found ${foundSections} of ${browseSectionIds.length} sections`);
}
// Initialize Paperless browser functionality when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
// Add event listeners for modal close buttons
const paperlessModal = document.getElementById('paperlessBrowserModal');
if (paperlessModal) {
// Close on backdrop click
paperlessModal.addEventListener('click', function(e) {
if (e.target === paperlessModal) {
closePaperlessBrowser();
}
});
// Close on close button click
const closeBtn = paperlessModal.querySelector('.close-btn');
if (closeBtn) {
closeBtn.addEventListener('click', closePaperlessBrowser);
}
// Close on cancel button click - but only the Cancel button, not all secondary buttons
const cancelBtn = paperlessModal.querySelector('.modal-footer .btn-secondary');
if (cancelBtn) {
cancelBtn.addEventListener('click', closePaperlessBrowser);
}
}
// Add event listeners for search and filters
const searchInput = document.getElementById('paperlessSearchInput');
if (searchInput) {
// Search on Enter key
searchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
currentPaperlessPage = 1; // Reset to first page
searchPaperlessDocuments();
}
});
// Search on input change (with debounce)
let searchTimeout;
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
currentPaperlessPage = 1; // Reset to first page
searchPaperlessDocuments();
}, 500);
});
}
// Add event listeners for filter dropdowns
const typeFilter = document.getElementById('paperlessTypeFilter');
if (typeFilter) {
typeFilter.addEventListener('change', function() {
currentPaperlessPage = 1; // Reset to first page
searchPaperlessDocuments();
});
}
const tagFilter = document.getElementById('paperlessTagFilter');
if (tagFilter) {
tagFilter.addEventListener('change', function() {
currentPaperlessPage = 1; // Reset to first page
searchPaperlessDocuments();
});
}
// Add event listener for search button
const searchBtn = document.getElementById('paperlessSearchBtn');
if (searchBtn) {
searchBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
currentPaperlessPage = 1; // Reset to first page
searchPaperlessDocuments();
});
}
// Add event listener for "Show All" button
const showAllBtn = document.getElementById('paperlessShowAllBtn');
if (showAllBtn) {
showAllBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
// Clear all filters
if (searchInput) searchInput.value = '';
if (typeFilter) typeFilter.value = '';
if (tagFilter) tagFilter.value = '';
// Reset page and load all documents
currentPaperlessPage = 1;
paperlessSearchQuery = '';
loadAllPaperlessDocuments();
});
}
// Toggle browse sections will be handled by initPaperlessNgxIntegration()
// togglePaperlessBrowseSections();
});
// Make functions available globally
window.openPaperlessBrowser = openPaperlessBrowser;
window.loadAllPaperlessDocuments = loadAllPaperlessDocuments;
window.selectPaperlessDocument = selectPaperlessDocument;
window.clearPaperlessSelection = clearPaperlessSelection;
window.changePage = changePage;
window.openPaperlessDocument = openPaperlessDocument;
window.openSecureFile = openSecureFile;
// ===== END PAPERLESS BROWSER FUNCTIONALITY =====