From e045aae2086903bcdb3e99a264836fbddacbf276 Mon Sep 17 00:00:00 2001 From: sassanix <39465071+sassanix@users.noreply.github.com> Date: Sun, 4 May 2025 13:02:51 -0300 Subject: [PATCH] Vendor, Date Format, Added security Refer to changelogs --- CHANGELOG.md | 110 +++ Docker/.env.example | 9 - README.md | 2 + backend/app.py | 695 +++++++++-------- backend/init.sql | 7 - ...l => 007_add_notification_preferences.sql} | 0 .../{007_add_tags.sql => 008_add_tags.sql} | 0 .../migrations/009_add_admin_flag_to_tags.sql | 6 + ...oles.sql => 010_configure_admin_roles.sql} | 0 ...s.sql => 011_ensure_admin_permissions.sql} | 0 ...column.sql => 012_add_timezone_column.sql} | 0 ...anty.sql => 013_add_lifetime_warranty.sql} | 0 .../migrations/013_add_updated_at_to_tags.sql | 5 - ...l => 014_add_updated_at_to_warranties.sql} | 0 ...> 015_allow_fractional_warranty_years.sql} | 0 .../migrations/016_add_updated_at_to_tags.sql | 5 + ...es.sql => 017_add_notes_to_warranties.sql} | 0 .../018_add_vendor_to_warranties.sql | 3 + .../migrations/019_add_date_format_column.sql | 18 + .../migrations/020_add_user_id_to_tags.sql | 32 + .../004_add_user_preferences.cpython-39.pyc | Bin 1321 -> 0 bytes ...eate_user_preferences_table.cpython-39.pyc | Bin 1322 -> 0 bytes ...d_expiring_soon_days_column.cpython-39.pyc | Bin 2546 -> 0 bytes .../007_add_expiring_soon_days.cpython-39.pyc | Bin 2547 -> 0 bytes backend/requirements.txt | 3 +- backend/tests/test_notifications.py | 167 ---- frontend/about.html | 28 +- frontend/api-test.html | 95 --- frontend/auth.js | 5 + frontend/fix-auth-buttons.js | 12 - frontend/img/favicon-512x512.png | Bin 0 -> 9340 bytes frontend/include-auth-new.js | 140 ++-- frontend/index.html | 16 +- frontend/login.html | 2 + frontend/manifest.json | 15 + frontend/register.html | 9 +- frontend/reset-password-request.html | 8 +- frontend/reset-password.html | 8 +- frontend/script.js | 724 ++++++++++++++---- frontend/settings-new.html | 19 + frontend/settings-new.js | 682 +++++++++-------- frontend/settings.js | 22 +- frontend/status.html | 3 + frontend/test-api.js | 59 -- frontend/test-auth-buttons.js | 1 - frontend/version-checker.js | 53 ++ .../003_add_notification_preferences.sql | 34 - migrations/apply_migrations.py | 156 ---- migrations/run_migrations.sh | 13 - nginx.conf | 3 +- ...20250303033711_dummy-invoice-1018x1440.png | Bin 68478 -> 0 bytes uploads/debug-test.txt | 1 - uploads/test.txt | 1 - 53 files changed, 1710 insertions(+), 1461 deletions(-) delete mode 100644 Docker/.env.example delete mode 100644 backend/init.sql rename backend/migrations/{006_add_notification_preferences.sql => 007_add_notification_preferences.sql} (100%) rename backend/migrations/{007_add_tags.sql => 008_add_tags.sql} (100%) create mode 100644 backend/migrations/009_add_admin_flag_to_tags.sql rename backend/migrations/{008_configure_admin_roles.sql => 010_configure_admin_roles.sql} (100%) rename backend/migrations/{009_ensure_admin_permissions.sql => 011_ensure_admin_permissions.sql} (100%) rename backend/migrations/{010_add_timezone_column.sql => 012_add_timezone_column.sql} (100%) rename backend/migrations/{011_add_lifetime_warranty.sql => 013_add_lifetime_warranty.sql} (100%) delete mode 100644 backend/migrations/013_add_updated_at_to_tags.sql rename backend/migrations/{012_add_updated_at_to_warranties.sql => 014_add_updated_at_to_warranties.sql} (100%) rename backend/migrations/{012_allow_fractional_warranty_years.sql => 015_allow_fractional_warranty_years.sql} (100%) create mode 100644 backend/migrations/016_add_updated_at_to_tags.sql rename backend/migrations/{014_add_notes_to_warranties.sql => 017_add_notes_to_warranties.sql} (100%) create mode 100644 backend/migrations/018_add_vendor_to_warranties.sql create mode 100644 backend/migrations/019_add_date_format_column.sql create mode 100644 backend/migrations/020_add_user_id_to_tags.sql delete mode 100644 backend/migrations/__pycache__/004_add_user_preferences.cpython-39.pyc delete mode 100644 backend/migrations/__pycache__/004_create_user_preferences_table.cpython-39.pyc delete mode 100644 backend/migrations/__pycache__/005_add_expiring_soon_days_column.cpython-39.pyc delete mode 100644 backend/migrations/__pycache__/007_add_expiring_soon_days.cpython-39.pyc delete mode 100644 backend/tests/test_notifications.py delete mode 100644 frontend/api-test.html create mode 100644 frontend/img/favicon-512x512.png create mode 100644 frontend/manifest.json delete mode 100644 frontend/test-api.js delete mode 100644 frontend/test-auth-buttons.js create mode 100644 frontend/version-checker.js delete mode 100644 migrations/003_add_notification_preferences.sql delete mode 100644 migrations/apply_migrations.py delete mode 100644 migrations/run_migrations.sh delete mode 100644 uploads/20250303033711_dummy-invoice-1018x1440.png delete mode 100644 uploads/debug-test.txt delete mode 100644 uploads/test.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 2045586..fe9c488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,115 @@ # Changelog +## [0.9.9.4] - 2025-05-04 + +### Fixed +- **Theme Persistence & Consistency:** + - Refactored dark mode logic to use only a single `localStorage` key (`darkMode`) as the source of truth for theme preference across all pages. + - Removed all legacy and user-prefixed theme keys (e.g., `user_darkMode`, `admin_darkMode`, `${prefix}darkMode`). + - Ensured all theme toggles and settings update only the `darkMode` key, and all pages read only this key for theme initialization. + - Verified that `theme-loader.js` is included early in every HTML file to prevent flashes of incorrect theme. + - Cleaned up redundant or conflicting theme logic in all frontend scripts and HTML files. + - Theme preference now persists reliably across logins, logouts, and browser sessions (except in incognito/private mode, where localStorage is temporary by design). + _Files: `frontend/script.js`, `frontend/settings-new.js`, `frontend/status.js`, `frontend/register.html`, `frontend/reset-password.html`, `frontend/reset-password-request.html`, `frontend/theme-loader.js`, all main HTML files._ + +### Added +- **Admin/User Tag Separation:** + - Implemented distinct tags for Admins and regular Users. + - Tags created by an Admin are only visible to other Admins. + - Tags created by a User are only visible to other Users. + - Added `is_admin_tag` boolean column to the `tags` table via migration (`backend/migrations/009_add_admin_flag_to_tags.sql`). + - Backend API endpoints (`/api/tags`, `/api/warranties`, `/api/warranties//tags`) updated to filter tags based on the logged-in user's role (`is_admin`). + - Tag creation (`POST /api/tags`) now automatically sets the `is_admin_tag` flag based on the creator's role. + - Tag update (`PUT /api/tags/`) and deletion (`DELETE /api/tags/`) endpoints now prevent users/admins from modifying tags belonging to the other role. + _Files: `backend/app.py`, `backend/migrations/009_add_admin_flag_to_tags.sql`_ + + - **Version Check:** + + - Added a version checker to the About page (`frontend/about.html`) that compares the current version with the latest GitHub release and displays the update status. + _Files: `frontend/about.html`, `frontend/version-checker.js`_ + +- **Mobile Home Screen Icon Support:** + - Added support for mobile devices to display the app icon when added to the home screen. + - Included `` for iOS devices, referencing a new 512x512 PNG icon. + - Added a web app manifest (`manifest.json`) referencing the 512x512 icon for Android/Chrome home screen support. + - Updated all main HTML files to include these tags for consistent experience across devices. + _Files: `frontend/index.html`, `frontend/login.html`, `frontend/register.html`, `frontend/about.html`, `frontend/reset-password.html`, `frontend/reset-password-request.html`, `frontend/status.html`, `frontend/settings-new.html`, `frontend/manifest.json`, `frontend/img/favicon-512x512.png`_ + +- **Optional Vendor Field for Warranties:** + - Users can now specify the vendor (e.g., Amazon, Best Buy) where a product was purchased, as an optional informational field for each warranty. + - The field is available when adding a new warranty and when editing an existing warranty. + - The vendor is displayed on warranty cards and in the summary tab of the add warranty wizard. + - The vendor field is now searchable alongside product name, notes, and tags. + - Backend API and database updated to support this field, including a migration to add the column to the warranties table. + - Editing a warranty now correctly updates the vendor field. + _Files: `backend/app.py`, `backend/migrations/017_add_vendor_to_warranties.sql`, `frontend/index.html`, `frontend/script.js`_ + +- **Serial Number Search:** + - Enhanced search functionality to include serial numbers. + - Updated search input placeholder text to reflect serial number search capability. + _Files: `frontend/script.js`, `frontend/index.html`_ + +- **CSV Import Vendor Field:** + - Added support for importing the `Vendor` field via CSV file upload. + - The CSV header should be `Vendor`. + _Files: `backend/app.py`, `frontend/script.js`_ + +- **Date Format Customization:** + - Users can now choose their preferred date display format in Settings > Preferences. + - Available formats include: + - Month/Day/Year (e.g., 12/31/2024) + - Day/Month/Year (e.g., 31/12/2024) + - Year-Month-Day (e.g., 2024-12-31) + - Mon Day, Year (e.g., Dec 31, 2024) + - Day Mon Year (e.g., 31 Dec 2024) + - Year Mon Day (e.g., 2024 Dec 31) + - The selected format is applied to purchase and expiration dates on warranty cards. + - The setting persists across sessions and is synchronized between open tabs. + _Files: `frontend/settings-new.html`, `frontend/settings-new.js`, `frontend/script.js`_ + +- **IPv6 Support:** Added `listen [::]:80;` directive to the Nginx configuration (`nginx.conf`) to enable listening on IPv6 interfaces alongside IPv4. + _Files: `nginx.conf`_ + +- **Cloudflare Compatibility:** Added ` + + + + @@ -88,7 +106,14 @@

