Files
Warracker/frontend/script.js
sassanix 60fa3402c9 Tags for warranties
Refer to changelogs
2025-04-01 21:24:12 -03:00

2666 lines
92 KiB
JavaScript

// Global variables
let warranties = [];
let currentTabIndex = 0;
let tabContents;
let editMode = false;
let currentWarrantyId = null;
let currentFilters = {
status: 'all',
tag: 'all',
search: '',
sortBy: 'expiration'
};
// Tag related variables
let allTags = [];
let selectedTags = []; // Will hold objects with id, name, color
// Global variable for edit mode tags
let editSelectedTags = [];
// DOM Elements
const warrantyForm = document.getElementById('warrantyForm');
const settingsBtn = document.getElementById('settingsBtn');
const settingsMenu = document.getElementById('settingsMenu');
const darkModeToggle = document.getElementById('darkModeToggle');
const warrantiesList = document.getElementById('warrantiesList');
const refreshBtn = document.getElementById('refreshBtn');
const searchInput = document.getElementById('searchWarranties');
const clearSearchBtn = document.getElementById('clearSearch');
const statusFilter = document.getElementById('statusFilter');
const sortBySelect = document.getElementById('sortBy');
const exportBtn = document.getElementById('exportBtn');
const gridViewBtn = document.getElementById('gridViewBtn');
const listViewBtn = document.getElementById('listViewBtn');
const tableViewBtn = document.getElementById('tableViewBtn');
const tableViewHeader = document.querySelector('.table-view-header');
const fileInput = document.getElementById('invoice');
const fileName = document.getElementById('fileName');
const manualInput = document.getElementById('manual');
const manualFileName = document.getElementById('manualFileName');
const editModal = document.getElementById('editModal');
const deleteModal = document.getElementById('deleteModal');
const editWarrantyForm = document.getElementById('editWarrantyForm');
const saveWarrantyBtn = document.getElementById('saveWarrantyBtn');
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
const loadingContainer = document.getElementById('loadingContainer');
const toastContainer = document.getElementById('toastContainer');
// Tag DOM Elements
const selectedTagsContainer = document.getElementById('selectedTags');
const tagSearch = document.getElementById('tagSearch');
const tagsList = document.getElementById('tagsList');
const manageTagsBtn = document.getElementById('manageTagsBtn');
const tagManagementModal = document.getElementById('tagManagementModal');
const newTagForm = document.getElementById('newTagForm');
const existingTagsContainer = document.getElementById('existingTags');
/**
* Get current user type (admin or user)
* @returns {string} 'admin' or 'user'
*/
function getUserType() {
try {
const userInfo = JSON.parse(localStorage.getItem('user_info') || '{}');
return userInfo.is_admin === true ? 'admin' : 'user';
} catch (e) {
console.error('Error determining user type:', e);
return 'user'; // Default to user if we can't determine
}
}
/**
* Get the appropriate localStorage key prefix based on user type
* @returns {string} The prefix to use for localStorage keys
*/
function getPreferenceKeyPrefix() {
return getUserType() === 'admin' ? 'admin_' : 'user_';
}
// Theme Management
function setTheme(isDark) {
// Get the appropriate key prefix based on user type
const prefix = getPreferenceKeyPrefix();
console.log(`Setting theme with prefix: ${prefix}`);
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
// Update darkMode settings
localStorage.setItem(`${prefix}darkMode`, isDark);
localStorage.setItem('darkMode', isDark); // Keep for backward compatibility
// Update DOM
if (isDark) {
document.body.classList.add('dark-mode');
} else {
document.body.classList.remove('dark-mode');
}
// Set toggle state
if (darkModeToggle) {
darkModeToggle.checked = isDark;
}
// Also update preferences in localStorage for consistency
try {
let userPrefs = {};
const storedPrefs = localStorage.getItem(`${prefix}preferences`);
if (storedPrefs) {
userPrefs = JSON.parse(storedPrefs);
}
userPrefs.theme = isDark ? 'dark' : 'light';
localStorage.setItem(`${prefix}preferences`, JSON.stringify(userPrefs));
} catch (e) {
console.error(`Error updating theme in ${prefix}preferences:`, e);
}
}
// Initialize theme based on user preference or system preference
function initializeTheme() {
// Get the appropriate key prefix based on user type
const prefix = getPreferenceKeyPrefix();
console.log(`Initializing theme with prefix: ${prefix}`);
// First check user-specific setting
const userDarkMode = localStorage.getItem(`${prefix}darkMode`);
if (userDarkMode !== null) {
console.log(`Found user-specific dark mode setting: ${userDarkMode}`);
setTheme(userDarkMode === 'true');
return;
}
// Then check global setting for backward compatibility
const globalDarkMode = localStorage.getItem('darkMode');
if (globalDarkMode !== null) {
console.log(`Found global dark mode setting: ${globalDarkMode}`);
setTheme(globalDarkMode === 'true');
return;
}
// Check for system preference if no stored preference
const prefersDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
console.log(`No saved preference, using system preference: ${prefersDarkMode}`);
setTheme(prefersDarkMode);
}
// Initialize theme when page loads
initializeTheme();
// Variables
let currentView = 'grid'; // Default view
let expiringSoonDays = 30; // Default value, will be updated from user preferences
// API URL
const API_URL = '/api/warranties';
// Form tab navigation variables
const formTabs = Array.from(document.querySelectorAll('.form-tab'));
const tabContentsArray = document.querySelectorAll('.tab-content');
tabContents = Array.from(tabContentsArray); // Convert NodeList to Array
const nextButton = document.querySelector('.next-tab');
const prevButton = document.querySelector('.prev-tab');
// Initialize form tabs
function initFormTabs() {
console.log('Initializing form tabs...');
// Hide the submit button initially
const submitButton = document.querySelector('button[type="submit"]');
if (submitButton) {
submitButton.style.display = 'none';
}
// Show/hide navigation buttons based on current tab
updateNavigationButtons();
// Remove any existing event listeners before adding new ones
if (nextButton && prevButton) {
const nextTabClone = nextButton.cloneNode(true);
const prevTabClone = prevButton.cloneNode(true);
nextButton.parentNode.replaceChild(nextTabClone, nextButton);
prevButton.parentNode.replaceChild(prevTabClone, prevButton);
// Add event listeners for tab navigation
document.querySelector('.next-tab').addEventListener('click', () => {
console.log('Next button clicked, current tab:', currentTabIndex);
if (validateTab(currentTabIndex)) {
switchToTab(currentTabIndex + 1);
} else {
showValidationErrors(currentTabIndex);
}
});
document.querySelector('.prev-tab').addEventListener('click', () => {
console.log('Previous button clicked, current tab:', currentTabIndex);
switchToTab(currentTabIndex - 1);
});
}
// Add click event for tab headers
formTabs.forEach((tab, index) => {
tab.addEventListener('click', () => {
// Only allow clicking on previous tabs or current tab
if (index <= currentTabIndex) {
switchToTab(index);
}
});
});
}
// Switch to a specific tab
function switchToTab(index) {
console.log(`Switching to tab ${index} from tab ${currentTabIndex}`);
// Ensure index is within bounds
if (index < 0 || index >= formTabs.length) {
console.log(`Invalid tab index: ${index}, not switching`);
return;
}
// Update active tab
formTabs.forEach(tab => tab.classList.remove('active'));
tabContents.forEach(content => content.classList.remove('active'));
formTabs[index].classList.add('active');
tabContents[index].classList.add('active');
// Update current tab index
currentTabIndex = index;
// Update progress indicator
document.querySelector('.form-tabs').setAttribute('data-step', currentTabIndex);
// Update completed tabs
updateCompletedTabs();
// Update navigation buttons
updateNavigationButtons();
// Update summary if on summary tab
if (index === formTabs.length - 1) {
updateSummary();
}
}
// Update navigation buttons based on current tab
function updateNavigationButtons() {
const prevButton = document.querySelector('.prev-tab');
const nextButton = document.querySelector('.next-tab');
const submitButton = document.querySelector('button[type="submit"]');
// Hide/show previous button
prevButton.style.display = currentTabIndex === 0 ? 'none' : 'block';
// Hide/show next button and submit button
if (currentTabIndex === formTabs.length - 1) {
nextButton.style.display = 'none';
submitButton.style.display = 'block';
} else {
nextButton.style.display = 'block';
submitButton.style.display = 'none';
}
}
// Update completed tabs
function updateCompletedTabs() {
formTabs.forEach((tab, index) => {
if (index < currentTabIndex) {
tab.classList.add('completed');
} else {
tab.classList.remove('completed');
}
});
}
// Validate a specific tab
function validateTab(tabIndex) {
const tabContent = tabContents[tabIndex];
const requiredInputs = tabContent.querySelectorAll('input[required]');
// If there are no required inputs in this tab, it's automatically valid
if (requiredInputs.length === 0) {
return true;
}
let isValid = true;
requiredInputs.forEach(input => {
if (!input.value.trim()) {
isValid = false;
input.classList.add('invalid');
} else {
input.classList.remove('invalid');
}
});
return isValid;
}
// Show validation errors for a specific tab
function showValidationErrors(tabIndex) {
const tabContent = tabContents[tabIndex];
const requiredInputs = tabContent.querySelectorAll('input[required]');
requiredInputs.forEach(input => {
if (!input.value.trim()) {
input.classList.add('invalid');
// Add validation message if not already present
let validationMessage = input.nextElementSibling;
if (!validationMessage || !validationMessage.classList.contains('validation-message')) {
validationMessage = document.createElement('div');
validationMessage.className = 'validation-message';
validationMessage.textContent = 'This field is required';
input.parentNode.insertBefore(validationMessage, input.nextSibling);
}
}
});
// Show toast message
showToast('Please fill in all required fields', 'error');
}
// Update summary tab with current form values
function updateSummary() {
// Product information
const summaryProductName = document.getElementById('summary-product-name');
if (summaryProductName) {
summaryProductName.textContent =
document.getElementById('productName')?.value || '-';
}
const summaryProductUrl = document.getElementById('summary-product-url');
if (summaryProductUrl) {
summaryProductUrl.textContent =
document.getElementById('productUrl')?.value || '-';
}
// Serial numbers
const serialNumbers = [];
document.querySelectorAll('input[name="serial_numbers[]"]').forEach(input => {
if (input && input.value && input.value.trim()) {
serialNumbers.push(input.value.trim());
}
});
const serialNumbersContainer = document.getElementById('summary-serial-numbers');
if (serialNumbersContainer) {
if (serialNumbers.length > 0) {
serialNumbersContainer.innerHTML = '<ul>' +
serialNumbers.map(sn => `<li>${sn}</li>`).join('') +
'</ul>';
} else {
serialNumbersContainer.textContent = 'None';
}
}
// Warranty details
const purchaseDate = document.getElementById('purchaseDate')?.value;
const summaryPurchaseDate = document.getElementById('summary-purchase-date');
if (summaryPurchaseDate) {
summaryPurchaseDate.textContent = purchaseDate ?
new Date(purchaseDate).toLocaleDateString() : '-';
}
const warrantyYears = document.getElementById('warrantyYears')?.value;
const summaryWarrantyYears = document.getElementById('summary-warranty-years');
if (summaryWarrantyYears) {
summaryWarrantyYears.textContent = warrantyYears ?
`${warrantyYears} ${warrantyYears > 1 ? 'years' : 'year'}` : '-';
}
// Calculate and display expiration date
const summaryExpirationDate = document.getElementById('summary-expiration-date');
if (summaryExpirationDate && purchaseDate && warrantyYears) {
const expirationDate = new Date(purchaseDate);
expirationDate.setFullYear(expirationDate.getFullYear() + parseInt(warrantyYears));
summaryExpirationDate.textContent = expirationDate.toLocaleDateString();
} else if (summaryExpirationDate) {
summaryExpirationDate.textContent = '-';
}
// Purchase price
const purchasePrice = document.getElementById('purchasePrice')?.value;
const summaryPurchasePrice = document.getElementById('summary-purchase-price');
if (summaryPurchasePrice) {
summaryPurchasePrice.textContent = purchasePrice ?
`$${parseFloat(purchasePrice).toFixed(2)}` : 'Not specified';
}
// Documents
const invoiceFile = document.getElementById('invoice')?.files[0];
const summaryInvoice = document.getElementById('summary-invoice');
if (summaryInvoice) {
summaryInvoice.textContent = invoiceFile ?
invoiceFile.name : 'No file selected';
}
const manualFile = document.getElementById('manual')?.files[0];
const summaryManual = document.getElementById('summary-manual');
if (summaryManual) {
summaryManual.textContent = manualFile ?
manualFile.name : 'No file selected';
}
// Tags
const summaryTags = document.getElementById('summary-tags');
if (summaryTags) {
if (selectedTags && selectedTags.length > 0) {
summaryTags.innerHTML = '';
selectedTags.forEach(tag => {
const tagElement = document.createElement('span');
tagElement.className = 'tag';
tagElement.style.backgroundColor = tag.color;
tagElement.style.color = getContrastColor(tag.color);
tagElement.textContent = tag.name;
summaryTags.appendChild(tagElement);
});
} else {
summaryTags.textContent = 'No tags selected';
}
}
}
// Add input event listeners to remove validation errors when user types
document.addEventListener('input', (e) => {
if (e.target.hasAttribute('required') && e.target.classList.contains('invalid')) {
if (e.target.value.trim()) {
e.target.classList.remove('invalid');
// Remove validation message if exists
const validationMessage = e.target.nextElementSibling;
if (validationMessage && validationMessage.classList.contains('validation-message')) {
validationMessage.remove();
}
}
}
});
// Function to reset the form and initialize serial number inputs
function resetForm() {
// Reset the form
warrantyForm.reset();
// Reset serial numbers container
serialNumbersContainer.innerHTML = '';
// Add the first serial number input
addSerialNumberInput();
// Reset form tabs
currentTabIndex = 0;
switchToTab(0);
// Clear any file input displays
fileName.textContent = '';
manualFileName.textContent = '';
}
async function exportWarranties() {
// Get filtered warranties
let warrantiesToExport = [...warranties];
// Apply current filters
if (currentFilters.search) {
const searchTerm = currentFilters.search.toLowerCase();
warrantiesToExport = warrantiesToExport.filter(warranty => {
// Check if product name contains search term
const productNameMatch = warranty.product_name.toLowerCase().includes(searchTerm);
// Check if any tag name contains search term
const tagMatch = warranty.tags && Array.isArray(warranty.tags) &&
warranty.tags.some(tag => tag.name.toLowerCase().includes(searchTerm));
// Return true if either product name or tag name matches
return productNameMatch || tagMatch;
});
}
if (currentFilters.status !== 'all') {
warrantiesToExport = warrantiesToExport.filter(warranty =>
warranty.status === currentFilters.status
);
}
// Apply tag filter
if (currentFilters.tag !== 'all') {
const tagId = parseInt(currentFilters.tag);
warrantiesToExport = warrantiesToExport.filter(warranty =>
warranty.tags && Array.isArray(warranty.tags) &&
warranty.tags.some(tag => tag.id === tagId)
);
}
// Create CSV content
let csvContent = "data:text/csv;charset=utf-8,";
// Add headers
csvContent += "Product Name,Purchase Date,Warranty Period,Expiration Date,Status,Serial Numbers,Tags\n";
// Add data rows
warrantiesToExport.forEach(warranty => {
// Format serial numbers as comma-separated string
const serialNumbers = Array.isArray(warranty.serial_numbers)
? warranty.serial_numbers.filter(s => s).join(', ')
: '';
// Format tags as comma-separated string
const tags = Array.isArray(warranty.tags)
? warranty.tags.map(tag => tag.name).join(', ')
: '';
// Format row data
const row = [
warranty.product_name || '',
formatDate(new Date(warranty.purchase_date)),
`${warranty.warranty_years || 0} ${warranty.warranty_years === 1 ? 'year' : 'years'}`,
formatDate(new Date(warranty.expiration_date)),
warranty.status || '',
serialNumbers,
tags
];
// Add row to CSV content
csvContent += row.map(field => `"${field.toString().replace(/"/g, '""')}"`).join(',') + '\n';
});
// Create a download link
const encodedUri = encodeURI(csvContent);
const link = document.createElement('a');
link.setAttribute('href', encodedUri);
link.setAttribute('download', `warranties_export_${formatDate(new Date())}.csv`);
document.body.appendChild(link);
// Trigger download
link.click();
// Clean up
document.body.removeChild(link);
// Show success notification
showToast('Warranties exported successfully', 'success');
}
// Switch view of warranties list
function switchView(viewType) {
console.log(`Switching to ${viewType} view...`);
// Update currentView
currentView = viewType;
// Remove active class from all view buttons
gridViewBtn.classList.remove('active');
listViewBtn.classList.remove('active');
tableViewBtn.classList.remove('active');
// Update the view
warrantiesList.className = `warranties-list ${viewType}-view`;
// Hide table header if not in table view
if (tableViewHeader) {
tableViewHeader.style.display = viewType === 'table' ? 'flex' : 'none';
}
// Add active class to the selected view button
if (viewType === 'grid') {
gridViewBtn.classList.add('active');
} else if (viewType === 'list') {
listViewBtn.classList.add('active');
} else if (viewType === 'table') {
tableViewBtn.classList.add('active');
}
// Re-render warranties with the new view
console.log('Applying filters after switching view...');
applyFilters();
// Get prefix for user-specific preferences
const prefix = getPreferenceKeyPrefix();
// Save view preference to localStorage with the appropriate prefix
localStorage.setItem(`${prefix}warrantyView`, viewType);
localStorage.setItem('warrantyView', viewType); // Keep global setting for backward compatibility
}
// Load saved view preference
function loadViewPreference() {
// Get prefix for user-specific preferences
const prefix = getPreferenceKeyPrefix();
// First check for user-specific warrantyView
const userSavedView = localStorage.getItem(`${prefix}warrantyView`);
if (userSavedView) {
console.log(`Found user-specific view preference: ${userSavedView}`);
switchView(userSavedView);
return;
}
// If not found, check for user-specific defaultView
const userDefaultView = localStorage.getItem(`${prefix}defaultView`);
if (userDefaultView) {
console.log(`Found user-specific default view: ${userDefaultView}`);
switchView(userDefaultView);
return;
}
// If no user-specific preferences found, check global preferences for backward compatibility
const globalSavedView = localStorage.getItem('warrantyView');
if (globalSavedView) {
console.log(`Found global view preference: ${globalSavedView}`);
switchView(globalSavedView);
return;
}
const globalDefaultView = localStorage.getItem('defaultView');
if (globalDefaultView) {
console.log(`Found global default view: ${globalDefaultView}`);
switchView(globalDefaultView);
return;
}
// Default to grid view if no preferences found
console.log('No view preference found, defaulting to grid view');
switchView('grid');
}
// Dark mode toggle
darkModeToggle.addEventListener('change', (e) => {
setTheme(e.target.checked);
});
const serialNumbersContainer = document.getElementById('serialNumbersContainer');
// Add event listener for adding new serial number inputs
serialNumbersContainer.addEventListener('click', (e) => {
if (e.target.closest('.add-serial-number')) {
addSerialNumberInput();
}
});
// Add a serial number input field
function addSerialNumberInput(container = serialNumbersContainer) {
if (!container) return;
// Create a new input group
const inputGroup = document.createElement('div');
inputGroup.className = 'serial-number-input';
// Create an input element
const input = document.createElement('input');
input.type = 'text';
input.className = 'form-control';
input.name = 'serial_numbers[]';
input.placeholder = 'Enter serial number';
// Check if this is the first serial number input
const isFirstInput = container.querySelectorAll('.serial-number-input').length === 0;
// Append input to the input group
inputGroup.appendChild(input);
// Only add remove button if this is not the first input
if (!isFirstInput) {
// Create a remove button
const removeButton = document.createElement('button');
removeButton.type = 'button';
removeButton.className = 'btn btn-sm btn-danger remove-serial';
removeButton.innerHTML = '<i class="fas fa-times"></i>';
// Add event listener to remove button
removeButton.addEventListener('click', function() {
container.removeChild(inputGroup);
});
// Append remove button to the input group
inputGroup.appendChild(removeButton);
}
// Insert the new input group before the add button
const addButton = container.querySelector('.add-serial');
if (addButton) {
container.insertBefore(inputGroup, addButton);
} else {
container.appendChild(inputGroup);
// Create and append an add button if it doesn't exist
const addButton = document.createElement('button');
addButton.type = 'button';
addButton.className = 'btn btn-sm btn-secondary add-serial';
addButton.innerHTML = '<i class="fas fa-plus"></i> Add Serial Number';
addButton.addEventListener('click', function() {
addSerialNumberInput(container);
});
container.appendChild(addButton);
}
}
// Functions
function showLoading() {
loadingContainer.classList.add('active');
}
function hideLoading() {
loadingContainer.classList.remove('active');
}
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.innerHTML = `
${message}
<button class="toast-close">&times;</button>
`;
// Add close event
toast.querySelector('.toast-close').addEventListener('click', () => {
toast.remove();
});
toastContainer.appendChild(toast);
// Auto remove after 3 seconds
setTimeout(() => {
if (toast.parentElement) {
toast.remove();
}
}, 3000);
}
// Update file name display when a file is selected
function updateFileName(event, inputId = 'invoice', outputId = 'fileName') {
const input = event ? event.target : document.getElementById(inputId);
const output = document.getElementById(outputId);
if (!input || !output) return;
if (input.files && input.files[0]) {
output.textContent = input.files[0].name;
} else {
output.textContent = '';
}
}
// Helper function to process warranty data
function processWarrantyData(warranty) {
console.log('Processing warranty data:', warranty);
// Create a copy of the warranty object to avoid modifying the original
const processedWarranty = { ...warranty };
// Ensure product_name exists
if (!processedWarranty.product_name) {
processedWarranty.product_name = 'Unnamed Product';
}
const today = new Date();
// Handle purchase date
let purchaseDate = null;
if (processedWarranty.purchase_date) {
purchaseDate = new Date(processedWarranty.purchase_date);
// Check if date is valid
if (isNaN(purchaseDate.getTime())) {
purchaseDate = null;
}
}
processedWarranty.purchaseDate = purchaseDate;
// Handle expiration date
let expirationDate = null;
if (processedWarranty.expiration_date) {
expirationDate = new Date(processedWarranty.expiration_date);
// Check if date is valid
if (isNaN(expirationDate.getTime())) {
expirationDate = null;
}
}
processedWarranty.expirationDate = expirationDate;
// Calculate days remaining only if expiration date is valid
let daysRemaining = null;
if (expirationDate && !isNaN(expirationDate.getTime())) {
daysRemaining = Math.floor((expirationDate - today) / (1000 * 60 * 60 * 24));
}
let statusClass = 'active';
let statusText = 'Active';
if (daysRemaining === null) {
statusClass = 'unknown';
statusText = 'Unknown status';
} else if (daysRemaining < 0) {
statusClass = 'expired';
statusText = 'Expired';
} else if (daysRemaining < expiringSoonDays) {
console.log(`Using expiringSoonDays: ${expiringSoonDays} for warranty: ${processedWarranty.product_name}`);
statusClass = 'expiring';
statusText = `Expiring Soon (${daysRemaining} days)`;
} else {
statusText = `${daysRemaining} days remaining`;
}
// Add status to warranty object
processedWarranty.status = statusClass;
processedWarranty.daysRemaining = daysRemaining;
processedWarranty.statusText = statusText;
console.log('Processed warranty data result:', processedWarranty);
return processedWarranty;
}
// Function to process all warranties in the array
function processAllWarranties() {
console.log('Processing all warranties in array...');
if (warranties && warranties.length > 0) {
warranties = warranties.map(warranty => processWarrantyData(warranty));
}
console.log('Processed warranties:', warranties);
}
async function loadWarranties() {
try {
console.log('Loading warranties...');
showLoading();
// Get expiring soon days from user preferences if available
try {
const prefsResponse = await fetch('/api/auth/preferences', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
}
});
if (prefsResponse.ok) {
const data = await prefsResponse.json();
if (data && data.expiring_soon_days) {
const oldValue = expiringSoonDays;
expiringSoonDays = data.expiring_soon_days;
console.log('Updated expiring soon days from preferences:', expiringSoonDays);
// If we already have warranties loaded and the value changed, reprocess them
if (warranties && warranties.length > 0 && oldValue !== expiringSoonDays) {
console.log('Reprocessing warranties with new expiringSoonDays value');
warranties = warranties.map(warranty => processWarrantyData(warranty));
renderWarrantiesTable(warranties);
}
}
}
} catch (error) {
console.error('Error loading preferences:', error);
// Continue with default value
}
// Use the full URL to avoid path issues
const apiUrl = window.location.origin + '/api/warranties';
// Check if auth is available and user is authenticated
if (!window.auth || !window.auth.isAuthenticated()) {
console.log('User not authenticated, showing empty state');
renderEmptyState('Please log in to view your warranties.');
hideLoading();
return;
}
// Get the auth token
const token = window.auth.getToken();
if (!token) {
console.log('No auth token available');
renderEmptyState('Authentication error. Please log in again.');
hideLoading();
return;
}
// Create request with auth header
const options = {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
};
console.log('Fetching warranties with auth token');
const response = await fetch(apiUrl, options);
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: `HTTP error ${response.status}` }));
console.error('Error loading warranties:', response.status, errorData);
throw new Error(`Error loading warranties: ${errorData.message || response.status}`);
}
const data = await response.json();
console.log('Received warranties from server:', data);
// Process each warranty to calculate status and days remaining
warranties = data.map(warranty => {
return processWarrantyData(warranty);
});
console.log('Processed warranties:', warranties);
if (warranties.length === 0) {
console.log('No warranties found, showing empty state');
renderEmptyState('No warranties found. Add your first warranty using the form.');
} else {
console.log('Applying filters to display warranties');
// Populate tag filter dropdown with tags from warranties
populateTagFilter();
applyFilters();
}
} catch (error) {
console.error('Error loading warranties:', error);
renderEmptyState('Error loading warranties. Please try again later.');
} finally {
hideLoading();
}
}
function renderEmptyState(message = 'No warranties yet. Add your first warranty to get started.') {
warrantiesList.innerHTML = `
<div class="empty-state">
<i class="fas fa-box-open"></i>
<h3>No warranties found</h3>
<p>${message}</p>
</div>
`;
}
function formatDate(date) {
if (!date) return 'N/A';
// If date is already a Date object, use it directly
const dateObj = date instanceof Date ? date : new Date(date);
// Check if date is valid
if (isNaN(dateObj.getTime())) {
return 'N/A';
}
return dateObj.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
async function renderWarranties(warrantiesToRender) {
console.log('renderWarranties called with:', warrantiesToRender);
if (!warrantiesToRender || warrantiesToRender.length === 0) {
renderEmptyState();
return;
}
const today = new Date();
warrantiesList.innerHTML = '';
// Apply sorting based on current sort selection
const sortedWarranties = [...warrantiesToRender].sort((a, b) => {
switch (currentFilters.sortBy) {
case 'name':
return a.product_name.localeCompare(b.product_name);
case 'purchase':
return new Date(b.purchase_date || 0) - new Date(a.purchase_date || 0);
case 'expiration':
default:
const dateA = new Date(a.expiration_date || 0);
const dateB = new Date(b.expiration_date || 0);
const isExpiredA = dateA < today;
const isExpiredB = dateB < today;
if (isExpiredA && !isExpiredB) return 1;
if (!isExpiredA && isExpiredB) return -1;
// Both active or both expired, sort by date
return dateA - dateB;
}
});
console.log('Sorted warranties:', sortedWarranties);
// Update the container class based on current view
warrantiesList.className = `warranties-list ${currentView}-view`;
// Show/hide table header for table view
if (tableViewHeader) {
tableViewHeader.classList.toggle('visible', currentView === 'table');
}
// Update view buttons to reflect current view
if (gridViewBtn && listViewBtn && tableViewBtn) {
gridViewBtn.classList.toggle('active', currentView === 'grid');
listViewBtn.classList.toggle('active', currentView === 'list');
tableViewBtn.classList.toggle('active', currentView === 'table');
}
sortedWarranties.forEach(warranty => {
// Use the pre-processed dates from the warranty object
const purchaseDate = warranty.purchaseDate;
const expirationDate = warranty.expirationDate;
// Use the pre-calculated status and days remaining from the warranty object
const statusClass = warranty.status || 'unknown';
const statusText = warranty.statusText || 'Unknown status';
// Debug file paths
console.log(`Warranty ID ${warranty.id} - Product: ${warranty.product_name}`);
console.log(`- Invoice path: ${warranty.invoice_path}`);
console.log(`- Manual path: ${warranty.manual_path}`);
// Make sure serial numbers array exists and is valid
const validSerialNumbers = Array.isArray(warranty.serial_numbers)
? warranty.serial_numbers.filter(sn => sn && typeof sn === 'string' && sn.trim() !== '')
: [];
// Prepare tags HTML
const tagsHtml = warranty.tags && warranty.tags.length > 0
? `<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>`
: '';
const cardElement = document.createElement('div');
cardElement.className = `warranty-card ${statusClass === 'expired' ? 'expired' : statusClass === 'expiring' ? 'expiring-soon' : 'active'}`;
if (currentView === 'grid') {
// Grid view HTML structure
cardElement.innerHTML = `
<div class="product-name-header">
<h3 class="warranty-title">${warranty.product_name || 'Unnamed Product'}</h3>
<div class="warranty-actions">
<button class="action-btn edit-btn" title="Edit" data-id="${warranty.id}">
<i class="fas fa-edit"></i>
</button>
<button class="action-btn delete-btn" title="Delete" data-id="${warranty.id}">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="warranty-content">
<div class="warranty-info">
<div>Purchased: <span>${formatDate(purchaseDate)}</span></div>
<div>Warranty: <span>${warranty.warranty_years !== undefined ? `${warranty.warranty_years} ${warranty.warranty_years === 1 ? 'year' : 'years'}` : 'N/A'}</span></div>
<div>Expires: <span>${formatDate(expirationDate)}</span></div>
${warranty.purchase_price ? `<div>Price: <span>$${parseFloat(warranty.purchase_price).toFixed(2)}</span></div>` : ''}
${validSerialNumbers.length > 0 ? `
<div class="serial-numbers">
<strong>Serial Numbers:</strong>
<ul>
${validSerialNumbers.map(sn => `<li>${sn}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
</div>
<div class="warranty-status-row status-${statusClass}">
<span>${statusText}</span>
</div>
<div class="document-links-row">
${warranty.product_url ? `
<a href="${warranty.product_url}" class="product-link" target="_blank">
<i class="fas fa-globe"></i> Product Website
</a>
` : ''}
${warranty.invoice_path && warranty.invoice_path !== 'null' ? `
<a href="#" onclick="openSecureFile('${warranty.invoice_path}'); return false;" class="invoice-link">
<i class="fas fa-file-invoice"></i> Invoice
</a>` : ''}
${warranty.manual_path && warranty.manual_path !== 'null' ? `
<a href="#" onclick="openSecureFile('${warranty.manual_path}'); return false;" class="manual-link">
<i class="fas fa-book"></i> Manual
</a>` : ''}
</div>
${tagsHtml}
`;
} else if (currentView === 'list') {
// List view HTML structure
cardElement.innerHTML = `
<div class="product-name-header">
<h3 class="warranty-title">${warranty.product_name || 'Unnamed Product'}</h3>
<div class="warranty-actions">
<button class="action-btn edit-btn" title="Edit" data-id="${warranty.id}">
<i class="fas fa-edit"></i>
</button>
<button class="action-btn delete-btn" title="Delete" data-id="${warranty.id}">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="warranty-content">
<div class="warranty-info">
<div>Purchased: <span>${formatDate(purchaseDate)}</span></div>
<div>Warranty: <span>${warranty.warranty_years !== undefined ? `${warranty.warranty_years} ${warranty.warranty_years === 1 ? 'year' : 'years'}` : 'N/A'}</span></div>
<div>Expires: <span>${formatDate(expirationDate)}</span></div>
${warranty.purchase_price ? `<div>Price: <span>$${parseFloat(warranty.purchase_price).toFixed(2)}</span></div>` : ''}
${validSerialNumbers.length > 0 ? `
<div class="serial-numbers">
<strong>Serial Numbers:</strong>
<ul>
${validSerialNumbers.map(sn => `<li>${sn}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
</div>
<div class="warranty-status-row status-${statusClass}">
<span>${statusText}</span>
</div>
<div class="document-links-row">
${warranty.product_url ? `
<a href="${warranty.product_url}" class="product-link" target="_blank">
<i class="fas fa-globe"></i> Product Website
</a>
` : ''}
${warranty.invoice_path && warranty.invoice_path !== 'null' ? `
<a href="#" onclick="openSecureFile('${warranty.invoice_path}'); return false;" class="invoice-link">
<i class="fas fa-file-invoice"></i> Invoice
</a>` : ''}
${warranty.manual_path && warranty.manual_path !== 'null' ? `
<a href="#" onclick="openSecureFile('${warranty.manual_path}'); return false;" class="manual-link">
<i class="fas fa-book"></i> Manual
</a>` : ''}
</div>
${tagsHtml}
`;
} else if (currentView === 'table') {
// Table view HTML structure
cardElement.innerHTML = `
<div class="product-name-header">
<h3 class="warranty-title">${warranty.product_name || 'Unnamed Product'}</h3>
<div class="warranty-actions">
<button class="action-btn edit-btn" title="Edit" data-id="${warranty.id}">
<i class="fas fa-edit"></i>
</button>
<button class="action-btn delete-btn" title="Delete" data-id="${warranty.id}">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="warranty-content">
<div class="warranty-info">
<div>Purchased: <span>${formatDate(purchaseDate)}</span></div>
<div>Expires: <span>${formatDate(expirationDate)}</span></div>
</div>
</div>
<div class="warranty-status-row status-${statusClass}">
<span>${statusText}</span>
</div>
<div class="document-links-row">
${warranty.product_url ? `
<a href="${warranty.product_url}" class="product-link" target="_blank">
<i class="fas fa-globe"></i>
</a>
` : ''}
${warranty.invoice_path && warranty.invoice_path !== 'null' ? `
<a href="#" onclick="openSecureFile('${warranty.invoice_path}'); return false;" class="invoice-link">
<i class="fas fa-file-invoice"></i>
</a>` : ''}
${warranty.manual_path && warranty.manual_path !== 'null' ? `
<a href="#" onclick="openSecureFile('${warranty.manual_path}'); return false;" class="manual-link">
<i class="fas fa-book"></i>
</a>` : ''}
</div>
${tagsHtml}
`;
}
// Add event listeners
warrantiesList.appendChild(cardElement);
// Edit button event listener
cardElement.querySelector('.edit-btn').addEventListener('click', () => {
openEditModal(warranty);
});
// Delete button event listener
cardElement.querySelector('.delete-btn').addEventListener('click', () => {
openDeleteModal(warranty.id, warranty.product_name);
});
});
}
function filterWarranties() {
const searchTerm = searchInput.value.toLowerCase();
// Show or hide the clear search button
clearSearchBtn.style.display = searchTerm ? 'flex' : 'none';
if (!searchTerm) {
renderWarranties();
return;
}
const filtered = warranties.filter(warranty => {
// Check product name
if (warranty.product_name.toLowerCase().includes(searchTerm)) {
return true;
}
// Check tags
if (warranty.tags && Array.isArray(warranty.tags)) {
return warranty.tags.some(tag => tag.name.toLowerCase().includes(searchTerm));
}
return false;
});
// Add visual feedback if no results found
if (filtered.length === 0) {
renderEmptyState(`No matches found for "${searchTerm}". Try a different search term.`);
} else {
renderWarranties(filtered);
}
}
function applyFilters() {
console.log('Applying filters with:', currentFilters);
// Filter warranties based on currentFilters
const filtered = warranties.filter(warranty => {
// Status filter
if (currentFilters.status !== 'all' && warranty.status !== currentFilters.status) {
return false;
}
// Tag filter
if (currentFilters.tag !== 'all') {
const tagId = parseInt(currentFilters.tag);
const hasTag = warranty.tags && Array.isArray(warranty.tags) &&
warranty.tags.some(tag => tag.id === tagId);
if (!hasTag) {
return false;
}
}
// Search filter
if (currentFilters.search) {
const searchTerm = currentFilters.search.toLowerCase();
// Check if product name contains search term
const productNameMatch = warranty.product_name.toLowerCase().includes(searchTerm);
// Check if any tag name contains search term
const tagMatch = warranty.tags && Array.isArray(warranty.tags) &&
warranty.tags.some(tag => tag.name.toLowerCase().includes(searchTerm));
// Return true if either product name or tag name matches
if (!productNameMatch && !tagMatch) {
return false;
}
}
return true;
});
console.log('Filtered warranties:', filtered);
// Render the filtered warranties
renderWarranties(filtered);
}
function openEditModal(warranty) {
currentWarrantyId = warranty.id;
// Populate form fields
document.getElementById('editProductName').value = warranty.product_name;
document.getElementById('editProductUrl').value = warranty.product_url || '';
document.getElementById('editPurchaseDate').value = warranty.purchase_date.split('T')[0];
document.getElementById('editWarrantyYears').value = warranty.warranty_years;
document.getElementById('editPurchasePrice').value = warranty.purchase_price || '';
// Clear existing serial number inputs
const editSerialNumbersContainer = document.getElementById('editSerialNumbersContainer');
editSerialNumbersContainer.innerHTML = '';
// Add event listener for adding new serial number inputs in edit modal
editSerialNumbersContainer.addEventListener('click', (e) => {
if (e.target.closest('.add-serial-number')) {
addSerialNumberInput(editSerialNumbersContainer);
}
});
const validSerialNumbers = Array.isArray(warranty.serial_numbers)
? warranty.serial_numbers.filter(sn => sn && typeof sn === 'string' && sn.trim() !== '')
: [];
if (validSerialNumbers.length === 0) {
// Add a single empty input if there are no serial numbers
addSerialNumberInput(editSerialNumbersContainer);
} else {
// Add the first serial number with an "Add Another" button only (no remove button)
const firstInput = document.createElement('div');
firstInput.className = 'serial-number-input';
firstInput.innerHTML = `
<input type="text" name="serial_numbers[]" class="form-control" placeholder="Enter serial number" value="${validSerialNumbers[0]}">
<button type="button" class="btn btn-sm btn-primary add-serial-number">
<i class="fas fa-plus"></i> Add Another
</button>
`;
// Add event listener for the Add button
firstInput.querySelector('.add-serial-number').addEventListener('click', function(e) {
e.stopPropagation(); // Stop event from bubbling up
addSerialNumberInput(editSerialNumbersContainer);
});
editSerialNumbersContainer.appendChild(firstInput);
// Add the rest of the serial numbers with "Remove" buttons
for (let i = 1; i < validSerialNumbers.length; i++) {
const newInput = document.createElement('div');
newInput.className = 'serial-number-input';
newInput.innerHTML = `
<input type="text" name="serial_numbers[]" class="form-control" placeholder="Enter serial number" value="${validSerialNumbers[i]}">
<button type="button" class="btn btn-sm btn-danger remove-serial-number">
<i class="fas fa-minus"></i> Remove
</button>
`;
// Add remove button functionality
newInput.querySelector('.remove-serial-number').addEventListener('click', function() {
this.parentElement.remove();
});
editSerialNumbersContainer.appendChild(newInput);
}
}
// Show current invoice if exists
const currentInvoiceElement = document.getElementById('currentInvoice');
if (currentInvoiceElement) {
if (warranty.invoice_path && warranty.invoice_path !== 'null') {
currentInvoiceElement.innerHTML = `
<span class="text-success">
<i class="fas fa-check-circle"></i> Current invoice:
<a href="#" onclick="openSecureFile('${warranty.invoice_path}'); return false;">View</a>
(Upload a new file to replace)
</span>
`;
} else {
currentInvoiceElement.innerHTML = '<span>No invoice uploaded</span>';
}
}
// Show current manual if exists
const currentManualElement = document.getElementById('currentManual');
if (currentManualElement) {
if (warranty.manual_path && warranty.manual_path !== 'null') {
currentManualElement.innerHTML = `
<span class="text-success">
<i class="fas fa-check-circle"></i> Current manual:
<a href="#" onclick="openSecureFile('${warranty.manual_path}'); return false;">View</a>
(Upload a new file to replace)
</span>
`;
} else {
currentManualElement.innerHTML = '<span>No manual uploaded</span>';
}
}
// Reset file inputs
document.getElementById('editInvoice').value = '';
document.getElementById('editManual').value = '';
document.getElementById('editFileName').textContent = '';
document.getElementById('editManualFileName').textContent = '';
// Initialize file input event listeners
const editInvoiceInput = document.getElementById('editInvoice');
if (editInvoiceInput) {
editInvoiceInput.addEventListener('change', function(event) {
updateFileName(event, 'editInvoice', 'editFileName');
});
}
const editManualInput = document.getElementById('editManual');
if (editManualInput) {
editManualInput.addEventListener('change', function(event) {
updateFileName(event, 'editManual', 'editManualFileName');
});
}
// Show edit modal
const modalBackdrop = document.getElementById('editModal');
if (modalBackdrop) {
modalBackdrop.classList.add('active'); // Add active class to display as flex
}
// Reset tabs to first tab
const editTabBtns = document.querySelectorAll('.edit-tab-btn');
editTabBtns.forEach(btn => btn.classList.remove('active'));
document.querySelector('.edit-tab-btn[data-tab="edit-product-info"]').classList.add('active');
// Reset tab content
document.querySelectorAll('.edit-tab-content').forEach(content => content.classList.remove('active'));
document.getElementById('edit-product-info').classList.add('active');
// Initialize edit mode tags
editSelectedTags = [];
// If warranty has tags, populate editSelectedTags
if (warranty.tags && Array.isArray(warranty.tags)) {
editSelectedTags = warranty.tags.map(tag => ({
id: tag.id,
name: tag.name,
color: tag.color
}));
}
// Render selected tags using the helper function
renderEditSelectedTags();
// Set up tag search in edit mode
const editTagSearch = document.getElementById('editTagSearch');
const editTagsList = document.getElementById('editTagsList');
if (editTagSearch && editTagsList) {
// Add event listeners for tag search
editTagSearch.addEventListener('focus', () => {
renderEditTagsList();
editTagsList.classList.add('show');
});
editTagSearch.addEventListener('input', () => {
renderEditTagsList(editTagSearch.value);
});
// Add event listener to close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!editTagSearch.contains(e.target) && !editTagsList.contains(e.target)) {
editTagsList.classList.remove('show');
}
});
}
// Set up manage tags button in edit mode
const editManageTagsBtn = document.getElementById('editManageTagsBtn');
if (editManageTagsBtn) {
editManageTagsBtn.addEventListener('click', (e) => {
e.preventDefault();
openTagManagementModal();
});
}
// Validate all tabs to update completion indicators
validateEditTab('edit-product-info');
validateEditTab('edit-warranty-details');
validateEditTab('edit-documents');
validateEditTab('edit-tags');
// Add input event listeners to update validation status
document.querySelectorAll('#editWarrantyForm input').forEach(input => {
input.addEventListener('input', function() {
// Find the tab this input belongs to
const tabContent = this.closest('.edit-tab-content');
if (tabContent) {
validateEditTab(tabContent.id);
}
});
});
}
function openDeleteModal(warrantyId, productName) {
currentWarrantyId = warrantyId;
const deleteProductNameElement = document.getElementById('deleteProductName');
if (deleteProductNameElement) {
deleteProductNameElement.textContent = productName || '';
}
const deleteModal = document.getElementById('deleteModal');
if (deleteModal) {
deleteModal.classList.add('active');
}
}
// Function to close all modals
function closeModals() {
document.querySelectorAll('.modal-backdrop').forEach(modal => {
modal.classList.remove('active');
});
}
// Validate file size before upload
function validateFileSize(formData, maxSizeMB = 32) {
let totalSize = 0;
// Check file sizes
if (formData.has('invoice') && formData.get('invoice').size > 0) {
totalSize += formData.get('invoice').size;
}
if (formData.has('manual') && formData.get('manual').size > 0) {
totalSize += formData.get('manual').size;
}
// Convert bytes to MB for comparison and display
const totalSizeMB = totalSize / (1024 * 1024);
console.log(`Total upload size: ${totalSizeMB.toFixed(2)} MB`);
// Check if total size exceeds limit
if (totalSizeMB > maxSizeMB) {
return {
valid: false,
message: `Total file size (${totalSizeMB.toFixed(2)} MB) exceeds the maximum allowed size of ${maxSizeMB} MB. Please reduce file sizes.`
};
}
return {
valid: true
};
}
// Submit form function - event handler for form submit
function submitForm(event) {
event.preventDefault();
// Validate all tabs
for (let i = 0; i < tabContents.length; i++) {
if (!validateTab(i)) {
// Switch to the first invalid tab
switchToTab(i);
return;
}
}
// Create form data object
const formData = new FormData(warrantyForm);
// Add serial numbers to form data
const serialInputs = document.querySelectorAll('#serialNumbersContainer input');
serialInputs.forEach(input => {
if (input.value.trim()) {
formData.append('serial_numbers', input.value.trim());
}
});
// Add tag IDs to form data as JSON string
if (selectedTags && selectedTags.length > 0) {
const tagIds = selectedTags.map(tag => tag.id);
formData.append('tag_ids', JSON.stringify(tagIds));
}
// Show loading spinner
showLoadingSpinner();
// Send the form data to the server
fetch('/api/warranties', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('auth_token')
},
body: formData
})
.then(response => {
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.error || 'Failed to add warranty');
});
}
return response.json();
})
.then(data => {
hideLoadingSpinner();
showToast('Warranty added successfully', 'success');
// Reset form and reload warranties
resetForm();
// Reset selected tags
selectedTags = [];
if (selectedTagsContainer) {
selectedTagsContainer.innerHTML = '';
}
loadWarranties();
})
.catch(error => {
hideLoadingSpinner();
console.error('Error adding warranty:', error);
showToast(error.message || 'Failed to add warranty', 'error');
});
}
// Initialize page
document.addEventListener('DOMContentLoaded', function() {
// Initialize the warranty form
initWarrantyForm();
// Load warranties
loadWarranties();
// Initialize theme
initializeTheme();
// Set up event listeners for other UI controls
setupUIEventListeners();
});
// Add this function to handle edit tab functionality
function initEditTabs() {
const editTabBtns = document.querySelectorAll('.edit-tab-btn');
editTabBtns.forEach(btn => {
btn.addEventListener('click', () => {
// Remove active class from all tabs
editTabBtns.forEach(b => b.classList.remove('active'));
// Add active class to clicked tab
btn.classList.add('active');
// Hide all tab content
document.querySelectorAll('.edit-tab-content').forEach(content => {
content.classList.remove('active');
});
// Show the selected tab content
const tabId = btn.getAttribute('data-tab');
document.getElementById(tabId).classList.add('active');
});
});
}
// Update validateEditTabs function
function validateEditTab(tabId) {
const tab = document.getElementById(tabId);
let isValid = true;
// Get all required inputs in this tab
const requiredInputs = tab.querySelectorAll('input[required]');
// Check if all required fields are filled
requiredInputs.forEach(input => {
if (!input.value) {
isValid = false;
input.classList.add('invalid');
} else {
input.classList.remove('invalid');
}
});
// Update the tab button to show completion status
const tabBtn = document.querySelector(`.edit-tab-btn[data-tab="${tabId}"]`);
if (isValid) {
tabBtn.classList.add('completed');
} else {
tabBtn.classList.remove('completed');
}
return isValid;
}
// Add this function for secure file access
function openSecureFile(filePath) {
if (!filePath || filePath === 'null') {
console.error('Invalid file path:', filePath);
showToast('Invalid file path', 'error');
return false;
}
console.log('Opening secure file:', filePath);
// Get the file name from the path
const fileName = filePath.split('/').pop();
// Get auth token
const token = window.auth.getToken();
if (!token) {
showToast('Authentication error. Please log in again.', 'error');
return false;
}
// Use fetch with proper authorization header
fetch(`/api/secure-file/${fileName}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(response => {
if (!response.ok) {
throw new Error(`Error: ${response.status} ${response.statusText}`);
}
return response.blob();
})
.then(blob => {
// Create a URL for the blob
const blobUrl = window.URL.createObjectURL(blob);
// Open in new tab
window.open(blobUrl, '_blank');
})
.catch(error => {
console.error('Error fetching file:', error);
showToast('Error opening file: ' + error.message, 'error');
});
return false;
}
// Initialize the warranty form and all its components
function initWarrantyForm() {
// Initialize form tabs
if (formTabs && tabContents) {
initFormTabs();
}
// Initialize serial number inputs
addSerialNumberInput();
// Initialize file input display
if (document.getElementById('invoice')) {
document.getElementById('invoice').addEventListener('change', function(event) {
updateFileName(event, 'invoice', 'fileName');
});
}
if (document.getElementById('manual')) {
document.getElementById('manual').addEventListener('change', function(event) {
updateFileName(event, 'manual', 'manualFileName');
});
}
// Initialize tag functionality
initTagFunctionality();
// Form submission
if (warrantyForm) {
warrantyForm.addEventListener('submit', submitForm);
}
}
// Initialize tag functionality
function initTagFunctionality() {
// Skip if tag elements don't exist
if (!tagSearch || !tagsList || !manageTagsBtn || !selectedTagsContainer) {
console.log('Tag elements not found, skipping tag initialization');
return;
}
// Load tags from API if not already loaded
if (allTags.length === 0) {
loadTags();
}
// Tag search input
tagSearch.addEventListener('focus', () => {
renderTagsList();
tagsList.classList.add('show');
});
tagSearch.addEventListener('input', () => {
renderTagsList(tagSearch.value);
});
document.addEventListener('click', (e) => {
if (!tagSearch.contains(e.target) && !tagsList.contains(e.target)) {
tagsList.classList.remove('show');
}
});
// Manage tags button
manageTagsBtn.addEventListener('click', (e) => {
e.preventDefault();
openTagManagementModal();
});
// Tag management form
if (newTagForm) {
newTagForm.addEventListener('submit', (e) => {
e.preventDefault();
createNewTag();
});
}
// Close modal buttons
if (tagManagementModal) {
const closeButtons = tagManagementModal.querySelectorAll('[data-dismiss="modal"]');
closeButtons.forEach(button => {
button.addEventListener('click', () => {
tagManagementModal.style.display = 'none';
});
});
}
}
// Function to load all tags
async function loadTags() {
try {
const token = localStorage.getItem('auth_token');
if (!token) {
console.error('No auth token found');
return;
}
showLoadingSpinner();
const response = await fetch('/api/tags', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error(`Failed to load tags: ${response.status}`);
}
const data = await response.json();
console.log('Loaded tags:', data);
// Store tags globally
allTags = data;
// Populate the tag filter
populateTagFilter();
// Render selected tags if any
if (selectedTagsContainer) {
renderSelectedTags();
}
hideLoadingSpinner();
return data;
} catch (error) {
console.error('Error loading tags:', error);
hideLoadingSpinner();
return [];
}
}
// Render the tags dropdown list
function renderTagsList(searchTerm = '') {
if (!tagsList) return;
tagsList.innerHTML = '';
// Filter tags based on search term
const filteredTags = allTags.filter(tag =>
!searchTerm || tag.name.toLowerCase().includes(searchTerm.toLowerCase())
);
// Add option to create new tag if search term is provided and not in list
if (searchTerm && !filteredTags.some(tag => tag.name.toLowerCase() === searchTerm.toLowerCase())) {
const createOption = document.createElement('div');
createOption.className = 'tag-option create-tag';
createOption.innerHTML = `<i class="fas fa-plus"></i> Create "${searchTerm}"`;
createOption.addEventListener('click', () => {
createTag(searchTerm);
tagsList.classList.remove('show');
});
tagsList.appendChild(createOption);
}
// Add existing tags to dropdown
filteredTags.forEach(tag => {
const option = document.createElement('div');
option.className = 'tag-option';
// Check if tag is already selected
const isSelected = selectedTags.some(selected => selected.id === tag.id);
option.innerHTML = `
<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');
}
// Render the selected tags
function renderSelectedTags() {
if (!selectedTagsContainer) return;
selectedTagsContainer.innerHTML = '';
if (selectedTags.length === 0) {
const placeholder = document.createElement('span');
placeholder.className = 'no-tags-selected';
placeholder.textContent = 'No tags selected';
selectedTagsContainer.appendChild(placeholder);
return;
}
selectedTags.forEach(tag => {
const tagElement = document.createElement('span');
tagElement.className = 'tag';
tagElement.style.backgroundColor = tag.color;
tagElement.style.color = getContrastColor(tag.color);
tagElement.innerHTML = `
${tag.name}
<span class="remove-tag" data-id="${tag.id}">&times;</span>
`;
// Add event listener for removing tag
tagElement.querySelector('.remove-tag').addEventListener('click', (e) => {
e.stopPropagation();
selectedTags = selectedTags.filter(t => t.id !== tag.id);
renderSelectedTags();
// Update summary if needed
if (document.getElementById('summary-tags')) {
updateSummary();
}
});
selectedTagsContainer.appendChild(tagElement);
});
}
// Create a new tag
function createTag(name) {
const token = localStorage.getItem('auth_token');
if (!token) {
console.error('No authentication token found');
return;
}
// Generate a random color for the tag
const color = '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
fetch('/api/tags', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({
name: name,
color: color
})
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to create tag');
}
return response.json();
})
.then(data => {
// Add new tag to allTags array
allTags.push({
id: data.id,
name: data.name,
color: data.color
});
// Select the new tag
selectedTags.push({
id: data.id,
name: data.name,
color: data.color
});
// Clear search and rerender
if (tagSearch) tagSearch.value = '';
renderSelectedTags();
showToast('Tag created successfully', 'success');
})
.catch(error => {
console.error('Error creating tag:', error);
showToast(error.message || 'Failed to create tag', 'error');
});
}
// Helper function to determine text color based on background color
function getContrastColor(hexColor) {
// Convert hex to RGB
const r = parseInt(hexColor.substr(1, 2), 16);
const g = parseInt(hexColor.substr(3, 2), 16);
const b = parseInt(hexColor.substr(5, 2), 16);
// Calculate luminance
const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
// Return black or white depending on luminance
return (yiq >= 128) ? '#000000' : '#ffffff';
}
// Open tag management modal
function openTagManagementModal() {
if (!tagManagementModal) return;
// Populate existing tags
renderExistingTags();
// Show modal
tagManagementModal.style.display = 'block';
}
// Render existing tags in the management modal
function renderExistingTags() {
if (!existingTagsContainer) return;
existingTagsContainer.innerHTML = '';
if (allTags.length === 0) {
existingTagsContainer.innerHTML = '<div class="no-tags">No tags created yet</div>';
return;
}
allTags.forEach(tag => {
const tagElement = document.createElement('div');
tagElement.className = 'existing-tag';
tagElement.innerHTML = `
<div class="existing-tag-info">
<div class="existing-tag-color" style="background-color: ${tag.color}"></div>
<div class="existing-tag-name">${tag.name}</div>
</div>
<div class="existing-tag-actions">
<button class="btn btn-sm btn-secondary edit-tag" data-id="${tag.id}">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-danger delete-tag" data-id="${tag.id}">
<i class="fas fa-trash"></i>
</button>
</div>
`;
// Add event listeners for edit and delete
tagElement.querySelector('.edit-tag').addEventListener('click', () => {
editTag(tag);
});
tagElement.querySelector('.delete-tag').addEventListener('click', () => {
deleteTag(tag.id);
});
existingTagsContainer.appendChild(tagElement);
});
}
// Edit a tag
function editTag(tag) {
const tagInfoElement = document.querySelector(`.existing-tag .existing-tag-info:has(+ .existing-tag-actions button[data-id="${tag.id}"])`);
if (!tagInfoElement) {
// Alternative selector for browsers that don't support :has
const tagElement = document.querySelector(`.existing-tag`);
const buttons = tagElement?.querySelectorAll(`.existing-tag-actions button[data-id="${tag.id}"]`);
if (buttons?.length > 0) {
const parent = buttons[0].closest('.existing-tag');
if (parent) {
const infoElement = parent.querySelector('.existing-tag-info');
if (infoElement) {
tagInfoElement = infoElement;
}
}
}
if (!tagInfoElement) return;
}
const originalHTML = tagInfoElement.innerHTML;
tagInfoElement.innerHTML = `
<input type="text" class="form-control edit-tag-name" value="${tag.name}" style="width: 60%;">
<input type="color" class="edit-tag-color" value="${tag.color}" style="width: 40px; height: 38px;">
<button class="btn btn-sm btn-primary save-edit" data-id="${tag.id}">Save</button>
<button class="btn btn-sm btn-secondary cancel-edit">Cancel</button>
`;
// Add event listeners
tagInfoElement.querySelector('.save-edit').addEventListener('click', () => {
const newName = tagInfoElement.querySelector('.edit-tag-name').value.trim();
const newColor = tagInfoElement.querySelector('.edit-tag-color').value;
if (!newName) {
showToast('Tag name is required', 'error');
return;
}
updateTag(tag.id, newName, newColor);
});
tagInfoElement.querySelector('.cancel-edit').addEventListener('click', () => {
// Restore original HTML
tagInfoElement.innerHTML = originalHTML;
});
}
// Update a tag
function updateTag(id, name, color) {
const token = localStorage.getItem('auth_token');
if (!token) {
console.error('No authentication token found');
return;
}
fetch(`/api/tags/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({
name: name,
color: color
})
})
.then(response => {
if (!response.ok) {
if (response.status === 409) {
throw new Error('A tag with this name already exists');
}
throw new Error('Failed to update tag');
}
return response.json();
})
.then(data => {
// Update tag in allTags array
const index = allTags.findIndex(tag => tag.id === id);
if (index !== -1) {
allTags[index].name = name;
allTags[index].color = color;
}
// Update tag in selectedTags if present
const selectedIndex = selectedTags.findIndex(tag => tag.id === id);
if (selectedIndex !== -1) {
selectedTags[selectedIndex].name = name;
selectedTags[selectedIndex].color = color;
}
// Rerender existing tags and selected tags
renderExistingTags();
renderSelectedTags();
// Update summary if needed
if (document.getElementById('summary-tags')) {
updateSummary();
}
showToast('Tag updated successfully', 'success');
})
.catch(error => {
console.error('Error updating tag:', error);
showToast(error.message || 'Failed to update tag', 'error');
});
}
// Delete a tag
function deleteTag(id) {
if (!confirm('Are you sure you want to delete this tag? It will be removed from all warranties.')) {
return;
}
const token = localStorage.getItem('auth_token');
if (!token) {
console.error('No authentication token found');
return;
}
fetch(`/api/tags/${id}`, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + token
}
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to delete tag');
}
return response.json();
})
.then(data => {
// Remove tag from allTags array
allTags = allTags.filter(tag => tag.id !== id);
// Remove tag from selectedTags if present
selectedTags = selectedTags.filter(tag => tag.id !== id);
// Rerender existing tags and selected tags
renderExistingTags();
renderSelectedTags();
// Update summary if needed
if (document.getElementById('summary-tags')) {
updateSummary();
}
showToast('Tag deleted successfully', 'success');
})
.catch(error => {
console.error('Error deleting tag:', error);
showToast('Failed to delete tag', 'error');
});
}
// Set up event listeners for UI controls
function setupUIEventListeners() {
// Initialize settings button
const settingsBtn = document.querySelector('.settings-btn');
const settingsMenu = document.querySelector('.settings-menu');
if (settingsBtn && settingsMenu) {
settingsBtn.addEventListener('click', (e) => {
e.stopPropagation();
settingsMenu.classList.toggle('active');
});
// Close settings menu when clicking outside
document.addEventListener('click', (e) => {
if (settingsMenu.classList.contains('active') &&
!settingsMenu.contains(e.target) &&
!settingsBtn.contains(e.target)) {
settingsMenu.classList.remove('active');
}
});
}
// Initialize edit tabs
initEditTabs();
// Close modals when clicking outside or on close button
document.querySelectorAll('.modal-backdrop, [data-dismiss="modal"]').forEach(element => {
element.addEventListener('click', (e) => {
if (e.target === element) {
closeModals();
}
});
});
// Prevent modal content clicks from closing the modal
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
e.stopPropagation();
});
});
// Filter event listeners
const searchInput = document.getElementById('searchWarranties');
const clearSearchBtn = document.getElementById('clearSearch');
const statusFilter = document.getElementById('statusFilter');
const tagFilter = document.getElementById('tagFilter');
const sortBySelect = document.getElementById('sortBy');
if (searchInput) {
searchInput.addEventListener('input', () => {
currentFilters.search = searchInput.value.toLowerCase();
// Show/hide clear button based on search input
if (clearSearchBtn) {
clearSearchBtn.style.display = searchInput.value ? 'flex' : 'none';
}
// Add visual feedback class to search box when active
if (searchInput.value) {
searchInput.parentElement.classList.add('active-search');
} else {
searchInput.parentElement.classList.remove('active-search');
}
applyFilters();
});
}
if (clearSearchBtn) {
clearSearchBtn.addEventListener('click', () => {
if (searchInput) {
searchInput.value = '';
currentFilters.search = '';
clearSearchBtn.style.display = 'none';
searchInput.parentElement.classList.remove('active-search');
searchInput.focus();
applyFilters();
}
});
}
if (statusFilter) {
statusFilter.addEventListener('change', () => {
currentFilters.status = statusFilter.value;
applyFilters();
});
}
if (tagFilter) {
tagFilter.addEventListener('change', () => {
currentFilters.tag = tagFilter.value;
applyFilters();
});
}
if (sortBySelect) {
sortBySelect.addEventListener('change', () => {
currentFilters.sortBy = sortBySelect.value;
applyFilters();
});
}
// View switcher event listeners
const gridViewBtn = document.getElementById('gridViewBtn');
const listViewBtn = document.getElementById('listViewBtn');
const tableViewBtn = document.getElementById('tableViewBtn');
if (gridViewBtn) gridViewBtn.addEventListener('click', () => switchView('grid'));
if (listViewBtn) listViewBtn.addEventListener('click', () => switchView('list'));
if (tableViewBtn) tableViewBtn.addEventListener('click', () => switchView('table'));
// Export button event listener
const exportBtn = document.getElementById('exportBtn');
if (exportBtn) exportBtn.addEventListener('click', exportWarranties);
// Refresh button
const refreshBtn = document.getElementById('refreshBtn');
if (refreshBtn) refreshBtn.addEventListener('click', loadWarranties);
// Save warranty changes
const saveWarrantyBtn = document.getElementById('saveWarrantyBtn');
if (saveWarrantyBtn) saveWarrantyBtn.addEventListener('click', saveWarranty);
// Confirm delete button
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
if (confirmDeleteBtn) confirmDeleteBtn.addEventListener('click', deleteWarranty);
// Load saved view preference
loadViewPreference();
}
// Function to show loading spinner
function showLoadingSpinner() {
if (loadingContainer) {
loadingContainer.style.display = 'flex';
}
}
// Function to hide loading spinner
function hideLoadingSpinner() {
if (loadingContainer) {
loadingContainer.style.display = 'none';
}
}
// Delete warranty function
function deleteWarranty() {
if (!currentWarrantyId) {
showToast('No warranty selected for deletion', 'error');
return;
}
const token = localStorage.getItem('auth_token');
if (!token) {
showToast('Authentication required', 'error');
return;
}
showLoadingSpinner();
fetch(`/api/warranties/${currentWarrantyId}`, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + token
}
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to delete warranty');
}
return response.json();
})
.then(data => {
hideLoadingSpinner();
showToast('Warranty deleted successfully', 'success');
closeModals();
loadWarranties();
})
.catch(error => {
hideLoadingSpinner();
console.error('Error deleting warranty:', error);
showToast('Failed to delete warranty', 'error');
});
}
// Save warranty updates
function saveWarranty() {
if (!currentWarrantyId) {
showToast('No warranty selected for update', 'error');
return;
}
const productName = document.getElementById('editProductName').value.trim();
const purchaseDate = document.getElementById('editPurchaseDate').value;
const warrantyYears = document.getElementById('editWarrantyYears').value;
// Basic validation
if (!productName) {
showToast('Product name is required', 'error');
return;
}
if (!purchaseDate) {
showToast('Purchase date is required', 'error');
return;
}
if (!warrantyYears || warrantyYears <= 0) {
showToast('Warranty period must be greater than 0', 'error');
return;
}
// Create form data
const formData = new FormData();
formData.append('product_name', productName);
formData.append('purchase_date', purchaseDate);
formData.append('warranty_years', warrantyYears);
// Optional fields
const productUrl = document.getElementById('editProductUrl').value.trim();
if (productUrl) {
formData.append('product_url', productUrl);
}
const purchasePrice = document.getElementById('editPurchasePrice').value;
if (purchasePrice) {
formData.append('purchase_price', purchasePrice);
}
// Serial numbers
const serialInputs = document.querySelectorAll('#editSerialNumbersContainer input');
serialInputs.forEach(input => {
if (input.value.trim()) {
formData.append('serial_numbers', input.value.trim());
}
});
// Tags - add tag IDs as JSON string
if (editSelectedTags && editSelectedTags.length > 0) {
const tagIds = editSelectedTags.map(tag => tag.id);
formData.append('tag_ids', JSON.stringify(tagIds));
} else {
// Send empty array to clear tags
formData.append('tag_ids', JSON.stringify([]));
}
// Files
const invoiceFile = document.getElementById('editInvoice').files[0];
if (invoiceFile) {
formData.append('invoice', invoiceFile);
}
const manualFile = document.getElementById('editManual').files[0];
if (manualFile) {
formData.append('manual', manualFile);
}
// Get auth token
const token = localStorage.getItem('auth_token');
if (!token) {
showToast('Authentication required', 'error');
return;
}
showLoadingSpinner();
// Send request
fetch(`/api/warranties/${currentWarrantyId}`, {
method: 'PUT',
headers: {
'Authorization': 'Bearer ' + token
},
body: formData
})
.then(response => {
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.error || 'Failed to update warranty');
});
}
return response.json();
})
.then(data => {
hideLoadingSpinner();
showToast('Warranty updated successfully', 'success');
closeModals();
loadWarranties();
})
.catch(error => {
hideLoadingSpinner();
console.error('Error updating warranty:', error);
showToast(error.message || 'Failed to update warranty', 'error');
});
}
// Render the tags dropdown list for edit mode
function renderEditTagsList(searchTerm = '') {
const editTagsList = document.getElementById('editTagsList');
if (!editTagsList) return;
editTagsList.innerHTML = '';
// Filter tags based on search term
const filteredTags = allTags.filter(tag =>
!searchTerm || tag.name.toLowerCase().includes(searchTerm.toLowerCase())
);
// Add option to create new tag if search term is provided and not in list
if (searchTerm && !filteredTags.some(tag => tag.name.toLowerCase() === searchTerm.toLowerCase())) {
const createOption = document.createElement('div');
createOption.className = 'tag-option create-tag';
createOption.innerHTML = `<i class="fas fa-plus"></i> Create "${searchTerm}"`;
createOption.addEventListener('click', () => {
createTag(searchTerm);
editTagsList.classList.remove('show');
});
editTagsList.appendChild(createOption);
}
// Add existing tags to dropdown
filteredTags.forEach(tag => {
const option = document.createElement('div');
option.className = 'tag-option';
// Check if tag is already selected
const isSelected = editSelectedTags.some(selected => selected.id === tag.id);
option.innerHTML = `
<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');
}
// Function to populate tag filter dropdown
function populateTagFilter() {
const tagFilter = document.getElementById('tagFilter');
if (!tagFilter) return;
// Clear existing options (except "All Tags")
while (tagFilter.options.length > 1) {
tagFilter.remove(1);
}
// Create a Set to store unique tag names
const uniqueTags = new Set();
// Collect all unique tags from warranties
warranties.forEach(warranty => {
if (warranty.tags && Array.isArray(warranty.tags)) {
warranty.tags.forEach(tag => {
uniqueTags.add(JSON.stringify({id: tag.id, name: tag.name, color: tag.color}));
});
}
});
// Sort tags alphabetically by name
const sortedTags = Array.from(uniqueTags)
.map(tagJson => JSON.parse(tagJson))
.sort((a, b) => a.name.localeCompare(b.name));
// Add options to the dropdown
sortedTags.forEach(tag => {
const option = document.createElement('option');
option.value = tag.id;
option.textContent = tag.name;
option.style.backgroundColor = tag.color;
tagFilter.appendChild(option);
});
}
// Helper function to render the edit selected tags
function renderEditSelectedTags() {
const editSelectedTagsContainer = document.getElementById('editSelectedTags');
if (!editSelectedTagsContainer) return;
editSelectedTagsContainer.innerHTML = '';
if (editSelectedTags.length > 0) {
editSelectedTags.forEach(tag => {
const tagElement = document.createElement('span');
tagElement.className = 'tag';
tagElement.style.backgroundColor = tag.color;
tagElement.style.color = getContrastColor(tag.color);
tagElement.innerHTML = `
${tag.name}
<span class="remove-tag" data-id="${tag.id}">&times;</span>
`;
// Add event listener for removing tag
const removeButton = tagElement.querySelector('.remove-tag');
removeButton.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault(); // Add this to prevent default action
// Prevent the event from bubbling up to parent elements
if (e.cancelBubble !== undefined) {
e.cancelBubble = true;
}
editSelectedTags = editSelectedTags.filter(t => t.id !== tag.id);
// Re-render just the tags
renderEditSelectedTags();
return false; // Add return false for older browsers
});
editSelectedTagsContainer.appendChild(tagElement);
});
} else {
const placeholder = document.createElement('span');
placeholder.className = 'no-tags-selected';
placeholder.textContent = 'No tags selected';
editSelectedTagsContainer.appendChild(placeholder);
}
}