mirror of
https://github.com/sassanix/Warracker.git
synced 2025-12-31 10:39:35 -06:00
- Moved warranty status row to the bottom of cards for better visual hierarchy in both grid and list views. - Ensured real-time UI updates on tag edits and deletions, fixing stale tag info across the app. - Added missing tag-related translation keys and warranty addition success messages across 17+ languages. - Resolved caching issues by updating service worker logic, asset versioning, and nginx headers. - Removed redundant Docker Compose commands causing migration duplication during updates. - Enhanced CSV import to support flexible date formats using `dateutil.parser`, improving user experience. - Fixed warranty duration selection limits by increasing max values in frontend and backend with DB migration. - Improved Paperless-ngx integration with duplicate document detection and user-friendly linking. - Fixed DB migration permission issues by granting `CREATEROLE` and removing redundant grants. - Restored missing product photos by correcting a broken image loader function call.
8479 lines
362 KiB
JavaScript
8479 lines
362 KiB
JavaScript
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 = '<ul>' +
|
||
serialNumbers.map(sn => `<li>${sn}</li>`).join('') +
|
||
'</ul>';
|
||
} else {
|
||
serialNumbersContainer.textContent = 'None';
|
||
}
|
||
}
|
||
|
||
// Warranty details
|
||
const purchaseDateStr = document.getElementById('purchaseDate')?.value;
|
||
const summaryPurchaseDate = document.getElementById('summary-purchase-date');
|
||
if (summaryPurchaseDate) {
|
||
if (purchaseDateStr) {
|
||
// Use the same logic as formatDate to handle YYYY-MM-DD
|
||
const parts = String(purchaseDateStr).split('-');
|
||
let formattedDate = '-'; // Default
|
||
if (parts.length === 3) {
|
||
const year = parseInt(parts[0], 10);
|
||
const month = parseInt(parts[1], 10) - 1; // JS months are 0-indexed
|
||
const day = parseInt(parts[2], 10);
|
||
const dateObj = new Date(Date.UTC(year, month, day));
|
||
if (!isNaN(dateObj.getTime())) {
|
||
// Format manually (example: Jan 1, 2023)
|
||
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||
formattedDate = `${monthNames[month]} ${day}, ${year}`;
|
||
}
|
||
}
|
||
summaryPurchaseDate.textContent = formattedDate;
|
||
} else {
|
||
summaryPurchaseDate.textContent = '-';
|
||
}
|
||
}
|
||
|
||
// --- Handle Lifetime in Summary ---
|
||
const isLifetime = isLifetimeCheckbox ? isLifetimeCheckbox.checked : false;
|
||
const 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 = '<i class="fas fa-times"></i>';
|
||
|
||
// Add event listener to remove button
|
||
removeButton.addEventListener('click', function() {
|
||
container.removeChild(div);
|
||
});
|
||
|
||
// Append remove button to the input group
|
||
div.appendChild(removeButton);
|
||
}
|
||
|
||
// Insert the new input group before the add button
|
||
const addButton = container.querySelector('.add-serial');
|
||
if (addButton) {
|
||
container.insertBefore(div, addButton);
|
||
} else {
|
||
container.appendChild(div);
|
||
|
||
// Create and append an add button if it doesn't exist
|
||
const addButton = document.createElement('button');
|
||
addButton.type = 'button';
|
||
addButton.className = 'btn btn-sm btn-secondary add-serial';
|
||
addButton.innerHTML = '<i class="fas fa-plus"></i> ' + (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 = `
|
||
<div class="empty-state">
|
||
<i class="fas fa-box-open"></i>
|
||
<h3>No warranties found</h3>
|
||
<p>${message}</p>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function formatDate(date) {
|
||
// Input 'date' should now be a Date object created by processWarrantyData (or null)
|
||
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
|
||
return 'N/A';
|
||
}
|
||
|
||
// Get the user's preferred format from localStorage, default to MDY
|
||
const formatPreference = localStorage.getItem('dateFormat') || 'MDY';
|
||
|
||
// Manually extract UTC components to avoid timezone discrepancies
|
||
const year = date.getUTCFullYear();
|
||
const monthIndex = date.getUTCMonth(); // 0-indexed for month names array
|
||
const day = date.getUTCDate();
|
||
|
||
// Padded numeric values
|
||
const monthPadded = (monthIndex + 1).toString().padStart(2, '0');
|
||
const dayPadded = day.toString().padStart(2, '0');
|
||
|
||
// Abbreviated month names
|
||
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||
const monthAbbr = monthNames[monthIndex];
|
||
|
||
switch (formatPreference) {
|
||
case 'DMY':
|
||
return `${dayPadded}/${monthPadded}/${year}`;
|
||
case 'YMD':
|
||
return `${year}-${monthPadded}-${dayPadded}`;
|
||
case 'MDY_WORDS': // Added
|
||
return `${monthAbbr} ${day}, ${year}`;
|
||
case 'DMY_WORDS': // Added
|
||
return `${day} ${monthAbbr} ${year}`;
|
||
case 'YMD_WORDS': // Added
|
||
return `${year} ${monthAbbr} ${day}`;
|
||
case 'MDY':
|
||
default:
|
||
return `${monthPadded}/${dayPadded}/${year}`;
|
||
}
|
||
}
|
||
|
||
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 = `<div><strong>${ownerLabel}:</strong> <span>${warranty.user_display_name}</span></div>`;
|
||
}
|
||
|
||
// Prepare tags HTML
|
||
const tagsHtml = warranty.tags && warranty.tags.length > 0
|
||
? `<div class="tags-row">
|
||
${warranty.tags.map(tag =>
|
||
`<span class="tag" style="background-color: ${tag.color}; color: ${getContrastColor(tag.color)}">
|
||
${tag.name}
|
||
</span>`
|
||
).join('')}
|
||
</div>`
|
||
: '';
|
||
// Add notes display button if present
|
||
let notesHtml = '';
|
||
const hasNotes = warranty.notes && warranty.notes.trim() !== '';
|
||
// Remove the button, and instead prepare a notes link for document-links-row
|
||
let notesLinkHtml = '';
|
||
if (hasNotes) {
|
||
const notesLabel = window.i18next ? window.i18next.t('warranties.notes') : 'Notes';
|
||
notesLinkHtml = `<a href="#" class="notes-link" data-id="${warranty.id}" title="View Notes"><i class='fas fa-sticky-note'></i> ${notesLabel}</a>`;
|
||
}
|
||
|
||
const hasDocuments = warranty.product_url || warranty.invoice_path || warranty.manual_path || warranty.other_document_path || hasNotes;
|
||
|
||
// Get current user ID to check warranty ownership
|
||
const currentUserId = (() => {
|
||
try {
|
||
const userInfo = JSON.parse(localStorage.getItem('user_info') || '{}');
|
||
return userInfo.id;
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
})();
|
||
|
||
// Check if current user can edit/delete this warranty
|
||
// Allow if: not in global view, user owns the warranty, or user is admin
|
||
const isAdmin = getUserType() === 'admin';
|
||
const canEdit = !isGlobalView || (warranty.user_id === currentUserId) || isAdmin;
|
||
|
||
// Generate action buttons HTML based on permissions
|
||
const actionButtonsHtml = canEdit ? `
|
||
<button class="action-btn edit-btn" title="Edit" data-id="${warranty.id}">
|
||
<i class="fas fa-edit"></i>
|
||
</button>
|
||
<button class="action-btn delete-btn" title="Delete" data-id="${warranty.id}">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
` : `
|
||
<span class="action-btn-placeholder" title="View only - not your warranty">
|
||
<i class="fas fa-eye" style="color: #666;"></i>
|
||
</span>
|
||
`;
|
||
|
||
const cardElement = document.createElement('div');
|
||
cardElement.className = `warranty-card ${statusClass === 'expired' ? 'expired' : statusClass === 'expiring' ? 'expiring-soon' : 'active'}`;
|
||
|
||
if (currentView === 'grid') {
|
||
// Grid view HTML structure
|
||
const photoThumbnailHtml = warranty.product_photo_path && warranty.product_photo_path !== 'null' ? `
|
||
<div class="product-photo-thumbnail">
|
||
<a href="#" onclick="openSecureFile('${warranty.product_photo_path}'); return false;" title="Click to view full size image">
|
||
<img data-secure-src="/api/secure-file/${warranty.product_photo_path.replace('uploads/', '')}" alt="Product Photo"
|
||
style="width: 80px; height: 80px; object-fit: cover; border-radius: 8px; border: 2px solid var(--border-color); cursor: pointer;"
|
||
onerror="this.style.display='none'" class="secure-image">
|
||
</a>
|
||
</div>
|
||
` : '';
|
||
|
||
cardElement.innerHTML = `
|
||
<div class="product-name-header">
|
||
<h3 class="warranty-title" title="${warranty.product_name || 'Unnamed Product'}">${warranty.product_name || 'Unnamed Product'}</h3>
|
||
<div class="warranty-actions">
|
||
${actionButtonsHtml}
|
||
</div>
|
||
</div>
|
||
<div class="warranty-content">
|
||
${photoThumbnailHtml}
|
||
<div class="warranty-info">
|
||
${userInfoHtml}
|
||
<div><i class="fas fa-calendar"></i> ${window.i18next ? window.i18next.t('warranties.age') : 'Age'}: <span>${productAge}</span></div>
|
||
<div><i class="fas fa-file-alt"></i> ${window.i18next ? window.i18next.t('warranties.warranty') : 'Warranty'}: <span>${warrantyDurationText}</span></div>
|
||
<div><i class="fas fa-wrench"></i> ${window.i18next ? window.i18next.t('warranties.warranty_ends') : 'Warranty Ends'}: <span>${expirationDateText}</span></div>
|
||
${warranty.purchase_price ? `<div><i class="fas fa-coins"></i> ${window.i18next ? window.i18next.t('warranties.price') : 'Price'}: <span>${formatCurrencyHTML(warranty.purchase_price, warranty.currency ? getCurrencySymbolByCode(warranty.currency) : getCurrencySymbol(), getCurrencyPosition())}</span></div>` : ''}
|
||
${validSerialNumbers.length > 0 ? `
|
||
<div><i class="fas fa-barcode"></i> ${window.i18next ? window.i18next.t('warranties.serial_number') : 'Serial Number'}: <span>${validSerialNumbers[0]}</span></div>
|
||
${validSerialNumbers.length > 1 ? `
|
||
<div style="margin-left: 28px;">
|
||
<ul style="margin-top: 5px;">
|
||
${validSerialNumbers.slice(1).map(sn => `<li>${sn}</li>`).join('')}
|
||
</ul>
|
||
</div>
|
||
` : ''}
|
||
` : ''}
|
||
${warranty.vendor ? `<div><i class="fas fa-store"></i> ${window.i18next ? window.i18next.t('warranties.vendor') : 'Vendor'}: <span>${warranty.vendor}</span></div>` : ''}
|
||
${warranty.warranty_type ? `<div><i class="fas fa-shield-alt"></i> ${window.i18next ? window.i18next.t('warranties.type') : 'Type'}: <span>${warranty.warranty_type}</span></div>` : ''}
|
||
</div>
|
||
</div>
|
||
${hasDocuments ? `
|
||
<div class="document-links-row">
|
||
<div class="document-links-inner-container">
|
||
${warranty.product_url ? `
|
||
<a href="${warranty.product_url}" class="product-link" target="_blank">
|
||
<i class="fas fa-globe"></i> ${window.i18next ? window.i18next.t('warranties.product_website') : 'Product Website'}
|
||
</a>
|
||
` : ''}
|
||
${generateDocumentLink(warranty, 'invoice')}
|
||
${generateDocumentLink(warranty, 'manual')}
|
||
${generateDocumentLink(warranty, 'other')}
|
||
${notesLinkHtml}
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
${tagsHtml}
|
||
<div class="warranty-status-row status-${statusClass}">
|
||
<span>${statusText}</span>
|
||
</div>
|
||
`;
|
||
} else if (currentView === 'list') {
|
||
// List view HTML structure
|
||
const photoThumbnailHtml = warranty.product_photo_path && warranty.product_photo_path !== 'null' ? `
|
||
<div class="product-photo-thumbnail">
|
||
<a href="#" onclick="openSecureFile('${warranty.product_photo_path}'); return false;" title="Click to view full size image">
|
||
<img data-secure-src="/api/secure-file/${warranty.product_photo_path.replace('uploads/', '')}" alt="Product Photo"
|
||
style="width: 180px; height: 180px; object-fit: cover; border-radius: 6px; border: 2px solid var(--border-color); cursor: pointer;"
|
||
onerror="this.style.display='none'" class="secure-image">
|
||
</a>
|
||
</div>
|
||
` : '';
|
||
|
||
cardElement.innerHTML = `
|
||
<div class="product-name-header">
|
||
<h3 class="warranty-title" title="${warranty.product_name || 'Unnamed Product'}">${warranty.product_name || 'Unnamed Product'}</h3>
|
||
<div class="warranty-actions">
|
||
${actionButtonsHtml}
|
||
</div>
|
||
</div>
|
||
<div class="warranty-content">
|
||
${photoThumbnailHtml}
|
||
<div class="warranty-info">
|
||
${userInfoHtml}
|
||
<div><i class="fas fa-calendar"></i> ${window.i18next ? window.i18next.t('warranties.age') : 'Age'}: <span>${productAge}</span></div>
|
||
<div><i class="fas fa-file-alt"></i> ${window.i18next ? window.i18next.t('warranties.warranty') : 'Warranty'}: <span>${warrantyDurationText}</span></div>
|
||
<div><i class="fas fa-wrench"></i> ${window.i18next ? window.i18next.t('warranties.warranty_ends') : 'Warranty Ends'}: <span>${expirationDateText}</span></div>
|
||
${warranty.purchase_price ? `<div><i class="fas fa-coins"></i> ${window.i18next ? window.i18next.t('warranties.price') : 'Price'}: <span>${formatCurrencyHTML(warranty.purchase_price, warranty.currency ? getCurrencySymbolByCode(warranty.currency) : getCurrencySymbol(), getCurrencyPosition())}</span></div>` : ''}
|
||
${validSerialNumbers.length > 0 ? `
|
||
<div><i class="fas fa-barcode"></i> ${window.i18next ? window.i18next.t('warranties.serial_number') : 'Serial Number'}: <span>${validSerialNumbers[0]}</span></div>
|
||
${validSerialNumbers.length > 1 ? `
|
||
<div style="margin-left: 28px;">
|
||
<ul style="margin-top: 5px;">
|
||
${validSerialNumbers.slice(1).map(sn => `<li>${sn}</li>`).join('')}
|
||
</ul>
|
||
</div>
|
||
` : ''}
|
||
` : ''}
|
||
${warranty.vendor ? `<div><i class="fas fa-store"></i> ${window.i18next ? window.i18next.t('warranties.vendor') : 'Vendor'}: <span>${warranty.vendor}</span></div>` : ''}
|
||
${warranty.warranty_type ? `<div><i class="fas fa-shield-alt"></i> ${window.i18next ? window.i18next.t('warranties.type') : 'Type'}: <span>${warranty.warranty_type}</span></div>` : ''}
|
||
</div>
|
||
</div>
|
||
${hasDocuments ? `
|
||
<div class="document-links-row">
|
||
<div class="document-links-inner-container">
|
||
${warranty.product_url ? `
|
||
<a href="${warranty.product_url}" class="product-link" target="_blank">
|
||
<i class="fas fa-globe"></i> ${window.i18next ? window.i18next.t('warranties.product_website') : 'Product Website'}
|
||
</a>
|
||
` : ''}
|
||
${generateDocumentLink(warranty, 'invoice')}
|
||
${generateDocumentLink(warranty, 'manual')}
|
||
${generateDocumentLink(warranty, 'other')}
|
||
${notesLinkHtml}
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
${tagsHtml}
|
||
<div class="warranty-status-row status-${statusClass}">
|
||
<span>${statusText}</span>
|
||
</div>
|
||
`;
|
||
} else if (currentView === 'table') {
|
||
// Table view HTML structure
|
||
const photoThumbnailHtml = warranty.product_photo_path && warranty.product_photo_path !== 'null' ? `
|
||
<div class="product-photo-thumbnail">
|
||
<a href="#" onclick="openSecureFile('${warranty.product_photo_path}'); return false;" title="Click to view full size image">
|
||
<img data-secure-src="/api/secure-file/${warranty.product_photo_path.replace('uploads/', '')}" alt="Product Photo"
|
||
style="width: 55px; height: 55px; object-fit: cover; border-radius: 4px; border: 1px solid var(--border-color); cursor: pointer;"
|
||
onerror="this.style.display='none'" class="secure-image">
|
||
</a>
|
||
</div>
|
||
` : '';
|
||
|
||
cardElement.innerHTML = `
|
||
<div class="product-name-header">
|
||
<h3 class="warranty-title" title="${warranty.product_name || 'Unnamed Product'}">${warranty.product_name || 'Unnamed Product'}</h3>
|
||
<div class="warranty-actions">
|
||
${actionButtonsHtml}
|
||
</div>
|
||
</div>
|
||
<div class="warranty-content">
|
||
${photoThumbnailHtml}
|
||
<div class="warranty-info">
|
||
${userInfoHtml}
|
||
<div><i class="fas fa-calendar"></i> ${window.i18next ? window.i18next.t('warranties.age') : 'Age'}: <span>${productAge}</span></div>
|
||
<div><i class="fas fa-file-alt"></i> ${window.i18next ? window.i18next.t('warranties.warranty') : 'Warranty'}: <span>${warrantyDurationText}</span></div>
|
||
<div><i class="fas fa-wrench"></i> ${window.i18next ? window.i18next.t('warranties.warranty_ends') : 'Warranty Ends'}: <span>${expirationDateText}</span></div>
|
||
${warranty.purchase_price ? `<div><i class="fas fa-coins"></i> ${window.i18next ? window.i18next.t('warranties.price') : 'Price'}: <span>${formatCurrencyHTML(warranty.purchase_price, warranty.currency ? getCurrencySymbolByCode(warranty.currency) : getCurrencySymbol(), getCurrencyPosition())}</span></div>` : ''}
|
||
${validSerialNumbers.length > 0 ? `
|
||
<div><i class="fas fa-barcode"></i> ${window.i18next ? window.i18next.t('warranties.serial_number') : 'Serial Number'}: <span>${validSerialNumbers[0]}</span></div>
|
||
${validSerialNumbers.length > 1 ? `
|
||
<div style="margin-left: 28px;">
|
||
<ul style="margin-top: 5px;">
|
||
${validSerialNumbers.slice(1).map(sn => `<li>${sn}</li>`).join('')}
|
||
</ul>
|
||
</div>
|
||
` : ''}
|
||
` : ''}
|
||
${warranty.vendor ? `<div><i class="fas fa-store"></i> ${window.i18next ? window.i18next.t('warranties.vendor') : 'Vendor'}: <span>${warranty.vendor}</span></div>` : ''}
|
||
${warranty.warranty_type ? `<div><i class="fas fa-shield-alt"></i> ${window.i18next ? window.i18next.t('warranties.type') : 'Type'}: <span>${warranty.warranty_type}</span></div>` : ''}
|
||
</div>
|
||
</div>
|
||
<div class="warranty-status-row status-${statusClass}">
|
||
<span>${statusText}</span>
|
||
</div>
|
||
${hasDocuments ? `
|
||
<div class="document-links-row">
|
||
<div class="document-links-inner-container">
|
||
${warranty.product_url ? `
|
||
<a href="${warranty.product_url}" class="product-link" target="_blank">
|
||
<i class="fas fa-globe"></i> ${window.i18next ? window.i18next.t('warranties.product_website') : 'Product Website'}
|
||
</a>
|
||
` : ''}
|
||
${generateDocumentLink(warranty, 'invoice')}
|
||
${generateDocumentLink(warranty, 'manual')}
|
||
${generateDocumentLink(warranty, 'other')}
|
||
${notesLinkHtml}
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
${tagsHtml}
|
||
`;
|
||
}
|
||
|
||
// Add event listeners
|
||
warrantiesList.appendChild(cardElement);
|
||
|
||
// Add event listeners only if user can edit (buttons exist)
|
||
if (canEdit) {
|
||
// Edit button event listener
|
||
const editBtn = cardElement.querySelector('.edit-btn');
|
||
if (editBtn) {
|
||
editBtn.addEventListener('click', async () => {
|
||
console.log('[DEBUG] Edit button clicked for warranty ID:', warranty.id);
|
||
// Find the current warranty data instead of using the potentially stale warranty object
|
||
const currentWarranty = warranties.find(w => w.id === warranty.id);
|
||
console.log('[DEBUG] Found current warranty:', currentWarranty ? 'Yes' : 'No', currentWarranty?.notes);
|
||
if (currentWarranty) {
|
||
await openEditModal(currentWarranty);
|
||
} else {
|
||
showToast(window.t('messages.warranty_not_found_refresh'), 'error');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Delete button event listener
|
||
const deleteBtn = cardElement.querySelector('.delete-btn');
|
||
if (deleteBtn) {
|
||
deleteBtn.addEventListener('click', () => {
|
||
openDeleteModal(warranty.id, warranty.product_name);
|
||
});
|
||
}
|
||
}
|
||
// View notes button event listener
|
||
const notesLink = cardElement.querySelector('.notes-link');
|
||
if (notesLink) {
|
||
notesLink.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
// Find the current warranty data instead of using the potentially stale warranty object
|
||
const currentWarranty = warranties.find(w => w.id === warranty.id);
|
||
if (currentWarranty) {
|
||
showNotesModal(currentWarranty.notes, currentWarranty);
|
||
} else {
|
||
showToast(window.t('messages.warranty_not_found_refresh'), 'error');
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// Load secure images with authentication after rendering
|
||
loadSecureImages();
|
||
|
||
// Improved: Align card heights after all images have loaded
|
||
if (currentView === 'grid') {
|
||
const cards = warrantiesList.querySelectorAll('.warranty-card');
|
||
if (cards.length > 0) {
|
||
const images = warrantiesList.querySelectorAll('.secure-image');
|
||
let loadedCount = 0;
|
||
const totalImages = images.length;
|
||
|
||
const alignHeights = () => {
|
||
let maxHeight = 0;
|
||
cards.forEach(card => {
|
||
card.style.minHeight = ''; // Reset
|
||
const height = card.getBoundingClientRect().height;
|
||
if (height > maxHeight) maxHeight = height;
|
||
});
|
||
cards.forEach(card => {
|
||
card.style.minHeight = `${maxHeight}px`;
|
||
});
|
||
};
|
||
|
||
if (totalImages === 0) {
|
||
alignHeights(); // No images, align immediately
|
||
} else {
|
||
images.forEach(img => {
|
||
if (img.complete) {
|
||
loadedCount++;
|
||
if (loadedCount === totalImages) alignHeights();
|
||
} else {
|
||
img.addEventListener('load', () => {
|
||
loadedCount++;
|
||
if (loadedCount === totalImages) alignHeights();
|
||
});
|
||
img.addEventListener('error', () => {
|
||
loadedCount++;
|
||
if (loadedCount === totalImages) alignHeights();
|
||
});
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// Update the timeline chart if on the status page or appropriate
|
||
if (typeof updateTimelineChart === 'function') {
|
||
updateTimelineChart();
|
||
}
|
||
|
||
console.log('Warranties rendered successfully');
|
||
}
|
||
|
||
function filterWarranties() {
|
||
const searchTerm = searchInput ? searchInput.value.toLowerCase() : ''; // Add null check for searchInput
|
||
|
||
// Show or hide the clear search button if it exists
|
||
if (clearSearchBtn) {
|
||
clearSearchBtn.style.display = searchTerm ? 'flex' : 'none';
|
||
}
|
||
|
||
if (!searchTerm) {
|
||
return warranties; // Return the full list if no search term
|
||
// REMOVED: renderWarranties();
|
||
// REMOVED: return;
|
||
}
|
||
|
||
const filtered = warranties.filter(warranty => {
|
||
// Check product name
|
||
if (warranty.product_name && warranty.product_name.toLowerCase().includes(searchTerm)) { // Add null check
|
||
return true;
|
||
}
|
||
|
||
// Check tags
|
||
if (warranty.tags && Array.isArray(warranty.tags)) {
|
||
if (warranty.tags.some(tag => tag.name && tag.name.toLowerCase().includes(searchTerm))) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// Check notes
|
||
if (warranty.notes && warranty.notes.toLowerCase().includes(searchTerm)) {
|
||
return true;
|
||
}
|
||
|
||
// Check vendor
|
||
if (warranty.vendor && warranty.vendor.toLowerCase().includes(searchTerm)) {
|
||
return true;
|
||
}
|
||
|
||
// Check if any serial number contains search term
|
||
if (warranty.serial_numbers && Array.isArray(warranty.serial_numbers)) {
|
||
if (warranty.serial_numbers.some(sn => sn && sn.toLowerCase().includes(searchTerm))) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
});
|
||
|
||
// REMOVED: Add visual feedback if no results found
|
||
// REMOVED: if (filtered.length === 0) {
|
||
// REMOVED: renderEmptyState(`No matches found for "${searchTerm}". Try a different search term.`);
|
||
// REMOVED: } else {
|
||
// REMOVED: renderWarranties(filtered);
|
||
// REMOVED: }
|
||
|
||
return filtered; // Return the filtered list
|
||
}
|
||
|
||
function applyFilters() {
|
||
console.log('[FILTER DEBUG] Applying filters with:', currentFilters);
|
||
console.log('[FILTER DEBUG] Total warranties before filtering:', warranties.length);
|
||
|
||
// Filter warranties based on currentFilters
|
||
const filtered = warranties.filter(warranty => {
|
||
// Status filter
|
||
if (currentFilters.status !== 'all' && warranty.status !== currentFilters.status) {
|
||
return false;
|
||
}
|
||
|
||
// Tag filter
|
||
if (currentFilters.tag !== 'all') {
|
||
const tagId = parseInt(currentFilters.tag);
|
||
const hasTag = warranty.tags && Array.isArray(warranty.tags) &&
|
||
warranty.tags.some(tag => tag.id === tagId);
|
||
if (!hasTag) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Vendor filter
|
||
if (currentFilters.vendor !== 'all' && (warranty.vendor || '').toLowerCase() !== currentFilters.vendor.toLowerCase()) {
|
||
return false;
|
||
}
|
||
|
||
// Warranty type filter
|
||
if (currentFilters.warranty_type !== 'all' && (warranty.warranty_type || '').toLowerCase() !== currentFilters.warranty_type.toLowerCase()) {
|
||
return false;
|
||
}
|
||
|
||
// Search filter
|
||
if (currentFilters.search) {
|
||
const searchTerm = currentFilters.search.toLowerCase();
|
||
// Check if product name contains search term
|
||
const productNameMatch = warranty.product_name.toLowerCase().includes(searchTerm);
|
||
// Check if any tag name contains search term
|
||
const tagMatch = warranty.tags && Array.isArray(warranty.tags) &&
|
||
warranty.tags.some(tag => tag.name.toLowerCase().includes(searchTerm));
|
||
// Check if notes contains search term
|
||
const notesMatch = warranty.notes && warranty.notes.toLowerCase().includes(searchTerm);
|
||
// Check if vendor contains search term
|
||
const vendorMatch = warranty.vendor && warranty.vendor.toLowerCase().includes(searchTerm);
|
||
// Check if any serial number contains search term
|
||
const serialNumberMatch = warranty.serial_numbers && Array.isArray(warranty.serial_numbers) &&
|
||
warranty.serial_numbers.some(sn => sn && sn.toLowerCase().includes(searchTerm));
|
||
// Return true if any match
|
||
if (!productNameMatch && !tagMatch && !notesMatch && !vendorMatch && !serialNumberMatch) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
});
|
||
|
||
console.log('[FILTER DEBUG] Filtered warranties:', filtered.length);
|
||
console.log('[FILTER DEBUG] Filtered warranty IDs:', filtered.map(w => w.id));
|
||
|
||
// Render the filtered warranties
|
||
renderWarranties(filtered);
|
||
}
|
||
|
||
async function openEditModal(warranty) {
|
||
// Close any existing modals first
|
||
closeModals();
|
||
|
||
currentWarrantyId = warranty.id;
|
||
|
||
// Load currencies for the dropdown and wait for it to complete
|
||
await loadCurrencies();
|
||
|
||
console.log('[DEBUG] Opening edit modal for warranty:', warranty.id, 'with notes:', warranty.notes);
|
||
|
||
// Populate form fields
|
||
document.getElementById('editProductName').value = warranty.product_name;
|
||
document.getElementById('editProductUrl').value = warranty.product_url || '';
|
||
document.getElementById('editPurchaseDate').value = warranty.purchase_date.split('T')[0];
|
||
// Populate new duration fields
|
||
document.getElementById('editWarrantyDurationYears').value = warranty.warranty_duration_years || 0;
|
||
document.getElementById('editWarrantyDurationMonths').value = warranty.warranty_duration_months || 0;
|
||
document.getElementById('editWarrantyDurationDays').value = warranty.warranty_duration_days || 0;
|
||
|
||
document.getElementById('editPurchasePrice').value = warranty.purchase_price || '';
|
||
|
||
// Set currency dropdown
|
||
const editCurrencySelect = document.getElementById('editCurrency');
|
||
if (editCurrencySelect && warranty.currency) {
|
||
editCurrencySelect.value = warranty.currency;
|
||
}
|
||
document.getElementById('editVendor').value = warranty.vendor || '';
|
||
|
||
// Handle warranty type - check if it's a predefined option or custom
|
||
const editWarrantyTypeSelect = document.getElementById('editWarrantyType');
|
||
const editWarrantyTypeCustom = document.getElementById('editWarrantyTypeCustom');
|
||
if (editWarrantyTypeSelect && warranty.warranty_type) {
|
||
// Check if the warranty type exists as an option in the dropdown
|
||
const options = Array.from(editWarrantyTypeSelect.options);
|
||
const matchingOption = options.find(option => option.value === warranty.warranty_type);
|
||
|
||
if (matchingOption) {
|
||
// It's a predefined option
|
||
editWarrantyTypeSelect.value = warranty.warranty_type;
|
||
if (editWarrantyTypeCustom) editWarrantyTypeCustom.style.display = 'none';
|
||
} else {
|
||
// It's a custom value
|
||
editWarrantyTypeSelect.value = 'other';
|
||
if (editWarrantyTypeCustom) {
|
||
editWarrantyTypeCustom.style.display = 'block';
|
||
editWarrantyTypeCustom.value = warranty.warranty_type;
|
||
}
|
||
}
|
||
} else if (editWarrantyTypeSelect) {
|
||
editWarrantyTypeSelect.value = '';
|
||
if (editWarrantyTypeCustom) editWarrantyTypeCustom.style.display = 'none';
|
||
}
|
||
|
||
// Clear existing serial number inputs
|
||
const editSerialNumbersContainer = document.getElementById('editSerialNumbersContainer');
|
||
editSerialNumbersContainer.innerHTML = '';
|
||
|
||
// Normalize serial_numbers to array of strings if needed
|
||
if (Array.isArray(warranty.serial_numbers) && warranty.serial_numbers.length > 0 && typeof warranty.serial_numbers[0] === 'object') {
|
||
warranty.serial_numbers = warranty.serial_numbers
|
||
.map(snObj => snObj && snObj.serial_number)
|
||
.filter(sn => typeof sn === 'string' && sn.trim() !== '');
|
||
}
|
||
|
||
// Add event listener for adding new serial number inputs in edit modal
|
||
editSerialNumbersContainer.addEventListener('click', (e) => {
|
||
if (e.target.closest('.add-serial-number')) {
|
||
addSerialNumberInput(editSerialNumbersContainer);
|
||
}
|
||
});
|
||
|
||
const validSerialNumbers = Array.isArray(warranty.serial_numbers)
|
||
? warranty.serial_numbers.filter(sn => sn && typeof sn === 'string' && sn.trim() !== '')
|
||
: [];
|
||
|
||
if (validSerialNumbers.length === 0) {
|
||
// Add a single empty input if there are no serial numbers
|
||
addSerialNumberInput(editSerialNumbersContainer);
|
||
} else {
|
||
// Add the first serial number with an "Add Another" button only (no remove button)
|
||
const firstInput = document.createElement('div');
|
||
firstInput.className = 'serial-number-input';
|
||
firstInput.innerHTML = `
|
||
<input type="text" name="serial_numbers[]" class="form-control" placeholder="${i18next.t('warranties.enter_serial_number')}" value="${validSerialNumbers[0]}">
|
||
<button type="button" class="btn btn-sm btn-primary add-serial-number">
|
||
<i class="fas fa-plus"></i> ${i18next.t('warranties.add_serial_number')}
|
||
</button>
|
||
`;
|
||
|
||
// Add event listener for the Add button
|
||
firstInput.querySelector('.add-serial-number').addEventListener('click', function(e) {
|
||
e.stopPropagation(); // Stop event from bubbling up
|
||
addSerialNumberInput(editSerialNumbersContainer);
|
||
});
|
||
|
||
editSerialNumbersContainer.appendChild(firstInput);
|
||
|
||
// Add the rest of the serial numbers with "Remove" buttons
|
||
for (let i = 1; i < validSerialNumbers.length; i++) {
|
||
const newInput = document.createElement('div');
|
||
newInput.className = 'serial-number-input';
|
||
newInput.innerHTML = `
|
||
<input type="text" name="serial_numbers[]" class="form-control" placeholder="${i18next.t('warranties.enter_serial_number')}" value="${validSerialNumbers[i]}">
|
||
<button type="button" class="btn btn-sm btn-danger remove-serial-number">
|
||
<i class="fas fa-minus"></i> ${i18next.t('actions.delete')}
|
||
</button>
|
||
`;
|
||
|
||
// Add remove button functionality
|
||
newInput.querySelector('.remove-serial-number').addEventListener('click', function() {
|
||
this.parentElement.remove();
|
||
});
|
||
|
||
editSerialNumbersContainer.appendChild(newInput);
|
||
}
|
||
}
|
||
|
||
// Show current invoice if exists
|
||
const currentInvoiceElement = document.getElementById('currentInvoice');
|
||
const deleteInvoiceBtn = document.getElementById('deleteInvoiceBtn');
|
||
if (currentInvoiceElement && deleteInvoiceBtn) {
|
||
const hasLocalInvoice = warranty.invoice_path && warranty.invoice_path !== 'null';
|
||
const hasPaperlessInvoice = warranty.paperless_invoice_id && warranty.paperless_invoice_id !== null;
|
||
|
||
if (hasLocalInvoice) {
|
||
currentInvoiceElement.innerHTML = `
|
||
<span class="text-success">
|
||
<i class="fas fa-check-circle"></i> ${i18next.t('warranties.current_invoice')}:
|
||
<a href="#" class="view-document-link" onclick="openSecureFile('${warranty.invoice_path}'); return false;">View</a>
|
||
(${i18next.t('warranties.upload_new_file_replace')})
|
||
</span>
|
||
`;
|
||
deleteInvoiceBtn.style.display = '';
|
||
} else if (hasPaperlessInvoice) {
|
||
currentInvoiceElement.innerHTML = `
|
||
<span class="text-success">
|
||
<i class="fas fa-check-circle"></i> ${i18next.t('warranties.current_invoice')}:
|
||
<a href="#" class="view-document-link" onclick="openPaperlessDocument(${warranty.paperless_invoice_id}); return false;">View</a>
|
||
<i class="fas fa-cloud" style="color: #4dabf7; margin-left: 4px; font-size: 0.8em;" title="Stored in Paperless-ngx"></i> (${i18next.t('warranties.upload_new_file_replace')})
|
||
</span>
|
||
`;
|
||
deleteInvoiceBtn.style.display = '';
|
||
} else {
|
||
currentInvoiceElement.innerHTML = `<span>${i18next.t('warranties.no_invoice_uploaded')}</span>`;
|
||
deleteInvoiceBtn.style.display = 'none';
|
||
}
|
||
// Reset delete state
|
||
deleteInvoiceBtn.dataset.delete = 'false';
|
||
deleteInvoiceBtn.onclick = function() {
|
||
deleteInvoiceBtn.dataset.delete = 'true';
|
||
currentInvoiceElement.innerHTML = `<span class="text-danger">${i18next.t('warranties.invoice_will_be_deleted')}</span>`;
|
||
deleteInvoiceBtn.style.display = 'none';
|
||
};
|
||
}
|
||
// Show current manual if exists
|
||
const currentManualElement = document.getElementById('currentManual');
|
||
const deleteManualBtn = document.getElementById('deleteManualBtn');
|
||
if (currentManualElement && deleteManualBtn) {
|
||
const hasLocalManual = warranty.manual_path && warranty.manual_path !== 'null';
|
||
const hasPaperlessManual = warranty.paperless_manual_id && warranty.paperless_manual_id !== null;
|
||
|
||
if (hasLocalManual) {
|
||
currentManualElement.innerHTML = `
|
||
<span class="text-success">
|
||
<i class="fas fa-check-circle"></i> ${i18next.t('warranties.current_manual')}:
|
||
<a href="#" class="view-document-link" onclick="openSecureFile('${warranty.manual_path}'); return false;">View</a>
|
||
(${i18next.t('warranties.upload_new_file_replace')})
|
||
</span>
|
||
`;
|
||
deleteManualBtn.style.display = '';
|
||
} else if (hasPaperlessManual) {
|
||
currentManualElement.innerHTML = `
|
||
<span class="text-success">
|
||
<i class="fas fa-check-circle"></i> ${i18next.t('warranties.current_manual')}:
|
||
<a href="#" class="view-document-link" onclick="openPaperlessDocument(${warranty.paperless_manual_id}); return false;">View</a>
|
||
<i class="fas fa-cloud" style="color: #4dabf7; margin-left: 4px; font-size: 0.8em;" title="Stored in Paperless-ngx"></i> (${i18next.t('warranties.upload_new_file_replace')})
|
||
</span>
|
||
`;
|
||
deleteManualBtn.style.display = '';
|
||
} else {
|
||
currentManualElement.innerHTML = `<span>${i18next.t('warranties.no_manual_uploaded')}</span>`;
|
||
deleteManualBtn.style.display = 'none';
|
||
}
|
||
// Reset delete state
|
||
deleteManualBtn.dataset.delete = 'false';
|
||
deleteManualBtn.onclick = function() {
|
||
deleteManualBtn.dataset.delete = 'true';
|
||
currentManualElement.innerHTML = `<span class="text-danger">${i18next.t('warranties.manual_will_be_deleted')}</span>`;
|
||
deleteManualBtn.style.display = 'none';
|
||
};
|
||
}
|
||
|
||
// Show current product photo if exists
|
||
const currentProductPhotoElement = document.getElementById('currentProductPhoto');
|
||
const deleteProductPhotoBtn = document.getElementById('deleteProductPhotoBtn');
|
||
if (currentProductPhotoElement && deleteProductPhotoBtn) {
|
||
const hasLocalPhoto = warranty.product_photo_path && warranty.product_photo_path !== 'null';
|
||
const hasPaperlessPhoto = warranty.paperless_photo_id && warranty.paperless_photo_id !== null;
|
||
|
||
if (hasLocalPhoto) {
|
||
currentProductPhotoElement.innerHTML = `
|
||
<span class="text-success">
|
||
<i class="fas fa-check-circle"></i> ${i18next.t('warranties.current_photo')}:
|
||
<img data-secure-src="/api/secure-file/${warranty.product_photo_path.replace('uploads/', '')}" alt="Current Photo" class="secure-image"
|
||
style="max-width: 100px; max-height: 100px; object-fit: cover; border-radius: 8px; margin-left: 10px; border: 2px solid var(--border-color);"
|
||
onerror="this.style.display='none'">
|
||
<br><small>(${i18next.t('warranties.upload_new_photo_replace')})</small>
|
||
</span>
|
||
`;
|
||
deleteProductPhotoBtn.style.display = '';
|
||
} else if (hasPaperlessPhoto) {
|
||
currentProductPhotoElement.innerHTML = `
|
||
<span class="text-success">
|
||
<i class="fas fa-check-circle"></i> ${i18next.t('warranties.current_photo')}:
|
||
<a href="#" class="view-document-link" onclick="openPaperlessDocument(${warranty.paperless_photo_id}); return false;">View</a>
|
||
<i class="fas fa-cloud" style="color: #4dabf7; margin-left: 4px; font-size: 0.8em;" title="Stored in Paperless-ngx"></i>
|
||
<br><small>(${i18next.t('warranties.upload_new_photo_replace')})</small>
|
||
</span>
|
||
`;
|
||
deleteProductPhotoBtn.style.display = '';
|
||
} else {
|
||
currentProductPhotoElement.innerHTML = `<span>${i18next.t('warranties.no_photo_uploaded')}</span>`;
|
||
deleteProductPhotoBtn.style.display = 'none';
|
||
}
|
||
// Reset delete state
|
||
deleteProductPhotoBtn.dataset.delete = 'false';
|
||
deleteProductPhotoBtn.onclick = function() {
|
||
deleteProductPhotoBtn.dataset.delete = 'true';
|
||
currentProductPhotoElement.innerHTML = `<span class="text-danger">${i18next.t('warranties.photo_will_be_deleted')}</span>`;
|
||
deleteProductPhotoBtn.style.display = 'none';
|
||
};
|
||
}
|
||
|
||
// Show current other document if exists
|
||
const currentOtherDocumentElement = document.getElementById('currentOtherDocument');
|
||
const deleteOtherDocumentBtn = document.getElementById('deleteOtherDocumentBtn');
|
||
if (currentOtherDocumentElement && deleteOtherDocumentBtn) {
|
||
const hasLocalOther = warranty.other_document_path && warranty.other_document_path !== 'null';
|
||
const hasPaperlessOther = warranty.paperless_other_id && warranty.paperless_other_id !== null;
|
||
|
||
if (hasLocalOther) {
|
||
currentOtherDocumentElement.innerHTML = `
|
||
<span class="text-success">
|
||
<i class="fas fa-check-circle"></i> ${i18next.t('warranties.current_other_document')}:
|
||
<a href="#" class="view-document-link" onclick="openSecureFile('${warranty.other_document_path}'); return false;">View</a>
|
||
(${i18next.t('warranties.upload_new_file_replace')})
|
||
</span>
|
||
`;
|
||
deleteOtherDocumentBtn.style.display = '';
|
||
} else if (hasPaperlessOther) {
|
||
currentOtherDocumentElement.innerHTML = `
|
||
<span class="text-success">
|
||
<i class="fas fa-check-circle"></i> ${i18next.t('warranties.current_other_document')}:
|
||
<a href="#" class="view-document-link" onclick="openPaperlessDocument(${warranty.paperless_other_id}); return false;">View</a>
|
||
<i class="fas fa-cloud" style="color: #4dabf7; margin-left: 4px; font-size: 0.8em;" title="Stored in Paperless-ngx"></i> (${i18next.t('warranties.upload_new_file_replace')})
|
||
</span>
|
||
`;
|
||
deleteOtherDocumentBtn.style.display = '';
|
||
} else {
|
||
currentOtherDocumentElement.innerHTML = `<span>${i18next.t('warranties.no_other_document_uploaded')}</span>`;
|
||
deleteOtherDocumentBtn.style.display = 'none';
|
||
}
|
||
// Reset delete state
|
||
deleteOtherDocumentBtn.dataset.delete = 'false';
|
||
deleteOtherDocumentBtn.onclick = function() {
|
||
deleteOtherDocumentBtn.dataset.delete = 'true';
|
||
currentOtherDocumentElement.innerHTML = `<span class="text-danger">${i18next.t('warranties.other_document_will_be_deleted')}</span>`;
|
||
deleteOtherDocumentBtn.style.display = 'none';
|
||
};
|
||
}
|
||
|
||
// Reset file inputs
|
||
document.getElementById('editProductPhoto').value = '';
|
||
document.getElementById('editInvoice').value = '';
|
||
document.getElementById('editManual').value = '';
|
||
document.getElementById('editOtherDocument').value = '';
|
||
document.getElementById('editProductPhotoFileName').textContent = '';
|
||
document.getElementById('editFileName').textContent = '';
|
||
document.getElementById('editManualFileName').textContent = '';
|
||
document.getElementById('editOtherDocumentFileName').textContent = '';
|
||
|
||
// Reset photo preview
|
||
const editPhotoPreview = document.getElementById('editProductPhotoPreview');
|
||
if (editPhotoPreview) {
|
||
editPhotoPreview.style.display = 'none';
|
||
}
|
||
|
||
// Set storage options based on current document storage
|
||
if (paperlessNgxEnabled) {
|
||
// Set product photo storage option
|
||
const editProductPhotoStorageRadios = document.getElementsByName('editProductPhotoStorage');
|
||
if (editProductPhotoStorageRadios.length > 0) {
|
||
const isPaperlessPhoto = warranty.paperless_photo_id && warranty.paperless_photo_id !== null;
|
||
editProductPhotoStorageRadios.forEach(radio => {
|
||
radio.checked = isPaperlessPhoto ? (radio.value === 'paperless') : (radio.value === 'local');
|
||
});
|
||
}
|
||
|
||
// Set invoice storage option
|
||
const editInvoiceStorageRadios = document.getElementsByName('editInvoiceStorage');
|
||
if (editInvoiceStorageRadios.length > 0) {
|
||
const isPaperlessInvoice = warranty.paperless_invoice_id && warranty.paperless_invoice_id !== null;
|
||
editInvoiceStorageRadios.forEach(radio => {
|
||
radio.checked = isPaperlessInvoice ? (radio.value === 'paperless') : (radio.value === 'local');
|
||
});
|
||
}
|
||
|
||
// Set manual storage option
|
||
const editManualStorageRadios = document.getElementsByName('editManualStorage');
|
||
if (editManualStorageRadios.length > 0) {
|
||
const isPaperlessManual = warranty.paperless_manual_id && warranty.paperless_manual_id !== null;
|
||
editManualStorageRadios.forEach(radio => {
|
||
radio.checked = isPaperlessManual ? (radio.value === 'paperless') : (radio.value === 'local');
|
||
});
|
||
}
|
||
|
||
// Set other document storage option
|
||
const editOtherDocumentStorageRadios = document.getElementsByName('editOtherDocumentStorage');
|
||
if (editOtherDocumentStorageRadios.length > 0) {
|
||
const isPaperlessOther = warranty.paperless_other_id && warranty.paperless_other_id !== null;
|
||
editOtherDocumentStorageRadios.forEach(radio => {
|
||
radio.checked = isPaperlessOther ? (radio.value === 'paperless') : (radio.value === 'local');
|
||
});
|
||
}
|
||
|
||
console.log('[Edit Modal] Storage options set based on current document storage:', {
|
||
photo: warranty.paperless_photo_id ? 'paperless' : 'local',
|
||
invoice: warranty.paperless_invoice_id ? 'paperless' : 'local',
|
||
manual: warranty.paperless_manual_id ? 'paperless' : 'local',
|
||
other: warranty.paperless_other_id ? 'paperless' : 'local'
|
||
});
|
||
}
|
||
|
||
// Initialize file input event listeners
|
||
const editProductPhotoInput = document.getElementById('editProductPhoto');
|
||
if (editProductPhotoInput) {
|
||
editProductPhotoInput.addEventListener('change', function(event) {
|
||
updateFileName(event, 'editProductPhoto', 'editProductPhotoFileName');
|
||
});
|
||
}
|
||
|
||
const editInvoiceInput = document.getElementById('editInvoice');
|
||
if (editInvoiceInput) {
|
||
editInvoiceInput.addEventListener('change', function(event) {
|
||
updateFileName(event, 'editInvoice', 'editFileName');
|
||
});
|
||
}
|
||
|
||
const editManualInput = document.getElementById('editManual');
|
||
if (editManualInput) {
|
||
editManualInput.addEventListener('change', function(event) {
|
||
updateFileName(event, 'editManual', 'editManualFileName');
|
||
});
|
||
}
|
||
|
||
const editOtherDocumentInput = document.getElementById('editOtherDocument');
|
||
if (editOtherDocumentInput) {
|
||
editOtherDocumentInput.addEventListener('change', function(event) {
|
||
updateFileName(event, 'editOtherDocument', 'editOtherDocumentFileName');
|
||
});
|
||
}
|
||
|
||
// Show edit modal
|
||
const modalBackdrop = document.getElementById('editModal');
|
||
if (modalBackdrop) {
|
||
modalBackdrop.classList.add('active'); // Add active class to display as flex
|
||
}
|
||
|
||
// Reset tabs to first tab
|
||
const editTabBtns = document.querySelectorAll('.edit-tab-btn');
|
||
editTabBtns.forEach(btn => btn.classList.remove('active'));
|
||
document.querySelector('.edit-tab-btn[data-tab="edit-product-info"]').classList.add('active');
|
||
|
||
// Reset tab content
|
||
document.querySelectorAll('.edit-tab-content').forEach(content => content.classList.remove('active'));
|
||
document.getElementById('edit-product-info').classList.add('active');
|
||
|
||
// Initialize edit mode tags
|
||
editSelectedTags = [];
|
||
|
||
// If warranty has tags, populate editSelectedTags
|
||
if (warranty.tags && Array.isArray(warranty.tags)) {
|
||
editSelectedTags = warranty.tags.map(tag => ({
|
||
id: tag.id,
|
||
name: tag.name,
|
||
color: tag.color
|
||
}));
|
||
}
|
||
|
||
// Render selected tags using the helper function
|
||
renderEditSelectedTags();
|
||
|
||
// Set up tag search in edit mode
|
||
const editTagSearch = document.getElementById('editTagSearch');
|
||
const editTagsList = document.getElementById('editTagsList');
|
||
|
||
if (editTagSearch && editTagsList) {
|
||
// Add event listeners for tag search
|
||
editTagSearch.addEventListener('focus', () => {
|
||
renderEditTagsList();
|
||
editTagsList.classList.add('show');
|
||
});
|
||
|
||
editTagSearch.addEventListener('input', () => {
|
||
renderEditTagsList(editTagSearch.value);
|
||
});
|
||
|
||
// Add event listener to close dropdown when clicking outside
|
||
document.addEventListener('click', (e) => {
|
||
if (!editTagSearch.contains(e.target) && !editTagsList.contains(e.target)) {
|
||
editTagsList.classList.remove('show');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Set up manage tags button in edit mode
|
||
const editManageTagsBtn = document.getElementById('editManageTagsBtn');
|
||
if (editManageTagsBtn) {
|
||
editManageTagsBtn.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
openTagManagementModal();
|
||
});
|
||
}
|
||
|
||
// Validate all tabs to update completion indicators
|
||
validateEditTab('edit-product-info');
|
||
validateEditTab('edit-warranty-details');
|
||
validateEditTab('edit-documents');
|
||
validateEditTab('edit-tags');
|
||
|
||
// Add input event listeners to update validation status
|
||
document.querySelectorAll('#editWarrantyForm input').forEach(input => {
|
||
input.addEventListener('input', function() {
|
||
// Find the tab this input belongs to
|
||
const tabContent = this.closest('.edit-tab-content');
|
||
if (tabContent) {
|
||
validateEditTab(tabContent.id);
|
||
}
|
||
});
|
||
});
|
||
|
||
// --- Set Lifetime Checkbox and Toggle Duration Fields ---
|
||
if (editIsLifetimeCheckbox && editWarrantyDurationFields) {
|
||
editIsLifetimeCheckbox.checked = warranty.is_lifetime || false;
|
||
handleEditLifetimeChange(); // Call handler to set initial state
|
||
|
||
// Remove previous listener if exists
|
||
editIsLifetimeCheckbox.removeEventListener('change', handleEditLifetimeChange);
|
||
// Add new listener
|
||
editIsLifetimeCheckbox.addEventListener('change', handleEditLifetimeChange);
|
||
|
||
// Set duration values only if NOT lifetime
|
||
if (!warranty.is_lifetime) {
|
||
document.getElementById('editWarrantyDurationYears').value = warranty.warranty_duration_years || 0;
|
||
document.getElementById('editWarrantyDurationMonths').value = warranty.warranty_duration_months || 0;
|
||
document.getElementById('editWarrantyDurationDays').value = warranty.warranty_duration_days || 0;
|
||
} else {
|
||
document.getElementById('editWarrantyDurationYears').value = '';
|
||
document.getElementById('editWarrantyDurationMonths').value = '';
|
||
document.getElementById('editWarrantyDurationDays').value = '';
|
||
}
|
||
} else {
|
||
console.error("Lifetime warranty elements or duration fields not found in edit form");
|
||
}
|
||
|
||
// --- Set Warranty Method Selection ---
|
||
if (editDurationMethodRadio && editExactDateMethodRadio && editExactExpirationDateInput) {
|
||
console.log('[DEBUG Edit Modal] Warranty method detection:', {
|
||
originalInputMethod: warranty.original_input_method,
|
||
isLifetime: warranty.is_lifetime,
|
||
expirationDate: warranty.expiration_date,
|
||
warrantyDurationYears: warranty.warranty_duration_years,
|
||
warrantyDurationMonths: warranty.warranty_duration_months,
|
||
warrantyDurationDays: warranty.warranty_duration_days
|
||
});
|
||
|
||
// Use the original input method if available, otherwise fall back to previous logic
|
||
if (!warranty.is_lifetime) {
|
||
if (warranty.original_input_method === 'exact_date') {
|
||
// Use exact date method
|
||
editExactDateMethodRadio.checked = true;
|
||
editDurationMethodRadio.checked = false;
|
||
editExactExpirationDateInput.value = warranty.expiration_date.split('T')[0];
|
||
console.log('[DEBUG Edit Modal] Selected exact date method based on original_input_method');
|
||
} else {
|
||
// Use duration method (either explicitly set or fallback)
|
||
editDurationMethodRadio.checked = true;
|
||
editExactDateMethodRadio.checked = false;
|
||
editExactExpirationDateInput.value = '';
|
||
console.log('[DEBUG Edit Modal] Selected duration method based on original_input_method or fallback');
|
||
}
|
||
}
|
||
|
||
// Set up event listeners for warranty method change
|
||
editDurationMethodRadio.removeEventListener('change', handleEditWarrantyMethodChange);
|
||
editExactDateMethodRadio.removeEventListener('change', handleEditWarrantyMethodChange);
|
||
editDurationMethodRadio.addEventListener('change', handleEditWarrantyMethodChange);
|
||
editExactDateMethodRadio.addEventListener('change', handleEditWarrantyMethodChange);
|
||
|
||
console.log('[DEBUG Edit Modal] Event listeners attached to warranty method radio buttons');
|
||
console.log('[DEBUG Edit Modal] Initial radio states:', {
|
||
durationChecked: editDurationMethodRadio.checked,
|
||
exactDateChecked: editExactDateMethodRadio.checked
|
||
});
|
||
|
||
// Call handler to set initial state
|
||
handleEditWarrantyMethodChange();
|
||
}
|
||
|
||
// Set notes
|
||
const notesInput = document.getElementById('editNotes');
|
||
if (notesInput) {
|
||
notesInput.value = warranty.notes || '';
|
||
}
|
||
|
||
// Update currency symbols and positioning for the edit form
|
||
const symbol = getCurrencySymbol();
|
||
const position = getCurrencyPosition();
|
||
updateFormCurrencyPosition(symbol, position);
|
||
|
||
// Trigger currency positioning after modal is visible
|
||
setTimeout(() => {
|
||
if (position === 'right') {
|
||
const editPriceInput = document.getElementById('editPurchasePrice');
|
||
const editCurrencySymbol = document.getElementById('editCurrencySymbol');
|
||
if (editPriceInput && editCurrencySymbol) {
|
||
// Force update the currency position now that modal is visible
|
||
const wrapper = editPriceInput.closest('.price-input-wrapper');
|
||
if (wrapper && wrapper.classList.contains('currency-right')) {
|
||
const updateEvent = new Event('focus');
|
||
editPriceInput.dispatchEvent(updateEvent);
|
||
const blurEvent = new Event('blur');
|
||
editPriceInput.dispatchEvent(blurEvent);
|
||
}
|
||
}
|
||
}
|
||
}, 200);
|
||
|
||
// Load secure images with authentication for the edit modal
|
||
setTimeout(() => loadSecureImages(), 100); // Small delay to ensure DOM is updated
|
||
}
|
||
|
||
function openDeleteModal(warrantyId, productName) {
|
||
currentWarrantyId = warrantyId;
|
||
|
||
const deleteProductNameElement = document.getElementById('deleteProductName');
|
||
if (deleteProductNameElement) {
|
||
deleteProductNameElement.textContent = productName || '';
|
||
}
|
||
|
||
const deleteModal = document.getElementById('deleteModal');
|
||
if (deleteModal) {
|
||
deleteModal.classList.add('active');
|
||
}
|
||
}
|
||
|
||
// Function to close all modals
|
||
function closeModals() {
|
||
document.querySelectorAll('.modal-backdrop').forEach(modal => {
|
||
modal.classList.remove('active');
|
||
});
|
||
}
|
||
|
||
// Validate file size before upload
|
||
function validateFileSize(formData, maxSizeMB = 32) {
|
||
let totalSize = 0;
|
||
|
||
// Check file sizes
|
||
if (formData.has('invoice') && formData.get('invoice').size > 0) {
|
||
totalSize += formData.get('invoice').size;
|
||
}
|
||
|
||
if (formData.has('manual') && formData.get('manual').size > 0) {
|
||
totalSize += formData.get('manual').size;
|
||
}
|
||
|
||
if (formData.has('other_document') && formData.get('other_document').size > 0) {
|
||
totalSize += formData.get('other_document').size;
|
||
}
|
||
|
||
// Convert bytes to MB for comparison and display
|
||
const totalSizeMB = totalSize / (1024 * 1024);
|
||
console.log(`Total upload size: ${totalSizeMB.toFixed(2)} MB`);
|
||
|
||
// Check if total size exceeds limit
|
||
if (totalSizeMB > maxSizeMB) {
|
||
return {
|
||
valid: false,
|
||
message: `Total file size (${totalSizeMB.toFixed(2)} MB) exceeds the maximum allowed size of ${maxSizeMB} MB. Please reduce file sizes.`
|
||
};
|
||
}
|
||
|
||
return {
|
||
valid: true
|
||
};
|
||
}
|
||
|
||
// Submit form function - event handler for form submit
|
||
async function handleFormSubmit(event) { // Made async to properly await paperless uploads
|
||
event.preventDefault();
|
||
|
||
const isLifetime = isLifetimeCheckbox.checked;
|
||
const isDurationMethod = durationMethodRadio && durationMethodRadio.checked;
|
||
const years = parseInt(warrantyDurationYearsInput.value || 0);
|
||
const months = parseInt(warrantyDurationMonthsInput.value || 0);
|
||
const days = parseInt(warrantyDurationDaysInput.value || 0);
|
||
const exactDate = exactExpirationDateInput ? exactExpirationDateInput.value : '';
|
||
|
||
// --- Updated Lifetime and Method Check ---
|
||
if (!isLifetime) {
|
||
if (isDurationMethod) {
|
||
// Validate duration fields
|
||
if (years === 0 && months === 0 && days === 0) {
|
||
showValidationErrors(1);
|
||
switchToTab(1); // Switch to warranty details tab
|
||
// Optionally focus the first duration input
|
||
if (warrantyDurationYearsInput) warrantyDurationYearsInput.focus();
|
||
// Add invalid class to the container or individual inputs if needed
|
||
if (warrantyDurationFields) warrantyDurationFields.classList.add('invalid-duration'); // Example
|
||
return;
|
||
}
|
||
} else {
|
||
// Validate exact expiration date
|
||
if (!exactDate) {
|
||
showValidationErrors(1);
|
||
switchToTab(1); // Switch to warranty details tab
|
||
if (exactExpirationDateInput) exactExpirationDateInput.focus();
|
||
return;
|
||
}
|
||
|
||
// Validate that expiration date is in the future relative to purchase date
|
||
const purchaseDate = document.getElementById('purchaseDate').value;
|
||
if (purchaseDate && exactDate <= purchaseDate) {
|
||
showToast(window.t('messages.expiration_date_after_purchase_date'), 'error');
|
||
switchToTab(1);
|
||
if (exactExpirationDateInput) exactExpirationDateInput.focus();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Remove invalid duration class if validation passes
|
||
if (warrantyDurationFields) warrantyDurationFields.classList.remove('invalid-duration');
|
||
|
||
// Validate all tabs
|
||
for (let i = 0; i < tabContents.length; i++) {
|
||
if (!validateTab(i)) {
|
||
// Switch to the first invalid tab
|
||
switchToTab(i);
|
||
showValidationErrors(i);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Create form data object
|
||
const formData = new FormData(warrantyForm);
|
||
|
||
// Handle warranty type - use custom value if "other" is selected
|
||
const warrantyTypeSelect = document.getElementById('warrantyType');
|
||
const warrantyTypeCustom = document.getElementById('warrantyTypeCustom');
|
||
if (warrantyTypeSelect && warrantyTypeSelect.value === 'other' && warrantyTypeCustom && warrantyTypeCustom.value.trim()) {
|
||
formData.set('warranty_type', warrantyTypeCustom.value.trim());
|
||
}
|
||
|
||
// Debug: Log all form data entries
|
||
console.log('=== DEBUG: Form Data Contents ===');
|
||
for (let [key, value] of formData.entries()) {
|
||
console.log(`${key}: ${value}`);
|
||
}
|
||
console.log('=== END DEBUG ===');
|
||
|
||
// Product URL handling
|
||
let productUrlValue = formData.get('product_url');
|
||
if (productUrlValue && typeof productUrlValue === 'string') {
|
||
productUrlValue = productUrlValue.trim();
|
||
if (productUrlValue && !productUrlValue.startsWith('http://') && !productUrlValue.startsWith('https://')) {
|
||
formData.set('product_url', 'https://' + productUrlValue);
|
||
} else if (productUrlValue) {
|
||
// Ensure trimmed value is set back if it was already valid
|
||
formData.set('product_url', productUrlValue);
|
||
}
|
||
}
|
||
|
||
// Remove old warranty_years if it exists in formData (it shouldn't if HTML is correct)
|
||
formData.delete('warranty_years');
|
||
|
||
// Append new duration fields (already handled by FormData constructor if names match)
|
||
// formData.append('warranty_duration_years', years);
|
||
// formData.append('warranty_duration_months', months);
|
||
// formData.append('warranty_duration_days', days);
|
||
|
||
// Add serial numbers to form data (using correct name 'serial_numbers[]')
|
||
const serialInputs = document.querySelectorAll('#serialNumbersContainer input[name="serial_numbers[]"]');
|
||
// Clear existing serial_numbers[] from formData before appending new ones
|
||
formData.delete('serial_numbers[]');
|
||
serialInputs.forEach(input => {
|
||
if (input.value.trim()) {
|
||
formData.append('serial_numbers[]', input.value.trim()); // Use [] for arrays
|
||
}
|
||
});
|
||
|
||
// Add tag IDs to form data as JSON string
|
||
if (selectedTags && selectedTags.length > 0) {
|
||
const tagIds = selectedTags.map(tag => tag.id);
|
||
formData.append('tag_ids', JSON.stringify(tagIds));
|
||
} else {
|
||
formData.append('tag_ids', JSON.stringify([])); // Send empty array if no tags
|
||
}
|
||
|
||
// --- Ensure is_lifetime is correctly added ---
|
||
// FormData already includes it if the checkbox is checked. If not checked, it's omitted.
|
||
// We need to explicitly add 'false' if it's not checked.
|
||
if (!isLifetimeCheckbox.checked) {
|
||
formData.append('is_lifetime', 'false');
|
||
|
||
// Add warranty method and exact expiration date if using exact date method
|
||
if (!isDurationMethod && exactDate) {
|
||
formData.append('exact_expiration_date', exactDate);
|
||
// Ensure duration fields are 0 when using exact date
|
||
formData.set('warranty_duration_years', '0');
|
||
formData.set('warranty_duration_months', '0');
|
||
formData.set('warranty_duration_days', '0');
|
||
}
|
||
} else {
|
||
// Ensure duration fields are 0 if lifetime is checked
|
||
formData.set('warranty_duration_years', '0');
|
||
formData.set('warranty_duration_months', '0');
|
||
formData.set('warranty_duration_days', '0');
|
||
}
|
||
|
||
// Add other_document file (always, as no storage selection for this)
|
||
const otherDocumentFile = document.getElementById('otherDocument').files[0];
|
||
if (otherDocumentFile) {
|
||
formData.append('other_document', otherDocumentFile);
|
||
}
|
||
|
||
// --- Only append invoice/manual files to FormData if storage is 'local' ---
|
||
const invoiceFile = document.getElementById('invoice')?.files[0];
|
||
const manualFile = document.getElementById('manual')?.files[0];
|
||
let invoiceStorage = 'local';
|
||
let manualStorage = 'local';
|
||
const invoiceStorageRadio = document.querySelector('input[name="invoiceStorage"]:checked');
|
||
const manualStorageRadio = document.querySelector('input[name="manualStorage"]:checked');
|
||
if (invoiceStorageRadio) invoiceStorage = invoiceStorageRadio.value;
|
||
if (manualStorageRadio) manualStorage = manualStorageRadio.value;
|
||
formData.set('invoiceStorage', invoiceStorage);
|
||
formData.set('manualStorage', manualStorage);
|
||
console.log('[DEBUG] Invoice storage:', invoiceStorage, 'Manual storage:', manualStorage);
|
||
console.log('[DEBUG] Invoice file:', invoiceFile);
|
||
console.log('[DEBUG] Manual file:', manualFile);
|
||
if (invoiceStorage === 'local' && invoiceFile) {
|
||
console.log('[DEBUG] Appending invoice file to FormData (local storage)');
|
||
formData.append('invoice', invoiceFile);
|
||
}
|
||
if (manualStorage === 'local' && manualFile) {
|
||
console.log('[DEBUG] Appending manual file to FormData (local storage)');
|
||
formData.append('manual', manualFile);
|
||
}
|
||
if (invoiceStorage === 'paperless') {
|
||
console.log('[DEBUG] Invoice should be uploaded to Paperless-ngx');
|
||
}
|
||
if (manualStorage === 'paperless') {
|
||
console.log('[DEBUG] Manual should be uploaded to Paperless-ngx');
|
||
}
|
||
|
||
// Add selected Paperless documents (for linking existing docs, not uploads)
|
||
const selectedPaperlessProductPhoto = document.getElementById('selectedPaperlessProductPhoto');
|
||
const selectedPaperlessInvoice = document.getElementById('selectedPaperlessInvoice');
|
||
const selectedPaperlessManual = document.getElementById('selectedPaperlessManual');
|
||
const selectedPaperlessOtherDocument = document.getElementById('selectedPaperlessOtherDocument');
|
||
if (selectedPaperlessProductPhoto && selectedPaperlessProductPhoto.value) {
|
||
formData.append('paperless_photo_id', selectedPaperlessProductPhoto.value);
|
||
}
|
||
if (selectedPaperlessInvoice && selectedPaperlessInvoice.value) {
|
||
formData.append('paperless_invoice_id', selectedPaperlessInvoice.value);
|
||
}
|
||
if (selectedPaperlessManual && selectedPaperlessManual.value) {
|
||
formData.append('paperless_manual_id', selectedPaperlessManual.value);
|
||
}
|
||
if (selectedPaperlessOtherDocument && selectedPaperlessOtherDocument.value) {
|
||
formData.append('paperless_other_id', selectedPaperlessOtherDocument.value);
|
||
}
|
||
|
||
// Show loading spinner
|
||
showLoadingSpinner();
|
||
|
||
try {
|
||
// Process Paperless-ngx uploads if enabled
|
||
const paperlessUploads = await processPaperlessNgxUploads(formData);
|
||
|
||
// Add Paperless-ngx document IDs to form data
|
||
Object.keys(paperlessUploads).forEach(key => {
|
||
formData.append(key, paperlessUploads[key]);
|
||
});
|
||
|
||
// Send the form data to the server
|
||
const response = await fetch('/api/warranties', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': 'Bearer ' + localStorage.getItem('auth_token')
|
||
},
|
||
body: formData
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json();
|
||
throw new Error(errorData.error || 'Failed to add warranty');
|
||
}
|
||
|
||
const data = await response.json();
|
||
hideLoadingSpinner();
|
||
showToast(window.t('messages.warranty_added_successfully'), 'success');
|
||
|
||
// Store the new warranty ID for auto-linking
|
||
const newWarrantyId = data.id;
|
||
|
||
// --- Store file info and storage type before upload for auto-link logic ---
|
||
const invoiceFileInput = document.getElementById('invoice');
|
||
const manualFileInput = document.getElementById('manual');
|
||
const invoiceFilePre = invoiceFileInput?.files[0];
|
||
const manualFilePre = manualFileInput?.files[0];
|
||
const invoiceStoragePre = formData.get('invoiceStorage');
|
||
const manualStoragePre = formData.get('manualStorage');
|
||
|
||
// Auto-link any documents that were uploaded to Paperless-ngx (match edit modal behavior)
|
||
const autoLinkTypes = [];
|
||
const fileInfo = {};
|
||
if (invoiceStoragePre === 'paperless' && invoiceFilePre) {
|
||
autoLinkTypes.push('invoice');
|
||
fileInfo.invoice = invoiceFilePre.name;
|
||
}
|
||
if (manualStoragePre === 'paperless' && manualFilePre) {
|
||
autoLinkTypes.push('manual');
|
||
fileInfo.manual = manualFilePre.name;
|
||
}
|
||
// Other document does not have a storage option, so skip unless you add support
|
||
// If you want to support auto-linking for 'other', add logic here
|
||
console.log('[Auto-Link DEBUG] newWarrantyId:', newWarrantyId, 'autoLinkTypes:', autoLinkTypes, 'fileInfo:', fileInfo, 'invoiceStorage:', invoiceStoragePre, 'manualStorage:', manualStoragePre);
|
||
if (autoLinkTypes.length > 0 && newWarrantyId) {
|
||
console.log('[Auto-Link] Starting automatic document linking after warranty creation (Paperless-ngx uploads only)', autoLinkTypes, fileInfo);
|
||
setTimeout(() => {
|
||
console.log('[Auto-Link DEBUG] Calling autoLinkRecentDocuments with:', newWarrantyId, autoLinkTypes, fileInfo);
|
||
autoLinkRecentDocuments(newWarrantyId, autoLinkTypes, 10, 10000, fileInfo);
|
||
}, 3000); // Wait 3 seconds for Paperless-ngx to process the documents
|
||
}
|
||
|
||
// Close and reset the modal on success
|
||
if (addWarrantyModal) {
|
||
addWarrantyModal.classList.remove('active');
|
||
}
|
||
resetAddWarrantyWizard(); // Reset the wizard form
|
||
|
||
try {
|
||
await loadWarranties(true);
|
||
console.log('Warranties reloaded after adding new warranty');
|
||
applyFilters();
|
||
|
||
// Load secure images for the new cards
|
||
setTimeout(() => {
|
||
console.log('Loading secure images for new warranty cards');
|
||
loadSecureImages();
|
||
}, 200);
|
||
} catch (error) {
|
||
console.error('Error reloading warranties after adding:', error);
|
||
}
|
||
} catch (error) {
|
||
hideLoadingSpinner();
|
||
console.error('Error adding warranty:', error);
|
||
showToast(error.message || window.t('messages.failed_to_add_warranty'), 'error');
|
||
}
|
||
}
|
||
|
||
// Initialize page
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// Initialize the warranty form *only* if the form element exists
|
||
if (warrantyForm) {
|
||
initWarrantyForm();
|
||
}
|
||
|
||
// Load warranties (might need checks if warrantiesList doesn't always exist)
|
||
if (warrantiesList) {
|
||
// REMOVED: loadWarranties(); // Now called after authStateReady
|
||
// REMOVED: loadViewPreference(); // Now called after authStateReady
|
||
loadTags(); // Load tags for the form
|
||
initTagFunctionality(); // Initialize tag search/selection
|
||
}
|
||
|
||
// Initialize theme (should be safe on all pages)
|
||
initializeTheme();
|
||
|
||
// Set up event listeners for other UI controls (should contain checks)
|
||
setupUIEventListeners();
|
||
setupModalTriggers(); // Add the new modal listeners
|
||
|
||
// Check if user is logged in and update UI
|
||
// checkLoginStatus(); // Removed undefined function
|
||
|
||
// Setup form submission
|
||
const form = document.getElementById('addWarrantyForm');
|
||
if (form) {
|
||
form.addEventListener('submit', handleFormSubmit); // Use renamed handler
|
||
}
|
||
|
||
// Setup settings menu toggle
|
||
// setupSettingsMenu(); // Removed: function not defined, handled by auth.js
|
||
|
||
// Initialize theme toggle state *after* DOM is loaded
|
||
// Find the header toggle (assuming ID 'darkModeToggle')
|
||
const headerToggle = document.getElementById('darkModeToggle');
|
||
if (headerToggle) {
|
||
// Set initial state based on theme applied by theme-loader.js
|
||
const currentTheme = document.documentElement.getAttribute('data-theme');
|
||
headerToggle.checked = currentTheme === 'dark';
|
||
|
||
// Add listener to update theme when toggled
|
||
headerToggle.addEventListener('change', function() {
|
||
setTheme(this.checked);
|
||
});
|
||
}
|
||
|
||
// REMOVE any direct calls to initializeTheme() from here or globally
|
||
// initializeTheme();
|
||
|
||
// Setup view switcher
|
||
// setupViewSwitcher(); // Removed undefined function
|
||
|
||
// Setup filter controls
|
||
// setupFilterControls(); // Removed: function not defined
|
||
|
||
// Setup form tabs and navigation
|
||
// initFormTabs(); // <-- Remove this line from DOMContentLoaded
|
||
|
||
// Initialize modal interactions
|
||
// initializeModals(); // Removed: function not defined, handled by setupModalTriggers
|
||
|
||
// Load preferences (if needed for things other than theme)
|
||
// loadPreferences(); // Consider if needed
|
||
|
||
// REMOVED: updateCurrencySymbols(); // Now called after authStateReady
|
||
});
|
||
|
||
// Add this function to handle edit tab functionality
|
||
function initEditTabs() {
|
||
const editTabBtns = document.querySelectorAll('.edit-tab-btn');
|
||
|
||
editTabBtns.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
// Remove active class from all tabs
|
||
editTabBtns.forEach(b => b.classList.remove('active'));
|
||
|
||
// Add active class to clicked tab
|
||
btn.classList.add('active');
|
||
|
||
// Hide all tab content
|
||
document.querySelectorAll('.edit-tab-content').forEach(content => {
|
||
content.classList.remove('active');
|
||
});
|
||
|
||
// Show the selected tab content
|
||
const tabId = btn.getAttribute('data-tab');
|
||
document.getElementById(tabId).classList.add('active');
|
||
});
|
||
});
|
||
}
|
||
|
||
// Update validateEditTabs function
|
||
function validateEditTab(tabId) {
|
||
const tab = document.getElementById(tabId);
|
||
if (!tab) {
|
||
console.warn('validateEditTab: Could not find tab with ID:', tabId);
|
||
return false; // Or true, depending on desired behavior for missing tabs
|
||
}
|
||
let isTabValid = true;
|
||
|
||
// Get all relevant form controls within the tab
|
||
const controls = tab.querySelectorAll('input, textarea, select');
|
||
|
||
controls.forEach(control => {
|
||
// Check the native HTML5 validity state
|
||
if (!control.validity.valid) {
|
||
isTabValid = false;
|
||
control.classList.add('invalid');
|
||
// Optionally, you could add logic here to display specific messages
|
||
// or rely on browser default behavior if the form is submitted.
|
||
} else {
|
||
control.classList.remove('invalid');
|
||
}
|
||
});
|
||
|
||
// Update the tab button to show completion status
|
||
const tabBtn = document.querySelector(`.edit-tab-btn[data-tab="${tabId}"]`);
|
||
if (tabBtn) {
|
||
if (isTabValid) {
|
||
tabBtn.classList.add('completed');
|
||
} else {
|
||
tabBtn.classList.remove('completed');
|
||
}
|
||
}
|
||
return isTabValid;
|
||
}
|
||
|
||
// Add this function for secure file access
|
||
function openSecureFile(filePath) {
|
||
console.log(`[openSecureFile] Opening file: ${filePath}`);
|
||
|
||
// Get the file name from the path, handling both uploads/ prefix and direct filenames
|
||
let fileName = filePath;
|
||
if (filePath.startsWith('uploads/')) {
|
||
fileName = filePath.substring(8); // Remove 'uploads/' prefix
|
||
} else if (filePath.startsWith('/uploads/')) {
|
||
fileName = filePath.substring(9); // Remove '/uploads/' prefix
|
||
}
|
||
|
||
console.log(`[openSecureFile] Processed filename: ${fileName}`);
|
||
|
||
const token = auth.getToken();
|
||
|
||
if (!token) {
|
||
showToast(window.t('messages.login_to_access_files'), 'error');
|
||
return false;
|
||
}
|
||
|
||
// Enhanced fetch with retry logic and better error handling
|
||
const fetchWithRetry = async (url, options, retries = 2) => {
|
||
for (let i = 0; i <= retries; i++) {
|
||
try {
|
||
console.log(`[openSecureFile] Attempt ${i + 1} to fetch: ${url}`);
|
||
const response = await fetch(url, options);
|
||
|
||
if (!response.ok) {
|
||
if (response.status === 401) {
|
||
throw new Error('Authentication error. Please log in again.');
|
||
} else if (response.status === 403) {
|
||
throw new Error('You are not authorized to access this file.');
|
||
} else if (response.status === 404) {
|
||
throw new Error('File not found. It may have been deleted.');
|
||
} else {
|
||
throw new Error(`Server error: ${response.status} ${response.statusText}`);
|
||
}
|
||
}
|
||
|
||
// Check if response has content-length header
|
||
const contentLength = response.headers.get('content-length');
|
||
console.log(`[openSecureFile] Response Content-Length: ${contentLength}`);
|
||
|
||
// Convert to blob with error handling
|
||
const blob = await response.blob();
|
||
console.log(`[openSecureFile] Blob size: ${blob.size} bytes`);
|
||
|
||
// Verify blob size matches content-length if available
|
||
if (contentLength && parseInt(contentLength) !== blob.size) {
|
||
console.warn(`[openSecureFile] Content-Length mismatch: header=${contentLength}, blob=${blob.size}`);
|
||
if (i < retries) {
|
||
console.log(`[openSecureFile] Retrying due to content-length mismatch...`);
|
||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second before retry
|
||
continue;
|
||
} else {
|
||
console.error(`[openSecureFile] Final attempt failed with content-length mismatch`);
|
||
}
|
||
}
|
||
|
||
return blob;
|
||
|
||
} catch (error) {
|
||
console.error(`[openSecureFile] Attempt ${i + 1} failed:`, error);
|
||
|
||
// If this is a content-length mismatch or network error, retry
|
||
if (i < retries && (
|
||
error.message.includes('content-length') ||
|
||
error.message.includes('Failed to fetch') ||
|
||
error.name === 'TypeError'
|
||
)) {
|
||
console.log(`[openSecureFile] Retrying after error: ${error.message}`);
|
||
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); // Exponential backoff
|
||
continue;
|
||
}
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
};
|
||
|
||
fetchWithRetry(`/api/secure-file/${fileName}`, {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||
'Pragma': 'no-cache'
|
||
}
|
||
})
|
||
.then(blob => {
|
||
console.log(`[openSecureFile] Successfully received blob of size: ${blob.size}`);
|
||
|
||
// Create a URL for the blob
|
||
const blobUrl = window.URL.createObjectURL(blob);
|
||
|
||
// Open in new tab
|
||
const newWindow = window.open(blobUrl, '_blank');
|
||
|
||
// Clean up the blob URL after a delay to prevent memory leaks
|
||
setTimeout(() => {
|
||
window.URL.revokeObjectURL(blobUrl);
|
||
}, 10000); // Clean up after 10 seconds
|
||
|
||
// Check if window was blocked by popup blocker
|
||
if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') {
|
||
showToast(window.t('messages.popup_blocked'), 'warning');
|
||
window.URL.revokeObjectURL(blobUrl); // Clean up immediately if blocked
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error fetching file:', error);
|
||
|
||
// Provide more specific error messages
|
||
let errorMessage = 'Error opening file';
|
||
if (error.message.includes('Authentication')) {
|
||
errorMessage = 'Authentication error. Please refresh and try again.';
|
||
} else if (error.message.includes('authorized')) {
|
||
errorMessage = 'You are not authorized to access this file.';
|
||
} else if (error.message.includes('not found')) {
|
||
errorMessage = 'File not found. It may have been deleted.';
|
||
} else if (error.message.includes('Failed to fetch') || error.name === 'TypeError') {
|
||
errorMessage = 'Network error. Please check your connection and try again.';
|
||
} else {
|
||
errorMessage = `Error opening file: ${error.message}`;
|
||
}
|
||
|
||
showToast(errorMessage, 'error');
|
||
});
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Open a Paperless-ngx document by ID
|
||
*/
|
||
|
||
/**
|
||
* Generate document link HTML for both local and Paperless-ngx documents
|
||
*/
|
||
function generateDocumentLink(warranty, docType) {
|
||
const docConfig = {
|
||
invoice: {
|
||
localPath: warranty.invoice_path,
|
||
paperlessId: warranty.paperless_invoice_id,
|
||
icon: 'fas fa-file-invoice',
|
||
label: 'Invoice',
|
||
className: 'invoice-link'
|
||
},
|
||
manual: {
|
||
localPath: warranty.manual_path,
|
||
paperlessId: warranty.paperless_manual_id,
|
||
icon: 'fas fa-book',
|
||
label: 'Manual',
|
||
className: 'manual-link'
|
||
},
|
||
other: {
|
||
localPath: warranty.other_document_path,
|
||
paperlessId: warranty.paperless_other_id,
|
||
icon: 'fas fa-file-alt',
|
||
label: 'Files',
|
||
className: 'other-document-link'
|
||
},
|
||
photo: {
|
||
localPath: warranty.product_photo_path,
|
||
paperlessId: warranty.paperless_photo_id,
|
||
icon: 'fas fa-image',
|
||
label: 'Photo',
|
||
className: 'photo-link'
|
||
}
|
||
};
|
||
|
||
const config = docConfig[docType];
|
||
if (!config) return '';
|
||
|
||
const hasLocal = config.localPath && config.localPath !== 'null';
|
||
const hasPaperless = config.paperlessId && config.paperlessId !== null;
|
||
|
||
|
||
if (hasLocal) {
|
||
return `<a href="#" onclick="openSecureFile('${config.localPath}'); return false;" class="${config.className}">
|
||
<i class="${config.icon}"></i> ${config.label}
|
||
</a>`;
|
||
} else if (hasPaperless) {
|
||
return `<a href="#" onclick="openPaperlessDocument(${config.paperlessId}); return false;" class="${config.className}">
|
||
<i class="${config.icon}"></i> ${config.label} <i class="fas fa-cloud" style="color: #4dabf7; margin-left: 4px; font-size: 0.8em;" title="Stored in Paperless-ngx"></i>
|
||
</a>`;
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
// Initialize the warranty form and all its components
|
||
function initWarrantyForm() {
|
||
// Initialize form tabs
|
||
if (formTabs && tabContents) {
|
||
initFormTabs();
|
||
}
|
||
|
||
// Initialize serial number inputs
|
||
addSerialNumberInput();
|
||
|
||
// Initialize file input display
|
||
if (document.getElementById('productPhoto')) {
|
||
document.getElementById('productPhoto').addEventListener('change', function(event) {
|
||
updateFileName(event, 'productPhoto', 'productPhotoFileName');
|
||
});
|
||
}
|
||
|
||
if (document.getElementById('invoice')) {
|
||
document.getElementById('invoice').addEventListener('change', function(event) {
|
||
updateFileName(event, 'invoice', 'fileName');
|
||
});
|
||
}
|
||
|
||
if (document.getElementById('manual')) {
|
||
document.getElementById('manual').addEventListener('change', function(event) {
|
||
updateFileName(event, 'manual', 'manualFileName');
|
||
});
|
||
}
|
||
|
||
if (document.getElementById('otherDocument')) {
|
||
document.getElementById('otherDocument').addEventListener('change', function(event) {
|
||
updateFileName(event, 'otherDocument', 'otherDocumentFileName');
|
||
});
|
||
}
|
||
|
||
// Initialize tag functionality
|
||
initTagFunctionality();
|
||
|
||
// Form submission
|
||
if (warrantyForm) {
|
||
warrantyForm.addEventListener('submit', handleFormSubmit); // Use renamed handler
|
||
}
|
||
|
||
// Initialize lifetime checkbox listener
|
||
if (isLifetimeCheckbox && warrantyDurationFields) { // Check for new container
|
||
isLifetimeCheckbox.addEventListener('change', handleLifetimeChange);
|
||
handleLifetimeChange(); // Initial check
|
||
} else {
|
||
console.error("Lifetime warranty elements or duration fields not found in add form");
|
||
}
|
||
}
|
||
|
||
// Initialize tag functionality
|
||
function initTagFunctionality() {
|
||
// This function now ONLY sets up listeners for the main "Add Warranty" form's tag interface.
|
||
// Assumes globalTagManagementModal listeners (new tag form, close buttons) are set up separately if the modal exists.
|
||
|
||
// Get main form tag elements
|
||
const mainFormTagSearch = document.getElementById('tagSearch');
|
||
const mainFormTagsList = document.getElementById('tagsList'); // Dropdown for search in main form
|
||
const mainFormManageTagsBtn = document.getElementById('manageTagsBtn'); // "Manage Tags" button in main form
|
||
const mainFormSelectedTagsContainer = document.getElementById('selectedTags'); // Container for selected tags in main form
|
||
|
||
// Skip if main form specific tag elements don't exist
|
||
if (!mainFormTagSearch || !mainFormTagsList || !mainFormManageTagsBtn || !mainFormSelectedTagsContainer) {
|
||
console.log('Main form tag UI elements (tagSearch, tagsList, manageTagsBtn, or selectedTagsContainer) not found, skipping main form tag UI initialization.');
|
||
return;
|
||
}
|
||
|
||
console.log('Initializing main form tag UI functionality (search, selection, manage button).');
|
||
|
||
// Load allTags if not already loaded (needed for search suggestions in the main form)
|
||
if (allTags.length === 0) {
|
||
loadTags(); // loadTags is async
|
||
}
|
||
|
||
mainFormTagSearch.addEventListener('focus', () => {
|
||
renderTagsList(); // Renders suggestions into mainFormTagsList based on allTags
|
||
mainFormTagsList.classList.add('show');
|
||
});
|
||
|
||
mainFormTagSearch.addEventListener('input', () => {
|
||
renderTagsList(mainFormTagSearch.value); // Filters suggestions
|
||
});
|
||
|
||
// Hide main form's tag suggestion dropdown when clicking outside
|
||
document.addEventListener('click', (e) => {
|
||
// Check if mainFormTagSearch and mainFormTagsList are still valid (e.g. not removed from DOM)
|
||
if (mainFormTagSearch && mainFormTagsList &&
|
||
!mainFormTagSearch.contains(e.target) &&
|
||
!mainFormTagsList.contains(e.target)) {
|
||
mainFormTagsList.classList.remove('show');
|
||
}
|
||
});
|
||
|
||
// "Manage Tags" button in the main form opens the global tagManagementModal
|
||
mainFormManageTagsBtn.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
openTagManagementModal(); // This function shows the global modal
|
||
});
|
||
|
||
// Initial rendering of selected tags for the main form (if any are pre-selected or loaded)
|
||
renderSelectedTags(); // Renders into mainFormSelectedTagsContainer
|
||
}
|
||
|
||
// Function to load all tags
|
||
async function loadTags() {
|
||
console.log('[script.js] loadTags() called. Current page:', window.location.pathname);
|
||
|
||
// Check if tags are already loaded and reasonably populated
|
||
if (allTags && allTags.length > 0) {
|
||
console.log('[script.js] Tags already loaded in allTags global. Skipping fetch. Count:', allTags.length);
|
||
// Optionally, re-dispatch the event if other components might need it on subsequent (though now less likely) calls
|
||
// document.dispatchEvent(new CustomEvent('allTagsLoaded', { detail: allTags }));
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const token = auth.getToken();
|
||
if (!token) {
|
||
console.warn('[script.js] No token available for loadTags. User might not be authenticated yet.');
|
||
allTags = []; // Ensure allTags is empty if we can't load
|
||
return;
|
||
}
|
||
const response = await fetch('/api/tags', {
|
||
headers: { 'Authorization': `Bearer ${token}` }
|
||
});
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
console.error('[script.js] Failed to load tags:', response.status, errorText);
|
||
allTags = []; // Default to empty on error
|
||
return;
|
||
}
|
||
const fetchedTags = await response.json();
|
||
// Assuming fetchedTags is an array of {id, name, color, ...} as expected by other functions
|
||
allTags = fetchedTags;
|
||
console.log('[script.js] All tags loaded into global allTags variable:', allTags.length, 'tags. Sample:', allTags.slice(0,2));
|
||
// Dispatch event for any components that might be waiting for tags (e.g., Tagify instances)
|
||
document.dispatchEvent(new CustomEvent('allTagsLoaded', { detail: allTags }));
|
||
|
||
} catch (error) {
|
||
console.error('[script.js] Error in loadTags():', error);
|
||
allTags = []; // Default to empty on critical error
|
||
}
|
||
}
|
||
|
||
// Render the tags dropdown list
|
||
function renderTagsList(searchTerm = '') {
|
||
if (!tagsList) return;
|
||
|
||
tagsList.innerHTML = '';
|
||
|
||
// Filter tags based on search term
|
||
const filteredTags = allTags.filter(tag =>
|
||
!searchTerm || tag.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||
);
|
||
|
||
// Add option to create new tag if search term is provided and not in list
|
||
if (searchTerm && !filteredTags.some(tag => tag.name.toLowerCase() === searchTerm.toLowerCase())) {
|
||
const createOption = document.createElement('div');
|
||
createOption.className = 'tag-option create-tag';
|
||
createOption.innerHTML = `<i class="fas fa-plus"></i> Create "${searchTerm}"`;
|
||
createOption.addEventListener('click', () => {
|
||
createTag(searchTerm).then(newTag => {
|
||
// Add the new tag to selectedTags
|
||
selectedTags.push(newTag);
|
||
renderSelectedTags();
|
||
renderTagsList(''); // Clear search and refresh list
|
||
});
|
||
tagsList.classList.remove('show');
|
||
});
|
||
tagsList.appendChild(createOption);
|
||
}
|
||
|
||
// Add existing tags to dropdown
|
||
filteredTags.forEach(tag => {
|
||
const option = document.createElement('div');
|
||
option.className = 'tag-option';
|
||
|
||
// Check if tag is already selected
|
||
const isSelected = selectedTags.some(selected => selected.id === tag.id);
|
||
|
||
option.innerHTML = `
|
||
<span class="tag-color" style="background-color: ${tag.color}"></span>
|
||
${tag.name}
|
||
<span class="tag-status">${isSelected ? '<i class="fas fa-check"></i>' : ''}</span>
|
||
`;
|
||
|
||
option.addEventListener('click', () => {
|
||
if (isSelected) {
|
||
// Remove tag if already selected
|
||
selectedTags = selectedTags.filter(selected => selected.id !== tag.id);
|
||
} else {
|
||
// Add tag if not selected
|
||
selectedTags.push({
|
||
id: tag.id,
|
||
name: tag.name,
|
||
color: tag.color
|
||
});
|
||
}
|
||
|
||
renderSelectedTags();
|
||
renderTagsList(searchTerm);
|
||
});
|
||
|
||
tagsList.appendChild(option);
|
||
});
|
||
|
||
// Show the dropdown
|
||
tagsList.classList.add('show');
|
||
}
|
||
|
||
// Update renderEditTagsList to add new tag to editSelectedTags after creation
|
||
function renderEditTagsList(searchTerm = '') {
|
||
const editTagsList = document.getElementById('editTagsList');
|
||
if (!editTagsList) return;
|
||
editTagsList.innerHTML = '';
|
||
// Filter tags based on search term
|
||
const filteredTags = allTags.filter(tag =>
|
||
!searchTerm || tag.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||
);
|
||
// Add option to create new tag if search term is provided and not in list
|
||
if (searchTerm && !filteredTags.some(tag => tag.name.toLowerCase() === searchTerm.toLowerCase())) {
|
||
const createOption = document.createElement('div');
|
||
createOption.className = 'tag-option create-tag';
|
||
createOption.innerHTML = `<i class="fas fa-plus"></i> Create "${searchTerm}"`;
|
||
createOption.addEventListener('click', () => {
|
||
createTag(searchTerm).then(newTag => {
|
||
// Add the new tag to editSelectedTags
|
||
editSelectedTags.push(newTag);
|
||
renderEditSelectedTags();
|
||
renderEditTagsList(''); // Clear search and refresh list
|
||
});
|
||
editTagsList.classList.remove('show');
|
||
});
|
||
editTagsList.appendChild(createOption);
|
||
}
|
||
// Add existing tags to dropdown
|
||
filteredTags.forEach(tag => {
|
||
const option = document.createElement('div');
|
||
option.className = 'tag-option';
|
||
|
||
// Check if tag is already selected
|
||
const isSelected = editSelectedTags.some(selected => selected.id === tag.id);
|
||
|
||
option.innerHTML = `
|
||
<span class="tag-color" style="background-color: ${tag.color}"></span>
|
||
${tag.name}
|
||
<span class="tag-status">${isSelected ? '<i class="fas fa-check"></i>' : ''}</span>
|
||
`;
|
||
|
||
option.addEventListener('click', () => {
|
||
if (isSelected) {
|
||
// Remove tag if already selected
|
||
editSelectedTags = editSelectedTags.filter(selected => selected.id !== tag.id);
|
||
} else {
|
||
// Add tag if not selected
|
||
editSelectedTags.push({
|
||
id: tag.id,
|
||
name: tag.name,
|
||
color: tag.color
|
||
});
|
||
}
|
||
|
||
// Use our helper function to render selected tags
|
||
renderEditSelectedTags();
|
||
|
||
renderEditTagsList(searchTerm);
|
||
});
|
||
|
||
editTagsList.appendChild(option);
|
||
});
|
||
|
||
// Show the dropdown
|
||
editTagsList.classList.add('show');
|
||
}
|
||
|
||
// Render the selected tags
|
||
function renderSelectedTags() {
|
||
if (!selectedTagsContainer) return;
|
||
|
||
selectedTagsContainer.innerHTML = '';
|
||
|
||
if (selectedTags.length === 0) {
|
||
const placeholder = document.createElement('span');
|
||
placeholder.className = 'no-tags-selected';
|
||
placeholder.textContent = 'No tags selected';
|
||
selectedTagsContainer.appendChild(placeholder);
|
||
return;
|
||
}
|
||
|
||
selectedTags.forEach(tag => {
|
||
const tagElement = document.createElement('span');
|
||
tagElement.className = 'tag';
|
||
tagElement.style.backgroundColor = tag.color;
|
||
tagElement.style.color = getContrastColor(tag.color);
|
||
|
||
tagElement.innerHTML = `
|
||
${tag.name}
|
||
<span class="remove-tag" data-id="${tag.id}">×</span>
|
||
`;
|
||
|
||
// Add event listener for removing tag
|
||
tagElement.querySelector('.remove-tag').addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
selectedTags = selectedTags.filter(t => t.id !== tag.id);
|
||
renderSelectedTags();
|
||
|
||
// Update summary if needed
|
||
if (document.getElementById('summary-tags')) {
|
||
updateSummary();
|
||
}
|
||
});
|
||
|
||
selectedTagsContainer.appendChild(tagElement);
|
||
});
|
||
}
|
||
|
||
// Helper function to render the edit selected tags
|
||
function renderEditSelectedTags() {
|
||
const editSelectedTagsContainer = document.getElementById('editSelectedTags');
|
||
if (!editSelectedTagsContainer) return;
|
||
|
||
editSelectedTagsContainer.innerHTML = '';
|
||
|
||
if (editSelectedTags.length > 0) {
|
||
editSelectedTags.forEach(tag => {
|
||
const tagElement = document.createElement('span');
|
||
tagElement.className = 'tag';
|
||
tagElement.style.backgroundColor = tag.color;
|
||
tagElement.style.color = getContrastColor(tag.color);
|
||
|
||
tagElement.innerHTML = `
|
||
${tag.name}
|
||
<span class="remove-tag" data-id="${tag.id}">×</span>
|
||
`;
|
||
|
||
// Add event listener for removing tag
|
||
const removeButton = tagElement.querySelector('.remove-tag');
|
||
removeButton.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
e.preventDefault(); // Add this to prevent default action
|
||
|
||
// Prevent the event from bubbling up to parent elements
|
||
if (e.cancelBubble !== undefined) {
|
||
e.cancelBubble = true;
|
||
}
|
||
|
||
editSelectedTags = editSelectedTags.filter(t => t.id !== tag.id);
|
||
|
||
// Re-render just the tags
|
||
renderEditSelectedTags();
|
||
return false; // Add return false for older browsers
|
||
});
|
||
|
||
editSelectedTagsContainer.appendChild(tagElement);
|
||
});
|
||
} else {
|
||
const placeholder = document.createElement('span');
|
||
placeholder.className = 'no-tags-selected';
|
||
placeholder.textContent = 'No tags selected';
|
||
editSelectedTagsContainer.appendChild(placeholder);
|
||
}
|
||
}
|
||
|
||
// Update createTag to return a Promise
|
||
function createTag(name) {
|
||
return new Promise((resolve, reject) => {
|
||
// Enhanced auth manager availability check
|
||
if (!window.auth) {
|
||
console.error('[createTag] Auth manager not available');
|
||
reject(new Error('Authentication system not ready. Please try again.'));
|
||
return;
|
||
}
|
||
|
||
// Use auth manager's getToken method instead of directly accessing localStorage
|
||
const token = window.auth.getToken();
|
||
console.log('[createTag] Debug info:', {
|
||
hasToken: !!token,
|
||
tokenLength: token ? token.length : 0,
|
||
hasUserInfo: !!localStorage.getItem('user_info'),
|
||
authManagerAvailable: !!window.auth,
|
||
isAuthenticated: window.auth.isAuthenticated(),
|
||
tokenSource: 'auth.getToken()'
|
||
});
|
||
|
||
if (!token) {
|
||
console.error('[createTag] No authentication token found');
|
||
reject(new Error('No authentication token found. Please try logging in again.'));
|
||
return;
|
||
}
|
||
// Generate a random color for the tag
|
||
const color = '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
|
||
fetch('/api/tags', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': 'Bearer ' + token
|
||
},
|
||
body: JSON.stringify({
|
||
name: name,
|
||
color: color
|
||
})
|
||
})
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
// Enhanced error handling to capture specific error details
|
||
return response.json().then(errorData => {
|
||
console.error('[createTag] API Error Response:', {
|
||
status: response.status,
|
||
statusText: response.statusText,
|
||
errorData: errorData
|
||
});
|
||
|
||
if (response.status === 409) {
|
||
reject(new Error('A tag with this name already exists'));
|
||
return;
|
||
}
|
||
if (response.status === 401) {
|
||
reject(new Error('Authentication failed. Please try logging in again.'));
|
||
return;
|
||
}
|
||
if (response.status === 403) {
|
||
reject(new Error('Permission denied. You may not have access to create tags.'));
|
||
return;
|
||
}
|
||
|
||
const errorMsg = errorData?.error || errorData?.message || 'Failed to create tag';
|
||
reject(new Error(errorMsg));
|
||
}).catch(() => {
|
||
// If response body is not JSON or is empty
|
||
console.error('[createTag] Non-JSON error response:', response.status, response.statusText);
|
||
reject(new Error(`Failed to create tag (${response.status})`));
|
||
});
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
if (!data) return;
|
||
const newTag = {
|
||
id: data.id,
|
||
name: data.name,
|
||
color: data.color
|
||
};
|
||
allTags.push(newTag);
|
||
renderExistingTags();
|
||
populateTagFilter();
|
||
showToast(window.t('messages.tag_created_successfully'), 'success');
|
||
resolve(newTag);
|
||
})
|
||
.catch(error => {
|
||
console.error('Error creating tag:', error);
|
||
showToast(error.message || window.t('messages.failed_to_create_tag'), 'error');
|
||
reject(error);
|
||
});
|
||
});
|
||
}
|
||
|
||
// Helper function to determine text color based on background color
|
||
function getContrastColor(hexColor) {
|
||
// Convert hex to RGB
|
||
const r = parseInt(hexColor.substr(1, 2), 16);
|
||
const g = parseInt(hexColor.substr(3, 2), 16);
|
||
const b = parseInt(hexColor.substr(5, 2), 16);
|
||
|
||
// Calculate luminance
|
||
const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
|
||
|
||
// Return black or white depending on luminance
|
||
return (yiq >= 128) ? '#000000' : '#ffffff';
|
||
}
|
||
|
||
// Open tag management modal
|
||
function openTagManagementModal() {
|
||
if (!tagManagementModal) return;
|
||
|
||
// Populate existing tags
|
||
renderExistingTags();
|
||
|
||
// Show modal
|
||
tagManagementModal.classList.add('active');
|
||
}
|
||
|
||
// Render existing tags in the management modal
|
||
function renderExistingTags() {
|
||
if (!existingTagsContainer) return;
|
||
|
||
existingTagsContainer.innerHTML = '';
|
||
|
||
if (allTags.length === 0) {
|
||
existingTagsContainer.innerHTML = '<div class="no-tags">No tags created yet</div>';
|
||
return;
|
||
}
|
||
|
||
allTags.forEach(tag => {
|
||
const tagElement = document.createElement('div');
|
||
tagElement.className = 'existing-tag';
|
||
|
||
tagElement.innerHTML = `
|
||
<div class="existing-tag-info">
|
||
<div class="existing-tag-color" style="background-color: ${tag.color}"></div>
|
||
<div class="existing-tag-name">${tag.name}</div>
|
||
</div>
|
||
<div class="existing-tag-actions">
|
||
<button class="btn btn-sm btn-secondary edit-tag" data-id="${tag.id}">
|
||
<i class="fas fa-edit"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-danger delete-tag" data-id="${tag.id}">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
// Add event listeners for edit and delete
|
||
tagElement.querySelector('.edit-tag').addEventListener('click', () => {
|
||
editTag(tag);
|
||
});
|
||
|
||
tagElement.querySelector('.delete-tag').addEventListener('click', () => {
|
||
deleteTag(tag.id);
|
||
});
|
||
|
||
existingTagsContainer.appendChild(tagElement);
|
||
});
|
||
}
|
||
|
||
// Edit a tag
|
||
function editTag(tag) {
|
||
const tagInfoElement = document.querySelector(`.existing-tag .existing-tag-info:has(+ .existing-tag-actions button[data-id="${tag.id}"])`);
|
||
|
||
if (!tagInfoElement) {
|
||
// Alternative selector for browsers that don't support :has
|
||
const tagElement = document.querySelector(`.existing-tag`);
|
||
const buttons = tagElement?.querySelectorAll(`.existing-tag-actions button[data-id="${tag.id}"]`);
|
||
if (buttons?.length > 0) {
|
||
const parent = buttons[0].closest('.existing-tag');
|
||
if (parent) {
|
||
const infoElement = parent.querySelector('.existing-tag-info');
|
||
if (infoElement) {
|
||
tagInfoElement = infoElement;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!tagInfoElement) return;
|
||
}
|
||
|
||
const originalHTML = tagInfoElement.innerHTML;
|
||
|
||
tagInfoElement.innerHTML = `
|
||
<input type="text" class="form-control edit-tag-name" value="${tag.name}" style="width: 60%;">
|
||
<input type="color" class="edit-tag-color" value="${tag.color}" style="width: 40px; height: 38px;">
|
||
<button class="btn btn-sm btn-primary save-edit" data-id="${tag.id}">Save</button>
|
||
<button class="btn btn-sm btn-secondary cancel-edit">Cancel</button>
|
||
`;
|
||
|
||
// Add event listeners
|
||
tagInfoElement.querySelector('.save-edit').addEventListener('click', () => {
|
||
const newName = tagInfoElement.querySelector('.edit-tag-name').value.trim();
|
||
const newColor = tagInfoElement.querySelector('.edit-tag-color').value;
|
||
|
||
if (!newName) {
|
||
showToast(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 = `
|
||
<div class="paperless-upload-modal">
|
||
<div class="paperless-upload-content">
|
||
<div class="paperless-upload-spinner"></div>
|
||
<h3>Uploading to Paperless-ngx</h3>
|
||
<p id="paperless-upload-status">Uploading document...</p>
|
||
<div class="paperless-upload-progress">
|
||
<div class="paperless-upload-progress-bar" id="paperless-progress-bar"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
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 = `
|
||
<div class="modal" style="max-width: 500px;">
|
||
<div class="modal-header">
|
||
<h3 class="modal-title">Warranty Notes</h3>
|
||
<button class="close-btn" id="closeNotesModal">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div id="notesModalContent" style="white-space: pre-line;"></div>
|
||
<textarea id="notesModalTextarea" style="display:none;width:100%;min-height:100px;"></textarea>
|
||
</div>
|
||
<div class="modal-footer" id="notesModalFooter">
|
||
<button class="btn btn-secondary" id="editNotesBtn">Edit Notes</button>
|
||
<button class="btn btn-info" id="editWarrantyBtn">Edit Warranty</button>
|
||
<button class="btn btn-primary" id="saveNotesBtn" style="display:none;">Save</button>
|
||
<button class="btn btn-danger" id="cancelEditNotesBtn" style="display:none;">Cancel</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(notesModal);
|
||
document.getElementById('closeNotesModal').addEventListener('click', () => {
|
||
notesModal.classList.remove('active');
|
||
});
|
||
|
||
// Add event listener for Edit Warranty button
|
||
document.getElementById('editWarrantyBtn').addEventListener('click', async () => {
|
||
// Find the current warranty data from the global array
|
||
const currentWarranty = warranties.find(w => w.id === notesModalWarrantyId);
|
||
if (currentWarranty) {
|
||
console.log('[DEBUG] Edit Warranty button clicked, opening edit modal with warranty:', currentWarranty.id, 'notes:', currentWarranty.notes);
|
||
// Close the notes modal first
|
||
notesModal.classList.remove('active');
|
||
// Open the edit modal with current data
|
||
await openEditModal(currentWarranty);
|
||
} else {
|
||
showToast(window.t('messages.warranty_not_found_refresh'), 'error');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Add global to track which warranty is being edited in the notes modal
|
||
let notesModalWarrantyId = null;
|
||
let notesModalWarrantyObj = null;
|
||
|
||
function showNotesModal(notes, warrantyOrId = null) {
|
||
const notesModal = document.getElementById('notesModal');
|
||
const notesModalContent = document.getElementById('notesModalContent');
|
||
const notesModalTextarea = document.getElementById('notesModalTextarea');
|
||
const editBtn = document.getElementById('editNotesBtn');
|
||
const saveBtn = document.getElementById('saveNotesBtn');
|
||
const cancelBtn = document.getElementById('cancelEditNotesBtn');
|
||
|
||
// Support both (notes, warrantyObj) and (notes, id) for backward compatibility
|
||
if (typeof warrantyOrId === 'object' && warrantyOrId !== null) {
|
||
notesModalWarrantyId = warrantyOrId.id;
|
||
notesModalWarrantyObj = warrantyOrId;
|
||
} else {
|
||
notesModalWarrantyId = warrantyOrId;
|
||
// Try to find the warranty object from global warranties array
|
||
notesModalWarrantyObj = warranties.find(w => w.id === notesModalWarrantyId) || null;
|
||
}
|
||
|
||
// Show note content, hide textarea and edit controls
|
||
notesModalContent.style.display = '';
|
||
notesModalContent.textContent = notes;
|
||
notesModalTextarea.style.display = 'none';
|
||
editBtn.style.display = '';
|
||
saveBtn.style.display = 'none';
|
||
cancelBtn.style.display = 'none';
|
||
|
||
// Edit button handler
|
||
editBtn.onclick = function() {
|
||
notesModalContent.style.display = 'none';
|
||
notesModalTextarea.style.display = '';
|
||
// Use the current content from the modal display instead of the stale notes parameter
|
||
notesModalTextarea.value = notesModalContent.textContent;
|
||
editBtn.style.display = 'none';
|
||
saveBtn.style.display = '';
|
||
cancelBtn.style.display = '';
|
||
notesModalTextarea.focus();
|
||
};
|
||
// Save button handler
|
||
saveBtn.onclick = async function() {
|
||
const newNote = notesModalTextarea.value.trim(); // Trim the note
|
||
if (!notesModalWarrantyId || !notesModalWarrantyObj) {
|
||
showToast('No warranty selected for note update', 'error');
|
||
return;
|
||
}
|
||
|
||
// Frontend check for invalid duration before attempting to save notes
|
||
if (!notesModalWarrantyObj.is_lifetime &&
|
||
(parseInt(notesModalWarrantyObj.warranty_duration_years) || 0) === 0 &&
|
||
(parseInt(notesModalWarrantyObj.warranty_duration_months) || 0) === 0 &&
|
||
(parseInt(notesModalWarrantyObj.warranty_duration_days) || 0) === 0 &&
|
||
!notesModalWarrantyObj.expiration_date) {
|
||
showToast('Cannot save notes: The warranty has an invalid duration. Please edit the full warranty details to set a valid duration first.', 'error', 7000); // Longer toast duration
|
||
return; // Prevent API call
|
||
}
|
||
|
||
// Save note via API, sending all required fields
|
||
try {
|
||
showLoadingSpinner();
|
||
const token = localStorage.getItem('auth_token');
|
||
const formData = new FormData();
|
||
// --- Populate with existing data to avoid clearing fields ---
|
||
formData.append('product_name', notesModalWarrantyObj.product_name);
|
||
formData.append('purchase_date', (notesModalWarrantyObj.purchase_date || '').split('T')[0]);
|
||
formData.append('is_lifetime', notesModalWarrantyObj.is_lifetime ? 'true' : 'false');
|
||
if (!notesModalWarrantyObj.is_lifetime) {
|
||
// Append duration components instead of warranty_years
|
||
formData.append('warranty_duration_years', notesModalWarrantyObj.warranty_duration_years || 0);
|
||
formData.append('warranty_duration_months', notesModalWarrantyObj.warranty_duration_months || 0);
|
||
formData.append('warranty_duration_days', notesModalWarrantyObj.warranty_duration_days || 0);
|
||
|
||
// If all duration fields are 0 but we have an expiration date, this was created with exact date method
|
||
const isExactDateWarranty = (notesModalWarrantyObj.warranty_duration_years || 0) === 0 &&
|
||
(notesModalWarrantyObj.warranty_duration_months || 0) === 0 &&
|
||
(notesModalWarrantyObj.warranty_duration_days || 0) === 0 &&
|
||
notesModalWarrantyObj.expiration_date;
|
||
|
||
if (isExactDateWarranty) {
|
||
// For exact date warranties, send the expiration date as exact_expiration_date
|
||
formData.append('exact_expiration_date', notesModalWarrantyObj.expiration_date.split('T')[0]);
|
||
}
|
||
}
|
||
if (notesModalWarrantyObj.product_url) {
|
||
formData.append('product_url', notesModalWarrantyObj.product_url);
|
||
}
|
||
if (notesModalWarrantyObj.purchase_price !== null && notesModalWarrantyObj.purchase_price !== undefined) { // Check for null/undefined
|
||
formData.append('purchase_price', notesModalWarrantyObj.purchase_price);
|
||
}
|
||
// Correctly append serial numbers
|
||
if (notesModalWarrantyObj.serial_numbers && Array.isArray(notesModalWarrantyObj.serial_numbers)) {
|
||
notesModalWarrantyObj.serial_numbers.forEach(sn => {
|
||
// Ensure sn is treated as a string before trim, and append with [] for array
|
||
if (sn && String(sn).trim() !== '') {
|
||
formData.append('serial_numbers[]', String(sn).trim());
|
||
}
|
||
});
|
||
}
|
||
// If notesModalWarrantyObj.serial_numbers is empty or not an array,
|
||
// no 'serial_numbers[]' fields will be appended, which is typically interpreted as an empty list by backends.
|
||
|
||
if (notesModalWarrantyObj.tags && Array.isArray(notesModalWarrantyObj.tags)) {
|
||
const tagIds = notesModalWarrantyObj.tags.map(tag => tag.id);
|
||
formData.append('tag_ids', JSON.stringify(tagIds));
|
||
}
|
||
// Send empty array if no tags exist or are provided
|
||
else {
|
||
formData.append('tag_ids', JSON.stringify([]));
|
||
}
|
||
// --- End Populate ---
|
||
|
||
formData.append('notes', newNote); // Append the potentially empty, trimmed note
|
||
|
||
// Add vendor/retailer to form data
|
||
const editVendorOrRetailer = document.getElementById('editVendorOrRetailer');
|
||
formData.append('vendor', editVendorOrRetailer ? editVendorOrRetailer.value.trim() : '');
|
||
|
||
const response = await fetch(`/api/warranties/${notesModalWarrantyId}`, { // Added await and response handling
|
||
method: 'PUT',
|
||
headers: {
|
||
'Authorization': 'Bearer ' + token
|
||
},
|
||
body: formData
|
||
});
|
||
|
||
if (!response.ok) { // Check if the API call was successful
|
||
const errorData = await response.json().catch(() => ({})); // Try to parse error, default to empty object
|
||
throw new Error(errorData.error || `Failed to update note (Status: ${response.status})`);
|
||
}
|
||
|
||
|
||
hideLoadingSpinner();
|
||
showToast('Note updated', 'success');
|
||
|
||
// Update the warranty in the global warranties array immediately
|
||
const warrantyIndex = warranties.findIndex(w => w.id === notesModalWarrantyId);
|
||
if (warrantyIndex !== -1) {
|
||
warranties[warrantyIndex].notes = newNote;
|
||
}
|
||
|
||
// --- Updated UI logic ---
|
||
if (newNote === '') {
|
||
// If the note is now empty, close the modal
|
||
document.getElementById('notesModal').classList.remove('active');
|
||
} else {
|
||
// If note is not empty, update the view and stay in the modal
|
||
notesModalContent.textContent = newNote;
|
||
notesModalContent.style.display = '';
|
||
notesModalTextarea.style.display = 'none';
|
||
editBtn.style.display = '';
|
||
saveBtn.style.display = 'none';
|
||
cancelBtn.style.display = 'none';
|
||
// Update the local warranty object's notes
|
||
if (notesModalWarrantyObj) {
|
||
notesModalWarrantyObj.notes = newNote;
|
||
}
|
||
}
|
||
// --- End Updated UI logic ---
|
||
|
||
// Refresh warranties list and THEN update UI
|
||
await loadWarranties(true); // Wait for data refresh
|
||
applyFilters(); // Re-render the list with updated data
|
||
|
||
} catch (e) {
|
||
hideLoadingSpinner();
|
||
console.error("Error updating note:", e); // Log the error
|
||
showToast(e.message || 'Failed to update note', 'error'); // Show specific error if available
|
||
}
|
||
};
|
||
// Cancel button handler
|
||
cancelBtn.onclick = function() {
|
||
notesModalContent.style.display = '';
|
||
notesModalTextarea.style.display = 'none';
|
||
editBtn.style.display = '';
|
||
saveBtn.style.display = 'none';
|
||
cancelBtn.style.display = 'none';
|
||
};
|
||
notesModal.classList.add('active');
|
||
}
|
||
|
||
// Utility to get currency symbol from preferences/localStorage
|
||
function getCurrencySymbol() {
|
||
// Use the global prefix determined after auth ready
|
||
let prefix = userPreferencePrefix; // Use let to allow default override
|
||
if (!prefix) {
|
||
console.warn('[getCurrencySymbol] User preference prefix not set yet, defaulting prefix to user_');
|
||
prefix = 'user_'; // Default prefix if called too early
|
||
}
|
||
console.log(`[getCurrencySymbol] Using determined prefix: ${prefix}`);
|
||
let symbol = '$'; // Default value
|
||
|
||
const rawValue = localStorage.getItem(`${prefix}currencySymbol`);
|
||
console.log(`[getCurrencySymbol Debug] Raw value read from localStorage key '${prefix}currencySymbol':`, rawValue);
|
||
// +++ END ADDED LOG +++
|
||
|
||
// --- Priority 1: Load from individual key --- (Saved by settings-new.js)
|
||
const individualSymbol = rawValue; // Use the already read value
|
||
if (individualSymbol) { // Check uses the already read value
|
||
symbol = individualSymbol;
|
||
console.log(`[getCurrencySymbol] Loaded symbol from individual key (${prefix}currencySymbol): ${symbol}`);
|
||
return symbol;
|
||
}
|
||
|
||
// --- Priority 2: Load from preferences object (Legacy/Fallback) ---
|
||
try {
|
||
const prefsString = localStorage.getItem(`${prefix}preferences`);
|
||
console.log(`[getCurrencySymbol] Read prefsString for ${prefix}preferences:`, prefsString);
|
||
if (prefsString) {
|
||
const prefs = JSON.parse(prefsString);
|
||
if (prefs && prefs.currency_symbol) {
|
||
symbol = prefs.currency_symbol;
|
||
console.log(`[getCurrencySymbol] Loaded symbol from object key (${prefix}preferences): ${symbol}`);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error(`Error reading ${prefix}preferences from localStorage:`, e);
|
||
// Keep the default '$' symbol in case of error parsing the object
|
||
}
|
||
|
||
console.log(`[getCurrencySymbol] Returning symbol (default or from object): ${symbol}`);
|
||
return symbol;
|
||
}
|
||
|
||
// Function to get user's preferred currency code
|
||
function getCurrencyCode() {
|
||
// Use the global prefix determined after auth ready
|
||
let prefix = userPreferencePrefix;
|
||
if (!prefix) {
|
||
console.warn('[getCurrencyCode] User preference prefix not set yet, defaulting prefix to user_');
|
||
prefix = 'user_';
|
||
}
|
||
console.log(`[getCurrencyCode] Using determined prefix: ${prefix}`);
|
||
|
||
// Default to USD
|
||
let currencyCode = 'USD';
|
||
|
||
// Try to get currency code from localStorage
|
||
const rawValue = localStorage.getItem(`${prefix}currencyCode`);
|
||
console.log(`[getCurrencyCode Debug] Raw value read from localStorage key '${prefix}currencyCode':`, rawValue);
|
||
|
||
if (rawValue) {
|
||
currencyCode = rawValue;
|
||
console.log(`[getCurrencyCode] Loaded currency code from individual key (${prefix}currencyCode): ${currencyCode}`);
|
||
return currencyCode;
|
||
}
|
||
|
||
// Fallback: Try to derive currency code from symbol
|
||
const symbol = getCurrencySymbol();
|
||
const symbolToCurrencyMap = {
|
||
'$': 'USD', '€': 'EUR', '£': 'GBP', '¥': 'JPY', '₹': 'INR', '₩': 'KRW',
|
||
'CHF': 'CHF', 'C$': 'CAD', 'A$': 'AUD', 'kr': 'SEK', 'zł': 'PLN',
|
||
'Kč': 'CZK', 'Ft': 'HUF', '₽': 'RUB', 'R$': 'BRL', '₦': 'NGN',
|
||
'₪': 'ILS', '₺': 'TRY', '₨': 'PKR', '৳': 'BDT', '฿': 'THB',
|
||
'₫': 'VND', 'RM': 'MYR', 'S$': 'SGD', 'Rp': 'IDR', '₱': 'PHP',
|
||
'NT$': 'TWD', 'HK$': 'HKD', '₮': 'MNT', '₸': 'KZT', '₼': 'AZN',
|
||
'₾': 'GEL', '₴': 'UAH', 'NZ$': 'NZD'
|
||
};
|
||
|
||
if (symbolToCurrencyMap[symbol]) {
|
||
currencyCode = symbolToCurrencyMap[symbol];
|
||
console.log(`[getCurrencyCode] Derived currency code from symbol '${symbol}': ${currencyCode}`);
|
||
} else {
|
||
console.log(`[getCurrencyCode] Could not derive currency code from symbol '${symbol}', using default: ${currencyCode}`);
|
||
}
|
||
|
||
return currencyCode;
|
||
}
|
||
|
||
// Function to load currencies from API and populate dropdowns
|
||
async function loadCurrencies() {
|
||
try {
|
||
const response = await fetch('/api/currencies');
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch currencies');
|
||
}
|
||
|
||
const currencies = await response.json();
|
||
|
||
// Get user's preferred currency code for default selection
|
||
const preferredCurrencyCode = getCurrencyCode();
|
||
|
||
// Populate add warranty currency dropdown
|
||
if (currencySelect) {
|
||
currencySelect.innerHTML = '';
|
||
currencies.forEach(currency => {
|
||
const option = document.createElement('option');
|
||
option.value = currency.code;
|
||
option.textContent = `${currency.code} - ${currency.name} (${currency.symbol})`;
|
||
currencySelect.appendChild(option);
|
||
});
|
||
|
||
// Set default selection to user's preferred currency
|
||
console.log(`[loadCurrencies] Preferred currency code: ${preferredCurrencyCode}`);
|
||
console.log(`[loadCurrencies] Available currency options:`, Array.from(currencySelect.options).map(opt => opt.value));
|
||
|
||
if (preferredCurrencyCode) {
|
||
// Use setTimeout to ensure DOM is fully updated
|
||
setTimeout(() => {
|
||
currencySelect.value = preferredCurrencyCode;
|
||
console.log(`[loadCurrencies] Set add warranty currency default to: ${preferredCurrencyCode}`);
|
||
console.log(`[loadCurrencies] Current selected value: ${currencySelect.value}`);
|
||
|
||
// Trigger change event to update any dependent UI
|
||
const changeEvent = new Event('change', { bubbles: true });
|
||
currencySelect.dispatchEvent(changeEvent);
|
||
}, 10);
|
||
} else {
|
||
console.log(`[loadCurrencies] No preferred currency code found, keeping default USD`);
|
||
}
|
||
}
|
||
|
||
// Populate edit warranty currency dropdown
|
||
if (editCurrencySelect) {
|
||
editCurrencySelect.innerHTML = '';
|
||
currencies.forEach(currency => {
|
||
const option = document.createElement('option');
|
||
option.value = currency.code;
|
||
option.textContent = `${currency.code} - ${currency.name} (${currency.symbol})`;
|
||
editCurrencySelect.appendChild(option);
|
||
});
|
||
}
|
||
|
||
console.log('Currencies loaded successfully');
|
||
} catch (error) {
|
||
console.error('Error loading currencies:', error);
|
||
// Fallback to USD if loading fails
|
||
if (currencySelect) {
|
||
currencySelect.innerHTML = '<option value="USD">USD - US Dollar ($)</option>';
|
||
}
|
||
if (editCurrencySelect) {
|
||
editCurrencySelect.innerHTML = '<option value="USD">USD - US Dollar ($)</option>';
|
||
}
|
||
}
|
||
}
|
||
|
||
function getCurrencySymbolByCode(currencyCode) {
|
||
const currencyMap = {
|
||
'USD': '$', 'EUR': '€', 'GBP': '£', 'JPY': '¥', 'CNY': '¥', 'INR': '₹', 'KRW': '₩',
|
||
'CHF': 'CHF', 'CAD': 'C$', 'AUD': 'A$', 'SEK': 'kr', 'NOK': 'kr', 'DKK': 'kr',
|
||
'PLN': 'zł', 'CZK': 'Kč', 'HUF': 'Ft', 'BGN': 'лв', 'RON': 'lei', 'HRK': 'kn',
|
||
'RUB': '₽', 'BRL': 'R$', 'MXN': '$', 'ARS': '$', 'CLP': '$', 'COP': '$',
|
||
'PEN': 'S/', 'VES': 'Bs', 'ZAR': 'R', 'EGP': '£', 'NGN': '₦', 'KES': 'KSh',
|
||
'GHS': '₵', 'MAD': 'DH', 'TND': 'DT', 'AED': 'AED', 'SAR': 'SR', 'QAR': 'QR',
|
||
'KWD': 'KD', 'BHD': 'BD', 'OMR': 'OR', 'JOD': 'JD', 'LBP': 'LL', 'ILS': '₪',
|
||
'TRY': '₺', 'IRR': '﷼', 'PKR': '₨', 'BDT': '৳', 'LKR': 'Rs', 'NPR': 'Rs',
|
||
'BTN': 'Nu', 'MMK': 'K', 'THB': '฿', 'VND': '₫', 'LAK': '₭', 'KHR': '៛',
|
||
'MYR': 'RM', 'SGD': 'S$', 'IDR': 'Rp', 'PHP': '₱', 'TWD': 'NT$', 'HKD': 'HK$',
|
||
'MOP': 'MOP', 'KPW': '₩', 'MNT': '₮', 'KZT': '₸', 'UZS': 'soʻm', 'TJS': 'SM',
|
||
'KGS': 'с', 'TMT': 'T', 'AFN': '؋', 'AMD': '֏', 'AZN': '₼', 'GEL': '₾',
|
||
'MDL': 'L', 'UAH': '₴', 'BYN': 'Br', 'RSD': 'дин', 'MKD': 'ден', 'ALL': 'L',
|
||
'BAM': 'KM', 'ISK': 'kr', 'FJD': 'FJ$', 'PGK': 'K', 'SBD': 'SI$', 'TOP': 'T$',
|
||
'VUV': 'VT', 'WST': 'WS$', 'XPF': '₣', 'NZD': 'NZ$'
|
||
};
|
||
return currencyMap[currencyCode] || currencyCode;
|
||
}
|
||
|
||
function getCurrencyPosition() {
|
||
let prefix = userPreferencePrefix;
|
||
if (!prefix) {
|
||
console.warn('[getCurrencyPosition] User preference prefix not set yet, defaulting prefix to user_');
|
||
prefix = 'user_';
|
||
}
|
||
|
||
let position = 'left'; // Default position
|
||
const rawValue = localStorage.getItem(`${prefix}currencyPosition`);
|
||
console.log(`[getCurrencyPosition] Raw value from localStorage (${prefix}currencyPosition):`, rawValue);
|
||
|
||
if (rawValue) {
|
||
position = rawValue;
|
||
console.log(`[getCurrencyPosition] Loaded position from localStorage: ${position}`);
|
||
} else {
|
||
console.log(`[getCurrencyPosition] No position found, using default: ${position}`);
|
||
}
|
||
|
||
return position;
|
||
}
|
||
|
||
function formatCurrencyHTML(amount, symbol, position) {
|
||
const formattedAmount = parseFloat(amount).toFixed(2);
|
||
|
||
if (position === 'right') {
|
||
return `<span>${formattedAmount}</span><span class="currency-symbol currency-right">${symbol}</span>`;
|
||
} else {
|
||
return `<span class="currency-symbol">${symbol}</span><span>${formattedAmount}</span>`;
|
||
}
|
||
}
|
||
|
||
function updateCurrencySymbols() {
|
||
const symbol = getCurrencySymbol();
|
||
const position = getCurrencyPosition();
|
||
console.log(`Updating currency symbols to: ${symbol}, position: ${position}`);
|
||
|
||
// Update all currency symbols
|
||
const elements = document.querySelectorAll('.currency-symbol');
|
||
console.log(`Found ${elements.length} elements with class 'currency-symbol'.`);
|
||
elements.forEach(el => {
|
||
el.textContent = symbol;
|
||
});
|
||
|
||
// Update form currency positioning
|
||
updateFormCurrencyPosition(symbol, position);
|
||
}
|
||
|
||
function updateFormCurrencyPosition(symbol, position) {
|
||
// Handle add warranty form
|
||
const addPriceWrapper = document.getElementById('addPriceInputWrapper');
|
||
const addCurrencySymbol = document.getElementById('addCurrencySymbol');
|
||
const addPriceInput = document.getElementById('purchasePrice');
|
||
|
||
if (addPriceWrapper && addCurrencySymbol) {
|
||
addCurrencySymbol.textContent = symbol;
|
||
if (position === 'right') {
|
||
addPriceWrapper.classList.add('currency-right');
|
||
// Set up dynamic positioning for right-aligned currency
|
||
if (addPriceInput) {
|
||
setupDynamicCurrencyPosition(addPriceInput, addCurrencySymbol);
|
||
}
|
||
} else {
|
||
addPriceWrapper.classList.remove('currency-right');
|
||
// Reset any dynamic positioning
|
||
if (addCurrencySymbol) {
|
||
addCurrencySymbol.style.right = '';
|
||
}
|
||
}
|
||
console.log(`Updated add form currency position: ${position}`);
|
||
}
|
||
|
||
// Handle edit warranty form
|
||
const editPriceWrapper = document.getElementById('editPriceInputWrapper');
|
||
const editCurrencySymbol = document.getElementById('editCurrencySymbol');
|
||
const editPriceInput = document.getElementById('editPurchasePrice');
|
||
|
||
if (editPriceWrapper && editCurrencySymbol) {
|
||
editCurrencySymbol.textContent = symbol;
|
||
if (position === 'right') {
|
||
editPriceWrapper.classList.add('currency-right');
|
||
// Set up dynamic positioning for right-aligned currency
|
||
if (editPriceInput) {
|
||
setupDynamicCurrencyPosition(editPriceInput, editCurrencySymbol);
|
||
}
|
||
} else {
|
||
editPriceWrapper.classList.remove('currency-right');
|
||
// Reset any dynamic positioning
|
||
if (editCurrencySymbol) {
|
||
editCurrencySymbol.style.right = '';
|
||
}
|
||
}
|
||
console.log(`Updated edit form currency position: ${position}`);
|
||
}
|
||
}
|
||
|
||
function setupDynamicCurrencyPosition(input, currencySymbol) {
|
||
if (!input || !currencySymbol) return;
|
||
|
||
function updatePosition() {
|
||
const wrapper = input.closest('.price-input-wrapper');
|
||
if (!wrapper || !wrapper.classList.contains('currency-right')) return;
|
||
|
||
// Wait for elements to be fully rendered
|
||
if (wrapper.offsetWidth === 0) {
|
||
setTimeout(updatePosition, 50);
|
||
return;
|
||
}
|
||
|
||
// Get the input value or placeholder
|
||
const text = input.value || input.placeholder || '0.00';
|
||
|
||
// Create a temporary element to measure text width
|
||
const tempSpan = document.createElement('span');
|
||
tempSpan.style.visibility = 'hidden';
|
||
tempSpan.style.position = 'absolute';
|
||
tempSpan.style.fontSize = window.getComputedStyle(input).fontSize;
|
||
tempSpan.style.fontFamily = window.getComputedStyle(input).fontFamily;
|
||
tempSpan.style.fontWeight = window.getComputedStyle(input).fontWeight;
|
||
tempSpan.style.letterSpacing = window.getComputedStyle(input).letterSpacing;
|
||
tempSpan.textContent = text;
|
||
|
||
document.body.appendChild(tempSpan);
|
||
const textWidth = tempSpan.offsetWidth;
|
||
document.body.removeChild(tempSpan);
|
||
|
||
// Calculate position: input padding + text width + small gap
|
||
const inputPaddingLeft = parseInt(window.getComputedStyle(input).paddingLeft) || 12;
|
||
const gap = 4; // Small gap between text and currency symbol
|
||
const wrapperWidth = wrapper.offsetWidth;
|
||
const rightPosition = Math.max(8, wrapperWidth - inputPaddingLeft - textWidth - gap - 20);
|
||
|
||
currencySymbol.style.right = rightPosition + 'px';
|
||
console.log(`[Dynamic Currency] Positioned currency symbol at ${rightPosition}px from right for text: "${text}"`);
|
||
}
|
||
|
||
// Update position on various events
|
||
input.addEventListener('input', updatePosition);
|
||
input.addEventListener('focus', updatePosition);
|
||
input.addEventListener('blur', updatePosition);
|
||
|
||
// Initial positioning with better timing
|
||
// Use requestAnimationFrame to ensure DOM is ready
|
||
requestAnimationFrame(() => {
|
||
updatePosition();
|
||
// Also set up additional fallback timers
|
||
setTimeout(updatePosition, 100);
|
||
setTimeout(updatePosition, 300);
|
||
});
|
||
}
|
||
|
||
// If you want to update currency symbols live when storage changes (e.g. settings page open in another tab):
|
||
window.addEventListener('storage', function(e) {
|
||
const prefix = getPreferenceKeyPrefix();
|
||
// Only update if the main preferences object for the current user type changed
|
||
if (e.key === `${prefix}preferences`) {
|
||
console.log(`Storage event detected for ${prefix}preferences. Updating currency symbols.`);
|
||
updateCurrencySymbols();
|
||
}
|
||
// Also update when currency position changes
|
||
if (e.key === `${prefix}currencyPosition`) {
|
||
console.log(`Storage event detected for ${prefix}currencyPosition. Re-rendering warranties to update currency position.`);
|
||
// Update forms immediately
|
||
const symbol = getCurrencySymbol();
|
||
const position = getCurrencyPosition();
|
||
updateFormCurrencyPosition(symbol, position);
|
||
// Re-render warranties to apply new currency position
|
||
if (typeof processAllWarranties === 'function') {
|
||
processAllWarranties();
|
||
}
|
||
}
|
||
});
|
||
|
||
// +++ NEW FUNCTION TO LOAD PREFS AND SAVE TO LOCALSTORAGE +++
|
||
async function loadAndApplyUserPreferences(isAuthenticated) { // Added isAuthenticated parameter
|
||
// Use the global prefix determined after auth ready
|
||
let prefix = userPreferencePrefix; // <<< CHANGED const to let
|
||
if (!prefix) {
|
||
console.error('[Prefs Loader] Cannot load preferences: User preference prefix not set yet. Defaulting to user_');
|
||
// Setting a default might be risky if the user *is* admin but prefix wasn't set in time.
|
||
// Consider how authStateReady ensures prefix is set before this runs.
|
||
// For now, let's try defaulting, but this might need review.
|
||
prefix = 'user_';
|
||
}
|
||
console.log(`[Prefs Loader] Attempting to load preferences using prefix: ${prefix}, isAuthenticated: ${isAuthenticated}`);
|
||
|
||
if (isAuthenticated && window.auth) { // Use passed isAuthenticated and check if window.auth exists
|
||
const token = window.auth.getToken(); // Still need token for the API call
|
||
if (!token) {
|
||
console.error('[Prefs Loader] Cannot load preferences: No auth token found, even though isAuthenticated was true.');
|
||
return; // Exit if no token
|
||
}
|
||
|
||
try {
|
||
console.log('[Prefs Loader] Fetching /api/auth/preferences with token.');
|
||
const response = await fetch('/api/auth/preferences', {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const apiPrefs = await response.json();
|
||
console.log('[Prefs Loader] Preferences loaded from API:', apiPrefs);
|
||
|
||
// Save relevant prefs to localStorage
|
||
if (apiPrefs.currency_symbol) {
|
||
localStorage.setItem(`${prefix}currencySymbol`, apiPrefs.currency_symbol);
|
||
console.log(`[Prefs Loader] Saved ${prefix}currencySymbol: ${apiPrefs.currency_symbol}`);
|
||
}
|
||
if (apiPrefs.currency_position) {
|
||
localStorage.setItem(`${prefix}currencyPosition`, apiPrefs.currency_position);
|
||
console.log(`[Prefs Loader] Saved ${prefix}currencyPosition: ${apiPrefs.currency_position}`);
|
||
}
|
||
if (apiPrefs.default_view) {
|
||
localStorage.setItem(`${prefix}defaultView`, apiPrefs.default_view);
|
||
console.log(`[Prefs Loader] Saved ${prefix}defaultView: ${apiPrefs.default_view}`);
|
||
}
|
||
if (apiPrefs.expiring_soon_days !== undefined) {
|
||
localStorage.setItem(`${prefix}expiringSoonDays`, apiPrefs.expiring_soon_days);
|
||
// Also update the global variable used by processWarrantyData
|
||
expiringSoonDays = apiPrefs.expiring_soon_days;
|
||
console.log(`[Prefs Loader] Saved ${prefix}expiringSoonDays: ${apiPrefs.expiring_soon_days}`);
|
||
console.log(`[Prefs Loader] Updated global expiringSoonDays variable to: ${expiringSoonDays}`);
|
||
}
|
||
if (apiPrefs.date_format) {
|
||
localStorage.setItem('dateFormat', apiPrefs.date_format);
|
||
console.log(`[Prefs Loader] Saved dateFormat: ${apiPrefs.date_format}`);
|
||
}
|
||
|
||
// Optionally trigger immediate UI updates if needed, although renderWarranties will use these new values
|
||
// updateCurrencySymbols();
|
||
|
||
} else {
|
||
const errorData = await response.json().catch(() => ({}));
|
||
console.warn(`[Prefs Loader] Failed to load preferences from API: ${response.status}`, errorData.message || '');
|
||
// Set defaults in localStorage maybe?
|
||
if (!localStorage.getItem('dateFormat')) localStorage.setItem('dateFormat', 'MDY');
|
||
if (!localStorage.getItem(`${prefix}currencySymbol`)) localStorage.setItem(`${prefix}currencySymbol`, '$');
|
||
// etc.
|
||
}
|
||
} catch (error) {
|
||
console.error('[Prefs Loader] Error fetching/applying preferences from API:', error);
|
||
// Set defaults in localStorage on error?
|
||
if (!localStorage.getItem('dateFormat')) localStorage.setItem('dateFormat', 'MDY');
|
||
if (!localStorage.getItem(`${prefix}currencySymbol`)) localStorage.setItem(`${prefix}currencySymbol`, '$');
|
||
// etc.
|
||
}
|
||
} else {
|
||
console.warn('[Prefs Loader] Cannot load preferences: User not authenticated or auth module not available.');
|
||
// Apply defaults if not authenticated?
|
||
if (!localStorage.getItem('dateFormat')) localStorage.setItem('dateFormat', 'MDY');
|
||
if (!localStorage.getItem(`${prefix}currencySymbol`)) localStorage.setItem(`${prefix}currencySymbol`, '$');
|
||
// etc.
|
||
}
|
||
}
|
||
// +++ END NEW FUNCTION +++
|
||
|
||
// Warranty method change handlers
|
||
function handleWarrantyMethodChange() {
|
||
console.log('[DEBUG] handleWarrantyMethodChange called');
|
||
const isLifetime = isLifetimeCheckbox && isLifetimeCheckbox.checked;
|
||
const isDurationMethod = durationMethodRadio && durationMethodRadio.checked;
|
||
|
||
console.log('[DEBUG] isLifetime:', isLifetime, 'isDurationMethod:', isDurationMethod);
|
||
console.log('[DEBUG] Elements found:', {
|
||
warrantyDurationFields: !!warrantyDurationFields,
|
||
exactExpirationField: !!exactExpirationField,
|
||
exactExpirationDateInput: !!exactExpirationDateInput
|
||
});
|
||
|
||
if (isLifetime) {
|
||
// Hide both methods when lifetime is selected
|
||
console.log('[DEBUG] Lifetime selected, hiding both methods');
|
||
if (warrantyDurationFields) warrantyDurationFields.style.display = 'none';
|
||
if (exactExpirationField) exactExpirationField.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
if (isDurationMethod) {
|
||
console.log('[DEBUG] Duration method selected');
|
||
if (warrantyDurationFields) warrantyDurationFields.style.display = 'block';
|
||
if (exactExpirationField) exactExpirationField.style.display = 'none';
|
||
// Clear exact date when switching to duration
|
||
if (exactExpirationDateInput) exactExpirationDateInput.value = '';
|
||
} else {
|
||
console.log('[DEBUG] Exact date method selected');
|
||
if (warrantyDurationFields) warrantyDurationFields.style.display = 'none';
|
||
if (exactExpirationField) exactExpirationField.style.display = 'block';
|
||
// Clear duration fields when switching to exact date
|
||
if (warrantyDurationYearsInput) warrantyDurationYearsInput.value = '';
|
||
if (warrantyDurationMonthsInput) warrantyDurationMonthsInput.value = '';
|
||
if (warrantyDurationDaysInput) warrantyDurationDaysInput.value = '';
|
||
}
|
||
}
|
||
|
||
function handleEditWarrantyMethodChange() {
|
||
console.log('[DEBUG] handleEditWarrantyMethodChange called');
|
||
const isLifetime = editIsLifetimeCheckbox && editIsLifetimeCheckbox.checked;
|
||
const isDurationMethod = editDurationMethodRadio && editDurationMethodRadio.checked;
|
||
|
||
console.log('[DEBUG Edit] isLifetime:', isLifetime, 'isDurationMethod:', isDurationMethod);
|
||
console.log('[DEBUG Edit] Radio button states:', {
|
||
editDurationMethodRadio: editDurationMethodRadio ? editDurationMethodRadio.checked : 'element not found',
|
||
editExactDateMethodRadio: editExactDateMethodRadio ? editExactDateMethodRadio.checked : 'element not found'
|
||
});
|
||
console.log('[DEBUG Edit] Elements found:', {
|
||
editWarrantyDurationFields: !!editWarrantyDurationFields,
|
||
editExactExpirationField: !!editExactExpirationField,
|
||
editExactExpirationDateInput: !!editExactExpirationDateInput
|
||
});
|
||
|
||
if (isLifetime) {
|
||
// Hide both methods when lifetime is selected
|
||
console.log('[DEBUG Edit] Lifetime selected, hiding both methods');
|
||
if (editWarrantyDurationFields) editWarrantyDurationFields.style.display = 'none';
|
||
if (editExactExpirationField) editExactExpirationField.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
if (isDurationMethod) {
|
||
console.log('[DEBUG Edit] Duration method selected');
|
||
if (editWarrantyDurationFields) {
|
||
editWarrantyDurationFields.style.display = 'block';
|
||
console.log('[DEBUG Edit] Set duration fields to block');
|
||
}
|
||
if (editExactExpirationField) {
|
||
editExactExpirationField.style.display = 'none';
|
||
console.log('[DEBUG Edit] Set exact date field to none');
|
||
}
|
||
// Clear exact date when switching to duration
|
||
if (editExactExpirationDateInput) editExactExpirationDateInput.value = '';
|
||
} else {
|
||
console.log('[DEBUG Edit] Exact date method selected');
|
||
if (editWarrantyDurationFields) {
|
||
editWarrantyDurationFields.style.display = 'none';
|
||
console.log('[DEBUG Edit] Set duration fields to none');
|
||
}
|
||
if (editExactExpirationField) {
|
||
editExactExpirationField.style.display = 'block';
|
||
console.log('[DEBUG Edit] Set exact date field to block');
|
||
}
|
||
// Clear duration fields when switching to exact date
|
||
if (editWarrantyDurationYearsInput) editWarrantyDurationYearsInput.value = '';
|
||
if (editWarrantyDurationMonthsInput) editWarrantyDurationMonthsInput.value = '';
|
||
if (editWarrantyDurationDaysInput) editWarrantyDurationDaysInput.value = '';
|
||
}
|
||
}
|
||
|
||
// Function to calculate duration between two dates
|
||
function calculateDurationFromDates(startDate, endDate) {
|
||
if (!startDate || !endDate) return null;
|
||
|
||
try {
|
||
const start = new Date(startDate);
|
||
const end = new Date(endDate);
|
||
|
||
if (isNaN(start.getTime()) || isNaN(end.getTime())) return null;
|
||
|
||
let years = end.getFullYear() - start.getFullYear();
|
||
let months = end.getMonth() - start.getMonth();
|
||
let days = end.getDate() - start.getDate();
|
||
|
||
// Adjust for negative days
|
||
if (days < 0) {
|
||
months--;
|
||
const prevMonth = new Date(end.getFullYear(), end.getMonth(), 0);
|
||
days += prevMonth.getDate();
|
||
}
|
||
|
||
// Adjust for negative months
|
||
if (months < 0) {
|
||
years--;
|
||
months += 12;
|
||
}
|
||
|
||
return { years, months, days };
|
||
} catch (error) {
|
||
console.error('Error calculating duration:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Load secure images with authentication
|
||
*/
|
||
async function loadSecureImages() {
|
||
const token = localStorage.getItem('auth_token');
|
||
if (!token) {
|
||
console.log('[DEBUG] No auth token available for secure image loading');
|
||
return;
|
||
}
|
||
|
||
// Also find images that may already have src but need to be refreshed
|
||
const secureImages = document.querySelectorAll('img.secure-image[data-secure-src]');
|
||
console.log(`[DEBUG] Found ${secureImages.length} secure images to load/refresh`);
|
||
|
||
for (const img of secureImages) {
|
||
try {
|
||
const secureUrl = img.getAttribute('data-secure-src');
|
||
console.log(`[DEBUG] Loading secure image: ${secureUrl}`);
|
||
|
||
// Clean up existing blob URL if present
|
||
const existingBlobUrl = img.getAttribute('data-blob-url');
|
||
if (existingBlobUrl) {
|
||
URL.revokeObjectURL(existingBlobUrl);
|
||
img.removeAttribute('data-blob-url');
|
||
}
|
||
|
||
const response = await fetch(secureUrl, {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const blob = await response.blob();
|
||
const blobUrl = URL.createObjectURL(blob);
|
||
img.src = blobUrl;
|
||
|
||
// Clean up blob URL when image is removed from DOM
|
||
img.addEventListener('load', () => {
|
||
console.log(`[DEBUG] Secure image loaded successfully: ${secureUrl}`);
|
||
}, { once: true });
|
||
|
||
// Store blob URL for cleanup
|
||
img.setAttribute('data-blob-url', blobUrl);
|
||
} else {
|
||
console.error(`[DEBUG] Failed to load secure image: ${secureUrl}, status: ${response.status}`);
|
||
img.style.display = 'none';
|
||
}
|
||
} catch (error) {
|
||
console.error(`[DEBUG] Error loading secure image:`, error);
|
||
img.style.display = 'none';
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Paperless-ngx Integration Functions
|
||
// ============================================================================
|
||
|
||
// Global variable to store Paperless-ngx enabled state
|
||
let paperlessNgxEnabled = false;
|
||
|
||
/**
|
||
* Check if Paperless-ngx integration is enabled
|
||
*/
|
||
async function checkPaperlessNgxStatus() {
|
||
try {
|
||
const token = localStorage.getItem('auth_token');
|
||
if (!token) return false;
|
||
|
||
const response = await fetch('/api/admin/settings', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const settings = await response.json();
|
||
paperlessNgxEnabled = settings.paperless_enabled === 'true';
|
||
window.paperlessNgxEnabled = paperlessNgxEnabled; // Set global variable
|
||
console.log('[Paperless-ngx] Integration status:', paperlessNgxEnabled);
|
||
return paperlessNgxEnabled;
|
||
}
|
||
} catch (error) {
|
||
console.error('[Paperless-ngx] Error checking status:', error);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Initialize Paperless-ngx integration UI
|
||
*/
|
||
async function initPaperlessNgxIntegration() {
|
||
// Check if Paperless-ngx is enabled
|
||
const isEnabled = await checkPaperlessNgxStatus();
|
||
|
||
if (isEnabled) {
|
||
// Show the info alert
|
||
const infoAlert = document.getElementById('paperlessInfoAlert');
|
||
if (infoAlert) {
|
||
infoAlert.style.display = 'block';
|
||
}
|
||
|
||
// Show storage selection options for add modal (only invoice and manual)
|
||
const storageSelections = [
|
||
'invoiceStorageSelection',
|
||
'manualStorageSelection'
|
||
];
|
||
|
||
storageSelections.forEach(id => {
|
||
const element = document.getElementById(id);
|
||
if (element) {
|
||
element.style.display = 'block';
|
||
}
|
||
});
|
||
|
||
// Show storage selection options for edit modal (only invoice and manual)
|
||
const editStorageSelections = [
|
||
'editInvoiceStorageSelection',
|
||
'editManualStorageSelection'
|
||
];
|
||
|
||
editStorageSelections.forEach(id => {
|
||
const element = document.getElementById(id);
|
||
if (element) {
|
||
element.style.display = 'block';
|
||
}
|
||
});
|
||
|
||
// Show Paperless browse sections
|
||
console.log('[Paperless-ngx] Calling togglePaperlessBrowseSections...');
|
||
togglePaperlessBrowseSections();
|
||
|
||
console.log('[Paperless-ngx] UI elements initialized and shown');
|
||
} else {
|
||
console.log('[Paperless-ngx] Integration disabled, hiding UI elements');
|
||
|
||
// Hide Paperless browse sections
|
||
console.log('[Paperless-ngx] Calling togglePaperlessBrowseSections (disabled)...');
|
||
togglePaperlessBrowseSections();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get selected storage option for a document type
|
||
* @param {string} documentType - The document type (productPhoto, invoice, manual, otherDocument)
|
||
* @param {boolean} isEdit - Whether this is for the edit modal
|
||
* @returns {string} - 'local' or 'paperless'
|
||
*/
|
||
function getStorageOption(documentType, isEdit = false) {
|
||
// Only allow Paperless-ngx storage for invoices and manuals
|
||
const paperlessAllowedTypes = ['invoice', 'manual'];
|
||
|
||
if (!paperlessAllowedTypes.includes(documentType)) {
|
||
return 'local'; // Force local storage for productPhoto and otherDocument
|
||
}
|
||
|
||
const prefix = isEdit ? 'edit' : '';
|
||
const capitalizedType = documentType.charAt(0).toUpperCase() + documentType.slice(1);
|
||
const name = `${prefix}${capitalizedType}Storage`;
|
||
const radio = document.querySelector(`input[name="${name}"]:checked`);
|
||
return radio ? radio.value : 'local';
|
||
}
|
||
|
||
/**
|
||
* Upload file to Paperless-ngx
|
||
* @param {File} file - The file to upload
|
||
* @param {string} documentType - The type of document for tagging
|
||
* @returns {Promise<Object>} - Upload result with document ID
|
||
*/
|
||
async function uploadToPaperlessNgx(file, documentType) {
|
||
try {
|
||
const token = localStorage.getItem('auth_token');
|
||
if (!token) {
|
||
throw new Error('Authentication token not available');
|
||
}
|
||
|
||
// Show upload loading screen
|
||
showPaperlessUploadLoading(documentType);
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
formData.append('document_type', documentType);
|
||
formData.append('title', `Warracker ${documentType} - ${file.name}`);
|
||
|
||
// Add tags for organization
|
||
const tags = ['warracker', documentType];
|
||
formData.append('tags', tags.join(','));
|
||
|
||
console.log('[Paperless-ngx] Upload FormData contents:');
|
||
console.log(' - file:', file.name, '(' + file.size + ' bytes, ' + file.type + ')');
|
||
console.log(' - document_type:', documentType);
|
||
console.log(' - title:', `Warracker ${documentType} - ${file.name}`);
|
||
console.log(' - tags:', tags.join(','));
|
||
|
||
updatePaperlessUploadStatus('Uploading file to Paperless-ngx...');
|
||
|
||
const response = await fetch('/api/paperless/upload', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
body: formData
|
||
});
|
||
|
||
if (!response.ok) {
|
||
let errorMessage = 'Failed to upload to Paperless-ngx';
|
||
try {
|
||
const errorData = await response.json();
|
||
errorMessage = errorData.error || errorData.message || errorMessage;
|
||
console.error('[Paperless-ngx] Server error details:', errorData);
|
||
} catch (parseError) {
|
||
console.error('[Paperless-ngx] Could not parse error response:', parseError);
|
||
errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
||
}
|
||
hidePaperlessUploadLoading();
|
||
throw new Error(errorMessage);
|
||
}
|
||
|
||
const result = await response.json();
|
||
console.log('[Paperless-ngx] Upload successful:', result);
|
||
|
||
// Update status based on result
|
||
if (result.document_id) {
|
||
updatePaperlessUploadStatus('Document uploaded and ready!');
|
||
} else {
|
||
updatePaperlessUploadStatus('Document uploaded, processing...', true);
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
document_id: result.document_id,
|
||
message: result.message,
|
||
error: result.error // Add this
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('[Paperless-ngx] Upload error:', error);
|
||
hidePaperlessUploadLoading();
|
||
return {
|
||
success: false,
|
||
error: error.message
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handle warranty form submission with Paperless-ngx integration
|
||
* This extends the existing saveWarranty function
|
||
*/
|
||
async function processPaperlessNgxUploads(formData) {
|
||
if (!paperlessNgxEnabled) {
|
||
return {}; // Return empty object if not enabled
|
||
}
|
||
|
||
const uploads = {};
|
||
// Only process invoice and manual for Paperless-ngx uploads
|
||
const documentTypes = ['invoice', 'manual'];
|
||
|
||
for (const docType of documentTypes) {
|
||
// Use storage option from formData, not DOM
|
||
const storageKey = docType + 'Storage';
|
||
const storageOption = formData.get(storageKey) || 'local';
|
||
const fileInput = document.getElementById(docType);
|
||
const file = fileInput?.files[0];
|
||
console.log(`[DEBUG][processPaperlessNgxUploads] docType:`, docType, '| storageOption (from formData):', storageOption, '| file:', file);
|
||
if (storageOption === 'paperless') {
|
||
if (file) {
|
||
console.log(`[Paperless-ngx] Uploading ${docType} to Paperless-ngx`);
|
||
// Upload to Paperless-ngx
|
||
const uploadResult = await uploadToPaperlessNgx(file, docType);
|
||
console.log(`[DEBUG][processPaperlessNgxUploads] uploadResult for ${docType}:`, uploadResult);
|
||
if (uploadResult.success || (uploadResult.error && uploadResult.error.includes("duplicate") && uploadResult.document_id)) {
|
||
// Map frontend document types to database column names
|
||
const fieldMapping = {
|
||
'productPhoto': 'paperless_photo_id',
|
||
'invoice': 'paperless_invoice_id',
|
||
'manual': 'paperless_manual_id',
|
||
'otherDocument': 'paperless_other_id'
|
||
};
|
||
const dbField = fieldMapping[docType];
|
||
if (dbField && uploadResult.document_id) {
|
||
uploads[dbField] = uploadResult.document_id;
|
||
console.log(`[Paperless-ngx] ${docType} uploaded/linked successfully, ID: ${uploadResult.document_id}, stored as: ${dbField}`);
|
||
// Hide loading screen immediately for direct uploads
|
||
hidePaperlessUploadLoading();
|
||
if (uploadResult.error && uploadResult.error.includes("duplicate")) {
|
||
showToast("Duplicate document detected in Paperless-ngx. Linked to existing document.", 'info');
|
||
}
|
||
} else if (dbField && !uploadResult.document_id) {
|
||
console.log(`[Paperless-ngx] ${docType} uploaded successfully but no document ID received (async processing). Not storing reference.`);
|
||
// Don't hide loading screen yet - auto-link will handle it
|
||
updatePaperlessUploadStatus('Document processing, searching for link...', true);
|
||
}
|
||
// ALWAYS remove the file from FormData since it's been uploaded to Paperless-ngx
|
||
// This prevents the backend from also saving it locally
|
||
if (formData.has(docType)) {
|
||
formData.delete(docType);
|
||
console.log(`[Paperless-ngx] Removed ${docType} from FormData to prevent local storage`);
|
||
}
|
||
} else {
|
||
console.error(`[Paperless-ngx] Failed to upload ${docType} to Paperless-ngx:`, uploadResult.error);
|
||
throw new Error(`Failed to upload ${docType} to Paperless-ngx: ${uploadResult.error}`);
|
||
}
|
||
} else {
|
||
console.log(`[DEBUG][processPaperlessNgxUploads] No file found for ${docType} with paperless storage option.`);
|
||
}
|
||
} else {
|
||
console.log(`[DEBUG][processPaperlessNgxUploads] Skipping ${docType}, storageOption is not paperless.`);
|
||
}
|
||
}
|
||
|
||
return uploads;
|
||
}
|
||
|
||
/**
|
||
* Handle warranty edit form submission with Paperless-ngx integration
|
||
* This extends the existing edit warranty functionality
|
||
*/
|
||
async function processEditPaperlessNgxUploads(formData) {
|
||
if (!paperlessNgxEnabled) {
|
||
return {}; // Return empty object if not enabled
|
||
}
|
||
|
||
const uploads = {};
|
||
// Only process invoice and manual for Paperless-ngx uploads
|
||
const documentTypes = ['invoice', 'manual'];
|
||
|
||
for (const docType of documentTypes) {
|
||
const storageOption = getStorageOption(docType, true); // true for edit modal
|
||
|
||
if (storageOption === 'paperless') {
|
||
const fileInput = document.getElementById(`edit${docType.charAt(0).toUpperCase() + docType.slice(1)}`);
|
||
const file = fileInput?.files[0];
|
||
|
||
if (file) {
|
||
console.log(`[Paperless-ngx] Uploading ${docType} to Paperless-ngx (edit mode)`);
|
||
|
||
// Upload to Paperless-ngx
|
||
const uploadResult = await uploadToPaperlessNgx(file, docType);
|
||
|
||
if (uploadResult.success || (uploadResult.error && uploadResult.error.includes("duplicate") && uploadResult.document_id)) {
|
||
// Map frontend document types to database column names
|
||
const fieldMapping = {
|
||
'productPhoto': 'paperless_photo_id',
|
||
'invoice': 'paperless_invoice_id',
|
||
'manual': 'paperless_manual_id',
|
||
'otherDocument': 'paperless_other_id'
|
||
};
|
||
|
||
const dbField = fieldMapping[docType];
|
||
if (dbField && uploadResult.document_id) {
|
||
uploads[dbField] = uploadResult.document_id;
|
||
console.log(`[Paperless-ngx] ${docType} uploaded/linked successfully (edit), ID: ${uploadResult.document_id}, stored as: ${dbField}`);
|
||
// Hide loading screen immediately for direct uploads
|
||
hidePaperlessUploadLoading();
|
||
if (uploadResult.error && uploadResult.error.includes("duplicate")) {
|
||
showToast("Duplicate document detected in Paperless-ngx. Linked to existing document.", 'info');
|
||
}
|
||
} else if (dbField && !uploadResult.document_id) {
|
||
console.log(`[Paperless-ngx] ${docType} uploaded successfully (edit) but no document ID received (async processing). Not storing reference.`);
|
||
// Don't hide loading screen yet - auto-link will handle it
|
||
updatePaperlessUploadStatus('Document processing, searching for link...', true);
|
||
}
|
||
|
||
// ALWAYS remove the file from FormData since it's been uploaded to Paperless-ngx
|
||
// This prevents the backend from also saving it locally
|
||
// Note: In edit mode, the form field names don't have 'edit' prefix in FormData
|
||
if (formData.has(docType)) {
|
||
formData.delete(docType);
|
||
console.log(`[Paperless-ngx] Removed ${docType} from FormData to prevent local storage`);
|
||
}
|
||
} else {
|
||
throw new Error(`Failed to upload ${docType} to Paperless-ngx: ${uploadResult.error}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return uploads;
|
||
}
|
||
|
||
// Initialize Paperless-ngx integration when the page loads
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// Initialize after a short delay to ensure other components are loaded
|
||
setTimeout(() => {
|
||
initPaperlessNgxIntegration();
|
||
}, 1000);
|
||
});
|
||
|
||
/**
|
||
* Debug Paperless-ngx configuration
|
||
*/
|
||
async function debugPaperlessConfiguration() {
|
||
try {
|
||
const token = localStorage.getItem('auth_token');
|
||
if (!token) {
|
||
console.error('[Paperless Debug] No auth token found');
|
||
return null;
|
||
}
|
||
|
||
console.log('[Paperless Debug] Checking configuration...');
|
||
|
||
const response = await fetch('/api/paperless/debug', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
console.error('[Paperless Debug] Debug endpoint failed:', response.status, response.statusText);
|
||
const errorText = await response.text();
|
||
console.error('[Paperless Debug] Error response:', errorText);
|
||
return null;
|
||
}
|
||
|
||
const result = await response.json();
|
||
console.log('[Paperless Debug] Configuration:', result);
|
||
|
||
return result;
|
||
} catch (error) {
|
||
console.error('[Paperless Debug] Error:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Open a Paperless-ngx document either in Warracker interface or in Paperless-ngx directly
|
||
*/
|
||
async function openPaperlessDocument(paperlessId) {
|
||
console.log(`[openPaperlessDocument] Opening Paperless document: ${paperlessId}`);
|
||
|
||
// First, debug the Paperless configuration
|
||
const debugInfo = await debugPaperlessConfiguration();
|
||
if (debugInfo) {
|
||
console.log('[openPaperlessDocument] Debug info:', debugInfo);
|
||
|
||
if (!debugInfo.paperless_enabled || debugInfo.paperless_enabled === 'false') {
|
||
showToast('Paperless-ngx integration is not enabled', 'error');
|
||
return;
|
||
}
|
||
|
||
if (!debugInfo.paperless_handler_available) {
|
||
showToast('Paperless-ngx is not properly configured. Please check the settings.', 'error');
|
||
console.error('[openPaperlessDocument] Paperless handler not available');
|
||
if (debugInfo.paperless_handler_error) {
|
||
console.error('[openPaperlessDocument] Handler error:', debugInfo.paperless_handler_error);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (debugInfo.test_connection_result && !debugInfo.test_connection_result.success) {
|
||
showToast(`Paperless-ngx connection failed: ${debugInfo.test_connection_result.message || debugInfo.test_connection_result.error}`, 'error');
|
||
return;
|
||
}
|
||
}
|
||
|
||
const token = localStorage.getItem('auth_token');
|
||
if (!token) {
|
||
console.error('[openPaperlessDocument] No auth token available');
|
||
showToast('Authentication required', 'error');
|
||
return;
|
||
}
|
||
|
||
// Check user preference for viewing documents
|
||
const viewInApp = await getUserPaperlessViewPreference();
|
||
console.log(`[openPaperlessDocument] User preference view in app: ${viewInApp}`);
|
||
|
||
if (viewInApp) {
|
||
// Open document in Warracker interface
|
||
console.log(`[openPaperlessDocument] Opening document ${paperlessId} in Warracker interface`);
|
||
const documentUrl = `/api/paperless-file/${paperlessId}?token=${encodeURIComponent(token)}`;
|
||
|
||
const newTab = window.open(documentUrl, '_blank');
|
||
if (!newTab) {
|
||
showToast('Please allow popups to view documents', 'warning');
|
||
} else {
|
||
showToast('Opening document in Warracker...', 'info');
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Default behavior: open in Paperless-ngx interface
|
||
try {
|
||
// Get the Paperless-ngx base URL
|
||
const response = await fetch('/api/paperless/url', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
console.error('[openPaperlessDocument] URL endpoint failed:', response.status, errorText);
|
||
throw new Error(`Failed to get Paperless-ngx URL: ${response.status}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
if (!result.success) {
|
||
throw new Error(result.message || 'Failed to get Paperless-ngx URL');
|
||
}
|
||
|
||
// Construct the direct link to the document in Paperless-ngx
|
||
const paperlessUrl = result.url.replace(/\/$/, ''); // Remove trailing slash
|
||
const documentUrl = `${paperlessUrl}/documents/${paperlessId}/details`;
|
||
|
||
console.log(`[openPaperlessDocument] Opening Paperless-ngx document at: ${documentUrl}`);
|
||
|
||
// Open the document directly in Paperless-ngx interface
|
||
const newTab = window.open(documentUrl, '_blank');
|
||
if (!newTab) {
|
||
showToast('Please allow popups to view documents in Paperless-ngx', 'warning');
|
||
} else {
|
||
showToast('Opening document in Paperless-ngx...', 'info');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error opening Paperless document:', error);
|
||
showToast(`Error opening document: ${error.message}`, 'error');
|
||
|
||
// Try to determine the base URL from debug info for fallback
|
||
if (debugInfo && debugInfo.paperless_url) {
|
||
const fallbackUrl = `${debugInfo.paperless_url.replace(/\/$/, '')}/documents/${paperlessId}/details`;
|
||
console.log(`[openPaperlessDocument] Trying fallback URL: ${fallbackUrl}`);
|
||
|
||
const fallbackTab = window.open(fallbackUrl, '_blank');
|
||
if (fallbackTab) {
|
||
showToast('Opened with fallback URL - please check if Paperless-ngx is accessible', 'warning');
|
||
}
|
||
} else {
|
||
// Last resort fallback
|
||
const genericFallbackUrl = `${window.location.protocol}//${window.location.hostname}:8000/documents/${paperlessId}/details`;
|
||
console.log(`[openPaperlessDocument] Trying generic fallback URL: ${genericFallbackUrl}`);
|
||
|
||
const genericTab = window.open(genericFallbackUrl, '_blank');
|
||
if (genericTab) {
|
||
showToast('Opened with generic fallback URL', 'warning');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get user preference for viewing Paperless documents in app
|
||
*/
|
||
async function getUserPaperlessViewPreference() {
|
||
// First check localStorage
|
||
const prefix = getPreferenceKeyPrefix();
|
||
const localPreference = localStorage.getItem(`${prefix}paperlessViewInApp`);
|
||
if (localPreference !== null) {
|
||
return localPreference === 'true';
|
||
}
|
||
|
||
// If not in localStorage, check API
|
||
if (window.auth && window.auth.isAuthenticated && window.auth.isAuthenticated()) {
|
||
try {
|
||
const response = await fetch('/api/auth/preferences', {
|
||
headers: {
|
||
'Authorization': `Bearer ${window.auth.getToken()}`
|
||
}
|
||
});
|
||
if (response.ok) {
|
||
const prefs = await response.json();
|
||
return prefs.paperless_view_in_app || false;
|
||
}
|
||
} catch (e) {
|
||
console.warn('Failed to load preferences from API:', e);
|
||
}
|
||
}
|
||
|
||
// Default to false (open in Paperless-ngx)
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Debug function to test Paperless document status
|
||
*/
|
||
async function debugPaperlessDocument(paperlessId) {
|
||
console.log(`[debugPaperlessDocument] Debugging Paperless document: ${paperlessId}`);
|
||
|
||
const token = auth.getToken();
|
||
if (!token) {
|
||
console.error('[debugPaperlessDocument] No auth token available');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/paperless/debug-document/${paperlessId}`, {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
console.error(`[debugPaperlessDocument] HTTP ${response.status}: ${errorText}`);
|
||
return;
|
||
}
|
||
|
||
const debugInfo = await response.json();
|
||
console.log(`[debugPaperlessDocument] Debug info for document ${paperlessId}:`, debugInfo);
|
||
|
||
// Show debug info in a more readable format
|
||
let debugMessage = `Debug info for Paperless document ${paperlessId}:\n\n`;
|
||
debugMessage += `Document exists: ${debugInfo.document_exists}\n`;
|
||
debugMessage += `Database references: ${debugInfo.database_references?.length || 0}\n\n`;
|
||
|
||
debugMessage += 'Endpoint test results:\n';
|
||
for (const [endpoint, result] of Object.entries(debugInfo.endpoints_tested || {})) {
|
||
debugMessage += `- ${endpoint}: ${result.success ? 'SUCCESS' : 'FAILED'} (${result.status_code || result.error})\n`;
|
||
}
|
||
|
||
if (debugInfo.recent_documents && Array.isArray(debugInfo.recent_documents)) {
|
||
debugMessage += `\nDocument in recent list: ${debugInfo.document_in_recent}\n`;
|
||
debugMessage += `Recent documents: ${debugInfo.recent_documents.map(d => `${d.id}: ${d.title}`).join(', ')}\n`;
|
||
}
|
||
|
||
alert(debugMessage);
|
||
|
||
} catch (error) {
|
||
console.error('Error debugging Paperless document:', error);
|
||
alert(`Debug failed: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Clean up invalid Paperless-ngx document references
|
||
*/
|
||
async function cleanupInvalidPaperlessDocuments() {
|
||
console.log('[cleanupInvalidPaperlessDocuments] Starting cleanup...');
|
||
|
||
const token = auth.getToken();
|
||
if (!token) {
|
||
console.error('[cleanupInvalidPaperlessDocuments] No auth token available');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/paperless/cleanup-invalid', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
console.error(`[cleanupInvalidPaperlessDocuments] HTTP ${response.status}: ${errorText}`);
|
||
return;
|
||
}
|
||
|
||
const result = await response.json();
|
||
console.log('[cleanupInvalidPaperlessDocuments] Cleanup result:', result);
|
||
|
||
// Show result to user
|
||
let message = result.message || 'Cleanup completed';
|
||
if (result.details) {
|
||
message += `\n\nDetails:\n`;
|
||
message += `- Documents checked: ${result.details.checked}\n`;
|
||
message += `- Invalid documents found: ${result.details.invalid_found}\n`;
|
||
message += `- References cleaned up: ${result.details.cleaned_up}\n`;
|
||
|
||
if (result.details.errors && result.details.errors.length > 0) {
|
||
message += `\nErrors:\n${result.details.errors.join('\n')}`;
|
||
}
|
||
}
|
||
|
||
alert(message);
|
||
|
||
// Reload warranties to reflect changes
|
||
if (result.details && result.details.cleaned_up > 0) {
|
||
console.log('[cleanupInvalidPaperlessDocuments] Reloading warranties after cleanup...');
|
||
await loadWarranties(true);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error cleaning up Paperless documents:', error);
|
||
alert(`Cleanup failed: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Search for and link a Paperless document by title
|
||
* Used when documents were uploaded with async processing and we lost the document ID
|
||
*/
|
||
async function searchAndLinkPaperlessDocument(warrantyId, documentType, searchTitle) {
|
||
try {
|
||
console.log(`[Paperless-ngx] Searching for document: ${searchTitle}`);
|
||
|
||
const response = await fetch('/api/paperless-search-and-link', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
||
},
|
||
body: JSON.stringify({
|
||
warranty_id: warrantyId,
|
||
document_type: documentType,
|
||
search_title: searchTitle
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
console.log(`[Paperless-ngx] Document linked successfully: ID ${result.document_id}`);
|
||
showToast('Document linked successfully! Refreshing...', 'success');
|
||
|
||
// Reload warranties to show the updated document links
|
||
setTimeout(async () => {
|
||
console.log('🔄 [Search&Link] Reloading warranties to show updated document links...');
|
||
await loadWarranties(true); // Pass isAuthenticated parameter
|
||
|
||
// Force re-render of the warranty cards
|
||
applyFilters();
|
||
|
||
// Also reload secure images to update cloud icons
|
||
await loadSecureImages();
|
||
|
||
console.log('✅ [Search&Link] Warranties reloaded and UI updated');
|
||
}, 1000);
|
||
|
||
return { success: true, document_id: result.document_id };
|
||
} else {
|
||
console.error(`[Paperless-ngx] Failed to link document: ${result.message}`);
|
||
showToast(`Failed to link document: ${result.message}`, 'error');
|
||
return { success: false, message: result.message };
|
||
}
|
||
} catch (error) {
|
||
console.error(`[Paperless-ngx] Error searching for document:`, error);
|
||
showToast('Error searching for document', 'error');
|
||
return { success: false, message: error.message };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Automatically search for and link recently uploaded documents
|
||
* This handles the case where Paperless-ngx async processing returns task_id instead of document_id
|
||
*/
|
||
async function autoLinkRecentDocuments(warrantyId, documentTypes = ['invoice', 'manual'], maxRetries = 10, retryDelay = 10000, fileInfo = {}) {
|
||
console.log(`[Auto-Link] Starting automatic document linking for warranty ${warrantyId}`);
|
||
|
||
const token = auth.getToken();
|
||
if (!token) {
|
||
console.error('[Auto-Link] No auth token available');
|
||
return;
|
||
}
|
||
|
||
let attempt = 0;
|
||
let linkedDocuments = [];
|
||
|
||
const tryLinking = async () => {
|
||
attempt++;
|
||
console.log(`[Auto-Link] Attempt ${attempt}/${maxRetries} for warranty ${warrantyId}`);
|
||
|
||
try {
|
||
// First check if Paperless-ngx is properly configured
|
||
const debugInfo = await debugPaperlessConfiguration();
|
||
if (!debugInfo) {
|
||
console.error('[Auto-Link] Could not get Paperless debug info');
|
||
return;
|
||
}
|
||
|
||
if (!debugInfo.paperless_enabled || debugInfo.paperless_enabled === 'false') {
|
||
console.log('[Auto-Link] Paperless-ngx integration is not enabled, skipping auto-link');
|
||
return;
|
||
}
|
||
|
||
if (!debugInfo.paperless_handler_available) {
|
||
console.error('[Auto-Link] Paperless handler not available:', debugInfo.paperless_handler_error || 'Unknown error');
|
||
return;
|
||
}
|
||
|
||
if (debugInfo.test_connection_result && !debugInfo.test_connection_result.success) {
|
||
console.error('[Auto-Link] Paperless connection test failed:', debugInfo.test_connection_result.message || debugInfo.test_connection_result.error);
|
||
return;
|
||
}
|
||
|
||
console.log(`[Auto-Link] Using intelligent filename-based search. File info:`, fileInfo);
|
||
|
||
// Strategy 1: Search by exact filename (most reliable)
|
||
let candidateDocuments = [];
|
||
|
||
for (const [docType, filename] of Object.entries(fileInfo)) {
|
||
if (!documentTypes.includes(docType)) continue;
|
||
|
||
console.log(`[Auto-Link] Searching for ${docType} with filename: "${filename}"`);
|
||
|
||
// Remove file extension for searching
|
||
const baseFilename = filename.replace(/\.[^/.]+$/, '');
|
||
|
||
// Try multiple search strategies
|
||
const searchQueries = [
|
||
filename, // Exact filename with extension
|
||
baseFilename, // Filename without extension
|
||
`"${filename}"`, // Quoted exact match
|
||
`"${baseFilename}"`, // Quoted base filename
|
||
`Warracker ${docType} - ${baseFilename}` // Warracker format
|
||
];
|
||
|
||
for (const query of searchQueries) {
|
||
try {
|
||
console.log(`[Auto-Link] Trying search query: "${query}"`);
|
||
|
||
const response = await fetch(`/api/paperless/search?ordering=-created&query=${encodeURIComponent(query)}`, {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
const docs = result.results || [];
|
||
|
||
console.log(`[Auto-Link] Query "${query}" found ${docs.length} documents`);
|
||
|
||
if (docs.length > 0) {
|
||
// Filter for recent documents (last 24 hours)
|
||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||
const recentDocs = docs.filter(doc => {
|
||
try {
|
||
const docDate = new Date(doc.created);
|
||
return docDate > oneDayAgo;
|
||
} catch {
|
||
return true; // Include if we can't parse the date
|
||
}
|
||
});
|
||
|
||
if (recentDocs.length > 0) {
|
||
console.log(`[Auto-Link] Found ${recentDocs.length} recent documents for ${docType}`);
|
||
candidateDocuments.push({
|
||
docType,
|
||
filename,
|
||
documents: recentDocs,
|
||
searchQuery: query
|
||
});
|
||
break; // Found documents, no need to try other queries for this file
|
||
} else if (docs.length > 0) {
|
||
// If no recent docs but we found some documents, include them anyway
|
||
console.log(`[Auto-Link] Found ${docs.length} older documents for ${docType}, including them anyway`);
|
||
candidateDocuments.push({
|
||
docType,
|
||
filename,
|
||
documents: docs.slice(0, 3), // Take up to 3 most recent
|
||
searchQuery: query
|
||
});
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error(`[Auto-Link] Error searching with query "${query}":`, error);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Strategy 2: Fallback to Warracker tag search if filename search fails
|
||
if (candidateDocuments.length === 0) {
|
||
console.log('[Auto-Link] No documents found by filename, trying Warracker tag search...');
|
||
|
||
const response = await fetch('/api/paperless/search?ordering=-created&created__gte=' +
|
||
new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const searchResult = await response.json();
|
||
let recentDocs = searchResult.results || [];
|
||
|
||
// Filter for Warracker documents
|
||
const warrackerDocs = recentDocs.filter(doc =>
|
||
doc.title && doc.title.includes('Warracker')
|
||
);
|
||
|
||
console.log(`[Auto-Link] Found ${warrackerDocs.length} Warracker documents from last 2 hours`);
|
||
|
||
// Group by document type
|
||
for (const docType of documentTypes) {
|
||
const typeDocs = warrackerDocs.filter(doc =>
|
||
doc.title && doc.title.includes(docType)
|
||
);
|
||
|
||
if (typeDocs.length > 0) {
|
||
candidateDocuments.push({
|
||
docType,
|
||
filename: fileInfo[docType] || `${docType} document`,
|
||
documents: typeDocs,
|
||
searchQuery: `Warracker ${docType}`
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Debug: Show what candidate documents we found
|
||
if (candidateDocuments.length > 0) {
|
||
console.log('[Auto-Link] Candidate documents found:');
|
||
candidateDocuments.forEach(candidate => {
|
||
console.log(` ${candidate.docType}: ${candidate.documents.length} documents found with query "${candidate.searchQuery}"`);
|
||
candidate.documents.forEach((doc, i) => {
|
||
console.log(` ${i+1}. ID: ${doc.id}, Title: "${doc.title}", Created: ${doc.created}`);
|
||
});
|
||
});
|
||
}
|
||
|
||
// Try to link the best candidate for each document type
|
||
for (const candidate of candidateDocuments) {
|
||
// Use the most recent document (first in the ordered list)
|
||
const doc = candidate.documents[0];
|
||
|
||
console.log(`[Auto-Link] Attempting to link ${candidate.docType}: ${doc.title} (ID: ${doc.id})`);
|
||
|
||
try {
|
||
const linkResponse = await fetch('/api/paperless-search-and-link', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
||
},
|
||
body: JSON.stringify({
|
||
warranty_id: warrantyId,
|
||
document_type: candidate.docType,
|
||
search_title: doc.title.replace('Warracker ' + candidate.docType + ' - ', '')
|
||
})
|
||
});
|
||
|
||
const linkResult = await linkResponse.json();
|
||
|
||
if (linkResult.success) {
|
||
console.log(`[Auto-Link] Successfully linked ${candidate.docType}: ${doc.title}`);
|
||
linkedDocuments.push({
|
||
type: candidate.docType,
|
||
title: doc.title,
|
||
id: doc.id,
|
||
filename: candidate.filename
|
||
});
|
||
} else {
|
||
console.log(`[Auto-Link] Failed to link ${candidate.docType}: ${linkResult.message}`);
|
||
}
|
||
} catch (error) {
|
||
console.error(`[Auto-Link] Error linking ${candidate.docType}:`, error);
|
||
}
|
||
}
|
||
|
||
// If we found and linked documents, we're done
|
||
if (linkedDocuments.length > 0) {
|
||
console.log(`[Auto-Link] Successfully linked ${linkedDocuments.length} documents:`, linkedDocuments);
|
||
|
||
// Update loading screen to show success
|
||
updatePaperlessUploadStatus('Documents linked successfully!');
|
||
|
||
// Show success message with filenames
|
||
const docInfo = linkedDocuments.map(d => `${d.type} (${d.filename || d.title})`).join(', ');
|
||
showToast(`Automatically linked ${linkedDocuments.length} document(s): ${docInfo}`, 'success');
|
||
|
||
// Reload warranties to show the updated document links
|
||
setTimeout(async () => {
|
||
console.log('🔄 [Auto-Link] Reloading warranties to show updated document links...');
|
||
await loadWarranties(true); // Pass isAuthenticated parameter
|
||
|
||
// Force re-render of the warranty cards
|
||
applyFilters();
|
||
|
||
// Also reload secure images to update cloud icons
|
||
await loadSecureImages();
|
||
|
||
console.log('✅ [Auto-Link] Warranties reloaded and UI updated');
|
||
|
||
// Hide loading screen after successful completion
|
||
hidePaperlessUploadLoading();
|
||
}, 1000);
|
||
|
||
return true;
|
||
}
|
||
|
||
// If no documents found and we have retries left, try again
|
||
if (attempt < maxRetries) {
|
||
console.log(`[Auto-Link] No documents found, retrying in ${retryDelay}ms...`);
|
||
updatePaperlessUploadStatus(`Searching for documents (attempt ${attempt + 1}/${maxRetries})...`, true);
|
||
setTimeout(tryLinking, retryDelay);
|
||
} else {
|
||
console.log(`[Auto-Link] No documents found after ${maxRetries} attempts`);
|
||
updatePaperlessUploadStatus('Document uploaded but could not auto-link');
|
||
showToast('Document uploaded to Paperless-ngx but could not be automatically linked. You can manually link it later.', 'warning');
|
||
// Hide loading screen after failed auto-link
|
||
setTimeout(() => {
|
||
hidePaperlessUploadLoading();
|
||
}, 2000);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error(`[Auto-Link] Error in attempt ${attempt}:`, error);
|
||
|
||
if (attempt < maxRetries) {
|
||
console.log(`[Auto-Link] Retrying in ${retryDelay}ms...`);
|
||
updatePaperlessUploadStatus(`Error occurred, retrying (${attempt + 1}/${maxRetries})...`, true);
|
||
setTimeout(tryLinking, retryDelay);
|
||
} else {
|
||
updatePaperlessUploadStatus('Upload completed with errors');
|
||
showToast('Document uploaded but auto-linking failed due to errors. You can manually link it later.', 'warning');
|
||
// Hide loading screen after final error
|
||
setTimeout(() => {
|
||
hidePaperlessUploadLoading();
|
||
}, 2000);
|
||
}
|
||
}
|
||
};
|
||
|
||
// Start the linking process
|
||
tryLinking();
|
||
}
|
||
|
||
// Make debug and cleanup functions available globally for console testing
|
||
window.debugPaperlessDocument = debugPaperlessDocument;
|
||
window.cleanupInvalidPaperlessDocuments = cleanupInvalidPaperlessDocuments;
|
||
window.searchAndLinkPaperlessDocument = searchAndLinkPaperlessDocument;
|
||
window.autoLinkRecentDocuments = autoLinkRecentDocuments;
|
||
|
||
// Helper function to manually link a specific document by title
|
||
window.manualLinkDocument = async function(warrantyId, documentType, titleSearchTerm) {
|
||
console.log(`🔗 Manually linking document for warranty ${warrantyId}`);
|
||
console.log(` Document type: ${documentType}`);
|
||
console.log(` Searching for title containing: "${titleSearchTerm}"`);
|
||
|
||
const token = auth.getToken();
|
||
if (!token) {
|
||
console.error('❌ No auth token available');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Search for documents containing the search term
|
||
const response = await fetch(`/api/paperless/search?ordering=-created&query=${encodeURIComponent(titleSearchTerm)}`, {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
console.error(`❌ Search failed: ${response.status}`);
|
||
return;
|
||
}
|
||
|
||
const result = await response.json();
|
||
const docs = result.results || [];
|
||
|
||
console.log(`📄 Found ${docs.length} documents matching "${titleSearchTerm}"`);
|
||
|
||
if (docs.length === 0) {
|
||
console.log('❌ No documents found. The document might still be processing in Paperless-ngx.');
|
||
return;
|
||
}
|
||
|
||
// Show all matching documents
|
||
docs.forEach((doc, index) => {
|
||
console.log(` ${index + 1}. ID: ${doc.id}, Title: "${doc.title}", Created: ${doc.created}`);
|
||
});
|
||
|
||
// Try to link the first matching document
|
||
const docToLink = docs[0];
|
||
console.log(`🔗 Attempting to link document ID ${docToLink.id}: "${docToLink.title}"`);
|
||
|
||
const linkResponse = await fetch('/api/paperless-search-and-link', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
body: JSON.stringify({
|
||
warranty_id: warrantyId,
|
||
document_type: documentType,
|
||
search_title: docToLink.title.replace('Warracker ' + documentType + ' - ', '')
|
||
})
|
||
});
|
||
|
||
const linkResult = await linkResponse.json();
|
||
|
||
if (linkResult.success) {
|
||
console.log(`✅ Successfully linked ${documentType}: ${docToLink.title}`);
|
||
showToast(`Document linked successfully: ${documentType}`, 'success');
|
||
|
||
// Reload warranties to show the updated document links
|
||
setTimeout(async () => {
|
||
console.log('🔄 Reloading warranties to show updated document links...');
|
||
await loadWarranties(true); // Pass isAuthenticated parameter
|
||
|
||
// Force re-render of the warranty cards
|
||
applyFilters();
|
||
|
||
// Also reload secure images to update cloud icons
|
||
await loadSecureImages();
|
||
|
||
console.log('✅ Warranties reloaded and UI updated');
|
||
}, 1000);
|
||
} else {
|
||
console.error(`❌ Failed to link ${documentType}: ${linkResult.message}`);
|
||
}
|
||
|
||
return docToLink;
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error in manual linking:', error);
|
||
}
|
||
};
|
||
|
||
// Helper function to search for documents in Paperless-ngx (debug function)
|
||
window.debugSearchPaperlessDocuments = async function(searchTerm = 'Warracker', limit = 10) {
|
||
console.log(`🔍 Searching for documents containing: "${searchTerm}"`);
|
||
|
||
const token = auth.getToken();
|
||
if (!token) {
|
||
console.error('❌ No auth token available');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/paperless/search?ordering=-created&query=${encodeURIComponent(searchTerm)}`, {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
console.error(`❌ Search failed: ${response.status}`);
|
||
return;
|
||
}
|
||
|
||
const result = await response.json();
|
||
const docs = result.results || [];
|
||
|
||
console.log(`📄 Found ${docs.length} documents:`);
|
||
docs.slice(0, limit).forEach((doc, index) => {
|
||
console.log(` ${index + 1}. ID: ${doc.id}, Title: "${doc.title}", Created: ${doc.created}`);
|
||
});
|
||
|
||
if (docs.length > limit) {
|
||
console.log(` ... and ${docs.length - limit} more documents`);
|
||
}
|
||
|
||
return docs;
|
||
} catch (error) {
|
||
console.error('❌ Error searching documents:', error);
|
||
}
|
||
};
|
||
|
||
// Helper function for users to debug Paperless-ngx configuration
|
||
window.debugPaperlessSetup = async function() {
|
||
console.log('🔍 Debugging Paperless-ngx setup...');
|
||
|
||
const debugInfo = await debugPaperlessConfiguration();
|
||
if (!debugInfo) {
|
||
console.error('❌ Could not get debug information');
|
||
return;
|
||
}
|
||
|
||
console.log('📋 Paperless-ngx Configuration:');
|
||
console.log(` Enabled: ${debugInfo.paperless_enabled}`);
|
||
console.log(` URL: ${debugInfo.paperless_url || 'Not set'}`);
|
||
console.log(` API Token Set: ${debugInfo.paperless_api_token_set}`);
|
||
console.log(` Handler Available: ${debugInfo.paperless_handler_available}`);
|
||
|
||
if (debugInfo.paperless_handler_error) {
|
||
console.error(` Handler Error: ${debugInfo.paperless_handler_error}`);
|
||
}
|
||
|
||
if (debugInfo.test_connection_result) {
|
||
console.log(` Connection Test: ${debugInfo.test_connection_result.success ? '✅ Success' : '❌ Failed'}`);
|
||
console.log(` Message: ${debugInfo.test_connection_result.message || debugInfo.test_connection_result.error}`);
|
||
}
|
||
|
||
// Provide recommendations
|
||
console.log('\n💡 Recommendations:');
|
||
|
||
if (!debugInfo.paperless_enabled || debugInfo.paperless_enabled === 'false') {
|
||
console.log(' - Enable Paperless-ngx integration in Settings');
|
||
}
|
||
|
||
if (!debugInfo.paperless_url) {
|
||
console.log(' - Set Paperless-ngx URL in Settings (e.g., http://paperless:8000)');
|
||
}
|
||
|
||
if (!debugInfo.paperless_api_token_set) {
|
||
console.log(' - Set API token in Settings (generate from Paperless-ngx → Settings → API Tokens)');
|
||
}
|
||
|
||
if (debugInfo.test_connection_result && !debugInfo.test_connection_result.success) {
|
||
console.log(' - Check if Paperless-ngx is running and accessible');
|
||
console.log(' - Verify URL and API token are correct');
|
||
console.log(' - Check network connectivity between Warracker and Paperless-ngx');
|
||
}
|
||
|
||
return debugInfo;
|
||
};
|
||
|
||
// ===== PAPERLESS DOCUMENT BROWSER FUNCTIONALITY =====
|
||
|
||
// Global variables for paperless browser
|
||
let currentPaperlessDocuments = [];
|
||
let selectedPaperlessDocument = null;
|
||
let currentPaperlessPage = 1;
|
||
let totalPaperlessPages = 1;
|
||
let currentDocumentType = '';
|
||
let paperlessSearchQuery = '';
|
||
|
||
/**
|
||
* Open the Paperless document browser modal
|
||
* @param {string} documentType - Type of document being selected (invoice, manual, product_photo, other_document)
|
||
*/
|
||
function openPaperlessBrowser(documentType) {
|
||
currentDocumentType = documentType;
|
||
selectedPaperlessDocument = null;
|
||
|
||
// Reset pagination state
|
||
currentPaperlessPage = 1;
|
||
totalPaperlessPages = 1;
|
||
paperlessSearchQuery = '';
|
||
|
||
// Show the modal
|
||
const modal = document.getElementById('paperlessBrowserModal');
|
||
modal.classList.add('active');
|
||
|
||
// Reset search and filters
|
||
document.getElementById('paperlessSearchInput').value = '';
|
||
document.getElementById('paperlessTypeFilter').value = '';
|
||
document.getElementById('paperlessTagFilter').value = '';
|
||
|
||
// Load documents
|
||
loadAllPaperlessDocuments();
|
||
|
||
// Load tags for filter
|
||
loadPaperlessTags();
|
||
|
||
// Hide select button initially
|
||
const selectBtn = document.getElementById('selectPaperlessDocBtn');
|
||
if (selectBtn) {
|
||
selectBtn.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Load all Paperless documents
|
||
*/
|
||
async function loadAllPaperlessDocuments() {
|
||
try {
|
||
showPaperlessLoading();
|
||
|
||
const params = new URLSearchParams();
|
||
const offset = (currentPaperlessPage - 1) * 25;
|
||
params.append('limit', '25');
|
||
params.append('offset', offset.toString());
|
||
|
||
const response = await fetch(`/api/paperless/search?${params.toString()}`, {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
currentPaperlessDocuments = data.results || [];
|
||
totalPaperlessPages = Math.ceil(data.count / 25) || 1;
|
||
|
||
renderPaperlessDocuments();
|
||
updatePaperlessPagination();
|
||
|
||
} catch (error) {
|
||
console.error('Error loading Paperless documents:', error);
|
||
showPaperlessError('Failed to load documents from Paperless-ngx');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Search Paperless documents
|
||
*/
|
||
async function searchPaperlessDocuments() {
|
||
const searchInput = document.getElementById('paperlessSearchInput');
|
||
const typeFilter = document.getElementById('paperlessTypeFilter');
|
||
const tagFilter = document.getElementById('paperlessTagFilter');
|
||
|
||
paperlessSearchQuery = searchInput.value.trim();
|
||
|
||
try {
|
||
showPaperlessLoading();
|
||
|
||
const params = new URLSearchParams();
|
||
if (paperlessSearchQuery) {
|
||
params.append('query', paperlessSearchQuery);
|
||
}
|
||
if (typeFilter.value) {
|
||
params.append('document_type', typeFilter.value);
|
||
}
|
||
if (tagFilter.value) {
|
||
params.append('tags__id__in', tagFilter.value);
|
||
}
|
||
|
||
// Add pagination
|
||
const offset = (currentPaperlessPage - 1) * 25;
|
||
params.append('limit', '25');
|
||
params.append('offset', offset.toString());
|
||
|
||
const response = await fetch(`/api/paperless/search?${params.toString()}`, {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
currentPaperlessDocuments = data.results || [];
|
||
totalPaperlessPages = Math.ceil(data.count / 25) || 1;
|
||
|
||
renderPaperlessDocuments();
|
||
updatePaperlessPagination();
|
||
|
||
} catch (error) {
|
||
console.error('Error searching Paperless documents:', error);
|
||
showPaperlessError('Failed to search documents');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Load Paperless tags for filter dropdown
|
||
*/
|
||
async function loadPaperlessTags() {
|
||
try {
|
||
const response = await fetch('/api/paperless/tags', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
const tagFilter = document.getElementById('paperlessTagFilter');
|
||
|
||
// Clear existing options except the first one
|
||
tagFilter.innerHTML = '<option value="">All Tags</option>';
|
||
|
||
// Add tag options
|
||
if (data.results) {
|
||
data.results.forEach(tag => {
|
||
const option = document.createElement('option');
|
||
option.value = tag.id;
|
||
option.textContent = tag.name;
|
||
tagFilter.appendChild(option);
|
||
});
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading Paperless tags:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Render the list of Paperless documents
|
||
*/
|
||
function renderPaperlessDocuments() {
|
||
const container = document.getElementById('paperlessDocumentsList');
|
||
|
||
if (currentPaperlessDocuments.length === 0) {
|
||
container.innerHTML = `
|
||
<div class="no-documents-message">
|
||
<i class="fas fa-file-alt"></i>
|
||
<h4>No documents found</h4>
|
||
<p>Try adjusting your search terms or filters.</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
const documentsHtml = currentPaperlessDocuments.map(doc => {
|
||
const createdDate = new Date(doc.created).toLocaleDateString();
|
||
const fileType = doc.mime_type || 'Unknown';
|
||
const tags = doc.tags || [];
|
||
|
||
return `
|
||
<div class="paperless-document-item" data-id="${doc.id}" onclick="selectPaperlessDocument(${doc.id})">
|
||
<div class="document-title">${escapeHtml(doc.title)}</div>
|
||
<div class="document-meta">
|
||
<span><i class="fas fa-calendar"></i> ${createdDate}</span>
|
||
<span><i class="fas fa-file"></i> ${fileType}</span>
|
||
${doc.correspondent ? `<span><i class="fas fa-user"></i> ${escapeHtml(doc.correspondent)}</span>` : ''}
|
||
</div>
|
||
${tags.length > 0 ? `
|
||
<div class="document-tags">
|
||
${tags.map(tag => `<span class="document-tag">${escapeHtml(tag)}</span>`).join('')}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
container.innerHTML = documentsHtml;
|
||
}
|
||
|
||
/**
|
||
* Select a Paperless document
|
||
* @param {number} documentId - ID of the document to select
|
||
*/
|
||
function selectPaperlessDocument(documentId) {
|
||
// Remove previous selection
|
||
document.querySelectorAll('.paperless-document-item').forEach(item => {
|
||
item.classList.remove('selected');
|
||
});
|
||
|
||
// Add selection to clicked item
|
||
const selectedItem = document.querySelector(`[data-id="${documentId}"]`);
|
||
if (selectedItem) {
|
||
selectedItem.classList.add('selected');
|
||
selectedPaperlessDocument = currentPaperlessDocuments.find(doc => doc.id === documentId);
|
||
|
||
// Show select button
|
||
const selectBtn = document.getElementById('selectPaperlessDocBtn');
|
||
selectBtn.style.display = 'inline-block';
|
||
selectBtn.onclick = () => confirmPaperlessSelection();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Confirm the selection of a Paperless document
|
||
*/
|
||
function confirmPaperlessSelection() {
|
||
if (!selectedPaperlessDocument) return;
|
||
|
||
// Update the UI to show the selected document
|
||
updatePaperlessSelectionUI();
|
||
|
||
// Close the modal
|
||
closePaperlessBrowser();
|
||
}
|
||
|
||
/**
|
||
* Update the UI to show the selected Paperless document
|
||
*/
|
||
function updatePaperlessSelectionUI() {
|
||
if (!selectedPaperlessDocument || !currentDocumentType) return;
|
||
|
||
const docName = selectedPaperlessDocument.title;
|
||
const docId = selectedPaperlessDocument.id;
|
||
|
||
// Map document types to their UI elements (only for invoice and manual)
|
||
const typeMapping = {
|
||
'invoice': {
|
||
selectedDiv: 'selectedInvoiceFromPaperless',
|
||
hiddenInput: 'selectedPaperlessInvoice'
|
||
},
|
||
'manual': {
|
||
selectedDiv: 'selectedManualFromPaperless',
|
||
hiddenInput: 'selectedPaperlessManual'
|
||
},
|
||
// Edit modal versions
|
||
'edit_invoice': {
|
||
selectedDiv: 'selectedEditInvoiceFromPaperless',
|
||
hiddenInput: 'selectedEditPaperlessInvoice'
|
||
},
|
||
'edit_manual': {
|
||
selectedDiv: 'selectedEditManualFromPaperless',
|
||
hiddenInput: 'selectedEditPaperlessManual'
|
||
}
|
||
};
|
||
|
||
const mapping = typeMapping[currentDocumentType];
|
||
if (!mapping) return;
|
||
|
||
// Show the selected document
|
||
const selectedDiv = document.getElementById(mapping.selectedDiv);
|
||
if (selectedDiv) {
|
||
selectedDiv.style.display = 'flex';
|
||
selectedDiv.querySelector('.selected-doc-name').textContent = docName;
|
||
}
|
||
|
||
// Create or update hidden input to store the document ID
|
||
let hiddenInput = document.getElementById(mapping.hiddenInput);
|
||
if (!hiddenInput) {
|
||
hiddenInput = document.createElement('input');
|
||
hiddenInput.type = 'hidden';
|
||
hiddenInput.id = mapping.hiddenInput;
|
||
hiddenInput.name = mapping.hiddenInput;
|
||
document.body.appendChild(hiddenInput);
|
||
}
|
||
hiddenInput.value = docId;
|
||
}
|
||
|
||
/**
|
||
* Clear the Paperless document selection
|
||
* @param {string} documentType - Type of document to clear
|
||
*/
|
||
function clearPaperlessSelection(documentType) {
|
||
const typeMapping = {
|
||
'invoice': {
|
||
selectedDiv: 'selectedInvoiceFromPaperless',
|
||
hiddenInput: 'selectedPaperlessInvoice'
|
||
},
|
||
'manual': {
|
||
selectedDiv: 'selectedManualFromPaperless',
|
||
hiddenInput: 'selectedPaperlessManual'
|
||
},
|
||
// Edit modal versions
|
||
'edit_invoice': {
|
||
selectedDiv: 'selectedEditInvoiceFromPaperless',
|
||
hiddenInput: 'selectedEditPaperlessInvoice'
|
||
},
|
||
'edit_manual': {
|
||
selectedDiv: 'selectedEditManualFromPaperless',
|
||
hiddenInput: 'selectedEditPaperlessManual'
|
||
}
|
||
};
|
||
|
||
const mapping = typeMapping[documentType];
|
||
if (!mapping) return;
|
||
|
||
// Hide the selected document display
|
||
const selectedDiv = document.getElementById(mapping.selectedDiv);
|
||
if (selectedDiv) {
|
||
selectedDiv.style.display = 'none';
|
||
}
|
||
|
||
// Clear the hidden input
|
||
const hiddenInput = document.getElementById(mapping.hiddenInput);
|
||
if (hiddenInput) {
|
||
hiddenInput.value = '';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Close the Paperless browser modal
|
||
*/
|
||
function closePaperlessBrowser() {
|
||
const modal = document.getElementById('paperlessBrowserModal');
|
||
modal.classList.remove('active');
|
||
|
||
// Reset state
|
||
selectedPaperlessDocument = null;
|
||
currentDocumentType = '';
|
||
|
||
// Hide select button
|
||
const selectBtn = document.getElementById('selectPaperlessDocBtn');
|
||
selectBtn.style.display = 'none';
|
||
}
|
||
|
||
/**
|
||
* Change page in Paperless document browser
|
||
* @param {number} direction - Direction to change page (-1 for previous, 1 for next)
|
||
*/
|
||
function changePage(direction) {
|
||
const newPage = currentPaperlessPage + direction;
|
||
if (newPage < 1 || newPage > totalPaperlessPages) return;
|
||
|
||
currentPaperlessPage = newPage;
|
||
|
||
// Check if we have any active filters
|
||
const searchInput = document.getElementById('paperlessSearchInput');
|
||
const typeFilter = document.getElementById('paperlessTypeFilter');
|
||
const tagFilter = document.getElementById('paperlessTagFilter');
|
||
|
||
const hasFilters = (searchInput && searchInput.value.trim()) ||
|
||
(typeFilter && typeFilter.value) ||
|
||
(tagFilter && tagFilter.value);
|
||
|
||
if (hasFilters) {
|
||
searchPaperlessDocuments();
|
||
} else {
|
||
loadAllPaperlessDocuments();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Update pagination controls
|
||
*/
|
||
function updatePaperlessPagination() {
|
||
const paginationDiv = document.getElementById('paperlessPagination');
|
||
const prevBtn = document.getElementById('prevPageBtn');
|
||
const nextBtn = document.getElementById('nextPageBtn');
|
||
const pageInfo = document.getElementById('pageInfo');
|
||
|
||
if (totalPaperlessPages <= 1) {
|
||
paginationDiv.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
paginationDiv.style.display = 'flex';
|
||
prevBtn.disabled = currentPaperlessPage <= 1;
|
||
nextBtn.disabled = currentPaperlessPage >= totalPaperlessPages;
|
||
pageInfo.textContent = `Page ${currentPaperlessPage} of ${totalPaperlessPages}`;
|
||
}
|
||
|
||
/**
|
||
* Show loading state in Paperless browser
|
||
*/
|
||
function showPaperlessLoading() {
|
||
const container = document.getElementById('paperlessDocumentsList');
|
||
container.innerHTML = `
|
||
<div class="loading-message" style="text-align: center; padding: 40px;">
|
||
<i class="fas fa-spinner fa-spin"></i> Loading documents...
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* Show error message in Paperless browser
|
||
* @param {string} message - Error message to display
|
||
*/
|
||
function showPaperlessError(message) {
|
||
const container = document.getElementById('paperlessDocumentsList');
|
||
container.innerHTML = `
|
||
<div class="no-documents-message">
|
||
<i class="fas fa-exclamation-triangle"></i>
|
||
<h4>Error</h4>
|
||
<p>${escapeHtml(message)}</p>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* Show/hide Paperless browse sections based on Paperless-ngx availability
|
||
*/
|
||
function togglePaperlessBrowseSections() {
|
||
const paperlessEnabled = window.paperlessNgxEnabled || false;
|
||
console.log('[togglePaperlessBrowseSections] Paperless enabled:', paperlessEnabled);
|
||
|
||
// List of paperless browse section IDs (only for invoice and manual)
|
||
const browseSectionIds = [
|
||
'invoicePaperlessBrowse',
|
||
'manualPaperlessBrowse',
|
||
'editInvoicePaperlessBrowse',
|
||
'editManualPaperlessBrowse'
|
||
];
|
||
|
||
let foundSections = 0;
|
||
browseSectionIds.forEach(id => {
|
||
const section = document.getElementById(id);
|
||
if (section) {
|
||
foundSections++;
|
||
section.style.display = paperlessEnabled ? 'block' : 'none';
|
||
console.log(`[togglePaperlessBrowseSections] ${id}: ${paperlessEnabled ? 'shown' : 'hidden'}`);
|
||
} else {
|
||
console.warn(`[togglePaperlessBrowseSections] Section not found: ${id}`);
|
||
}
|
||
});
|
||
|
||
console.log(`[togglePaperlessBrowseSections] Found ${foundSections} of ${browseSectionIds.length} sections`);
|
||
}
|
||
|
||
// Initialize Paperless browser functionality when DOM is loaded
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// Add event listeners for modal close buttons
|
||
const paperlessModal = document.getElementById('paperlessBrowserModal');
|
||
if (paperlessModal) {
|
||
// Close on backdrop click
|
||
paperlessModal.addEventListener('click', function(e) {
|
||
if (e.target === paperlessModal) {
|
||
closePaperlessBrowser();
|
||
}
|
||
});
|
||
|
||
// Close on close button click
|
||
const closeBtn = paperlessModal.querySelector('.close-btn');
|
||
if (closeBtn) {
|
||
closeBtn.addEventListener('click', closePaperlessBrowser);
|
||
}
|
||
|
||
// Close on cancel button click - but only the Cancel button, not all secondary buttons
|
||
const cancelBtn = paperlessModal.querySelector('.modal-footer .btn-secondary');
|
||
if (cancelBtn) {
|
||
cancelBtn.addEventListener('click', closePaperlessBrowser);
|
||
}
|
||
}
|
||
|
||
// Add event listeners for search and filters
|
||
const searchInput = document.getElementById('paperlessSearchInput');
|
||
if (searchInput) {
|
||
// Search on Enter key
|
||
searchInput.addEventListener('keypress', function(e) {
|
||
if (e.key === 'Enter') {
|
||
currentPaperlessPage = 1; // Reset to first page
|
||
searchPaperlessDocuments();
|
||
}
|
||
});
|
||
|
||
// Search on input change (with debounce)
|
||
let searchTimeout;
|
||
searchInput.addEventListener('input', function() {
|
||
clearTimeout(searchTimeout);
|
||
searchTimeout = setTimeout(() => {
|
||
currentPaperlessPage = 1; // Reset to first page
|
||
searchPaperlessDocuments();
|
||
}, 500);
|
||
});
|
||
}
|
||
|
||
// Add event listeners for filter dropdowns
|
||
const typeFilter = document.getElementById('paperlessTypeFilter');
|
||
if (typeFilter) {
|
||
typeFilter.addEventListener('change', function() {
|
||
currentPaperlessPage = 1; // Reset to first page
|
||
searchPaperlessDocuments();
|
||
});
|
||
}
|
||
|
||
const tagFilter = document.getElementById('paperlessTagFilter');
|
||
if (tagFilter) {
|
||
tagFilter.addEventListener('change', function() {
|
||
currentPaperlessPage = 1; // Reset to first page
|
||
searchPaperlessDocuments();
|
||
});
|
||
}
|
||
|
||
// Add event listener for search button
|
||
const searchBtn = document.getElementById('paperlessSearchBtn');
|
||
if (searchBtn) {
|
||
searchBtn.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
currentPaperlessPage = 1; // Reset to first page
|
||
searchPaperlessDocuments();
|
||
});
|
||
}
|
||
|
||
// Add event listener for "Show All" button
|
||
const showAllBtn = document.getElementById('paperlessShowAllBtn');
|
||
if (showAllBtn) {
|
||
showAllBtn.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
// Clear all filters
|
||
if (searchInput) searchInput.value = '';
|
||
if (typeFilter) typeFilter.value = '';
|
||
if (tagFilter) tagFilter.value = '';
|
||
|
||
// Reset page and load all documents
|
||
currentPaperlessPage = 1;
|
||
paperlessSearchQuery = '';
|
||
loadAllPaperlessDocuments();
|
||
});
|
||
}
|
||
|
||
// Toggle browse sections will be handled by initPaperlessNgxIntegration()
|
||
// togglePaperlessBrowseSections();
|
||
});
|
||
|
||
// Make functions available globally
|
||
window.openPaperlessBrowser = openPaperlessBrowser;
|
||
window.loadAllPaperlessDocuments = loadAllPaperlessDocuments;
|
||
window.selectPaperlessDocument = selectPaperlessDocument;
|
||
window.clearPaperlessSelection = clearPaperlessSelection;
|
||
window.changePage = changePage;
|
||
|
||
// ===== END PAPERLESS BROWSER FUNCTIONALITY =====
|