About Warracker

-

Version: v0.9.9.3

+

Version: v0.9.9.4

+ +
+

Update Status: Checking for updates...

+ +

GitHub Repository: @@ -137,6 +162,7 @@ + - - - - - - - - - - - -

-
-
- -

Warranty Tracker

-
- -
-
- - -
-
-

API Connection Test

- -
-
-

Testing API Connection

-
-
-
-

Checking API connection...

-
- -
-

Manual API Test

-

You can also test the API manually with tools like curl:

-
curl -X GET -H "Authorization: Bearer YOUR_AUTH_TOKEN" http://localhost:8005/api/warranties
-
- -
- - - Back to Home - -
-
-
-
-
- - - - - \ No newline at end of file diff --git a/frontend/auth.js b/frontend/auth.js index 562c4f8..cc03297 100644 --- a/frontend/auth.js +++ b/frontend/auth.js @@ -121,6 +121,11 @@ function checkAuthState() { * Update UI elements for authenticated user */ function updateUIForAuthenticatedUser() { + // Fire a global event so other scripts (like script.js) can react to authentication being ready + setTimeout(() => { + console.log('[auth.js] Dispatching authStateReady event'); + window.dispatchEvent(new Event('authStateReady')); + }, 0); console.log('auth.js: Updating UI for authenticated user'); // Log the user data being used diff --git a/frontend/fix-auth-buttons.js b/frontend/fix-auth-buttons.js index 49e301f..c4575bf 100644 --- a/frontend/fix-auth-buttons.js +++ b/frontend/fix-auth-buttons.js @@ -182,16 +182,4 @@ document.addEventListener('DOMContentLoaded', () => { console.log('DOMContentLoaded event triggered, updating auth buttons'); updateAuthButtons(); setupUserMenuDropdown(); - - // Set up periodic check (every 2 seconds) - setInterval(updateAuthButtons, 2000); - setInterval(setupUserMenuDropdown, 2000); -}); - -// Update auth buttons when localStorage changes -window.addEventListener('storage', (event) => { - if (event.key === 'auth_token' || event.key === 'user_info') { - console.log('Auth data changed, updating auth buttons'); - updateAuthButtons(); - } }); \ No newline at end of file diff --git a/frontend/img/favicon-512x512.png b/frontend/img/favicon-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..d6700948301d35637a8370375e7ff98769b19eeb GIT binary patch literal 9340 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 literal 0 HcmV?d00001 diff --git a/frontend/include-auth-new.js b/frontend/include-auth-new.js index d5c21ab..56e55f2 100644 --- a/frontend/include-auth-new.js +++ b/frontend/include-auth-new.js @@ -7,107 +7,87 @@ console.log('include-auth-new.js: Running immediate auth check'); -// Immediately check if user is logged in -if (localStorage.getItem('auth_token')) { - console.log('include-auth-new.js: Auth token found, hiding login/register buttons immediately'); - - // Create and inject CSS to hide auth buttons - var style = document.createElement('style'); - style.textContent = ` - #authContainer, .auth-buttons, a[href="login.html"], a[href="register.html"], - .login-btn, .register-btn, .auth-btn.login-btn, .auth-btn.register-btn { - display: none !important; - visibility: hidden !important; +// Function to update UI based on auth state (extracted for reuse) +function updateAuthUI() { + if (localStorage.getItem('auth_token')) { + console.log('include-auth-new.js: Updating UI for authenticated user'); + // Inject CSS to hide auth buttons and show user menu + const styleId = 'auth-ui-style'; + let style = document.getElementById(styleId); + if (!style) { + style = document.createElement('style'); + style.id = styleId; + document.head.appendChild(style); } - - #userMenu, .user-menu { - display: block !important; - visibility: visible !important; - } - `; - document.head.appendChild(style); - - // Set the display style as soon as DOM is ready - window.addEventListener('DOMContentLoaded', function() { - console.log('include-auth-new.js: DOM loaded, ensuring buttons are hidden'); - - // Hide auth container - var authContainer = document.getElementById('authContainer'); - if (authContainer) { - authContainer.style.display = 'none'; - authContainer.style.visibility = 'hidden'; - } - - // Hide all login/register buttons - document.querySelectorAll('a[href="login.html"], a[href="register.html"], .login-btn, .register-btn, .auth-btn').forEach(function(button) { - button.style.display = 'none'; - button.style.visibility = 'hidden'; - }); - - // Show user menu - var userMenu = document.getElementById('userMenu'); - if (userMenu) { - userMenu.style.display = 'block'; - userMenu.style.visibility = 'visible'; - } - - // Update user information + style.textContent = ` + #authContainer, .auth-buttons, a[href="login.html"], a[href="register.html"], + .login-btn, .register-btn, .auth-btn.login-btn, .auth-btn.register-btn { + display: none !important; + visibility: hidden !important; + } + #userMenu, .user-menu { + display: block !important; + visibility: visible !important; + } + `; + + // Update user info display elements immediately try { var userInfoStr = localStorage.getItem('user_info'); if (userInfoStr) { var userInfo = JSON.parse(userInfoStr); var displayName = userInfo.username || 'User'; - var userDisplayName = document.getElementById('userDisplayName'); - if (userDisplayName) { - userDisplayName.textContent = displayName; - } - + if (userDisplayName) userDisplayName.textContent = displayName; var userName = document.getElementById('userName'); if (userName) { userName.textContent = (userInfo.first_name || '') + ' ' + (userInfo.last_name || ''); if (!userName.textContent.trim()) userName.textContent = userInfo.username || 'User'; } - var userEmail = document.getElementById('userEmail'); - if (userEmail && userInfo.email) { - userEmail.textContent = userInfo.email; - } + if (userEmail && userInfo.email) userEmail.textContent = userInfo.email; } } catch (e) { - console.error('include-auth-new.js: Error updating user info:', e); + console.error('include-auth-new.js: Error updating user info display:', e); } - }); -} else { - console.log('include-auth-new.js: No auth token found, showing login/register buttons'); - - // Create and inject CSS to show auth buttons - var style = document.createElement('style'); - style.textContent = ` - #authContainer, .auth-buttons { - display: flex !important; - visibility: visible !important; + + } else { + console.log('include-auth-new.js: Updating UI for logged-out user'); + // Inject CSS to show auth buttons and hide user menu + const styleId = 'auth-ui-style'; + let style = document.getElementById(styleId); + if (!style) { + style = document.createElement('style'); + style.id = styleId; + document.head.appendChild(style); } - - a[href="login.html"], a[href="register.html"], - .login-btn, .register-btn, .auth-btn.login-btn, .auth-btn.register-btn { - display: inline-block !important; - visibility: visible !important; - } - - #userMenu, .user-menu { - display: none !important; - visibility: hidden !important; - } - `; - document.head.appendChild(style); + style.textContent = ` + #authContainer, .auth-buttons { + display: flex !important; /* Use flex for container */ + visibility: visible !important; + } + a[href="login.html"], a[href="register.html"], + .login-btn, .register-btn, .auth-btn.login-btn, .auth-btn.register-btn { + display: inline-block !important; /* Use inline-block for buttons */ + visibility: visible !important; + } + #userMenu, .user-menu { + display: none !important; + visibility: hidden !important; + } + `; + } } -// Listen for changes to localStorage +// Immediately check auth state and update UI +updateAuthUI(); + +// Listen for changes to localStorage and update UI without reloading window.addEventListener('storage', function(event) { if (event.key === 'auth_token' || event.key === 'user_info') { - console.log('include-auth-new.js: Auth data changed, reloading page to update UI'); - window.location.reload(); + console.log(`include-auth-new.js: Storage event detected for ${event.key}. Updating UI.`); + updateAuthUI(); // Update UI instead of reloading + // window.location.reload(); // <-- Keep commented out / Remove permanently } }); diff --git a/frontend/index.html b/frontend/index.html index 53f5094..1fc2799 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -20,6 +20,8 @@ + + @@ -167,7 +169,7 @@
+
+ + +
@@ -398,6 +404,10 @@ Serial Numbers: -
+
+ Vendor: + - +

Warranty Details

@@ -503,6 +513,10 @@
+
+ + +
diff --git a/frontend/login.html b/frontend/login.html index 96fa0b4..285d8b4 100644 --- a/frontend/login.html +++ b/frontend/login.html @@ -11,6 +11,8 @@ + + diff --git a/frontend/manifest.json b/frontend/manifest.json new file mode 100644 index 0000000..82723fa --- /dev/null +++ b/frontend/manifest.json @@ -0,0 +1,15 @@ +{ + "name": "Warracker", + "short_name": "Warracker", + "icons": [ + { + "src": "img/favicon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "start_url": "./index.html", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#ffffff" +} \ No newline at end of file diff --git a/frontend/register.html b/frontend/register.html index 46ab107..6f82ec6 100644 --- a/frontend/register.html +++ b/frontend/register.html @@ -11,6 +11,8 @@ + + @@ -574,16 +576,9 @@ authMessage.className = 'auth-message'; authMessage.classList.add(type); authMessage.style.display = 'block'; - // Scroll to message authMessage.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } - - // Check for dark mode preference - const darkModeEnabled = localStorage.getItem('darkMode') === 'enabled'; - if (darkModeEnabled) { - document.body.classList.add('dark-mode'); - } }); // Theme initialization diff --git a/frontend/reset-password-request.html b/frontend/reset-password-request.html index 96c8ce9..9fdb5da 100644 --- a/frontend/reset-password-request.html +++ b/frontend/reset-password-request.html @@ -8,6 +8,8 @@ + + @@ -210,11 +212,7 @@ authMessage.style.display = 'block'; } - // Check for dark mode preference - const darkModeEnabled = localStorage.getItem('darkMode') === 'enabled'; - if (darkModeEnabled) { - document.body.classList.add('dark-mode'); - } + // No longer check for 'enabled' value. Theme is handled by theme-loader.js and below. }); // Theme initialization diff --git a/frontend/reset-password.html b/frontend/reset-password.html index 1e62325..862fda7 100644 --- a/frontend/reset-password.html +++ b/frontend/reset-password.html @@ -8,6 +8,8 @@ + + @@ -398,11 +400,7 @@ } } - // Check for dark mode preference - const darkModeEnabled = localStorage.getItem('darkMode') === 'enabled'; - if (darkModeEnabled) { - document.body.classList.add('dark-mode'); - } + // No longer check for 'enabled' value. Theme is handled by theme-loader.js and below. }); // Theme initialization diff --git a/frontend/script.js b/frontend/script.js index e965deb..d7a3a24 100644 --- a/frontend/script.js +++ b/frontend/script.js @@ -1,9 +1,13 @@ +// alert('script.js loaded!'); // Remove alert after confirming script loads +console.log('[DEBUG] script.js loaded and running'); + // Global variables let warranties = []; let currentTabIndex = 0; let tabContents = []; // Initialize as empty array let editMode = false; let currentWarrantyId = null; +let userPreferencePrefix = null; // <<< ADDED GLOBAL PREFIX VARIABLE let currentFilters = { status: 'all', tag: 'all', @@ -117,15 +121,18 @@ function setTheme(isDark) { // Initialization logic on DOMContentLoaded document.addEventListener('DOMContentLoaded', function() { + console.log('[DEBUG] Registering authStateReady event handler'); // ... other initialization ... // REMOVE call to undefined checkLoginStatus - Handled by auth.js // checkLoginStatus(); - - // Load warranties (assuming warrantiesList exists on this page) - if (document.getElementById('warrantiesList')) { - loadWarranties(); - } + + // --- DEFERRED WARRANTY LOADING --- + // Don't load warranties immediately. Wait for authentication. + // if (document.getElementById('warrantiesList')) { + // loadWarranties(); // <<< OLD CALL - REMOVED + // } + // --- END DEFERRED WARRANTY LOADING --- // Setup form submission (assuming addWarrantyForm exists) const form = document.getElementById('addWarrantyForm'); @@ -137,32 +144,32 @@ document.addEventListener('DOMContentLoaded', function() { // REMOVED setupSettingsMenu - Handled by auth.js // setupSettingsMenu(); - + // Initialize theme toggle state *after* DOM is loaded // ... (theme toggle init logic) ... - + // Setup view switcher (assuming view switcher elements exist) if (document.getElementById('gridViewBtn')) { // setupViewSwitcher(); // Removed undefined function loadViewPreference(); } - + // Setup filter controls (assuming filter controls exist) if (document.getElementById('filterControls')) { // setupFilterControls(); // Removed: function not defined populateTagFilter(); } - + // Initialize modal interactions // initializeModals(); // Removed: function not defined, handled by setupModalTriggers setupModalTriggers(); - + // Initialize Tag functionality (assuming tag elements exist) if (document.getElementById('tagSearchInput')) { initTagFunctionality(); loadTags(); } - + // Initialize form-specific lifetime checkbox handler const lifetimeCheckbox = document.getElementById('isLifetime'); if (lifetimeCheckbox) { @@ -170,35 +177,56 @@ document.addEventListener('DOMContentLoaded', function() { handleLifetimeChange({ target: lifetimeCheckbox }); // Initial check } - updateCurrencySymbols(); + // --- LOAD WARRANTIES AFTER AUTH --- + // Listen for an event from auth.js indicating authentication is complete and user context is ready. + // ** IMPORTANT: Replace 'authStateReady' with the actual event name fired by auth.js ** + window.addEventListener('authStateReady', async function handleAuthReady() { // <-- Make handler async + console.log('[DEBUG] authStateReady handler called'); + console.log("Auth state ready event received. Preparing preferences and warranties..."); + // Ensure this listener runs only once + window.removeEventListener('authStateReady', handleAuthReady); + + // Set prefix + userPreferencePrefix = getPreferenceKeyPrefix(); + console.log(`[authStateReady] Determined and stored global prefix: ${userPreferencePrefix}`); + + // Load preferences + await loadAndApplyUserPreferences(); + + // Load warranty data (fetches, processes, populates global array) + if (document.getElementById('warrantiesList')) { + console.log("[authStateReady] Loading warranty data..."); + await loadWarranties(); // Waits for fetch/process + console.log('[DEBUG] After loadWarranties, warranties array:', warranties); + } else { + console.log("[authStateReady] Warranties list element not found."); + } + + // Now that data and preferences are ready, apply view/currency and render via applyFilters + console.log("[authStateReady] 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(); + } + + }, { once: true }); // Use { once: true } as a fallback if removeEventListener isn't reliable across scripts + // --- 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() { - // 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; + // 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); } - - // 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); } // Variables @@ -219,7 +247,79 @@ const nextButton = document.querySelector('.next-tab'); // Keep these if needed const prevButton = document.querySelector('.prev-tab'); // Keep these if needed globally, otherwise might remove // --- Add near other DOM Element declarations --- -// ... existing code ... + // ... 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('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) { + formData.append('warranty_years', notesModalWarrantyObj.warranty_years || ''); + } + 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.serial_numbers && Array.isArray(notesModalWarrantyObj.serial_numbers)) { + notesModalWarrantyObj.serial_numbers.forEach(sn => { + if (sn && sn.trim() !== '') { + formData.append('serial_numbers', sn); + } + }); + } else if (!formData.has('serial_numbers')) { + formData.append('serial_numbers', JSON.stringify([])); + } + 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('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 || 'Failed to update notes', 'error'); + } + }; + } // Initialize form tabs function initFormTabs() { @@ -488,11 +588,29 @@ function updateSummary() { } // Warranty details - const purchaseDate = document.getElementById('purchaseDate')?.value; + const purchaseDateStr = document.getElementById('purchaseDate')?.value; const summaryPurchaseDate = document.getElementById('summary-purchase-date'); if (summaryPurchaseDate) { - summaryPurchaseDate.textContent = purchaseDate ? - new Date(purchaseDate).toLocaleDateString() : '-'; + 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 --- @@ -513,8 +631,8 @@ function updateSummary() { // Calculate and display expiration date const summaryExpirationDate = document.getElementById('summary-expiration-date'); - if (summaryExpirationDate && purchaseDate && warrantyYears) { - const expirationDate = new Date(purchaseDate); + if (summaryExpirationDate && purchaseDateStr && warrantyYears) { + const expirationDate = new Date(Date.UTC(parseInt(purchaseDateStr.split('-')[0]), parseInt(purchaseDateStr.split('-')[1]) - 1, parseInt(purchaseDateStr.split('-')[2]))); const yearsNum = parseFloat(warrantyYears); if (!isNaN(yearsNum)) { expirationDate.setFullYear(expirationDate.getFullYear() + Math.floor(yearsNum)); @@ -569,6 +687,10 @@ function updateSummary() { 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 @@ -621,8 +743,11 @@ async function exportWarranties() { 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; + // 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; }); } @@ -645,7 +770,7 @@ async function exportWarranties() { let csvContent = "data:text/csv;charset=utf-8,"; // Add headers - csvContent += "Product Name,Purchase Date,Warranty Period,Expiration Date,Status,Serial Numbers,Tags\n"; + csvContent += "Product Name,Purchase Date,Warranty Period,Expiration Date,Status,Serial Numbers,Tags,Vendor\n"; // Add data rows warrantiesToExport.forEach(warranty => { @@ -667,7 +792,8 @@ async function exportWarranties() { formatDate(new Date(warranty.expiration_date)), warranty.status || '', serialNumbers, - tags + tags, + warranty.vendor || '' ]; // Add row to CSV content @@ -692,18 +818,60 @@ async function exportWarranties() { } // Switch view of warranties list -function switchView(viewType) { +async function switchView(viewType) { // Added async console.log(`Switching to view: ${viewType}`); currentView = viewType; - // Get the appropriate key prefix based on user type const prefix = getPreferenceKeyPrefix(); - // --- BEGIN EDIT: Save to all relevant keys --- - localStorage.setItem(`${prefix}viewPreference`, viewType); // Keep this one - localStorage.setItem(`${prefix}defaultView`, viewType); // Add for consistency with settings load priority - localStorage.setItem(`${prefix}warrantyView`, viewType); // Add for consistency with settings legacy save - localStorage.setItem('viewPreference', viewType); // Keep general key - // --- END EDIT --- + 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.`); + } + + // --- BEGIN ADDED: Save preference to API --- + if (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 { + console.warn('Cannot save view preference to API: User not authenticated or auth module not loaded.'); + } + // --- END ADDED: Save preference to API --- // Make sure warrantiesList exists before modifying classes if (warrantiesList) { @@ -716,13 +884,12 @@ function switchView(viewType) { 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'); } - // Make sure the specific button exists - if (viewType === 'grid' && gridViewBtn) gridViewBtn.classList.add('active'); - if (viewType === 'list' && listViewBtn) listViewBtn.classList.add('active'); - if (viewType === 'table' && tableViewBtn) tableViewBtn.classList.add('active'); - // Show/hide table header only if it exists if (tableViewHeader) { tableViewHeader.classList.toggle('visible', viewType === 'table'); @@ -730,7 +897,7 @@ function switchView(viewType) { // Re-render warranties only if warrantiesList exists if (warrantiesList) { - renderWarranties(filterWarranties()); + renderWarranties(filterWarranties()); // Assuming filterWarranties() returns the correct array } } @@ -907,8 +1074,48 @@ function processWarrantyData(warranty) { const today = new Date(); today.setHours(0, 0, 0, 0); // Normalize today to midnight for accurate date comparisons - processedWarranty.purchaseDate = processedWarranty.purchase_date ? new Date(processedWarranty.purchase_date) : null; - processedWarranty.expirationDate = processedWarranty.expiration_date ? new Date(processedWarranty.expiration_date) : null; + // 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) { @@ -954,53 +1161,93 @@ function processAllWarranties() { } async function loadWarranties() { + // +++ REMOVED: Ensure Preferences are loaded FIRST (Now handled by authStateReady) +++ + // await loadAndApplyUserPreferences(); + // +++ Preferences Loaded +++ + try { - console.log('Loading warranties...'); + console.log('[DEBUG] Entered loadWarranties'); showLoading(); - // Get expiring soon days from user preferences if available + // 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 ${localStorage.getItem('auth_token')}` + 'Authorization': `Bearer ${token}` } }); if (prefsResponse.ok) { - const data = await prefsResponse.json(); - if (data && data.expiring_soon_days) { + 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 = data.expiring_soon_days; + expiringSoonDays = prefsData.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); - } + // 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); - // Continue with default value + // 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 --- // 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'); + console.log('[DEBUG] Early return: User not authenticated'); 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'); + console.log('[DEBUG] Early return: No auth token available'); renderEmptyState('Authentication error. Please log in again.'); hideLoading(); return; @@ -1017,22 +1264,23 @@ async function loadWarranties() { 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); - + console.log('[DEBUG] Received warranties from server:', data); + if (!Array.isArray(data)) { + console.error('[DEBUG] API did not return an array! Data:', data); + } // Process each warranty to calculate status and days remaining - warranties = data.map(warranty => { - return processWarrantyData(warranty); - }); - - console.log('Processed warranties:', warranties); + 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); if (warranties.length === 0) { console.log('No warranties found, showing empty state'); @@ -1043,10 +1291,10 @@ async function loadWarranties() { // Populate tag filter dropdown with tags from warranties populateTagFilter(); - applyFilters(); + // REMOVED: applyFilters(); // Now called from authStateReady after data and prefs are loaded } } catch (error) { - console.error('Error loading warranties:', error); + console.error('[DEBUG] Error loading warranties:', error); renderEmptyState('Error loading warranties. Please try again later.'); } finally { hideLoading(); @@ -1064,21 +1312,43 @@ function renderEmptyState(message = 'No warranties yet. Add your first warranty } 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())) { + // Input 'date' should now be a Date object created by processWarrantyData (or null) + if (!date || !(date instanceof Date) || isNaN(date.getTime())) { return 'N/A'; } - - return dateObj.toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric' - }); + + // 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}`; + } } async function renderWarranties(warrantiesToRender) { @@ -1188,6 +1458,7 @@ async function renderWarranties(warrantiesToRender) {
Warranty: ${warrantyYearsText}
Expires: ${expirationDateText}
${warranty.purchase_price ? `
Price: ${symbol}${parseFloat(warranty.purchase_price).toFixed(2)}
` : ''} + ${warranty.vendor ? `
Vendor: ${warranty.vendor}
` : ''} ${validSerialNumbers.length > 0 ? `
Serial Numbers: @@ -1241,6 +1512,7 @@ async function renderWarranties(warrantiesToRender) {
Warranty: ${warrantyYearsText}
Expires: ${expirationDateText}
${warranty.purchase_price ? `
Price: ${symbol}${parseFloat(warranty.purchase_price).toFixed(2)}
` : ''} + ${warranty.vendor ? `
Vendor: ${warranty.vendor}
` : ''} ${validSerialNumbers.length > 0 ? `
Serial Numbers: @@ -1374,6 +1646,18 @@ function filterWarranties() { 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; }); @@ -1417,8 +1701,13 @@ function applyFilters() { 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) { + if (!productNameMatch && !tagMatch && !notesMatch && !vendorMatch && !serialNumberMatch) { return false; } } @@ -1441,6 +1730,7 @@ function openEditModal(warranty) { document.getElementById('editPurchaseDate').value = warranty.purchase_date.split('T')[0]; document.getElementById('editWarrantyYears').value = warranty.warranty_years; document.getElementById('editPurchasePrice').value = warranty.purchase_price || ''; + document.getElementById('editVendor').value = warranty.vendor || ''; // Clear existing serial number inputs const editSerialNumbersContainer = document.getElementById('editSerialNumbersContainer'); @@ -1798,7 +2088,9 @@ function submitForm(event) { resetAddWarrantyWizard(); // Reset the wizard form // --- End modification --- - loadWarranties(); // Reload the list + loadWarranties().then(() => { + applyFilters(); + }); // Reload the list and update UI }) .catch(error => { hideLoadingSpinner(); @@ -1816,8 +2108,8 @@ document.addEventListener('DOMContentLoaded', function() { // Load warranties (might need checks if warrantiesList doesn't always exist) if (warrantiesList) { - loadWarranties(); - loadViewPreference(); // Load user's preferred view + // REMOVED: loadWarranties(); // Now called after authStateReady + // REMOVED: loadViewPreference(); // Now called after authStateReady loadTags(); // Load tags for the form initTagFunctionality(); // Initialize tag search/selection } @@ -1873,7 +2165,7 @@ document.addEventListener('DOMContentLoaded', function() { // Load preferences (if needed for things other than theme) // loadPreferences(); // Consider if needed - updateCurrencySymbols(); + // REMOVED: updateCurrencySymbols(); // Now called after authStateReady }); // Add this function to handle edit tab functionality @@ -2773,7 +3065,7 @@ function setupUIEventListeners() { if (confirmDeleteBtn) confirmDeleteBtn.addEventListener('click', deleteWarranty); // Load saved view preference - loadViewPreference(); + // loadViewPreference(); // Disabled: now called after authStateReady } // Function to show loading spinner @@ -2821,7 +3113,16 @@ function deleteWarranty() { hideLoadingSpinner(); showToast('Warranty deleted successfully', 'success'); closeModals(); - loadWarranties(); + + // --- 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(); @@ -2937,6 +3238,10 @@ function saveWarranty() { 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 + // Get auth token const token = localStorage.getItem('auth_token'); if (!token) { @@ -2966,44 +3271,15 @@ function saveWarranty() { hideLoadingSpinner(); showToast('Warranty updated successfully', 'success'); closeModals(); - // Update the notes in the card immediately if present - if (typeof currentWarrantyId !== 'undefined' && currentWarrantyId !== null) { - const card = document.querySelector(`.warranty-card .edit-btn[data-id="${currentWarrantyId}"]`); - if (card) { - const cardElement = card.closest('.warranty-card'); - if (cardElement) { - // Remove old notes button if present - const oldNotesBtn = cardElement.querySelector('.view-notes-btn'); - if (oldNotesBtn) oldNotesBtn.remove(); - // Get the new notes value - const newNotes = document.getElementById('editNotes').value; - if (newNotes && newNotes.trim() !== '') { - // Add the button if not present - let notesBtn = cardElement.querySelector('.view-notes-btn'); - if (!notesBtn) { - const btn = document.createElement('button'); - btn.className = 'btn btn-secondary btn-sm view-notes-btn'; - btn.setAttribute('data-id', currentWarrantyId); - btn.style.margin = '10px 0 0 0'; - btn.innerHTML = ' View Notes'; - btn.addEventListener('click', () => showNotesModal(newNotes)); - // Insert after tags row if present, else at end - const tagsRow = cardElement.querySelector('.tags-row'); - if (tagsRow && tagsRow.nextSibling) { - cardElement.insertBefore(btn, tagsRow.nextSibling); - } else { - cardElement.appendChild(btn); - } - } - } else { - // If notes are empty, ensure the button is removed - const notesBtn = cardElement.querySelector('.view-notes-btn'); - if (notesBtn) notesBtn.remove(); - } - } + // Instantly reload and re-render the warranties list + loadWarranties().then(() => { + applyFilters(); + // 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'; } - } - loadWarranties(); // Still reload to ensure all data is fresh + }); }) .catch(error => { hideLoadingSpinner(); @@ -3272,7 +3548,14 @@ async function handleImport(file) { await loadTags(); // Fetch the updated list of all tags // ***** END FIX ***** - loadWarranties(); // Refresh the list + // 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(); + + // Force a UI refresh by reapplying filters + applyFilters(); } else { showToast(`Import failed: ${result.error || 'Unknown error'}`, 'error'); if (result.errors) { @@ -3305,6 +3588,7 @@ window.addEventListener('storage', (event) => { `${prefix}viewPreference` ]; + // Check for view preference changes if (viewKeys.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 @@ -3317,6 +3601,28 @@ window.addEventListener('storage', (event) => { 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 === `${prefix}currencySymbol` && event.newValue) { + console.log(`Storage event detected for ${prefix}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 --- @@ -3437,6 +3743,10 @@ function showNotesModal(notes, warrantyOrId = null) { 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: { @@ -3473,8 +3783,10 @@ function showNotesModal(notes, warrantyOrId = null) { } // --- End Updated UI logic --- - // Refresh warranties list to update the card UI state (e.g., show/hide notes link) - loadWarranties(); + // Refresh warranties list and THEN update UI + await loadWarranties(); // Wait for data refresh + applyFilters(); // Re-render the list with updated data + } catch (e) { hideLoadingSpinner(); console.error("Error updating note:", e); // Log the error @@ -3494,24 +3806,44 @@ function showNotesModal(notes, warrantyOrId = null) { // Utility to get currency symbol from preferences/localStorage function getCurrencySymbol() { - const prefix = getPreferenceKeyPrefix(); - console.log(`[getCurrencySymbol] Using prefix: ${prefix}`); // Log prefix + // 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); // Log raw string + console.log(`[getCurrencySymbol] Read prefsString for ${prefix}preferences:`, prefsString); if (prefsString) { const prefs = JSON.parse(prefsString); - // Use the symbol from prefs if it exists, otherwise keep the default 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 + // Keep the default '$' symbol in case of error parsing the object } - console.log(`[getCurrencySymbol] Returning symbol: ${symbol}`); // Log final symbol + + console.log(`[getCurrencySymbol] Returning symbol (default or from object): ${symbol}`); return symbol; } @@ -3534,4 +3866,84 @@ window.addEventListener('storage', function(e) { console.log(`Storage event detected for ${prefix}preferences. Updating currency symbols.`); updateCurrencySymbols(); } -}); \ No newline at end of file +}); + +// +++ NEW FUNCTION TO LOAD PREFS AND SAVE TO LOCALSTORAGE +++ +async function loadAndApplyUserPreferences() { + // 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}`); + + if (window.auth && window.auth.isAuthenticated()) { + const token = window.auth.getToken(); + if (!token) { + console.error('[Prefs Loader] Cannot load preferences: No auth token found.'); + return; // Exit if no token + } + + try { + 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.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 +++ diff --git a/frontend/settings-new.html b/frontend/settings-new.html index 15805ad..2747295 100644 --- a/frontend/settings-new.html +++ b/frontend/settings-new.html @@ -14,6 +14,8 @@ + + @@ -236,6 +238,23 @@
+ +
+
+
+ +

Choose how dates are displayed

+
+ +
+
diff --git a/frontend/settings-new.js b/frontend/settings-new.js index a69d12e..440684d 100644 --- a/frontend/settings-new.js +++ b/frontend/settings-new.js @@ -47,6 +47,9 @@ const currencySymbolInput = document.getElementById('currencySymbol'); const currencySymbolSelect = document.getElementById('currencySymbolSelect'); const currencySymbolCustom = document.getElementById('currencySymbolCustom'); +// Add dateFormatSelect near other DOM element declarations if not already there +const dateFormatSelect = document.getElementById('dateFormat'); + /** * Set theme (dark/light) - Unified and persistent * @param {boolean} isDark - Whether to use dark mode @@ -64,10 +67,10 @@ function setTheme(isDark) { localStorage.setItem('darkMode', isDark); // Sync both toggles if present if (typeof darkModeToggle !== 'undefined' && darkModeToggle) { - darkModeToggle.checked = isDarkMode; + darkModeToggle.checked = isDark; } if (typeof darkModeToggleSetting !== 'undefined' && darkModeToggleSetting) { - darkModeToggleSetting.checked = isDarkMode; + darkModeToggleSetting.checked = isDark; } // Also update user_preferences.theme for backward compatibility try { @@ -87,23 +90,46 @@ function setTheme(isDark) { * Initialize dark mode toggle and synchronize state */ function initDarkModeToggle() { - // Always check the single source of truth in localStorage + // Always check the single source of truth in localStorage (fallback) const isDarkMode = localStorage.getItem('darkMode') === 'true'; // Apply theme to DOM if not already set document.documentElement.setAttribute('data-theme', isDarkMode ? 'dark' : 'light'); document.body.classList.toggle('dark-mode', isDarkMode); - // Sync both toggles + // Sync both toggles and add unified handler + const syncToggles = (val) => { + if (typeof darkModeToggle !== 'undefined' && darkModeToggle) darkModeToggle.checked = val; + if (typeof darkModeToggleSetting !== 'undefined' && darkModeToggleSetting) darkModeToggleSetting.checked = val; + }; + syncToggles(isDarkMode); + // Handler to update theme, localStorage, backend + const handleToggle = async function(checked) { + setTheme(checked); + syncToggles(checked); + // Save to backend if authenticated + if (window.auth && window.auth.isAuthenticated && window.auth.isAuthenticated()) { + try { + let prefs = {}; + const storedPrefs = localStorage.getItem('user_preferences'); + if (storedPrefs) prefs = JSON.parse(storedPrefs); + prefs.theme = checked ? 'dark' : 'light'; + await fetch('/api/auth/preferences', { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${window.auth.getToken()}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(prefs) + }); + } catch (e) { + console.warn('Failed to save dark mode to backend:', e); + } + } + }; if (typeof darkModeToggle !== 'undefined' && darkModeToggle) { - darkModeToggle.checked = isDarkMode; - darkModeToggle.addEventListener('change', function() { - setTheme(this.checked); - }); + darkModeToggle.onchange = function() { handleToggle(this.checked); }; } if (typeof darkModeToggleSetting !== 'undefined' && darkModeToggleSetting) { - darkModeToggleSetting.checked = isDarkMode; - darkModeToggleSetting.addEventListener('change', function() { - setTheme(this.checked); - }); + darkModeToggleSetting.onchange = function() { handleToggle(this.checked); }; } } @@ -126,6 +152,12 @@ document.addEventListener('DOMContentLoaded', function() { // Initialize dark mode toggle initDarkModeToggle(); + // Clear dark mode preference on logout for privacy + if (window.auth && window.auth.onLogout) { + window.auth.onLogout(() => { + localStorage.removeItem('darkMode'); + }); + } // REMOVED initSettingsMenu() call - Handled by auth.js @@ -361,22 +393,34 @@ async function loadUserData() { } } - // Update localStorage + // Update localStorage ONLY if data has changed const currentUser = window.auth.getCurrentUser(); let first_name = userData.first_name; let last_name = userData.last_name; - if (!last_name) first_name = ''; + if (!last_name) first_name = ''; // Reset first name if last name is empty + const updatedUser = { - ...(currentUser || {}), // Handle case where currentUser might be null - first_name, - last_name, - // Ensure email and username are preserved if they existed + ...(currentUser || {}), // Preserve existing fields + first_name, // Update first name + last_name, // Update last name + // Preserve other essential fields from fetched data if currentUser was null email: currentUser ? currentUser.email : userData.email, username: currentUser ? currentUser.username : userData.username, is_admin: currentUser ? currentUser.is_admin : userData.is_admin, id: currentUser ? currentUser.id : userData.id }; - localStorage.setItem('user_info', JSON.stringify(updatedUser)); + + // Convert both to JSON strings for reliable comparison + const currentUserString = JSON.stringify(currentUser); + const updatedUserString = JSON.stringify(updatedUser); + + if (currentUserString !== updatedUserString) { + console.log('User data changed, updating localStorage.'); + localStorage.setItem('user_info', updatedUserString); + } else { + console.log('User data from API matches localStorage, skipping update.'); + } + // localStorage.setItem('user_info', JSON.stringify(updatedUser)); // OLD LINE } else { const errorData = await response.json().catch(() => ({ message: 'Failed to parse error response' })); console.warn('API error fetching user data:', errorData.message); @@ -425,198 +469,194 @@ function getPreferenceKeyPrefix() { /** * Load user preferences */ -function loadPreferences() { +async function loadPreferences() { console.log('Loading preferences...'); - showLoading(); - - // Get the appropriate key prefix based on user type const prefix = getPreferenceKeyPrefix(); - console.log(`Loading preferences with prefix: ${prefix}`); - - // First check if we're in dark mode by checking the body class - const isDarkMode = document.body.classList.contains('dark-mode'); - - // Set the toggle state based on the current theme - if (darkModeToggle) { - darkModeToggle.checked = isDarkMode; - } - - if (darkModeToggleSetting) { - darkModeToggleSetting.checked = isDarkMode; - } - - // --- BEGIN EDIT: Load Default View with Priority --- - let defaultViewLoaded = false; - if (defaultViewSelect) { - const userSpecificView = localStorage.getItem(`${prefix}defaultView`); - const generalView = localStorage.getItem('viewPreference'); - const legacyWarrantyView = localStorage.getItem(`${prefix}warrantyView`); // Check legacy key + console.log('Loading preferences with prefix:', prefix); - if (userSpecificView) { - defaultViewSelect.value = userSpecificView; - defaultViewLoaded = true; - console.log(`Loaded default view from ${prefix}defaultView:`, userSpecificView); - } else if (generalView) { - defaultViewSelect.value = generalView; - defaultViewLoaded = true; - console.log('Loaded default view from viewPreference:', generalView); - } else if (legacyWarrantyView) { - defaultViewSelect.value = legacyWarrantyView; - defaultViewLoaded = true; - console.log(`Loaded default view from legacy ${prefix}warrantyView:`, legacyWarrantyView); - } - } - // --- END EDIT --- - - // Load other preferences from localStorage using the appropriate prefix (only if default view not loaded yet) - if (!defaultViewLoaded) { + let darkModeFromAPI = null; + let apiPrefs = null; + // Try to load preferences from backend if authenticated + if (window.auth && window.auth.isAuthenticated && window.auth.isAuthenticated()) { try { - const userPrefs = localStorage.getItem(`${prefix}preferences`); - if (userPrefs) { - const preferences = JSON.parse(userPrefs); - - // Default view preference (load only if not loaded above) - if (defaultViewSelect && preferences.default_view && !defaultViewLoaded) { - defaultViewSelect.value = preferences.default_view; - defaultViewLoaded = true; // Mark as loaded - console.log(`Loaded default view from ${prefix}preferences object:`, preferences.default_view); + const response = await fetch('/api/auth/preferences', { + headers: { + 'Authorization': `Bearer ${window.auth.getToken()}` } - - // Email notifications preference - if (emailNotificationsToggle && typeof preferences.email_notifications !== 'undefined') { // Check for undefined - emailNotificationsToggle.checked = preferences.email_notifications; - } - - // Expiring soon days preference - if (expiringSoonDaysInput && preferences.expiring_soon_days) { - expiringSoonDaysInput.value = preferences.expiring_soon_days; - } - - // Notification frequency preference - if (notificationFrequencySelect && preferences.notification_frequency) { - notificationFrequencySelect.value = preferences.notification_frequency; - } - - // Notification time preference - if (notificationTimeInput && preferences.notification_time) { - notificationTimeInput.value = preferences.notification_time; - } - - // Timezone preference - if (timezoneSelect && preferences.timezone) { - timezoneSelect.value = preferences.timezone; - } - - // Currency symbol preference - let symbol = '$'; - if (preferences && preferences.currency_symbol) { - symbol = preferences.currency_symbol; - } - if (currencySymbolSelect && currencySymbolCustom) { - // If symbol is in the dropdown, select it; else, select 'other' and show custom - const found = Array.from(currencySymbolSelect.options).some(opt => opt.value === symbol); - if (found) { - currencySymbolSelect.value = symbol; - currencySymbolCustom.style.display = 'none'; - currencySymbolCustom.value = ''; - } else { - currencySymbolSelect.value = 'other'; - currencySymbolCustom.style.display = ''; - currencySymbolCustom.value = symbol; - } + }); + if (response.ok) { + apiPrefs = await response.json(); + if (apiPrefs && apiPrefs.theme) { + darkModeFromAPI = apiPrefs.theme === 'dark'; + setTheme(darkModeFromAPI); + // Sync localStorage + localStorage.setItem('darkMode', darkModeFromAPI); } } } catch (e) { - console.error('Error loading preferences from localStorage:', e); - // Fallback to default '$' using the correct elements - if (currencySymbolSelect) currencySymbolSelect.value = '$'; - if (currencySymbolCustom) currencySymbolCustom.style.display = 'none'; + console.warn('Failed to load preferences from backend:', e); } - } else { - // Fallback to default '$' if no localStorage and defaultViewLoaded is true - if (currencySymbolSelect) currencySymbolSelect.value = '$'; - if (currencySymbolCustom) currencySymbolCustom.style.display = 'none'; + } + // Fallback: use localStorage if not authenticated or API fails + if (darkModeFromAPI === null) { + const storedDarkMode = localStorage.getItem('darkMode') === 'true'; + setTheme(storedDarkMode); + } + // --- Load Date Format --- Add this section + const storedDateFormat = localStorage.getItem('dateFormat'); + if (storedDateFormat && dateFormatSelect) { + dateFormatSelect.value = storedDateFormat; + console.log(`Loaded dateFormat from localStorage: ${storedDateFormat}`); + } else if (dateFormatSelect) { + dateFormatSelect.value = 'MDY'; // Default if not found + console.log('dateFormat not found in localStorage, defaulting to MDY'); + } + // --- End Date Format Section --- + + // Default View + const storedView = localStorage.getItem(`${prefix}defaultView`); + if (storedView && defaultViewSelect) { + defaultViewSelect.value = storedView; + console.log(`Loaded default view from ${prefix}defaultView: ${storedView}`); + } else if (defaultViewSelect) { + defaultViewSelect.value = 'grid'; // Default + console.log(`${prefix}defaultView not found, defaulting view to grid`); } - // Load preferences from API (API data should override if available, except maybe for default view if already loaded) - fetch('/api/auth/preferences', { - method: 'GET', - headers: { - 'Authorization': 'Bearer ' + localStorage.getItem('auth_token'), - 'Content-Type': 'application/json' - } - }) - .then(response => { - if (!response.ok) { - throw new Error('Failed to load preferences from API'); - } - return response.json(); - }) - .then(data => { - console.log('Preferences loaded from API:', data); - - // API returns preferences directly, not nested under a 'preferences' key - const apiPrefs = data; - - // Update UI with API preferences - // Default View: Only update if not already loaded from higher priority localStorage keys - if (defaultViewSelect && apiPrefs.default_view && !defaultViewLoaded) { - defaultViewSelect.value = apiPrefs.default_view; - console.log('Loaded default view from API:', apiPrefs.default_view); - } - - // Other preferences always updated from API if available - if (emailNotificationsToggle && typeof apiPrefs.email_notifications !== 'undefined') { // Check for undefined - emailNotificationsToggle.checked = apiPrefs.email_notifications; - } - - if (expiringSoonDaysInput && apiPrefs.expiring_soon_days) { - expiringSoonDaysInput.value = apiPrefs.expiring_soon_days; - } - - if (notificationFrequencySelect && apiPrefs.notification_frequency) { - notificationFrequencySelect.value = apiPrefs.notification_frequency; - } - - if (notificationTimeInput && apiPrefs.notification_time) { - notificationTimeInput.value = apiPrefs.notification_time; - } - - if (timezoneSelect && apiPrefs.timezone) { - console.log('API provided timezone:', apiPrefs.timezone); - // Will be populated once timezones are loaded - setTimeout(() => { - if (timezoneSelect.options.length > 1) { - timezoneSelect.value = apiPrefs.timezone; - console.log('Applied timezone from API:', apiPrefs.timezone, 'Current select value:', timezoneSelect.value); - } - }, 500); - } - - // Currency symbol from API - let apiSymbol = data.currency_symbol || '$'; - if (currencySymbolSelect && currencySymbolCustom) { - const found = Array.from(currencySymbolSelect.options).some(opt => opt.value === apiSymbol); - if (found) { - currencySymbolSelect.value = apiSymbol; - currencySymbolCustom.style.display = 'none'; - currencySymbolCustom.value = ''; + // Currency Symbol + const storedCurrency = localStorage.getItem(`${prefix}currencySymbol`); + if (storedCurrency) { + if (currencySymbolSelect) { + // Check if the stored symbol is a standard option + const standardOption = Array.from(currencySymbolSelect.options).find(opt => opt.value === storedCurrency); + if (standardOption) { + currencySymbolSelect.value = storedCurrency; + if (currencySymbolCustom) currencySymbolCustom.style.display = 'none'; } else { + // It's a custom symbol currencySymbolSelect.value = 'other'; - currencySymbolCustom.style.display = ''; - currencySymbolCustom.value = apiSymbol; + if (currencySymbolCustom) { + currencySymbolCustom.value = storedCurrency; + currencySymbolCustom.style.display = 'inline-block'; + } } + console.log(`Loaded currency symbol from ${prefix}currencySymbol: ${storedCurrency}`); } - - // Store in localStorage with the appropriate prefix - localStorage.setItem(`${prefix}preferences`, JSON.stringify(apiPrefs)); - }) - .catch(error => { - console.error('Error loading preferences from API:', error); - }) - .finally(() => { - hideLoading(); - }); + } else { + // Default to '$' if nothing stored + if (currencySymbolSelect) currencySymbolSelect.value = '$'; + if (currencySymbolCustom) currencySymbolCustom.style.display = 'none'; + console.log(`${prefix}currencySymbol not found, defaulting to $`); + } + + // Expiring Soon Days + const storedExpiringDays = localStorage.getItem(`${prefix}expiringSoonDays`); + if (storedExpiringDays && expiringSoonDaysInput) { + expiringSoonDaysInput.value = storedExpiringDays; + console.log(`Loaded expiring soon days from ${prefix}expiringSoonDays: ${storedExpiringDays}`); + } else if (expiringSoonDaysInput) { + expiringSoonDaysInput.value = 30; // Default + console.log(`${prefix}expiringSoonDays not found, defaulting to 30`); + } + + // Now, try fetching preferences from API to override/confirm + if (window.auth && window.auth.isAuthenticated()) { + try { + const token = window.auth.getToken(); + const response = await fetch('/api/auth/preferences', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.ok) { + const apiPrefs = await response.json(); + console.log('Preferences loaded from API:', apiPrefs); + + // Update UI elements with API data where available + if (apiPrefs.default_view && defaultViewSelect) { + // Only update if different from localStorage value (or if localStorage was empty) + const storedView = localStorage.getItem(`${prefix}defaultView`) || 'grid'; // Default if null + if (apiPrefs.default_view !== storedView) { + console.log(`API default_view (${apiPrefs.default_view}) differs from localStorage (${storedView}). Updating UI.`); + defaultViewSelect.value = apiPrefs.default_view; + } + } + // --- MODIFIED CURRENCY SYMBOL HANDLING --- + const storedCurrency = localStorage.getItem(`${prefix}currencySymbol`); // Get localStorage value again for comparison + if (apiPrefs.currency_symbol && currencySymbolSelect) { + // Only update UI from API if the API value is different from what was in localStorage + // Or if localStorage didn't have a value initially + if (!storedCurrency || apiPrefs.currency_symbol !== storedCurrency) { + console.log(`API currency_symbol (${apiPrefs.currency_symbol}) differs from localStorage (${storedCurrency}). Updating UI.`); + // Logic to handle standard vs custom symbol from API + const standardOption = Array.from(currencySymbolSelect.options).find(opt => opt.value === apiPrefs.currency_symbol); + if (standardOption) { + currencySymbolSelect.value = apiPrefs.currency_symbol; + if (currencySymbolCustom) currencySymbolCustom.style.display = 'none'; + } else { + currencySymbolSelect.value = 'other'; + if (currencySymbolCustom) { + currencySymbolCustom.value = apiPrefs.currency_symbol; + currencySymbolCustom.style.display = 'inline-block'; + } + } + } else { + console.log(`API currency_symbol (${apiPrefs.currency_symbol}) matches localStorage (${storedCurrency}). Skipping UI update.`); + } + } + // --- END MODIFIED CURRENCY SYMBOL HANDLING --- + if (apiPrefs.expiring_soon_days && expiringSoonDaysInput) { + // Only update if different from localStorage value (or if localStorage was empty) + const storedExpiringDays = localStorage.getItem(`${prefix}expiringSoonDays`) || '30'; // Default if null + if (String(apiPrefs.expiring_soon_days) !== storedExpiringDays) { + console.log(`API expiring_soon_days (${apiPrefs.expiring_soon_days}) differs from localStorage (${storedExpiringDays}). Updating UI.`); + expiringSoonDaysInput.value = apiPrefs.expiring_soon_days; + } + } + + // --- Update Date Format from API Prefs --- Add this check + const storedDateFormat = localStorage.getItem('dateFormat') || 'MDY'; // Default if null + if (apiPrefs.date_format && dateFormatSelect) { + if (apiPrefs.date_format !== storedDateFormat) { + console.log(`API date_format (${apiPrefs.date_format}) differs from localStorage (${storedDateFormat}). Updating UI.`); + dateFormatSelect.value = apiPrefs.date_format; + } + } + // --- End Date Format Check --- + + // Update Email Settings from API + if (emailNotificationsToggle) { + emailNotificationsToggle.checked = apiPrefs.email_notifications !== false; // Default true if null/undefined + } + if (notificationFrequencySelect && apiPrefs.notification_frequency) { + notificationFrequencySelect.value = apiPrefs.notification_frequency; + } + if (notificationTimeInput && apiPrefs.notification_time) { + notificationTimeInput.value = apiPrefs.notification_time.substring(0, 5); // HH:MM format + } + // Load and set timezone from API + if (timezoneSelect && apiPrefs.timezone) { + console.log('API provided timezone:', apiPrefs.timezone); + // Ensure the option exists before setting + if (Array.from(timezoneSelect.options).some(option => option.value === apiPrefs.timezone)) { + timezoneSelect.value = apiPrefs.timezone; + console.log('Applied timezone from API:', timezoneSelect.value, 'Current select value:', timezoneSelect.value); + } else { + console.warn(`Timezone '${apiPrefs.timezone}' from API not found in dropdown.`); + } + } else { + console.log('No timezone preference found in API or timezone select element missing.'); + } + + } else { + const errorData = await response.json().catch(() => ({})); + console.warn(`Failed to load preferences from API: ${response.status}`, errorData.message || ''); + } + } catch (error) { + console.error('Error fetching preferences from API:', error); + } + } } /** @@ -945,109 +985,80 @@ async function saveProfile() { /** * Save user preferences */ -function savePreferences() { - showLoading(); - - try { - // Get the appropriate key prefix based on user type - const prefix = getPreferenceKeyPrefix(); - console.log(`Saving preferences with prefix: ${prefix}`); - - // Get values - const isDarkMode = darkModeToggleSetting.checked; - const defaultView = defaultViewSelect.value; - const emailNotifications = emailNotificationsToggle.checked; - const expiringSoonDays = parseInt(expiringSoonDaysInput.value); - const notificationFrequency = notificationFrequencySelect.value; - const notificationTime = notificationTimeInput.value; - const timezone = timezoneSelect.value; - let currencySymbol = '$'; - if (currencySymbolSelect && currencySymbolSelect.value === 'other') { - currencySymbol = currencySymbolCustom && currencySymbolCustom.value ? currencySymbolCustom.value : '$'; - } else if (currencySymbolSelect) { +async function savePreferences() { + console.log('Saving preferences...'); + const prefix = getPreferenceKeyPrefix(); + + // --- Prepare data to save --- Add dateFormat and dark mode + const preferencesToSave = { + default_view: defaultViewSelect ? defaultViewSelect.value : 'grid', + expiring_soon_days: expiringSoonDaysInput ? parseInt(expiringSoonDaysInput.value) : 30, + date_format: dateFormatSelect ? dateFormatSelect.value : 'MDY', + theme: (localStorage.getItem('darkMode') === 'true') ? 'dark' : 'light', + }; + + // Handle currency symbol (standard or custom) + let currencySymbol = '$'; // Default + if (currencySymbolSelect) { + if (currencySymbolSelect.value === 'other' && currencySymbolCustom) { + currencySymbol = currencySymbolCustom.value.trim() || '$'; // Use custom or default to $ if empty + } else { currencySymbol = currencySymbolSelect.value; } - - // Validate inputs - if (isNaN(expiringSoonDays) || expiringSoonDays < 1 || expiringSoonDays > 365) { - showToast('Expiring soon days must be between 1 and 365', 'error'); + } + preferencesToSave.currency_symbol = currencySymbol; + // --- End data preparation --- + + // +++ ADDED DEBUG LOGGING +++ + console.log(`[SavePrefs Debug] Currency Select Value: ${currencySymbolSelect ? currencySymbolSelect.value : 'N/A'}`); + console.log(`[SavePrefs Debug] Custom Input Value: ${currencySymbolCustom ? currencySymbolCustom.value : 'N/A'}`); + console.log(`[SavePrefs Debug] Final currencySymbol value determined: ${currencySymbol}`); + // +++ END DEBUG LOGGING +++ + + // Save Dark Mode separately (using the single source of truth) + const isDark = darkModeToggleSetting ? darkModeToggleSetting.checked : false; + setTheme(isDark); + console.log(`Saved dark mode: ${isDark}`); + + // Save simple preferences to localStorage immediately + localStorage.setItem('dateFormat', preferencesToSave.date_format); // Added + localStorage.setItem(`${prefix}defaultView`, preferencesToSave.default_view); + localStorage.setItem(`${prefix}currencySymbol`, preferencesToSave.currency_symbol); + localStorage.setItem(`${prefix}expiringSoonDays`, preferencesToSave.expiring_soon_days); + + console.log('Preferences saved to localStorage (prefix:', prefix, '):', preferencesToSave); + console.log(`Value of dateFormat in localStorage: ${localStorage.getItem('dateFormat')}`); + + // Try saving to API + if (window.auth && window.auth.isAuthenticated()) { + try { + showLoading(); + const token = window.auth.getToken(); + const response = await fetch('/api/auth/preferences', { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(preferencesToSave) + }); + hideLoading(); - return; - } - - if (!timezone) { - showToast('Please select a timezone', 'error'); - hideLoading(); - return; - } - - // Create preferences object - const preferences = { - theme: isDarkMode ? 'dark' : 'light', - default_view: defaultView, - email_notifications: emailNotifications, - expiring_soon_days: expiringSoonDays, - notification_frequency: notificationFrequency, - notification_time: notificationTime, - timezone: timezone, - currency_symbol: currencySymbol - }; - - // Save to API - fetch('/api/auth/preferences', { - method: 'PUT', - headers: { - 'Authorization': 'Bearer ' + localStorage.getItem('auth_token'), - 'Content-Type': 'application/json' - }, - body: JSON.stringify(preferences) - }) - .then(response => { - if (!response.ok) { - throw new Error('Failed to save preferences'); + if (response.ok) { + showToast('Preferences saved successfully.', 'success'); + console.log('Preferences successfully saved to API.'); + } else { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.message || `Failed to save preferences to API: ${response.status}`); } - return response.json(); - }) - .then(data => { - // Save to localStorage with the appropriate prefix - localStorage.setItem(`${prefix}preferences`, JSON.stringify(data)); - - // Also save individual preferences for backward compatibility and general preference - localStorage.setItem(`${prefix}defaultView`, defaultView); - localStorage.setItem(`${prefix}warrantyView`, defaultView); // Keep saving legacy key for now - localStorage.setItem(`${prefix}emailNotifications`, emailNotifications); - localStorage.setItem('viewPreference', defaultView); // --- EDIT: Save general key --- - - // Save the dark mode setting for the current user type - localStorage.setItem(`${prefix}darkMode`, isDarkMode); - - // Apply theme immediately - document.body.classList.toggle('dark-mode', isDarkMode); - - showToast('Preferences saved successfully', 'success'); - }) - .catch(error => { - console.error('Error saving preferences:', error); - showToast('Error saving preferences', 'error'); - - // Save to localStorage as fallback with the appropriate prefix - localStorage.setItem(`${prefix}theme`, isDarkMode ? 'dark' : 'light'); - localStorage.setItem(`${prefix}defaultView`, defaultView); - localStorage.setItem(`${prefix}warrantyView`, defaultView); // Keep saving legacy key for now - localStorage.setItem(`${prefix}emailNotifications`, emailNotifications); - localStorage.setItem(`${prefix}darkMode`, isDarkMode); - localStorage.setItem('viewPreference', defaultView); // --- EDIT: Save general key --- - - // Apply theme even if API save fails - document.body.classList.toggle('dark-mode', isDarkMode); - }) - .finally(() => { + } catch (error) { hideLoading(); - }); - } catch (error) { - console.error('Error in savePreferences:', error); - showToast('Error saving preferences', 'error'); - hideLoading(); + console.error('Error saving preferences to API:', error); + showToast(`Preferences saved locally, but failed to sync with server: ${error.message}`, 'warning'); + } + } else { + // No auth, just show local save success + showToast('Preferences saved locally.', 'success'); } } @@ -2910,31 +2921,80 @@ function saveEmailSettings() { // --- Add Storage Event Listener for Real-time Sync --- window.addEventListener('storage', (event) => { - const prefix = getPreferenceKeyPrefix(); - const viewKeys = [ - `${prefix}defaultView`, - 'viewPreference', - `${prefix}warrantyView`, - // Add `${prefix}viewPreference` if still used/relevant - `${prefix}viewPreference` - ]; + console.log(`[Settings Storage Listener] Event received: key=${event.key}, newValue=${event.newValue}`); // Log all events - if (viewKeys.includes(event.key) && event.newValue) { - console.log(`Storage event detected for view preference (${event.key}) in settings. New value: ${event.newValue}`); - // Ensure the dropdown element exists and the value is different - if (defaultViewSelect && defaultViewSelect.value !== event.newValue) { - // Check if the new value is a valid option in the select + const prefix = getPreferenceKeyPrefix(); + const targetKey = `${prefix}defaultView`; + + // Only react to changes in the specific default view key for the current user type + if (event.key === targetKey) { // Check key match first + console.log(`[Settings Storage Listener] Matched key: ${targetKey}`); + console.log(`[Settings Storage Listener] defaultViewSelect exists: ${!!defaultViewSelect}`); + if (defaultViewSelect) { + console.log(`[Settings Storage Listener] Current dropdown value: ${defaultViewSelect.value}`); + } + console.log(`[Settings Storage Listener] Event newValue: ${event.newValue}`); + + if (event.newValue && defaultViewSelect && defaultViewSelect.value !== event.newValue) { + console.log(`[Settings Storage Listener] Value changed and dropdown exists. Checking options...`); const optionExists = [...defaultViewSelect.options].some(option => option.value === event.newValue); + console.log(`[Settings Storage Listener] Option ${event.newValue} exists: ${optionExists}`); if (optionExists) { defaultViewSelect.value = event.newValue; - console.log('Updated settings default view dropdown.'); + console.log(`[Settings Storage Listener] SUCCESS: Updated settings default view dropdown via storage event to ${event.newValue}.`); } else { - console.warn(`Storage event value (${event.newValue}) not found in dropdown options.`); + console.warn(`[Settings Storage Listener] Storage event value (${event.newValue}) not found in dropdown options.`); } - } else if (defaultViewSelect) { - console.log('Storage event value matches current dropdown selection, ignoring.'); + } else if (!event.newValue) { + console.log(`[Settings Storage Listener] Ignoring event for ${targetKey} because newValue is null/empty.`); + } else if (!defaultViewSelect) { + console.log(`[Settings Storage Listener] Ignoring event for ${targetKey} because defaultViewSelect element not found.`); + } else { + console.log(`[Settings Storage Listener] Ignoring event for ${targetKey} because value hasn't changed (${defaultViewSelect.value} === ${event.newValue}).`); } } + + // Add similar checks for other preferences if needed, e.g., dateFormat, currencySymbol + if (event.key === 'dateFormat') { // Simplified log for other keys + console.log(`[Settings Storage Listener] dateFormat changed to ${event.newValue}`); + if (event.newValue && dateFormatSelect && dateFormatSelect.value !== event.newValue) { + const optionExists = [...dateFormatSelect.options].some(option => option.value === event.newValue); + if (optionExists) { + dateFormatSelect.value = event.newValue; + console.log('[Settings Storage Listener] Updated settings date format dropdown via storage event.'); + } + } + } + + if (event.key === `${prefix}currencySymbol`) { // Simplified log for other keys + console.log(`[Settings Storage Listener] ${prefix}currencySymbol changed to ${event.newValue}`); + if (event.newValue && currencySymbolSelect && currencySymbolSelect.value !== event.newValue) { + // Handle standard vs custom symbol update + const standardOption = Array.from(currencySymbolSelect.options).find(opt => opt.value === event.newValue); + if (standardOption) { + currencySymbolSelect.value = event.newValue; + if (currencySymbolCustom) currencySymbolCustom.style.display = 'none'; + console.log('[Settings Storage Listener] Updated settings currency dropdown via storage event.'); + } else if (currencySymbolSelect.value !== 'other' || (currencySymbolCustom && currencySymbolCustom.value !== event.newValue)) { + currencySymbolSelect.value = 'other'; + if (currencySymbolCustom) { + currencySymbolCustom.value = event.newValue; + currencySymbolCustom.style.display = 'inline-block'; + } + console.log('[Settings Storage Listener] Updated settings currency dropdown to custom via storage event.'); + } + } + } + + // Add check for expiringSoonDays + if (event.key === `${prefix}expiringSoonDays`) { // Simplified log for other keys + console.log(`[Settings Storage Listener] ${prefix}expiringSoonDays changed to ${event.newValue}`); + if (event.newValue && expiringSoonDaysInput && expiringSoonDaysInput.value !== event.newValue) { + expiringSoonDaysInput.value = event.newValue; + console.log('[Settings Storage Listener] Updated settings expiring soon days input via storage event.'); + } + } + }); // --- End Storage Event Listener --- diff --git a/frontend/settings.js b/frontend/settings.js index 57e7451..84099f2 100644 --- a/frontend/settings.js +++ b/frontend/settings.js @@ -327,20 +327,16 @@ async function saveProfile() { * Save user preferences */ function savePreferences() { - // Save dark mode preference - const isDark = darkModeToggleSetting.checked; + // Save dark mode + const isDark = darkModeToggle.checked; setTheme(isDark); - darkModeToggle.checked = isDark; - - // Save default view preference - const defaultView = defaultViewSelect.value; - localStorage.setItem('defaultView', defaultView); - localStorage.setItem('warrantyView', defaultView); - - // Save email notifications preference - const emailNotifications = emailNotificationsToggle.checked; - localStorage.setItem('emailNotifications', emailNotifications); - + + // Save default view + localStorage.setItem('defaultView', defaultViewSelect.value); + + // Save email notifications + localStorage.setItem('emailNotifications', emailNotificationsToggle.checked); + showToast('Preferences saved successfully', 'success'); } diff --git a/frontend/status.html b/frontend/status.html index 351a935..e422d5a 100644 --- a/frontend/status.html +++ b/frontend/status.html @@ -17,6 +17,8 @@ + + @@ -27,6 +29,7 @@ +