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 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');
// --- 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();
if (result.enabled && adminViewSwitcher) {
// Global view is enabled, show view switcher
adminViewSwitcher.style.display = 'flex';
// Add event listeners for view buttons
if (personalViewBtn) {
personalViewBtn.addEventListener('click', () => switchToPersonalView());
}
if (globalViewBtn) {
globalViewBtn.addEventListener('click', () => switchToGlobalView());
}
// Load and apply saved view scope preference
const savedScope = loadViewScopePreference();
if (savedScope === 'global') {
// Apply global view silently without saving preference again
isGlobalView = true;
personalViewBtn.classList.remove('active');
globalViewBtn.classList.add('active');
updateWarrantiesPanelTitle(true);
} else {
// Apply personal view (default)
isGlobalView = false;
personalViewBtn.classList.add('active');
globalViewBtn.classList.remove('active');
updateWarrantiesPanelTitle(false);
}
} else if (adminViewSwitcher) {
// Global view is disabled, hide view switcher
adminViewSwitcher.style.display = 'none';
// If currently in global view, switch back to personal view
if (isGlobalView) {
isGlobalView = false;
updateWarrantiesPanelTitle(false);
// Reload warranties
await loadWarranties(true);
applyFilters();
}
}
} else {
console.error('Failed to check global view status');
// Default to showing view switcher if check fails
if (adminViewSwitcher) {
adminViewSwitcher.style.display = 'flex';
// Add event listeners for view buttons
if (personalViewBtn) {
personalViewBtn.addEventListener('click', () => switchToPersonalView());
}
if (globalViewBtn) {
globalViewBtn.addEventListener('click', () => switchToGlobalView());
}
}
}
} catch (error) {
console.error('Error checking global view status:', error);
// Default to showing view switcher if check fails
if (adminViewSwitcher) {
adminViewSwitcher.style.display = 'flex';
// Add event listeners for view buttons
if (personalViewBtn) {
personalViewBtn.addEventListener('click', () => switchToPersonalView());
}
if (globalViewBtn) {
globalViewBtn.addEventListener('click', () => switchToGlobalView());
}
}
}
}
/**
* 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);
// 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);
// 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;
}
}
// 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("Could not load tags. Please try again.", "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);
} 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';
// 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 (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);
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 summaryInvoice = document.getElementById('summary-invoice');
if (summaryInvoice) {
summaryInvoice.textContent = invoiceFile ?
invoiceFile.name : 'No file selected';
}
const manualFile = document.getElementById('manual')?.files[0];
const summaryManual = document.getElementById('summary-manual');
if (summaryManual) {
summaryManual.textContent = manualFile ?
manualFile.name : 'No file selected';
}
const otherDocumentFile = document.getElementById('otherDocument')?.files[0];
const summaryOtherDocument = document.getElementById('summary-other-document');
if (summaryOtherDocument) {
summaryOtherDocument.textContent = otherDocumentFile ?
otherDocumentFile.name : 'No file selected';
}
// Tags
const summaryTags = document.getElementById('summary-tags');
if (summaryTags) {
if (selectedTags && selectedTags.length > 0) {
summaryTags.innerHTML = '';
selectedTags.forEach(tag => {
const tagElement = document.createElement('span');
tagElement.className = 'tag';
tagElement.style.backgroundColor = tag.color;
tagElement.style.color = getContrastColor(tag.color);
tagElement.textContent = tag.name;
summaryTags.appendChild(tagElement);
});
} else {
summaryTags.textContent = 'No tags selected';
}
}
// Vendor/Retailer
const vendor = document.getElementById('vendor');
document.getElementById('summary-vendor').textContent = vendor && vendor.value ? vendor.value : '-';
}
// Add input event listeners to remove validation errors when user types
document.addEventListener('input', (e) => {
if (e.target.hasAttribute('required') && e.target.classList.contains('invalid')) {
if (e.target.value.trim()) {
e.target.classList.remove('invalid');
// Remove validation message if exists
const validationMessage = e.target.nextElementSibling;
if (validationMessage && validationMessage.classList.contains('validation-message')) {
validationMessage.remove();
}
}
}
});
// Function to reset the form and initialize serial number inputs
function resetForm() {
// Reset the form
warrantyForm.reset();
// Reset serial numbers container
serialNumbersContainer.innerHTML = '';
// Add the first serial number input
addSerialNumberInput();
// Reset form tabs
currentTabIndex = 0;
switchToTab(0);
// Clear any file input displays
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-render warranties only if warrantiesList exists AND warranties have been loaded from API
if (warrantiesList && warrantiesLoaded) {
renderWarranties(filterWarranties()); // Assuming filterWarranties() returns the correct array
}
}
// Load view preference from localStorage
function loadViewPreference() {
// Get the appropriate key prefix based on user type
const prefix = getPreferenceKeyPrefix();
let savedView = null;
// --- BEGIN EDIT: Check keys in priority order ---
const userSpecificView = localStorage.getItem(`${prefix}defaultView`);
const generalView = localStorage.getItem('viewPreference');
const legacyWarrantyView = localStorage.getItem(`${prefix}warrantyView`);
if (userSpecificView) {
savedView = userSpecificView;
console.log(`Loaded view preference from ${prefix}defaultView:`, savedView);
} else if (generalView) {
savedView = generalView;
console.log('Loaded view preference from viewPreference:', savedView);
} else if (legacyWarrantyView) {
savedView = legacyWarrantyView;
console.log(`Loaded view preference from legacy ${prefix}warrantyView:`, savedView);
}
// --- END EDIT ---
// Default to grid view if no preference is saved
savedView = savedView || 'grid';
console.log(`Applying view preference 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) => {
setTheme(e.target.checked);
});
}
// Add event listener for adding new serial number inputs
// Add check for serialNumbersContainer before adding listener
if (serialNumbersContainer) {
serialNumbersContainer.addEventListener('click', (e) => {
if (e.target.closest('.add-serial-number')) {
addSerialNumberInput();
}
});
}
// Add a serial number input field
function addSerialNumberInput(container = serialNumbersContainer) {
// Check if the container exists before proceeding
if (!container) {
console.warn('Serial numbers container not found, cannot add input.');
return;
}
const div = document.createElement('div');
div.className = 'serial-number-input d-flex mb-2';
// Create an input element
const input = document.createElement('input');
input.type = 'text';
input.className = 'form-control';
input.name = 'serial_numbers[]';
input.placeholder = 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 };
// 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;
const apiUrl = shouldUseGlobalView ? `${baseUrl}/api/warranties/global` : `${baseUrl}/api/warranties`;
console.log(`[DEBUG] Using API endpoint based on saved preference '${savedScope}': ${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}`);
// Process each warranty to calculate status and days remaining
warranties = Array.isArray(data) ? data.map(warranty => {
const processed = processWarrantyData(warranty);
console.log('[DEBUG] Processed warranty:', processed);
return processed;
}) : [];
console.log('[DEBUG] Final warranties array:', warranties);
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
// REMOVED: applyFilters(); // Now called from authStateReady after data and prefs are loaded
}
} 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 = `
No warranties found
${message}
`;
}
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;
}
if (!warrantiesToRender || warrantiesToRender.length === 0) {
renderEmptyState(); // 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`;
// Show/hide table header for table view
if (tableViewHeader) {
tableViewHeader.classList.toggle('visible', currentView === 'table');
}
// Update view buttons to reflect current view
if (gridViewBtn && listViewBtn && tableViewBtn) {
gridViewBtn.classList.toggle('active', currentView === 'grid');
listViewBtn.classList.toggle('active', currentView === 'list');
tableViewBtn.classList.toggle('active', currentView === 'table');
}
sortedWarranties.forEach(warranty => {
// --- Use processed data ---
const purchaseDate = warranty.purchaseDate;
const expirationDate = warranty.expirationDate;
const isLifetime = warranty.is_lifetime;
const statusClass = warranty.status || 'unknown';
const statusText = warranty.statusText || 'Unknown Status';
// 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 = `
`;
// Add event listeners for edit and delete
tagElement.querySelector('.edit-tag').addEventListener('click', () => {
editTag(tag);
});
tagElement.querySelector('.delete-tag').addEventListener('click', () => {
deleteTag(tag.id);
});
existingTagsContainer.appendChild(tagElement);
});
}
// Edit a tag
function editTag(tag) {
const tagInfoElement = document.querySelector(`.existing-tag .existing-tag-info:has(+ .existing-tag-actions button[data-id="${tag.id}"])`);
if (!tagInfoElement) {
// Alternative selector for browsers that don't support :has
const tagElement = document.querySelector(`.existing-tag`);
const buttons = tagElement?.querySelectorAll(`.existing-tag-actions button[data-id="${tag.id}"]`);
if (buttons?.length > 0) {
const parent = buttons[0].closest('.existing-tag');
if (parent) {
const infoElement = parent.querySelector('.existing-tag-info');
if (infoElement) {
tagInfoElement = infoElement;
}
}
}
if (!tagInfoElement) return;
}
const originalHTML = tagInfoElement.innerHTML;
tagInfoElement.innerHTML = `
`;
// Add event listeners
tagInfoElement.querySelector('.save-edit').addEventListener('click', () => {
const newName = tagInfoElement.querySelector('.edit-tag-name').value.trim();
const newColor = tagInfoElement.querySelector('.edit-tag-color').value;
if (!newName) {
showToast(window.t('messages.tag_name_required'), 'error');
return;
}
updateTag(tag.id, newName, newColor);
});
tagInfoElement.querySelector('.cancel-edit').addEventListener('click', () => {
// Restore original HTML
tagInfoElement.innerHTML = originalHTML;
});
}
// Update a tag
function updateTag(id, name, color) {
const token = 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('Failed to update tag');
}
return response.json();
})
.then(data => {
// Update tag in allTags array
const index = allTags.findIndex(tag => tag.id === id);
if (index !== -1) {
allTags[index].name = name;
allTags[index].color = color;
}
// Update tag in selectedTags if present
const selectedIndex = selectedTags.findIndex(tag => tag.id === id);
if (selectedIndex !== -1) {
selectedTags[selectedIndex].name = name;
selectedTags[selectedIndex].color = color;
}
// Update tag in editSelectedTags if present
const editSelectedIndex = editSelectedTags.findIndex(tag => tag.id === id);
if (editSelectedIndex !== -1) {
editSelectedTags[editSelectedIndex].name = name;
editSelectedTags[editSelectedIndex].color = color;
}
// Update tags in warranties array
warranties.forEach(warranty => {
if (warranty.tags && Array.isArray(warranty.tags)) {
warranty.tags.forEach(tag => {
if (tag.id === id) {
tag.name = name;
tag.color = color;
}
});
}
});
// Rerender existing tags and selected tags
renderExistingTags();
renderSelectedTags();
renderEditSelectedTags();
// Update summary if needed
if (document.getElementById('summary-tags')) {
updateSummary();
}
// Update tag filter dropdown
populateTagFilter();
// Re-render warranty cards to show updated tag colors
renderWarranties(warranties);
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');
});
}
// 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("Could not load tags. Please try again.", "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 the 'addWarrantyModal' or 'editModal', then DO NOTHING.
if ((modalToClose.id === 'addWarrantyModal' || modalToClose.id === 'editModal') && e.target === modalToClose) {
return; // Ignore backdrop click for addWarrantyModal and editModal
}
// *** 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
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();
}, 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();
}
});
}
if (statusFilter) {
statusFilter.addEventListener('change', () => {
currentFilters.status = statusFilter.value;
applyFilters();
});
}
if (tagFilter) {
tagFilter.addEventListener('change', () => {
currentFilters.tag = tagFilter.value;
applyFilters();
});
}
if (vendorFilter) { // Added event listener for vendor filter
vendorFilter.addEventListener('change', () => {
currentFilters.vendor = vendorFilter.value;
applyFilters();
});
}
if (warrantyTypeFilter) { // Added event listener for warranty type filter
warrantyTypeFilter.addEventListener('change', () => {
currentFilters.warranty_type = warrantyTypeFilter.value;
applyFilters();
});
}
if (sortBySelect) {
sortBySelect.addEventListener('change', () => {
currentFilters.sortBy = sortBySelect.value;
applyFilters();
});
}
// View switcher event listeners
const gridViewBtn = document.getElementById('gridViewBtn');
const listViewBtn = document.getElementById('listViewBtn');
const tableViewBtn = document.getElementById('tableViewBtn');
if (gridViewBtn) gridViewBtn.addEventListener('click', () => switchView('grid'));
if (listViewBtn) listViewBtn.addEventListener('click', () => switchView('list'));
if (tableViewBtn) tableViewBtn.addEventListener('click', () => switchView('table'));
// Export button event listener
const exportBtn = document.getElementById('exportBtn');
if (exportBtn) exportBtn.addEventListener('click', exportWarranties);
// Import button event listener
if (importBtn && csvFileInput) {
importBtn.addEventListener('click', () => {
csvFileInput.click(); // Trigger hidden file input
});
csvFileInput.addEventListener('change', (event) => {
if (event.target.files && event.target.files.length > 0) {
handleImport(event.target.files[0]);
}
});
}
// Refresh button
const refreshBtn = document.getElementById('refreshBtn');
if (refreshBtn) refreshBtn.addEventListener('click', loadWarranties);
// 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);
// 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([]));
}
// 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 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, [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(); // 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 = `