Removed vite
Removed vite, and consolidated files to frontend folder
146
CHANGELOG.md
@@ -2,55 +2,65 @@
|
||||
## 1.0.3 - 2025-11-07
|
||||
|
||||
### Added
|
||||
- **Vite build tool (multi-page app) for the frontend**
|
||||
- Initialized npm in `frontend/` with `dev`, `build`, and `preview` scripts.
|
||||
- Introduced `vite.config.js` with `root: 'src'`, `outDir: '../dist'`, `publicDir: 'public'`, a development `/api` proxy, and explicit multi-page HTML inputs.
|
||||
- Restructured frontend: moved sources to `frontend/src/` and static/non-module assets to `frontend/public/`.
|
||||
- _Files: `frontend/package.json`, `frontend/vite.config.js`, `frontend/src/**/*`, `frontend/public/**/*`_
|
||||
- **Modular Entry Orchestrator and Feature Modules**
|
||||
- Added a lean ES module entry orchestrator `index.js` that initializes auth and feature modules after `authStateReady`.
|
||||
- Created feature-specific modules to prepare deconstruction of the monolith while preserving identical behavior:
|
||||
- `warrantyListController.js` (list loading/filtering/rendering controller surface)
|
||||
- `tagManager.js` (tag management: load/render/create/update/delete)
|
||||
- `addWarrantyForm.js` (multi-step add warranty wizard surface)
|
||||
- Wired pages to load the new module entry with `<script type="module" src="./js/index.js"></script>` alongside legacy `script.js` for zero-regression rollout.
|
||||
- _Files: `frontend/js/index.js`, `frontend/js/controllers/warrantyListController.js`, `frontend/js/components/tagManager.js`, `frontend/js/components/addWarrantyForm.js`, `frontend/index.html`, `frontend/about.html`, `frontend/status.html`, `frontend/register.html`, `frontend/reset-password.html`, `frontend/reset-password-request.html`_
|
||||
|
||||
- **Centralized Authentication Service**
|
||||
- New module `authService.js` exposes `initAuth`, `login`, `logout`, `getToken`, `getCurrentUser`, `isAuthenticated`.
|
||||
- Backwards compatibility: attaches a `window.auth` facade so existing code continues to work.
|
||||
- Handles header UI state, user menu/settings dropdown toggling, and dispatches a unified `authStateReady` event.
|
||||
- Robustly hides “Create Account” UI when registration is disabled (with a MutationObserver to keep it hidden after other scripts mutate the DOM).
|
||||
- _Files: `frontend/src/js/services/authService.js`, `frontend/src/*.html` (module includes)_
|
||||
- _Files: `frontend/js/services/authService.js`, `frontend/*.html` (module includes)_
|
||||
|
||||
- **Dedicated API Service**
|
||||
- New `apiService.js` provides a `baseRequest` wrapper that injects the `Authorization` header and normalizes error handling.
|
||||
- Exposes named helpers (e.g., `getWarranties`, `updateWarranty`, `deleteWarranty`, `getStatistics`, `savePreferences`) and a `window.api` shim for non-module scripts.
|
||||
- Safe global `fetch` shim adds the Authorization header for `/api/...` calls when missing.
|
||||
- _Files: `frontend/src/js/services/apiService.js`, `frontend/src/*.html` (module includes)_
|
||||
- _Files: `frontend/js/services/apiService.js`, `frontend/*.html` (module includes)_
|
||||
|
||||
- **Central State Management Store**
|
||||
- Introduced a `store.js` module to act as a single source of truth for application data (warranties, filters, loading states), eliminating scattered global variables.
|
||||
- UI updates are now driven by custom events, creating a predictable data flow.
|
||||
- _Files: `frontend/src/js/store.js`_
|
||||
- _Files: `frontend/js/store.js`_
|
||||
|
||||
- **Component-Based UI Modules**
|
||||
- Created a suite of new, reusable UI component modules to replace HTML string concatenation. Each is responsible for building a specific part of the UI via safe DOM manipulation.
|
||||
- Modules include: `warrantyCard.js`, `tag.js`, `modals.js`, `editModal.js`, `claims.js`, `notes.js`, `paperless.js`, and `ui.js`.
|
||||
- _Files: `frontend/src/js/components/*`_
|
||||
- _Files: `frontend/js/components/*`_
|
||||
|
||||
- **HTML `<template>` for Warranty Cards**
|
||||
- Added a `<template id="warranty-card-template">` to `index.html` to define the warranty card structure, separating markup from rendering logic for improved performance and maintainability.
|
||||
- _Files: `frontend/src/index.html`_
|
||||
- _Files: `frontend/index.html`_
|
||||
|
||||
### Enhanced
|
||||
- **Dockerfile: multi-stage frontend build + runtime serving**
|
||||
- Added `frontend-build` stage using `node:20-alpine` to run `npm ci && npm run build`.
|
||||
- Final stage copies `frontend/dist/` to `/var/www/html` and includes `frontend/public/` assets in the build.
|
||||
- Unpinned Node digest to resolve `not found` errors on some hosts.
|
||||
- **Dockerfile: simplified static file serving**
|
||||
- Removed build stage; frontend files are now copied directly to `/var/www/html/` for static serving.
|
||||
- No build step required; all frontend assets are served as-is.
|
||||
- _Files: `Dockerfile`_
|
||||
|
||||
- **HTML and asset loading**
|
||||
- Switched all page `<script>` and `<link>` paths to root-relative (e.g., `/script.js`).
|
||||
- Switched all page `<script>` and `<link>` paths to relative paths (e.g., `./script.js`) for static file serving.
|
||||
- Ensured `script.js` is loaded with `defer` across pages to avoid early DOM access issues.
|
||||
- Moved non-module assets (e.g., `i18n.js`, `i18next*.js`, `script.js`, `theme-loader.js`, etc.) to `frontend/public/` so they are served verbatim in production.
|
||||
- _Files: `frontend/src/*.html`, `frontend/public/**/*`_
|
||||
- All assets are served directly from the `frontend/` directory without a build step.
|
||||
- _Files: `frontend/*.html`, `frontend/**/*`_
|
||||
|
||||
- **Build stability**
|
||||
- Moved `settings-styles.css` to `public/` to bypass a PostCSS parse error in production builds (no functional/visual changes).
|
||||
- _Files: `frontend/public/settings-styles.css`_
|
||||
- **Frontend structure**
|
||||
- Consolidated all frontend files into a single `frontend/` directory for simplified static serving.
|
||||
- _Files: `frontend/settings-styles.css`_
|
||||
|
||||
- **Global State Management Refactor**
|
||||
- Migrated `frontend/script.js` to read/write state exclusively via the centralized `window.store` API.
|
||||
- Replaced direct usages of `warranties`, `currentFilters`, and `allTags` with `store.getWarranties/setWarranties`, `store.getFilters/updateFilter`, and `store.getAllTags/setAllTags`.
|
||||
- Updated filter event listeners, export flow, tag management (create/update/delete), and the filtering/rendering pipeline (`applyFilters`, `renderWarranties`) to consume store-backed data.
|
||||
- `loadTags()` now persists tags to the store and dispatches events to refresh dependent UIs.
|
||||
- Replaced remaining direct reads of removed globals with store getters in `script.js` (e.g., card action handlers, `filterWarranties()`, `openClaimsModal()`, and notes modal handlers). App behavior unchanged; all reads/writes now flow through `window.store`.
|
||||
- _Files: `frontend/script.js`_
|
||||
|
||||
- **Major UI Refactor for Maintainability & Security**
|
||||
- Replaced nearly all `innerHTML` assignments with a robust, component-based rendering approach. This makes the UI easier to debug, maintain, and more secure against XSS vulnerabilities.
|
||||
@@ -58,39 +68,113 @@
|
||||
- **Edit Modal:** The complex dynamic sections (serial numbers, current document displays for invoices/manuals/photos, and tag selection) are now built with the `editModal.js` component.
|
||||
- **Claims Modal:** The entire claims view, including the header, loading/error states, and claims list, is now rendered by the `claims.js` component.
|
||||
- **Other Modals:** Refactored the rendering logic for the Tag Management, Notes, and Paperless-ngx document browser modals to use their respective new components.
|
||||
- _Files: `frontend/src/script.js`, `frontend/src/js/components/*`_
|
||||
- _Files: `frontend/script.js`, `frontend/js/components/*`_
|
||||
|
||||
- **Begin Extraction of UI Event Listeners**
|
||||
- Moved the global UI event initialization (`setupUIEventListeners`) into `warrantyListController.initEventListeners()` as part of the modularization.
|
||||
- Legacy `frontend/script.js` delegates to the controller to attach listeners, maintaining identical behavior during the staged rollout.
|
||||
- _Files: `frontend/js/controllers/warrantyListController.js`, `frontend/script.js`_
|
||||
|
||||
- **Controller-based Filter Pipeline**
|
||||
- Migrated `applyFilters` logic into `warrantyListController.js` to centralize filtering while preserving identical behavior.
|
||||
- Legacy `applyFilters` in `frontend/script.js` now delegates to the controller to avoid duplication.
|
||||
- _Files: `frontend/js/controllers/warrantyListController.js`, `frontend/script.js`_
|
||||
|
||||
- **Filter Dropdowns and Preference Persistence Extraction**
|
||||
- Moved filter dropdown population functions (`populateTagFilter`, `populateVendorFilter`, `populateWarrantyTypeFilter`) and preference persistence (`saveFilterPreferences`, `loadFilterAndSortPreferences`) into `warrantyListController.js`.
|
||||
- Legacy functions in `frontend/script.js` now delegate to the controller, ensuring a single source of truth.
|
||||
- _Files: `frontend/js/controllers/warrantyListController.js`, `frontend/script.js`_
|
||||
|
||||
- **Read-only State Bridges for Module Access**
|
||||
- Exposed read-only getters on `window` for `currentView`, `lastLoadedArchived`, and `lastLoadedIncludesArchived` to allow module code to read legacy flags safely without changing their source of truth.
|
||||
- _Files: `frontend/script.js`_
|
||||
|
||||
- **Delegated List Actions Fallback in Controller**
|
||||
- Added robust delegated handlers in `warrantyListController.initEventListeners()` for `#warrantiesList` (claims, edit, delete, notes, archive/unarchive) that activate only when the legacy initializer is not present, preventing double bindings.
|
||||
- _Files: `frontend/js/controllers/warrantyListController.js`_
|
||||
|
||||
- **Controller-based Data Loader (`loadWarranties`)**
|
||||
- Migrated the full `loadWarranties` flow into `warrantyListController.js` while preserving behavior: authentication check, scope selection (Personal/Global), Archived vs All handling with archived merge, processing via `processWarrantyData`, filter dropdown repopulation, and final render through `applyFilters()`.
|
||||
- Legacy `frontend/script.js` now delegates `loadWarranties` to the controller.
|
||||
- _Files: `frontend/js/controllers/warrantyListController.js`, `frontend/script.js`_
|
||||
|
||||
- **Warranty List Rendering (component-based refactor, public bundle)**
|
||||
- Rewrote `renderWarranties` in `frontend/script.js` to stop concatenating HTML strings and instead delegate card creation to `window.components.createWarrantyCard(warranty, options)`.
|
||||
- Builds `actionsHtml`, `contentHtml`, `statusText/statusClass`, and view-specific root class via helpers; appends tags using `window.components.appendTags`; preserves document links row placement per view.
|
||||
- Uses `DocumentFragment` to batch DOM insertions for performance and `window.components.ui.renderEmptyState` for empty states.
|
||||
- Centralized card action handling with a single delegated listener on `#warrantiesList` for edit, delete, archive/unarchive, claims, and notes.
|
||||
- Updated all call sites to the no-arg `renderWarranties()`; visual output and behavior remain identical across Grid, List, and Table views.
|
||||
- _Files: `frontend/script.js`_
|
||||
|
||||
- **Finalized Legacy Script Delegation**
|
||||
- Completed migration of UI event listeners from `frontend/script.js` (`setupUIEventListeners`) into `warrantyListController.initEventListeners()`:
|
||||
- Filters: `statusFilter`, `tagFilter`, `vendorFilter`, `warrantyTypeFilter`
|
||||
- Sort: `sortBy`
|
||||
- View switchers: `gridViewBtn`, `listViewBtn`, `tableViewBtn`
|
||||
- Actions: `refreshBtn`, `exportBtn`, `importBtn`
|
||||
- Replaced the legacy initializer call site and removed `setupUIEventListeners` from `script.js` (now delegates to the controller).
|
||||
- Delegated modal openings to component layer: `openDeleteModal`/`openArchiveModal` now call `window.components.modals.openDeleteModal/openArchiveModal` which owns DOM updates and visibility.
|
||||
- Moved Edit modal save flow out of legacy script: `saveWarranty` now lives in `frontend/js/components/editModal.js`, uses `apiService.updateWarranty`, and is exposed as `window.components.editModal.saveWarranty`.
|
||||
- Updated Save button wiring: controller binds Save to `editModal.saveWarranty`, preserving `setupSaveWarrantyObserver` wrapping when present.
|
||||
- Consolidated card action delegation: the single `#warrantiesList` delegated listener resides exclusively in the controller; removed the overlapping legacy delegation from `script.js`.
|
||||
- `script.js` is now a thin delegator with identical external behavior.
|
||||
- _Files: `frontend/script.js`, `frontend/js/controllers/warrantyListController.js`, `frontend/js/components/modals.js`, `frontend/js/components/editModal.js`_
|
||||
|
||||
- **Page-Specific Script Modularization (`status.js`)**
|
||||
- Converted `frontend/status.js` from a legacy script into a self-contained ES Module.
|
||||
- Replaced all `fetch` calls with methods from the imported `apiService.js` and all authentication logic with `authService.js`, removing dependencies on the global scope.
|
||||
- The module now initializes itself via a `DOMContentLoaded` listener, making it independent of the legacy `script.js`.
|
||||
- Updated `frontend/status.html` to load `status.js` with `type="module"`, ensuring modern script handling.
|
||||
- _Files: `frontend/status.js`, `frontend/status.html`_
|
||||
|
||||
- **API Service Extension**
|
||||
- Added a generic `apiService.request()` method to handle various API calls, which was utilized in the refactoring of `status.js`.
|
||||
- _Files: `frontend/js/services/apiService.js`_
|
||||
|
||||
|
||||
### Fixed
|
||||
- 404s for non-module assets in production by serving them from Vite `public/`.
|
||||
- 404s for non-module assets in production by ensuring all assets are properly referenced with relative paths.
|
||||
- User menu not opening: restored click/close listeners within `authService.js`.
|
||||
- Login page “Create Account” visible while registration disabled: force-hidden with strong CSS and observer.
|
||||
- Status page edit actions blocked by early `document.body.appendChild(...)`: added `defer` to `script.js` includes.
|
||||
- **Thumbnail sizing on initial load (Grid/List/Table)**
|
||||
- Removed inline image sizing in the renderer so thumbnail sizes are now driven by CSS view classes, ensuring correct sizes apply immediately without hard reload or manual view switch.
|
||||
- Applied view preference before first render during data load to guarantee correct view class on first paint.
|
||||
- Tuned responsive CSS for very small screens (≤360px): List view 60×60, Table view 20×20; preserved existing ≤768px sizes.
|
||||
- _Files: `frontend/script.js`, `frontend/style.css`, `frontend/js/controllers/warrantyListController.js`_
|
||||
- **Security:** Mitigated potential cross-site scripting (XSS) vulnerabilities by removing reliance on building the UI with `innerHTML` from dynamic data.
|
||||
- **Performance:** Improved rendering performance by using efficient DOM creation and appending (`<template>` clones, `document.createElement`) instead of causing the browser to re-parse large HTML strings on every update.
|
||||
- **Code Quality:** Drastically reduced code complexity and repetition in `script.js` by delegating UI creation to specialized component modules.
|
||||
- Eliminated 404s for removed stylesheet by deleting the `/styles.css` link in `frontend/src/about.html` and removing it from the service worker pre-cache (`frontend/public/sw.js`).
|
||||
- _Files: `frontend/src/about.html`, `frontend/public/sw.js`_
|
||||
- Eliminated 404s for removed stylesheet by deleting the `/styles.css` link in `frontend/about.html` and removing it from the service worker pre-cache (`frontend/sw.js`).
|
||||
- _Files: `frontend/about.html`, `frontend/sw.js`_
|
||||
- Resolved transient `ReferenceError` messages (e.g., `allTags`/`warranties`/`currentFilters` not defined) by completing the store-backed migration in `frontend/script.js`.
|
||||
|
||||
### Removed
|
||||
- Legacy, scattered auth scripts now replaced by `authService.js`:
|
||||
- `frontend/src/auth.js`, `frontend/src/auth-new.js`, `frontend/src/include-auth-new.js`, `frontend/src/fix-auth-buttons.js`, `frontend/src/fix-auth-buttons-loader.js`, `frontend/src/registration-status.js`.
|
||||
- `frontend/auth.js`, `frontend/auth-new.js`, `frontend/include-auth-new.js`, `frontend/fix-auth-buttons.js`, `frontend/fix-auth-buttons-loader.js`, `frontend/registration-status.js`.
|
||||
- **Global State Variables**
|
||||
- Eliminated global variables like `let warranties = []` and `let currentFilters = {}` from `script.js`, centralizing all state management within `store.js`.
|
||||
- **Hardcoded HTML Strings**
|
||||
- Removed large, multi-line HTML string templates from JavaScript files, which were fragile and difficult to maintain.
|
||||
|
||||
- Frontend duplicate/unused assets removed in favor of `frontend/public` copies:
|
||||
- `frontend/src/js/i18n.js`, `frontend/src/js/i18n-debug.js`, and `frontend/src/js/lib/*` (kept `frontend/public/js` versions)
|
||||
- Root-level duplicates in `frontend/src/`: `chart.js`, `file-utils.js`, `footer-content.js`, `footer-fix.js`, `mobile-menu.js`, `script.js`, `settings-new.js`, `theme-loader.js`, `version-checker.js`, `auth-redirect.js`, `sw.js`
|
||||
- Removed unused stylesheet: `frontend/src/styles.css`
|
||||
- Removed debug files: `frontend/public/js/i18n-debug.js`, `frontend/src/temp-toast-debug.js`
|
||||
- Frontend duplicate/unused assets removed:
|
||||
- Removed unused stylesheet: `frontend/styles.css`
|
||||
- Removed debug files: `frontend/js/i18n-debug.js`, `frontend/temp-toast-debug.js`
|
||||
|
||||
- Backend cleanup: removed unused `backend/fix_notification_columns.py`.
|
||||
|
||||
### House Cleaning
|
||||
- Deleted generated artifacts not needed in repo: `frontend/dist`, `frontend/node_modules` (Docker builds frontend in-image)
|
||||
- Deleted generated artifacts not needed in repo: `frontend/dist`, `frontend/node_modules` (frontend is now served statically without build)
|
||||
- Purged Python caches: `backend/**/__pycache__`, stray `.pyc`
|
||||
- Removed old backup: `backend/fix_permissions.sql.bak`
|
||||
- **Code Refinement in `settings-new.js`**
|
||||
- **Settings page refactor — ES Module & event wiring**
|
||||
- Converted `frontend/settings-new.js` into a self-contained ES Module and updated `settings-new.html` to load it with a `<script type="module" src="./settings-new.js"></script>` tag. The module imports `authService` and `apiService` rather than relying on global functions.
|
||||
- Removed inline `onclick` handlers from the Settings HTML (notably the Delete User modal) and rewired UI interactions using `addEventListener` inside the module so handlers are module-scoped instead of being attached to `window`.
|
||||
- Extended `frontend/js/services/apiService.js` with settings/admin endpoints consumed by the module (for example: `getUser`, `getUsers`, `updateUser`, `deleteUser`, `getSiteSettings`, `saveSiteSettings`, `getAuditTrail`, `triggerNotifications`) and preserved the `window.api` shim for backward compatibility.
|
||||
- Fixed a porting-induced syntax/parsing issue (unbalanced brace) and removed a leftover global (`window.currentDeleteUserId`) in favor of a module-scoped `currentDeleteUserId` to avoid leaking state.
|
||||
- The refactored module parses without syntax errors. Runtime smoke tests are planned/encouraged (see next steps) and final decommission of the legacy `script.js` will follow once runtime verification is complete.
|
||||
- _Files: `frontend/settings-new.html`, `frontend/settings-new.js`, `frontend/js/services/apiService.js`_
|
||||
|
||||
|
||||
## 1.0.2 - 2025-10-30
|
||||
|
||||
15
Dockerfile
@@ -54,17 +54,6 @@ COPY backend/requirements.txt /tmp/requirements.txt
|
||||
RUN pip install --no-cache-dir -r /tmp/requirements.txt
|
||||
|
||||
|
||||
FROM node:20-alpine AS frontend-build
|
||||
|
||||
WORKDIR /app/frontend
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
COPY frontend/vite.config.js ./
|
||||
COPY frontend/public ./public
|
||||
COPY frontend/src ./src
|
||||
|
||||
RUN npm ci --no-audit --no-fund && npm run build
|
||||
|
||||
|
||||
FROM python:3.13-slim-trixie@sha256:079601253d5d25ae095110937ea8cfd7403917b53b077870bccd8b026dc7c42f AS runtime
|
||||
|
||||
ARG NGINX_VERSION
|
||||
@@ -123,8 +112,8 @@ COPY --chown=warracker:warracker backend/migrations/ ./migrations/
|
||||
COPY --chown=warracker:warracker locales/ ./locales/
|
||||
COPY --chown=warracker:warracker locales/ /var/www/html/locales/
|
||||
|
||||
# 4. Frontend (built with Vite)
|
||||
COPY --chown=warracker:warracker --from=frontend-build /app/frontend/dist/ /var/www/html/
|
||||
# 4. Frontend (static files, no build step)
|
||||
COPY --chown=warracker:warracker frontend/ /var/www/html/
|
||||
|
||||
# 5. Backend (bundled in one instruction)
|
||||
COPY --chown=warracker:warracker backend/ ./backend/
|
||||
|
||||
@@ -7,32 +7,33 @@
|
||||
<title data-i18n="about.title">About - Warracker</title>
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png?v=2">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png?v=2">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/favicon-512x512.png">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="./img/favicon-16x16.png?v=2">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="./img/favicon-32x32.png?v=2">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./img/favicon-512x512.png">
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
|
||||
<!-- Stylesheets -->
|
||||
<link rel="stylesheet" href="/style.css?v=20250119004"> <!-- Main stylesheet -->
|
||||
<link rel="stylesheet" href="./style.css?v=20250119004"> <!-- Main stylesheet -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css"> <!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="/header-fix.css?v=20250119001"> <!-- Header specific styles -->
|
||||
<link rel="stylesheet" href="./header-fix.css?v=20250119001"> <!-- Header specific styles -->
|
||||
<!-- Mobile Header specific styles -->
|
||||
<link rel="stylesheet" href="/mobile-header.css?v=20250119002">
|
||||
<link rel="stylesheet" href="./mobile-header.css?v=20250119002">
|
||||
|
||||
<!-- Scripts loaded in head -->
|
||||
<script src="/theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
|
||||
<script type="module" src="/js/services/authService.js"></script>
|
||||
<script type="module" src="/js/services/apiService.js"></script>
|
||||
<script src="./theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
|
||||
<script type="module" src="./js/services/authService.js"></script>
|
||||
<script type="module" src="./js/services/apiService.js"></script>
|
||||
|
||||
|
||||
<script src="/script.js?v=20250119001" defer></script>
|
||||
<script src="./script.js?v=20250119001" defer></script>
|
||||
<script type="module" src="./js/index.js"></script>
|
||||
<!-- i18next Local Scripts -->
|
||||
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
<!-- i18n initialization script -->
|
||||
<script src="/js/i18n.js?v=20250119001"></script>
|
||||
<script src="./js/i18n.js?v=20250119001"></script>
|
||||
|
||||
<style>
|
||||
.about-hero {
|
||||
@@ -414,13 +415,13 @@
|
||||
|
||||
|
||||
<!-- Version Checker -->
|
||||
<script src="/version-checker.js?v=20251030001" defer></script> <!-- Version checker script -->
|
||||
<script src="./version-checker.js?v=20251030001" defer></script> <!-- Version checker script -->
|
||||
|
||||
<!-- Footer Width Fix -->
|
||||
<script src="/footer-fix.js?v=20251024001"></script>
|
||||
<script src="./footer-fix.js?v=20251024001"></script>
|
||||
|
||||
<!-- Footer Content Manager -->
|
||||
<script src="/footer-content.js?v=20250119001"></script>
|
||||
<script src="./footer-content.js?v=20250119001"></script>
|
||||
|
||||
<!-- Powered by Warracker Footer -->
|
||||
<footer class="warracker-footer" id="warrackerFooter">
|
||||
@@ -4,10 +4,10 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Authenticating...</title>
|
||||
<link rel="stylesheet" href="/style.css?v=20250119004">
|
||||
<script src="/theme-loader.js?v=20250119001"></script>
|
||||
<script type="module" src="/js/services/authService.js"></script>
|
||||
<script type="module" src="/js/services/apiService.js"></script>
|
||||
<link rel="stylesheet" href="./style.css?v=20250119004">
|
||||
<script src="./theme-loader.js?v=20250119001"></script>
|
||||
<script type="module" src="./js/services/authService.js"></script>
|
||||
<script type="module" src="./js/services/apiService.js"></script>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
@@ -161,20 +161,20 @@
|
||||
</script>
|
||||
|
||||
<!-- i18next Local Scripts -->
|
||||
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
|
||||
<!-- i18n Configuration -->
|
||||
<script src="/js/i18n.js?v=20250119001"></script>
|
||||
<script src="./js/i18n.js?v=20250119001"></script>
|
||||
|
||||
<script src="/auth-redirect.js?v=20250119001"></script>
|
||||
<script src="./auth-redirect.js?v=20250119001"></script>
|
||||
|
||||
<!-- Footer Width Fix -->
|
||||
<script src="/footer-fix.js?v=20251024001"></script>
|
||||
<script src="./footer-fix.js?v=20251024001"></script>
|
||||
|
||||
<!-- Footer Content Manager -->
|
||||
<script src="/footer-content.js?v=20250119001"></script>
|
||||
<script src="./footer-content.js?v=20250119001"></script>
|
||||
|
||||
<!-- Powered by Warracker Footer -->
|
||||
<footer class="warracker-footer" id="warrackerFooter">
|
||||
@@ -106,14 +106,14 @@
|
||||
</div>
|
||||
|
||||
<!-- i18next Local Scripts -->
|
||||
<script type="module" src="/js/services/authService.js"></script>
|
||||
<script type="module" src="/js/services/apiService.js"></script>
|
||||
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
<script type="module" src="./js/services/authService.js"></script>
|
||||
<script type="module" src="./js/services/apiService.js"></script>
|
||||
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
|
||||
<!-- i18n Configuration -->
|
||||
<script src="/js/i18n.js?v=20250119001"></script>
|
||||
<script src="./js/i18n.js?v=20250119001"></script>
|
||||
|
||||
<script>
|
||||
let warranties = [];
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 571 B After Width: | Height: | Size: 571 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
@@ -2,56 +2,56 @@
|
||||
<html>
|
||||
<head>
|
||||
<!-- Authentication redirect script -->
|
||||
<script src="/auth-redirect.js?v=20250119001" data-protected="true"></script>
|
||||
<script src="./auth-redirect.js?v=20250119001" data-protected="true"></script>
|
||||
|
||||
<!-- Include authentication script first to handle login state immediately -->
|
||||
|
||||
|
||||
<!-- File utilities script for secure file handling -->
|
||||
<script src="/file-utils.js?v=20250119001"></script>
|
||||
<script src="./file-utils.js?v=20250119001"></script>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Warracker - Warranty Tracker</title>
|
||||
<!-- Add standard favicon.ico link -->
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
||||
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
|
||||
<!-- Replace the old favicon link -->
|
||||
<!-- <link rel="icon" type="image/png" href="img/favicon.png"> -->
|
||||
<!-- Add new favicon links -->
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png?v=2">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png?v=2">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/favicon-512x512.png">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="stylesheet" href="/style.css?v=20250119004">
|
||||
<script src="/theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
|
||||
<script type="module" src="/js/store.js"></script>
|
||||
<script type="module" src="/js/services/authService.js"></script>
|
||||
<script type="module" src="/js/services/apiService.js"></script>
|
||||
<script type="module" src="/js/components/warrantyCard.js"></script>
|
||||
<script type="module" src="/js/components/tag.js"></script>
|
||||
<script type="module" src="/js/components/editModal.js"></script>
|
||||
<script type="module" src="/js/components/modals.js"></script>
|
||||
<script type="module" src="/js/components/claims.js"></script>
|
||||
<script type="module" src="/js/components/notes.js"></script>
|
||||
<script type="module" src="/js/components/paperless.js"></script>
|
||||
<script type="module" src="/js/components/ui.js"></script>
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="./img/favicon-16x16.png?v=2">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="./img/favicon-32x32.png?v=2">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./img/favicon-512x512.png">
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
<link rel="stylesheet" href="./style.css?v=20250119004">
|
||||
<script src="./theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
|
||||
<script type="module" src="./js/store.js"></script>
|
||||
<script type="module" src="./js/services/authService.js"></script>
|
||||
<script type="module" src="./js/services/apiService.js"></script>
|
||||
<script type="module" src="./js/components/warrantyCard.js"></script>
|
||||
<script type="module" src="./js/components/tag.js"></script>
|
||||
<script type="module" src="./js/components/editModal.js"></script>
|
||||
<script type="module" src="./js/components/modals.js"></script>
|
||||
<script type="module" src="./js/components/claims.js"></script>
|
||||
<script type="module" src="./js/components/notes.js"></script>
|
||||
<script type="module" src="./js/components/paperless.js"></script>
|
||||
<script type="module" src="./js/components/ui.js"></script>
|
||||
|
||||
<!-- Font Awesome for icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
||||
<!-- Load header fix styles to ensure consistent header styling -->
|
||||
<link rel="stylesheet" href="/header-fix.css?v=20250119001">
|
||||
<link rel="stylesheet" href="./header-fix.css?v=20250119001">
|
||||
<!-- Load fix for auth buttons -->
|
||||
|
||||
<!-- Mobile Header specific styles -->
|
||||
<link rel="stylesheet" href="/mobile-header.css?v=20250119002">
|
||||
<link rel="stylesheet" href="./mobile-header.css?v=20250119002">
|
||||
|
||||
<!-- i18next Local Scripts -->
|
||||
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
|
||||
<!-- i18n Configuration -->
|
||||
<script src="/js/i18n.js?v=20250119001"></script>
|
||||
<script src="./js/i18n.js?v=20250119001"></script>
|
||||
|
||||
<!-- Immediate authentication check script -->
|
||||
<script>
|
||||
@@ -1337,7 +1337,8 @@
|
||||
</div>
|
||||
|
||||
|
||||
<script src="/script.js?v=20250119002" defer></script>
|
||||
<script src="./script.js?v=20250119002" defer></script>
|
||||
<script type="module" src="./js/index.js"></script>
|
||||
<script>
|
||||
// Lightweight UI logic for Filter/Sort popovers (no change to core logic)
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
@@ -1646,10 +1647,10 @@
|
||||
</script>
|
||||
|
||||
<!-- Footer Width Fix -->
|
||||
<script src="/footer-fix.js?v=20251024001"></script>
|
||||
<script src="./footer-fix.js?v=20251024001"></script>
|
||||
|
||||
<!-- Footer Content Manager -->
|
||||
<script src="/footer-content.js?v=20250119001"></script>
|
||||
<script src="./footer-content.js?v=20250119001"></script>
|
||||
|
||||
<!-- Powered by Warracker Footer -->
|
||||
<footer class="warracker-footer" id="warrackerFooter">
|
||||
83
frontend/js/components/addWarrantyForm.js
Normal file
@@ -0,0 +1,83 @@
|
||||
// Add Warranty Form Wizard (compat layer)
|
||||
// Delegates to existing global implementations to preserve identical behavior.
|
||||
|
||||
function callIfExists(fn, ...args) {
|
||||
if (typeof fn === 'function') {
|
||||
return fn(...args);
|
||||
}
|
||||
}
|
||||
|
||||
export function initFormTabs() {
|
||||
return callIfExists(window.initFormTabs);
|
||||
}
|
||||
|
||||
export function switchToTab(index) {
|
||||
return callIfExists(window.switchToTab, index);
|
||||
}
|
||||
|
||||
export function updateNavigationButtons() {
|
||||
return callIfExists(window.updateNavigationButtons);
|
||||
}
|
||||
|
||||
export function updateCompletedTabs() {
|
||||
return callIfExists(window.updateCompletedTabs);
|
||||
}
|
||||
|
||||
export function validateTab(tabIndex) {
|
||||
return callIfExists(window.validateTab, tabIndex);
|
||||
}
|
||||
|
||||
export function showValidationErrors(tabIndex) {
|
||||
return callIfExists(window.showValidationErrors, tabIndex);
|
||||
}
|
||||
|
||||
export function updateSummary() {
|
||||
return callIfExists(window.updateSummary);
|
||||
}
|
||||
|
||||
export function handleFormSubmit(event) {
|
||||
return callIfExists(window.handleFormSubmit, event);
|
||||
}
|
||||
|
||||
export function resetAddWarrantyWizard() {
|
||||
return callIfExists(window.resetAddWarrantyWizard);
|
||||
}
|
||||
|
||||
export function handleLifetimeChange(event) {
|
||||
return callIfExists(window.handleLifetimeChange, event);
|
||||
}
|
||||
|
||||
export function handleWarrantyMethodChange() {
|
||||
return callIfExists(window.handleWarrantyMethodChange);
|
||||
}
|
||||
|
||||
export function init() {
|
||||
// Ensure tab structure initialized if present
|
||||
initFormTabs();
|
||||
// Wire form submit if form exists
|
||||
const form = document.getElementById('addWarrantyForm');
|
||||
if (form && !form._awfBound) {
|
||||
form.addEventListener('submit', (e) => handleFormSubmit(e));
|
||||
form._awfBound = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Backward compatibility on window if needed
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addWarrantyForm = {
|
||||
init,
|
||||
initFormTabs,
|
||||
switchToTab,
|
||||
updateNavigationButtons,
|
||||
updateCompletedTabs,
|
||||
validateTab,
|
||||
showValidationErrors,
|
||||
updateSummary,
|
||||
handleFormSubmit,
|
||||
resetAddWarrantyWizard,
|
||||
handleLifetimeChange,
|
||||
handleWarrantyMethodChange
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Edit modal helper components (DOM-based, no innerHTML templates)
|
||||
import { updateWarranty } from '../services/apiService.js';
|
||||
|
||||
function createElement(tag, className, text) {
|
||||
const el = document.createElement(tag);
|
||||
@@ -245,7 +246,213 @@ if (typeof window !== 'undefined') {
|
||||
renderCurrentManual,
|
||||
renderCurrentPhoto,
|
||||
renderCurrentOther,
|
||||
saveWarranty,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Submit handler for the Edit Warranty modal
|
||||
export async function saveWarranty() {
|
||||
const currentId = typeof window !== 'undefined' ? window.currentWarrantyId : null;
|
||||
if (!currentId) {
|
||||
if (window && window.showToast) window.showToast(window.t ? window.t('messages.no_warranty_selected_for_update') : 'No warranty selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Gather inputs
|
||||
const productName = (document.getElementById('editProductName') || {}).value?.trim() || '';
|
||||
const purchaseDate = (document.getElementById('editPurchaseDate') || {}).value || '';
|
||||
const isLifetime = !!(document.getElementById('editIsLifetime') || {}).checked;
|
||||
const editDurationMethodRadioLocal = document.getElementById('editDurationMethod');
|
||||
const isDurationMethod = !!(editDurationMethodRadioLocal && editDurationMethodRadioLocal.checked);
|
||||
const years = parseInt((document.getElementById('editWarrantyDurationYears') || {}).value || 0);
|
||||
const months = parseInt((document.getElementById('editWarrantyDurationMonths') || {}).value || 0);
|
||||
const days = parseInt((document.getElementById('editWarrantyDurationDays') || {}).value || 0);
|
||||
const editExactExpirationDateInputLocal = document.getElementById('editExactExpirationDate');
|
||||
const exactDate = editExactExpirationDateInputLocal ? editExactExpirationDateInputLocal.value : '';
|
||||
const editWarrantyDurationFieldsLocal = document.getElementById('editWarrantyDurationFields');
|
||||
|
||||
// Basic validation
|
||||
if (!productName) {
|
||||
if (window && window.showToast) window.showToast(window.t ? window.t('messages.product_name_required') : 'Product name is required', 'error');
|
||||
return;
|
||||
}
|
||||
if (!purchaseDate) {
|
||||
if (window && window.showToast) window.showToast(window.t ? window.t('messages.purchase_date_required') : 'Purchase date is required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validation for lifetime/duration/exact date
|
||||
if (!isLifetime) {
|
||||
if (isDurationMethod) {
|
||||
if (years === 0 && months === 0 && days === 0) {
|
||||
if (window && window.showToast) window.showToast(window.t ? window.t('messages.warranty_duration_required') : 'Warranty duration is required', 'error');
|
||||
const yearsInput = document.getElementById('editWarrantyDurationYears');
|
||||
if (yearsInput) {
|
||||
yearsInput.focus();
|
||||
if (editWarrantyDurationFieldsLocal) editWarrantyDurationFieldsLocal.classList.add('invalid-duration');
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!exactDate) {
|
||||
if (window && window.showToast) window.showToast(window.t ? window.t('messages.exact_expiration_date_required') : 'Exact expiration date is required', 'error');
|
||||
if (editExactExpirationDateInputLocal) editExactExpirationDateInputLocal.focus();
|
||||
return;
|
||||
}
|
||||
if (purchaseDate && exactDate <= purchaseDate) {
|
||||
if (window && window.showToast) window.showToast(window.t ? window.t('messages.expiration_date_after_purchase_date') : 'Expiration date must be after purchase date', 'error');
|
||||
if (editExactExpirationDateInputLocal) editExactExpirationDateInputLocal.focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (editWarrantyDurationFieldsLocal) editWarrantyDurationFieldsLocal.classList.remove('invalid-duration');
|
||||
|
||||
// Build FormData
|
||||
const formData = new FormData();
|
||||
formData.append('product_name', productName);
|
||||
formData.append('purchase_date', purchaseDate);
|
||||
|
||||
let productUrl = (document.getElementById('editProductUrl') || {}).value?.trim() || '';
|
||||
if (productUrl) {
|
||||
if (!productUrl.startsWith('http://') && !productUrl.startsWith('https://')) {
|
||||
productUrl = 'https://' + productUrl;
|
||||
}
|
||||
formData.append('product_url', productUrl);
|
||||
}
|
||||
const purchasePrice = (document.getElementById('editPurchasePrice') || {}).value;
|
||||
const currency = (document.getElementById('editCurrency') || {}).value;
|
||||
if (purchasePrice) formData.append('purchase_price', purchasePrice);
|
||||
if (currency) formData.append('currency', currency);
|
||||
|
||||
// Serial numbers
|
||||
const serialInputs = document.querySelectorAll('#editSerialNumbersContainer input[name="serial_numbers[]"]');
|
||||
formData.delete('serial_numbers[]');
|
||||
serialInputs.forEach(input => {
|
||||
if (input.value && input.value.trim()) formData.append('serial_numbers[]', input.value.trim());
|
||||
});
|
||||
|
||||
// Tags
|
||||
const editSelectedTags = (typeof window !== 'undefined' && window.editSelectedTags) ? window.editSelectedTags : [];
|
||||
formData.append('tag_ids', JSON.stringify(Array.isArray(editSelectedTags) ? editSelectedTags.map(t => t.id) : []));
|
||||
|
||||
// Document URLs
|
||||
formData.append('invoice_url', (document.getElementById('editInvoiceUrl') || {}).value || '');
|
||||
formData.append('manual_url', (document.getElementById('editManualUrl') || {}).value || '');
|
||||
formData.append('other_document_url', (document.getElementById('editOtherDocumentUrl') || {}).value || '');
|
||||
|
||||
// Files
|
||||
const invoiceFile = (document.getElementById('editInvoice') || {}).files ? document.getElementById('editInvoice').files[0] : null;
|
||||
const manualFile = (document.getElementById('editManual') || {}).files ? document.getElementById('editManual').files[0] : null;
|
||||
const otherDocumentFile = (document.getElementById('editOtherDocument') || {}).files ? document.getElementById('editOtherDocument').files[0] : null;
|
||||
const productPhotoFile = (document.getElementById('editProductPhoto') || {}).files ? document.getElementById('editProductPhoto').files[0] : null;
|
||||
if (invoiceFile) formData.append('invoice', invoiceFile);
|
||||
if (manualFile) formData.append('manual', manualFile);
|
||||
if (otherDocumentFile) formData.append('other_document', otherDocumentFile);
|
||||
if (productPhotoFile) formData.append('product_photo', productPhotoFile);
|
||||
|
||||
// Deletion flags
|
||||
const deleteInvoiceBtn = document.getElementById('deleteInvoiceBtn');
|
||||
if (deleteInvoiceBtn && deleteInvoiceBtn.dataset.delete === 'true') formData.append('delete_invoice', 'true');
|
||||
const deleteManualBtn = document.getElementById('deleteManualBtn');
|
||||
if (deleteManualBtn && deleteManualBtn.dataset.delete === 'true') formData.append('delete_manual', 'true');
|
||||
const deleteOtherDocumentBtn = document.getElementById('deleteOtherDocumentBtn');
|
||||
if (deleteOtherDocumentBtn && deleteOtherDocumentBtn.dataset.delete === 'true') formData.append('delete_other_document', 'true');
|
||||
const deleteProductPhotoBtn = document.getElementById('deleteProductPhotoBtn');
|
||||
if (deleteProductPhotoBtn && deleteProductPhotoBtn.dataset.delete === 'true') formData.append('delete_product_photo', 'true');
|
||||
|
||||
// Lifetime/duration
|
||||
formData.append('is_lifetime', String(isLifetime));
|
||||
if (!isLifetime) {
|
||||
if (isDurationMethod) {
|
||||
formData.append('warranty_duration_years', years);
|
||||
formData.append('warranty_duration_months', months);
|
||||
formData.append('warranty_duration_days', days);
|
||||
} else {
|
||||
formData.append('exact_expiration_date', exactDate);
|
||||
formData.append('warranty_duration_years', 0);
|
||||
formData.append('warranty_duration_months', 0);
|
||||
formData.append('warranty_duration_days', 0);
|
||||
}
|
||||
} else {
|
||||
formData.append('warranty_duration_years', 0);
|
||||
formData.append('warranty_duration_months', 0);
|
||||
formData.append('warranty_duration_days', 0);
|
||||
}
|
||||
|
||||
// Notes and misc fields
|
||||
const notes = (document.getElementById('editNotes') || {}).value || '';
|
||||
formData.append('notes', notes.trim() !== '' ? notes : '');
|
||||
const editModelNumber = document.getElementById('editModelNumber');
|
||||
formData.append('model_number', editModelNumber && editModelNumber.value.trim() ? editModelNumber.value.trim() : '');
|
||||
const editVendorInput = document.getElementById('editVendor');
|
||||
formData.append('vendor', editVendorInput ? (editVendorInput.value || '').trim() : '');
|
||||
const editWarrantyTypeInput = document.getElementById('editWarrantyType');
|
||||
const editWarrantyTypeCustomInput = document.getElementById('editWarrantyTypeCustom');
|
||||
let warrantyTypeValue = '';
|
||||
if (editWarrantyTypeInput) {
|
||||
warrantyTypeValue = (editWarrantyTypeInput.value === 'other' && editWarrantyTypeCustomInput && editWarrantyTypeCustomInput.value.trim())
|
||||
? editWarrantyTypeCustomInput.value.trim()
|
||||
: editWarrantyTypeInput.value.trim();
|
||||
}
|
||||
formData.append('warranty_type', warrantyTypeValue);
|
||||
|
||||
// Debug logs preserved
|
||||
console.log('[DEBUG saveWarranty] isLifetime:', isLifetime);
|
||||
console.log('[DEBUG saveWarranty] isDurationMethod:', isDurationMethod);
|
||||
console.log('[DEBUG saveWarranty] exactDate:', exactDate);
|
||||
console.log('[DEBUG saveWarranty] years/months/days:', years, months, days);
|
||||
for (let [key, value] of formData.entries()) {
|
||||
console.log(`[DEBUG saveWarranty] FormData: ${key} = ${value}`);
|
||||
}
|
||||
|
||||
if (window && window.showLoadingSpinner) window.showLoadingSpinner();
|
||||
try {
|
||||
// Paperless uploads pipeline (delegated to legacy/global)
|
||||
const uploads = (window && typeof window.processEditPaperlessNgxUploads === 'function')
|
||||
? await window.processEditPaperlessNgxUploads(formData)
|
||||
: {};
|
||||
Object.keys(uploads || {}).forEach(k => formData.append(k, uploads[k]));
|
||||
|
||||
// Submit update via API service (auto-injects Authorization header)
|
||||
await updateWarranty(currentId, formData);
|
||||
|
||||
if (window && window.hideLoadingSpinner) window.hideLoadingSpinner();
|
||||
if (window && window.showToast) window.showToast('Warranty updated successfully', 'success');
|
||||
if (window && typeof window.closeModals === 'function') window.closeModals();
|
||||
|
||||
// Reload warranties to pick up processed assets/paths
|
||||
if (window && window.warrantyListController && typeof window.warrantyListController.loadWarranties === 'function') {
|
||||
try {
|
||||
await window.warrantyListController.loadWarranties(true);
|
||||
if (window && typeof window.applyFilters === 'function') window.applyFilters();
|
||||
setTimeout(() => {
|
||||
if (window && typeof window.loadSecureImages === 'function') {
|
||||
window.loadSecureImages();
|
||||
}
|
||||
}, 200);
|
||||
const notesModal = document.getElementById('notesModal');
|
||||
if (notesModal && notesModal.style.display === 'block') notesModal.style.display = 'none';
|
||||
// Auto-link paperless docs post-update
|
||||
if ((invoiceFile || manualFile || otherDocumentFile) && currentId) {
|
||||
setTimeout(() => {
|
||||
if (typeof window.autoLinkRecentDocuments === 'function') {
|
||||
const fileInfo = {};
|
||||
if (invoiceFile) fileInfo.invoice = invoiceFile.name;
|
||||
if (manualFile) fileInfo.manual = manualFile.name;
|
||||
if (otherDocumentFile) fileInfo.other = otherDocumentFile.name;
|
||||
window.autoLinkRecentDocuments(currentId, ['invoice', 'manual', 'other'], 10, 10000, fileInfo);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error reloading warranties after edit:', e);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (window && window.hideLoadingSpinner) window.hideLoadingSpinner();
|
||||
console.error('Error updating warranty:', error);
|
||||
if (window && window.showToast) window.showToast(error?.message || 'Failed to update warranty', 'error');
|
||||
}
|
||||
}
|
||||
95
frontend/js/components/tagManager.js
Normal file
@@ -0,0 +1,95 @@
|
||||
// Tag Manager (compat layer)
|
||||
// Encapsulates tag management by delegating to existing global implementations to keep behavior identical.
|
||||
|
||||
function callIfExists(fn, ...args) {
|
||||
if (typeof fn === 'function') {
|
||||
return fn(...args);
|
||||
}
|
||||
}
|
||||
|
||||
export function loadTags(force = false) {
|
||||
return callIfExists(window.loadTags, force);
|
||||
}
|
||||
|
||||
export function initTagFunctionality() {
|
||||
return callIfExists(window.initTagFunctionality);
|
||||
}
|
||||
|
||||
export function renderTagsList(searchTerm = '') {
|
||||
return callIfExists(window.renderTagsList, searchTerm);
|
||||
}
|
||||
|
||||
export function renderEditTagsList(searchTerm = '') {
|
||||
return callIfExists(window.renderEditTagsList, searchTerm);
|
||||
}
|
||||
|
||||
export function renderSelectedTags() {
|
||||
return callIfExists(window.renderSelectedTags);
|
||||
}
|
||||
|
||||
export function renderEditSelectedTags() {
|
||||
return callIfExists(window.renderEditSelectedTags);
|
||||
}
|
||||
|
||||
export function createTag(name) {
|
||||
return callIfExists(window.createTag, name);
|
||||
}
|
||||
|
||||
export function updateTag(id, name, color) {
|
||||
return callIfExists(window.updateTag, id, name, color);
|
||||
}
|
||||
|
||||
export function deleteTag(id) {
|
||||
return callIfExists(window.deleteTag, id);
|
||||
}
|
||||
|
||||
export function openTagManagementModal() {
|
||||
return callIfExists(window.openTagManagementModal);
|
||||
}
|
||||
|
||||
export function renderExistingTags() {
|
||||
return callIfExists(window.renderExistingTags);
|
||||
}
|
||||
|
||||
export function addTagManagementEventListeners() {
|
||||
return callIfExists(window.addTagManagementEventListeners);
|
||||
}
|
||||
|
||||
export function init() {
|
||||
// Ensure tag functionality on main form if present
|
||||
initTagFunctionality();
|
||||
// Attach global Manage Tags button if present
|
||||
const btn = document.getElementById('globalManageTagsBtn');
|
||||
if (btn && !btn._tmBound) {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await loadTags();
|
||||
} finally {
|
||||
openTagManagementModal();
|
||||
}
|
||||
});
|
||||
btn._tmBound = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Backward compatibility on window
|
||||
if (typeof window !== 'undefined') {
|
||||
window.tagManager = {
|
||||
init,
|
||||
loadTags,
|
||||
initTagFunctionality,
|
||||
renderTagsList,
|
||||
renderEditTagsList,
|
||||
renderSelectedTags,
|
||||
renderEditSelectedTags,
|
||||
createTag,
|
||||
updateTag,
|
||||
deleteTag,
|
||||
openTagManagementModal,
|
||||
renderExistingTags,
|
||||
addTagManagementEventListeners
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
622
frontend/js/controllers/warrantyListController.js
Normal file
@@ -0,0 +1,622 @@
|
||||
// Warranty List Controller
|
||||
// Incremental extraction of main list responsibilities with strict backward compatibility.
|
||||
|
||||
function callIfExists(fn, ...args) {
|
||||
if (typeof fn === 'function') return fn(...args);
|
||||
}
|
||||
|
||||
// Public API (delegations kept for now until full extraction of implementations)
|
||||
export async function loadWarranties(isAuthenticated) {
|
||||
// Show loading (tolerant to whichever loader exists)
|
||||
callIfExists(window.showLoading) || callIfExists(window.showLoadingSpinner);
|
||||
|
||||
try {
|
||||
// Ensure view preference is applied BEFORE first render so thumbnails use correct sizes
|
||||
callIfExists(window.loadViewPreference);
|
||||
|
||||
const __filters = (window.store && window.store.getFilters && window.store.getFilters()) || {};
|
||||
|
||||
// Check auth state (match legacy behavior)
|
||||
if (!isAuthenticated) {
|
||||
if (typeof window.i18next !== 'undefined') {
|
||||
callIfExists(window.renderEmptyState, window.i18next.t('messages.login_to_view_warranties'));
|
||||
} else {
|
||||
callIfExists(window.renderEmptyState, 'Please log in to view warranties.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const token = window.auth && window.auth.getToken ? window.auth.getToken() : null;
|
||||
if (!token) {
|
||||
if (typeof window.i18next !== 'undefined') {
|
||||
callIfExists(window.renderEmptyState, window.i18next.t('messages.authentication_error_login_again'));
|
||||
} else {
|
||||
callIfExists(window.renderEmptyState, 'Authentication error. Please log in again.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine scope and endpoint (mirrors legacy logic)
|
||||
const savedScope = callIfExists(window.loadViewScopePreference) || 'personal';
|
||||
const shouldUseGlobalView = savedScope === 'global';
|
||||
const baseUrl = window.location.origin;
|
||||
const isArchivedView = __filters && __filters.status === 'archived';
|
||||
const apiUrl = isArchivedView
|
||||
? (shouldUseGlobalView ? `${baseUrl}/api/warranties/global/archived` : `${baseUrl}/api/warranties/archived`)
|
||||
: (shouldUseGlobalView ? `${baseUrl}/api/warranties/global` : `${baseUrl}/api/warranties`);
|
||||
|
||||
// Fetch warranties
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
|
||||
});
|
||||
if (!response.ok) {
|
||||
let msg = `HTTP ${response.status}`;
|
||||
try {
|
||||
const j = await response.json();
|
||||
if (j && j.message) msg = j.message;
|
||||
} catch {}
|
||||
throw new Error(msg);
|
||||
}
|
||||
let data = await response.json();
|
||||
if (!Array.isArray(data)) data = [];
|
||||
|
||||
// Keep isGlobalView in sync with loaded scope
|
||||
try { if (typeof window.isGlobalView !== 'undefined') window.isGlobalView = shouldUseGlobalView; } catch {}
|
||||
|
||||
// Merge archived when Status=All (mirrors legacy behavior for both scopes)
|
||||
let combinedData = Array.isArray(data) ? data : [];
|
||||
try {
|
||||
// Reset includesArchived flag
|
||||
try { if (typeof window.lastLoadedIncludesArchived !== 'undefined') window.lastLoadedIncludesArchived = false; } catch {}
|
||||
if (!isArchivedView && __filters && __filters.status === 'all') {
|
||||
const archivedUrl = shouldUseGlobalView ? `${baseUrl}/api/warranties/global/archived` : `${baseUrl}/api/warranties/archived`;
|
||||
const archivedResp = await fetch(archivedUrl, { headers: { 'Authorization': `Bearer ${token}` } });
|
||||
if (archivedResp.ok) {
|
||||
const archivedData = await archivedResp.json();
|
||||
const archivedMarked = Array.isArray(archivedData) ? archivedData.map(w => ({ ...w, __isArchived: true })) : [];
|
||||
combinedData = combinedData.concat(archivedMarked);
|
||||
try { if (typeof window.lastLoadedIncludesArchived !== 'undefined') window.lastLoadedIncludesArchived = true; } catch {}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Process warranties using legacy processor if available
|
||||
const processed = Array.isArray(combinedData)
|
||||
? combinedData.map(w => {
|
||||
const p = callIfExists(window.processWarrantyData, w);
|
||||
return p !== undefined ? p : w;
|
||||
})
|
||||
: [];
|
||||
|
||||
if (window.store && window.store.setWarranties) window.store.setWarranties(processed);
|
||||
|
||||
// Update archived-load flag
|
||||
try { if (typeof window.lastLoadedArchived !== 'undefined') window.lastLoadedArchived = !!isArchivedView; } catch {}
|
||||
|
||||
const loaded = (window.store && window.store.getWarranties && window.store.getWarranties()) || [];
|
||||
if (loaded.length === 0) {
|
||||
if (typeof window.i18next !== 'undefined') {
|
||||
callIfExists(window.renderEmptyState, window.i18next.t('messages.no_warranties_found_add_first'));
|
||||
} else {
|
||||
callIfExists(window.renderEmptyState, 'No warranties yet. Add your first warranty to get started.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate filters and render
|
||||
try { populateTagFilter(); } catch {}
|
||||
try { populateVendorFilter(); } catch {}
|
||||
try { populateWarrantyTypeFilter(); } catch {}
|
||||
|
||||
callIfExists(window.applyFilters) || applyFilters();
|
||||
} catch (error) {
|
||||
console.error('[controller.loadWarranties] Error:', error);
|
||||
if (typeof window.i18next !== 'undefined') {
|
||||
callIfExists(window.renderEmptyState, window.i18next.t('messages.error_loading_warranties_try_again'));
|
||||
} else {
|
||||
callIfExists(window.renderEmptyState, 'Error loading warranties. Please try again.');
|
||||
}
|
||||
} finally {
|
||||
callIfExists(window.hideLoading) || callIfExists(window.hideLoadingSpinner);
|
||||
}
|
||||
}
|
||||
export function applyFilters() {
|
||||
// Mirrors legacy script.js logic to preserve identical behavior
|
||||
const __filters = (window.store && window.store.getFilters && window.store.getFilters()) || {};
|
||||
const __warranties = (window.store && window.store.getWarranties && window.store.getWarranties()) || [];
|
||||
|
||||
// Ensure data source correctness around archived/all toggles
|
||||
const wasArchivedLoaded = !!(window.lastLoadedArchived);
|
||||
const includesArchivedLoaded = !!(window.lastLoadedIncludesArchived);
|
||||
if (__filters.status === 'archived') {
|
||||
if (!wasArchivedLoaded) {
|
||||
loadWarranties(true);
|
||||
return;
|
||||
}
|
||||
} else if (wasArchivedLoaded) {
|
||||
loadWarranties(true);
|
||||
return;
|
||||
} else if (__filters.status === 'all' && !includesArchivedLoaded) {
|
||||
loadWarranties(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter warranties based on current filters
|
||||
const filtered = __warranties.filter(warranty => {
|
||||
// Exclude archived in specific status views (only show in 'all' or 'archived')
|
||||
if (warranty.is_archived && __filters.status !== 'all' && __filters.status !== 'archived') return false;
|
||||
// Status filter: allow archived items to pass in All view
|
||||
if (__filters.status !== 'all' && __filters.status !== 'archived' && warranty.status !== __filters.status) return false;
|
||||
// Tag filter
|
||||
if (__filters.tag !== 'all') {
|
||||
const tagId = parseInt(__filters.tag);
|
||||
const hasTag = warranty.tags && Array.isArray(warranty.tags) && warranty.tags.some(tag => tag.id === tagId);
|
||||
if (!hasTag) return false;
|
||||
}
|
||||
// Vendor filter
|
||||
if (__filters.vendor !== 'all' && (warranty.vendor || '').toLowerCase() !== (__filters.vendor || '').toLowerCase()) return false;
|
||||
// Warranty type filter
|
||||
if (__filters.warranty_type !== 'all' && (warranty.warranty_type || '').toLowerCase() !== (__filters.warranty_type || '').toLowerCase()) return false;
|
||||
// Search filter
|
||||
if (__filters.search) {
|
||||
const searchTerm = (__filters.search || '').toLowerCase();
|
||||
const productNameMatch = (warranty.product_name || '').toLowerCase().includes(searchTerm);
|
||||
const tagMatch = warranty.tags && Array.isArray(warranty.tags) && warranty.tags.some(tag => (tag.name || '').toLowerCase().includes(searchTerm));
|
||||
const notesMatch = (warranty.notes || '').toLowerCase().includes(searchTerm);
|
||||
const vendorMatch = (warranty.vendor || '').toLowerCase().includes(searchTerm);
|
||||
const modelNumberMatch = (warranty.model_number || '').toLowerCase().includes(searchTerm);
|
||||
const serialNumberMatch = warranty.serial_numbers && Array.isArray(warranty.serial_numbers) && warranty.serial_numbers.some(sn => sn && String(sn).toLowerCase().includes(searchTerm));
|
||||
if (!productNameMatch && !tagMatch && !notesMatch && !vendorMatch && !modelNumberMatch && !serialNumberMatch) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Preserve legacy behavior: just render (renderWarranties re-applies filters during render)
|
||||
renderWarranties();
|
||||
}
|
||||
export function renderWarranties() { return callIfExists(window.renderWarranties); }
|
||||
export function filterWarranties() { return callIfExists(window.filterWarranties); }
|
||||
|
||||
// Extracted implementations: dropdown population
|
||||
export function populateTagFilter() {
|
||||
const tagFilter = document.getElementById('tagFilter');
|
||||
if (!tagFilter) return;
|
||||
while (tagFilter.options.length > 1) tagFilter.remove(1);
|
||||
const uniqueTags = new Set();
|
||||
const all = ((window.store && window.store.getWarranties && window.store.getWarranties()) || []);
|
||||
all.forEach(warranty => {
|
||||
if (warranty.tags && Array.isArray(warranty.tags)) {
|
||||
warranty.tags.forEach(tag => uniqueTags.add(JSON.stringify({ id: tag.id, name: tag.name, color: tag.color })));
|
||||
}
|
||||
});
|
||||
const sortedTags = Array.from(uniqueTags).map(j => JSON.parse(j)).sort((a, b) => a.name.localeCompare(b.name));
|
||||
sortedTags.forEach(tag => {
|
||||
const option = document.createElement('option');
|
||||
option.value = tag.id;
|
||||
option.textContent = tag.name;
|
||||
tagFilter.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
export function populateVendorFilter() {
|
||||
const vendorFilterElement = document.getElementById('vendorFilter');
|
||||
if (!vendorFilterElement) return;
|
||||
while (vendorFilterElement.options.length > 1) vendorFilterElement.remove(1);
|
||||
const uniqueVendors = new Set();
|
||||
const all = ((window.store && window.store.getWarranties && window.store.getWarranties()) || []);
|
||||
all.forEach(warranty => {
|
||||
if (warranty.vendor && warranty.vendor.trim() !== '') uniqueVendors.add(warranty.vendor.trim().toLowerCase());
|
||||
});
|
||||
const sortedVendors = Array.from(uniqueVendors).sort((a, b) => a.localeCompare(b));
|
||||
sortedVendors.forEach(vendor => {
|
||||
const option = document.createElement('option');
|
||||
option.value = vendor;
|
||||
option.textContent = vendor.charAt(0).toUpperCase() + vendor.slice(1);
|
||||
vendorFilterElement.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
export function populateWarrantyTypeFilter() {
|
||||
const warrantyTypeFilterElement = document.getElementById('warrantyTypeFilter');
|
||||
if (!warrantyTypeFilterElement) return;
|
||||
while (warrantyTypeFilterElement.options.length > 1) warrantyTypeFilterElement.remove(1);
|
||||
const uniqueTypes = new Set();
|
||||
const all = ((window.store && window.store.getWarranties && window.store.getWarranties()) || []);
|
||||
all.forEach(warranty => {
|
||||
if (warranty.warranty_type && warranty.warranty_type.trim() !== '') uniqueTypes.add(warranty.warranty_type.trim().toLowerCase());
|
||||
});
|
||||
const sorted = Array.from(uniqueTypes).sort((a, b) => a.localeCompare(b));
|
||||
sorted.forEach(type => {
|
||||
const option = document.createElement('option');
|
||||
option.value = type;
|
||||
option.textContent = type.charAt(0).toUpperCase() + type.slice(1);
|
||||
warrantyTypeFilterElement.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Extracted implementations: persist and restore filters/sort
|
||||
export async function saveFilterPreferences(saveToApi = true) {
|
||||
try {
|
||||
const prefix = (typeof window.getPreferenceKeyPrefix === 'function') ? window.getPreferenceKeyPrefix() : '';
|
||||
const filters = (window.store && window.store.getFilters && window.store.getFilters()) || {};
|
||||
const filtersToSave = {
|
||||
status: filters.status || 'all',
|
||||
tag: filters.tag || 'all',
|
||||
vendor: filters.vendor || 'all',
|
||||
warranty_type: filters.warranty_type || 'all',
|
||||
search: filters.search || '',
|
||||
sortBy: filters.sortBy || 'expiration'
|
||||
};
|
||||
localStorage.setItem(`${prefix}warrantyFilters`, JSON.stringify(filtersToSave));
|
||||
|
||||
if (saveToApi && window.auth && window.auth.isAuthenticated && window.auth.isAuthenticated()) {
|
||||
const token = window.auth.getToken && window.auth.getToken();
|
||||
if (token) {
|
||||
try {
|
||||
await fetch('/api/auth/preferences', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ saved_filters: filtersToSave })
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[Filters] Error saving filter preferences to API:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to save filter preferences', e);
|
||||
}
|
||||
}
|
||||
|
||||
export function loadFilterAndSortPreferences() {
|
||||
try {
|
||||
const prefix = (typeof window.getPreferenceKeyPrefix === 'function') ? window.getPreferenceKeyPrefix() : '';
|
||||
const savedFiltersRaw = localStorage.getItem(`${prefix}warrantyFilters`);
|
||||
if (!savedFiltersRaw) return;
|
||||
const savedFilters = JSON.parse(savedFiltersRaw);
|
||||
if (!savedFilters || typeof savedFilters !== 'object') return;
|
||||
|
||||
const existing = (window.store && window.store.getFilters && window.store.getFilters()) || {};
|
||||
if (window.store && window.store.updateFilter) {
|
||||
if (savedFilters.status !== undefined) window.store.updateFilter('status', savedFilters.status || existing.status);
|
||||
if (savedFilters.tag !== undefined) window.store.updateFilter('tag', savedFilters.tag || existing.tag);
|
||||
if (savedFilters.vendor !== undefined) window.store.updateFilter('vendor', savedFilters.vendor || existing.vendor);
|
||||
if (savedFilters.warranty_type !== undefined) window.store.updateFilter('warranty_type', savedFilters.warranty_type || existing.warranty_type);
|
||||
if (savedFilters.search !== undefined) window.store.updateFilter('search', savedFilters.search || '');
|
||||
if (savedFilters.sortBy !== undefined) window.store.updateFilter('sortBy', savedFilters.sortBy || existing.sortBy);
|
||||
}
|
||||
|
||||
const searchInput = document.getElementById('searchWarranties');
|
||||
const clearSearchBtn = document.getElementById('clearSearch');
|
||||
if (searchInput && savedFilters.search) {
|
||||
searchInput.value = savedFilters.search;
|
||||
if (clearSearchBtn) clearSearchBtn.style.display = 'flex';
|
||||
if (searchInput.parentElement) searchInput.parentElement.classList.add('active-search');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load filter/sort preferences', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Extracted: setupUIEventListeners -> initEventListeners
|
||||
export function initEventListeners() {
|
||||
// --- Global Manage Tags Button ---
|
||||
const globalManageTagsBtn = document.getElementById('globalManageTagsBtn');
|
||||
if (globalManageTagsBtn && !globalManageTagsBtn._wlcBound) {
|
||||
globalManageTagsBtn.addEventListener('click', async () => {
|
||||
const __tags = (window.store && window.store.getAllTags && window.store.getAllTags()) || [];
|
||||
if (!__tags || __tags.length === 0) {
|
||||
callIfExists(window.showLoadingSpinner);
|
||||
try {
|
||||
await callIfExists(window.loadTags);
|
||||
} catch (error) {
|
||||
console.error('Failed to load tags before opening modal:', error);
|
||||
callIfExists(window.showToast, window.t ? window.t('messages.could_not_load_tags') : 'Could not load tags', 'error');
|
||||
callIfExists(window.hideLoadingSpinner);
|
||||
return;
|
||||
}
|
||||
callIfExists(window.hideLoadingSpinner);
|
||||
}
|
||||
callIfExists(window.openTagManagementModal);
|
||||
});
|
||||
globalManageTagsBtn._wlcBound = true;
|
||||
}
|
||||
|
||||
// Initialize edit tabs if available
|
||||
callIfExists(window.initEditTabs);
|
||||
|
||||
// Close modals on backdrop or dismiss buttons
|
||||
document.querySelectorAll('.modal-backdrop, [data-dismiss="modal"]').forEach(element => {
|
||||
if (element._wlcBound) return;
|
||||
element.addEventListener('click', (e) => {
|
||||
if (e.target === element || e.target.matches('[data-dismiss="modal"]')) {
|
||||
const modalToClose = e.target.closest('.modal-backdrop');
|
||||
if (modalToClose) {
|
||||
if ((modalToClose.id === 'addWarrantyModal' || modalToClose.id === 'editModal' || modalToClose.id === 'claimsModal' || modalToClose.id === 'claimFormModal') && e.target === modalToClose) {
|
||||
return;
|
||||
}
|
||||
modalToClose.classList.remove('active');
|
||||
if (modalToClose.id === 'editModal') {
|
||||
// optional reset area
|
||||
} else if (modalToClose.id === 'addWarrantyModal') {
|
||||
callIfExists(window.resetAddWarrantyWizard);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
element._wlcBound = true;
|
||||
});
|
||||
|
||||
// Prevent modal content clicks from closing the modal
|
||||
document.querySelectorAll('.modal').forEach(modal => {
|
||||
if (modal._wlcBound) return;
|
||||
modal.addEventListener('click', (e) => e.stopPropagation());
|
||||
modal._wlcBound = true;
|
||||
});
|
||||
|
||||
// Filter controls
|
||||
const searchInput = document.getElementById('searchWarranties');
|
||||
const clearSearchBtn = document.getElementById('clearSearch');
|
||||
const statusFilter = document.getElementById('statusFilter');
|
||||
const tagFilter = document.getElementById('tagFilter');
|
||||
const sortBySelect = document.getElementById('sortBy');
|
||||
const vendorFilter = document.getElementById('vendorFilter');
|
||||
const warrantyTypeFilter = document.getElementById('warrantyTypeFilter');
|
||||
|
||||
if (searchInput && !searchInput._wlcBound) {
|
||||
let searchDebounceTimeout;
|
||||
searchInput.addEventListener('input', () => {
|
||||
if (window.store && window.store.updateFilter) window.store.updateFilter('search', searchInput.value.toLowerCase());
|
||||
if (clearSearchBtn) clearSearchBtn.style.display = searchInput.value ? 'flex' : 'none';
|
||||
if (searchInput.value) {
|
||||
searchInput.parentElement.classList.add('active-search');
|
||||
} else {
|
||||
searchInput.parentElement.classList.remove('active-search');
|
||||
}
|
||||
if (searchDebounceTimeout) clearTimeout(searchDebounceTimeout);
|
||||
searchDebounceTimeout = setTimeout(() => {
|
||||
callIfExists(window.applyFilters);
|
||||
callIfExists(window.saveFilterPreferences);
|
||||
}, 300);
|
||||
});
|
||||
searchInput._wlcBound = true;
|
||||
}
|
||||
|
||||
if (clearSearchBtn && !clearSearchBtn._wlcBound) {
|
||||
clearSearchBtn.addEventListener('click', () => {
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
if (window.store && window.store.updateFilter) window.store.updateFilter('search', '');
|
||||
clearSearchBtn.style.display = 'none';
|
||||
searchInput.parentElement.classList.remove('active-search');
|
||||
searchInput.focus();
|
||||
callIfExists(window.applyFilters);
|
||||
callIfExists(window.saveFilterPreferences);
|
||||
}
|
||||
});
|
||||
clearSearchBtn._wlcBound = true;
|
||||
}
|
||||
|
||||
if (statusFilter && !statusFilter._wlcBound) {
|
||||
statusFilter.addEventListener('change', () => {
|
||||
if (window.store && window.store.updateFilter) window.store.updateFilter('status', statusFilter.value);
|
||||
callIfExists(window.applyFilters);
|
||||
callIfExists(window.saveFilterPreferences);
|
||||
});
|
||||
statusFilter._wlcBound = true;
|
||||
}
|
||||
|
||||
if (tagFilter && !tagFilter._wlcBound) {
|
||||
tagFilter.addEventListener('change', () => {
|
||||
if (window.store && window.store.updateFilter) window.store.updateFilter('tag', tagFilter.value);
|
||||
callIfExists(window.applyFilters);
|
||||
callIfExists(window.saveFilterPreferences);
|
||||
});
|
||||
tagFilter._wlcBound = true;
|
||||
}
|
||||
|
||||
if (vendorFilter && !vendorFilter._wlcBound) {
|
||||
vendorFilter.addEventListener('change', () => {
|
||||
if (window.store && window.store.updateFilter) window.store.updateFilter('vendor', vendorFilter.value);
|
||||
callIfExists(window.applyFilters);
|
||||
callIfExists(window.saveFilterPreferences);
|
||||
});
|
||||
vendorFilter._wlcBound = true;
|
||||
}
|
||||
|
||||
if (warrantyTypeFilter && !warrantyTypeFilter._wlcBound) {
|
||||
warrantyTypeFilter.addEventListener('change', () => {
|
||||
if (window.store && window.store.updateFilter) window.store.updateFilter('warranty_type', warrantyTypeFilter.value);
|
||||
callIfExists(window.applyFilters);
|
||||
callIfExists(window.saveFilterPreferences);
|
||||
});
|
||||
warrantyTypeFilter._wlcBound = true;
|
||||
}
|
||||
|
||||
if (sortBySelect && !sortBySelect._wlcBound) {
|
||||
sortBySelect.addEventListener('change', () => {
|
||||
if (window.store && window.store.updateFilter) window.store.updateFilter('sortBy', sortBySelect.value);
|
||||
callIfExists(window.applyFilters);
|
||||
callIfExists(window.saveFilterPreferences);
|
||||
});
|
||||
sortBySelect._wlcBound = true;
|
||||
}
|
||||
|
||||
// View switchers
|
||||
const gridViewBtn = document.getElementById('gridViewBtn');
|
||||
const listViewBtn = document.getElementById('listViewBtn');
|
||||
const tableViewBtn = document.getElementById('tableViewBtn');
|
||||
if (gridViewBtn && !gridViewBtn._wlcBound) { gridViewBtn.addEventListener('click', () => callIfExists(window.switchView, 'grid')); gridViewBtn._wlcBound = true; }
|
||||
if (listViewBtn && !listViewBtn._wlcBound) { listViewBtn.addEventListener('click', () => callIfExists(window.switchView, 'list')); listViewBtn._wlcBound = true; }
|
||||
if (tableViewBtn && !tableViewBtn._wlcBound) { tableViewBtn.addEventListener('click', () => callIfExists(window.switchView, 'table')); tableViewBtn._wlcBound = true; }
|
||||
|
||||
// Export / Import
|
||||
const exportBtn = document.getElementById('exportBtn');
|
||||
if (exportBtn && !exportBtn._wlcBound) { exportBtn.addEventListener('click', () => callIfExists(window.exportWarranties)); exportBtn._wlcBound = true; }
|
||||
const importBtn = document.getElementById('importBtn');
|
||||
const csvFileInput = document.getElementById('csvFileInput');
|
||||
if (importBtn && csvFileInput && !importBtn._wlcBound) {
|
||||
importBtn.addEventListener('click', () => csvFileInput.click());
|
||||
csvFileInput.addEventListener('change', (event) => {
|
||||
if (event.target.files && event.target.files.length > 0) {
|
||||
callIfExists(window.handleImport, event.target.files[0]);
|
||||
}
|
||||
});
|
||||
importBtn._wlcBound = true;
|
||||
}
|
||||
|
||||
// Refresh
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
if (refreshBtn && !refreshBtn._wlcBound) {
|
||||
refreshBtn.addEventListener('click', () => loadWarranties(true));
|
||||
refreshBtn._wlcBound = true;
|
||||
}
|
||||
|
||||
// Warranty Type custom input handling
|
||||
const warrantyTypeInput = document.getElementById('warrantyType');
|
||||
const warrantyTypeCustomInput = document.getElementById('warrantyTypeCustom');
|
||||
if (warrantyTypeInput && warrantyTypeCustomInput && !warrantyTypeInput._wlcBound) {
|
||||
warrantyTypeInput.addEventListener('change', () => {
|
||||
if (warrantyTypeInput.value === 'other') {
|
||||
warrantyTypeCustomInput.style.display = 'block';
|
||||
warrantyTypeCustomInput.focus();
|
||||
} else {
|
||||
warrantyTypeCustomInput.style.display = 'none';
|
||||
warrantyTypeCustomInput.value = '';
|
||||
}
|
||||
callIfExists(window.updateSummary);
|
||||
});
|
||||
warrantyTypeCustomInput.addEventListener('input', () => callIfExists(window.updateSummary));
|
||||
warrantyTypeInput._wlcBound = true;
|
||||
}
|
||||
|
||||
const editWarrantyTypeInput = document.getElementById('editWarrantyType');
|
||||
const editWarrantyTypeCustomInput = document.getElementById('editWarrantyTypeCustom');
|
||||
if (editWarrantyTypeInput && editWarrantyTypeCustomInput && !editWarrantyTypeInput._wlcBound) {
|
||||
editWarrantyTypeInput.addEventListener('change', () => {
|
||||
if (editWarrantyTypeInput.value === 'other') {
|
||||
editWarrantyTypeCustomInput.style.display = 'block';
|
||||
editWarrantyTypeCustomInput.focus();
|
||||
} else {
|
||||
editWarrantyTypeCustomInput.style.display = 'none';
|
||||
editWarrantyTypeCustomInput.value = '';
|
||||
}
|
||||
});
|
||||
editWarrantyTypeInput._wlcBound = true;
|
||||
}
|
||||
|
||||
// Save warranty changes
|
||||
const saveWarrantyBtn = document.getElementById('saveWarrantyBtn');
|
||||
if (saveWarrantyBtn && !saveWarrantyBtn._wlcBound) {
|
||||
let baseSave =
|
||||
(window.components && window.components.editModal && window.components.editModal.saveWarranty)
|
||||
? window.components.editModal.saveWarranty
|
||||
: window.saveWarranty;
|
||||
let functionToAttachOnClick = baseSave;
|
||||
if (typeof window.setupSaveWarrantyObserver === 'function') {
|
||||
try {
|
||||
functionToAttachOnClick = window.setupSaveWarrantyObserver(baseSave);
|
||||
window.saveWarrantyObserverAttachedByScriptJS = true;
|
||||
} catch (e) {
|
||||
console.error('[warrantyListController] Failed to wrap saveWarranty:', e);
|
||||
}
|
||||
}
|
||||
saveWarrantyBtn.addEventListener('click', () => {
|
||||
if (typeof functionToAttachOnClick === 'function') functionToAttachOnClick();
|
||||
});
|
||||
saveWarrantyBtn._wlcBound = true;
|
||||
}
|
||||
|
||||
// Confirm delete/archive
|
||||
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
|
||||
if (confirmDeleteBtn && !confirmDeleteBtn._wlcBound) { confirmDeleteBtn.addEventListener('click', () => callIfExists(window.deleteWarranty)); confirmDeleteBtn._wlcBound = true; }
|
||||
const confirmArchiveBtn = document.getElementById('confirmArchiveBtn');
|
||||
if (confirmArchiveBtn && !confirmArchiveBtn._wlcBound) { confirmArchiveBtn.addEventListener('click', () => callIfExists(window.confirmArchive)); confirmArchiveBtn._wlcBound = true; }
|
||||
|
||||
// List delegated events (claims/edit/delete/notes/archive/unarchive) - controller owns this exclusively
|
||||
const list = document.getElementById('warrantiesList');
|
||||
if (list && !list._wlcDelegated) {
|
||||
list.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.claims-link')) {
|
||||
e.preventDefault();
|
||||
const id = parseInt(e.target.closest('.claims-link')?.dataset?.id);
|
||||
if (typeof window.openClaimsModal === 'function' && id) window.openClaimsModal(id);
|
||||
return;
|
||||
}
|
||||
if (e.target.closest('.edit-btn')) {
|
||||
e.preventDefault();
|
||||
const el = e.target.closest('.edit-btn');
|
||||
if (el?.disabled) return;
|
||||
const id = parseInt(el?.dataset?.id);
|
||||
const warranty = ((window.store && window.store.getWarranties && window.store.getWarranties()) || []).find(w => w.id === id);
|
||||
if (warranty && typeof window.openEditModal === 'function') window.openEditModal(warranty);
|
||||
return;
|
||||
}
|
||||
if (e.target.closest('.delete-btn')) {
|
||||
e.preventDefault();
|
||||
const el = e.target.closest('.delete-btn');
|
||||
const id = parseInt(el?.dataset?.id);
|
||||
const card = el?.closest('.warranty-card');
|
||||
const titleEl = card ? card.querySelector('.product-name-header .warranty-title') : null;
|
||||
const productName = titleEl ? titleEl.textContent.trim() : '';
|
||||
if (typeof window.openDeleteModal === 'function') window.openDeleteModal(id, productName);
|
||||
return;
|
||||
}
|
||||
if (e.target.closest('.notes-link')) {
|
||||
e.preventDefault();
|
||||
const el = e.target.closest('.notes-link');
|
||||
const id = parseInt(el?.dataset?.id);
|
||||
const warranty = ((window.store && window.store.getWarranties && window.store.getWarranties()) || []).find(w => w.id === id);
|
||||
if (warranty && typeof window.showNotesModal === 'function') window.showNotesModal(warranty.notes, warranty);
|
||||
return;
|
||||
}
|
||||
if (e.target.closest('.archive-btn')) {
|
||||
e.preventDefault();
|
||||
const el = e.target.closest('.archive-btn');
|
||||
const id = parseInt(el?.dataset?.id);
|
||||
const card = el?.closest('.warranty-card');
|
||||
const titleEl = card ? card.querySelector('.product-name-header .warranty-title') : null;
|
||||
const productName = titleEl ? titleEl.textContent.trim() : '';
|
||||
if (typeof window.openArchiveModal === 'function') window.openArchiveModal(id, productName);
|
||||
return;
|
||||
}
|
||||
if (e.target.closest('.unarchive-btn')) {
|
||||
e.preventDefault();
|
||||
const id = parseInt(e.target.closest('.unarchive-btn')?.dataset?.id);
|
||||
if (typeof window.toggleArchiveStatus === 'function' && id) window.toggleArchiveStatus(id, false);
|
||||
return;
|
||||
}
|
||||
});
|
||||
list._wlcDelegated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Backward compatibility for legacy callers
|
||||
if (typeof window !== 'undefined') {
|
||||
window.warrantyListController = {
|
||||
loadWarranties,
|
||||
applyFilters,
|
||||
renderWarranties,
|
||||
filterWarranties,
|
||||
populateTagFilter,
|
||||
populateVendorFilter,
|
||||
populateWarrantyTypeFilter,
|
||||
saveFilterPreferences,
|
||||
loadFilterAndSortPreferences,
|
||||
initEventListeners
|
||||
};
|
||||
// Legacy globals to avoid breaking existing references during staged extraction
|
||||
window.populateTagFilter = populateTagFilter;
|
||||
window.populateVendorFilter = populateVendorFilter;
|
||||
window.populateWarrantyTypeFilter = populateWarrantyTypeFilter;
|
||||
window.saveFilterPreferences = saveFilterPreferences;
|
||||
window.loadFilterAndSortPreferences = loadFilterAndSortPreferences;
|
||||
}
|
||||
|
||||
export const init = initEventListeners;
|
||||
|
||||
|
||||
25
frontend/js/index.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// Main Application Entry Point
|
||||
import authService from './services/authService.js';
|
||||
import { init as initListController, loadWarranties } from './controllers/warrantyListController.js';
|
||||
import { init as initTagManager } from './components/tagManager.js';
|
||||
import { init as initAddForm } from './components/addWarrantyForm.js';
|
||||
|
||||
// Initialize auth first (idempotent; authService also auto-inits on DOMContentLoaded)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
authService.initAuth();
|
||||
});
|
||||
|
||||
// Wait for authentication to be confirmed before loading protected data and initializing auth-dependent modules
|
||||
window.addEventListener('authStateReady', (event) => {
|
||||
if (event.detail && event.detail.isAuthenticated) {
|
||||
// Initialize all feature modules
|
||||
initListController();
|
||||
initTagManager();
|
||||
initAddForm();
|
||||
|
||||
// Trigger the initial data load for the application
|
||||
loadWarranties(true);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import authService from '/js/services/authService.js';
|
||||
import authService from './authService.js';
|
||||
|
||||
async function baseRequest(path, options = {}) {
|
||||
const headers = new Headers(options.headers || {});
|
||||
@@ -48,6 +48,60 @@ export async function getStatistics() {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// --- Settings & Admin endpoints ---
|
||||
export async function getUser() {
|
||||
const res = await baseRequest('/api/auth/user');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getSiteSettings() {
|
||||
const res = await baseRequest('/api/admin/settings');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function saveSiteSettings(settings) {
|
||||
const res = await baseRequest('/api/admin/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getUsers() {
|
||||
const res = await baseRequest('/api/admin/users');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function updateUser(userId, data) {
|
||||
const res = await baseRequest(`/api/admin/users/${userId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function deleteUser(userId) {
|
||||
await baseRequest(`/api/admin/users/${userId}`, { method: 'DELETE' });
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function getAuditTrail() {
|
||||
const res = await baseRequest('/api/admin/audit-trail');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function triggerNotifications() {
|
||||
const res = await baseRequest('/api/admin/send-notifications', { method: 'POST' });
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// Generic request helper for arbitrary endpoints
|
||||
export async function request(path, options = {}) {
|
||||
return baseRequest(path, options);
|
||||
}
|
||||
|
||||
export async function savePreferences(prefs) {
|
||||
const res = await baseRequest('/api/auth/preferences', {
|
||||
method: 'POST',
|
||||
@@ -81,7 +135,17 @@ if (typeof window !== 'undefined') {
|
||||
updateWarranty,
|
||||
deleteWarranty,
|
||||
getStatistics,
|
||||
request,
|
||||
savePreferences,
|
||||
// settings & admin
|
||||
getUser,
|
||||
getSiteSettings,
|
||||
saveSiteSettings,
|
||||
getUsers,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
getAuditTrail,
|
||||
triggerNotifications,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,7 +154,17 @@ export default {
|
||||
updateWarranty,
|
||||
deleteWarranty,
|
||||
getStatistics,
|
||||
request,
|
||||
savePreferences,
|
||||
// settings & admin
|
||||
getUser,
|
||||
getSiteSettings,
|
||||
saveSiteSettings,
|
||||
getUsers,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
getAuditTrail,
|
||||
triggerNotifications,
|
||||
};
|
||||
|
||||
|
||||
20
frontend/js/services/i18nAdapter.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// Small adapter to provide a stable i18n interface for modules.
|
||||
// It prefers a global `window.i18next` (loaded via non-module scripts),
|
||||
// but keeps a safe fallback API so callers can import this module.
|
||||
const i18nAdapter = {
|
||||
t: (...args) => {
|
||||
if (typeof window !== 'undefined' && window.i18next && typeof window.i18next.t === 'function') return window.i18next.t(...args);
|
||||
// fallback: return default or key
|
||||
return args[1] || args[0] || '';
|
||||
},
|
||||
changeLanguage: (lang) => {
|
||||
if (typeof window !== 'undefined' && window.i18next && typeof window.i18next.changeLanguage === 'function') return window.i18next.changeLanguage(lang);
|
||||
return Promise.resolve();
|
||||
},
|
||||
getCurrentLanguage: () => {
|
||||
if (typeof window !== 'undefined' && window.i18next && window.i18next.language) return window.i18next.language;
|
||||
return 'en';
|
||||
}
|
||||
};
|
||||
|
||||
export default i18nAdapter;
|
||||
@@ -1,19 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="/auth-redirect.js?v=20250119001" data-protected="false"></script>
|
||||
<script src="./auth-redirect.js?v=20250119001" data-protected="false"></script>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title data-i18n="auth.login_title">Login - Warracker</title>
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png?v=2">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png?v=2">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/favicon-512x512.png">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="stylesheet" href="/style.css?v=20250119004">
|
||||
<script src="/theme-loader.js?v=20250119001"></script>
|
||||
<script type="module" src="/js/services/authService.js"></script>
|
||||
<script type="module" src="/js/services/apiService.js"></script>
|
||||
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="./img/favicon-16x16.png?v=2">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="./img/favicon-32x32.png?v=2">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./img/favicon-512x512.png">
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
<link rel="stylesheet" href="./style.css?v=20250119004">
|
||||
<script src="./theme-loader.js?v=20250119001"></script>
|
||||
<script type="module" src="./js/services/authService.js"></script>
|
||||
<script type="module" src="./js/services/apiService.js"></script>
|
||||
<script>
|
||||
// If user is authenticated and has a theme in user_info (from previous sessions), reflect it early
|
||||
try {
|
||||
@@ -24,10 +24,10 @@
|
||||
} catch (_) {}
|
||||
</script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
||||
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
<script src="/js/i18n.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
<script src="./js/i18n.js?v=20250119001"></script>
|
||||
|
||||
|
||||
</head>
|
||||
@@ -84,7 +84,7 @@
|
||||
</form>
|
||||
<div id="ssoSection" style="display: none;">
|
||||
<div class="separator"><span>OR</span></div>
|
||||
<a href="/api/oidc/login" id="oidcLoginButton" class="btn btn-secondary btn-block btn-sso" style="text-decoration: none;">
|
||||
<a href="./api/oidc/login" id="oidcLoginButton" class="btn btn-secondary btn-block btn-sso" style="text-decoration: none;">
|
||||
<i class="fab fa-openid" style="margin-right: 8px;"></i> <span>Login with SSO Provider</span>
|
||||
</a>
|
||||
</div>
|
||||
1061
frontend/package-lock.json
generated
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "auth-new.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"devDependencies": {
|
||||
"vite": "^7.1.12"
|
||||
}
|
||||
}
|
||||
@@ -2,36 +2,37 @@
|
||||
<html>
|
||||
<head>
|
||||
<!-- Authentication redirect script -->
|
||||
<script src="/auth-redirect.js?v=20250119001" data-protected="false"></script>
|
||||
<script src="./auth-redirect.js?v=20250119001" data-protected="false"></script>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title data-i18n="auth.register">Register - Warracker</title>
|
||||
<!-- Favicons -->
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png?v=2">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png?v=2">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/favicon-512x512.png">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="stylesheet" href="/style.css?v=20250119004">
|
||||
<script src="/theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
|
||||
<script type="module" src="/js/services/authService.js"></script>
|
||||
<script type="module" src="/js/services/apiService.js"></script>
|
||||
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="./img/favicon-16x16.png?v=2">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="./img/favicon-32x32.png?v=2">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./img/favicon-512x512.png">
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
<link rel="stylesheet" href="./style.css?v=20250119004">
|
||||
<script src="./theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
|
||||
<script type="module" src="./js/services/authService.js"></script>
|
||||
<script type="module" src="./js/services/apiService.js"></script>
|
||||
<!-- Font Awesome for icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
||||
<!-- Load header fix styles to ensure consistent header styling -->
|
||||
<link rel="stylesheet" href="/header-fix.css?v=20250119001">
|
||||
<link rel="stylesheet" href="./header-fix.css?v=20250119001">
|
||||
<!-- Mobile Header specific styles -->
|
||||
<link rel="stylesheet" href="/mobile-header.css?v=20250119002">
|
||||
<link rel="stylesheet" href="./mobile-header.css?v=20250119002">
|
||||
<!-- i18next Local Scripts -->
|
||||
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
<!-- i18n initialization script -->
|
||||
<script src="/js/i18n.js?v=20250119001"></script>
|
||||
<script src="./js/i18n.js?v=20250119001"></script>
|
||||
<!-- Load fix for auth buttons -->
|
||||
|
||||
<script src="/script.js?v=20250119001" defer></script>
|
||||
<script src="./script.js?v=20250119001" defer></script>
|
||||
<script type="module" src="./js/index.js"></script>
|
||||
<style>
|
||||
.auth-container {
|
||||
max-width: 500px;
|
||||
@@ -621,10 +622,10 @@
|
||||
</script>
|
||||
|
||||
<!-- Footer Width Fix -->
|
||||
<script src="/footer-fix.js?v=20251024001"></script>
|
||||
<script src="./footer-fix.js?v=20251024001"></script>
|
||||
|
||||
<!-- Footer Content Manager -->
|
||||
<script src="/footer-content.js?v=20250119001"></script>
|
||||
<script src="./footer-content.js?v=20250119001"></script>
|
||||
|
||||
<!-- Powered by Warracker Footer -->
|
||||
<footer class="warracker-footer" id="warrackerFooter">
|
||||
@@ -5,30 +5,31 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title data-i18n="auth.reset_password_title">Warracker - Reset Password</title>
|
||||
<!-- Favicons -->
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png?v=2">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png?v=2">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/favicon-512x512.png">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="stylesheet" href="/style.css?v=20250119004">
|
||||
<script src="/theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
|
||||
<script type="module" src="/js/services/authService.js"></script>
|
||||
<script type="module" src="/js/services/apiService.js"></script>
|
||||
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="./img/favicon-16x16.png?v=2">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="./img/favicon-32x32.png?v=2">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./img/favicon-512x512.png">
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
<link rel="stylesheet" href="./style.css?v=20250119004">
|
||||
<script src="./theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
|
||||
<script type="module" src="./js/services/authService.js"></script>
|
||||
<script type="module" src="./js/services/apiService.js"></script>
|
||||
<!-- Font Awesome for icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
||||
<!-- Load header fix styles to ensure consistent header styling -->
|
||||
<link rel="stylesheet" href="/header-fix.css?v=20250119001">
|
||||
<link rel="stylesheet" href="./header-fix.css?v=20250119001">
|
||||
<!-- Mobile Header specific styles -->
|
||||
<link rel="stylesheet" href="/mobile-header.css?v=20250119002">
|
||||
<link rel="stylesheet" href="./mobile-header.css?v=20250119002">
|
||||
<!-- i18next Local Scripts -->
|
||||
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
<!-- i18n initialization script -->
|
||||
<script src="/js/i18n.js?v=20250119001"></script>
|
||||
<script src="./js/i18n.js?v=20250119001"></script>
|
||||
<!-- Load fix for auth buttons -->
|
||||
|
||||
<script src="/script.js?v=20250119001" defer></script>
|
||||
<script src="./script.js?v=20250119001" defer></script>
|
||||
<script type="module" src="./js/index.js"></script>
|
||||
<style>
|
||||
.auth-container {
|
||||
max-width: 400px;
|
||||
@@ -264,10 +265,10 @@
|
||||
|
||||
|
||||
<!-- Footer Width Fix -->
|
||||
<script src="/footer-fix.js?v=20251024001"></script>
|
||||
<script src="./footer-fix.js?v=20251024001"></script>
|
||||
|
||||
<!-- Footer Content Manager -->
|
||||
<script src="/footer-content.js?v=20250119001"></script>
|
||||
<script src="./footer-content.js?v=20250119001"></script>
|
||||
|
||||
<!-- Powered by Warracker Footer -->
|
||||
<footer class="warracker-footer" id="warrackerFooter">
|
||||
@@ -5,30 +5,31 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title data-i18n="auth.set_new_password_title">Warracker - Set New Password</title>
|
||||
<!-- Favicons -->
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png?v=2">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png?v=2">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/favicon-512x512.png">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="stylesheet" href="/style.css?v=20250119004">
|
||||
<script src="/theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
|
||||
<script type="module" src="/js/services/authService.js"></script>
|
||||
<script type="module" src="/js/services/apiService.js"></script>
|
||||
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="./img/favicon-16x16.png?v=2">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="./img/favicon-32x32.png?v=2">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./img/favicon-512x512.png">
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
<link rel="stylesheet" href="./style.css?v=20250119004">
|
||||
<script src="./theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
|
||||
<script type="module" src="./js/services/authService.js"></script>
|
||||
<script type="module" src="./js/services/apiService.js"></script>
|
||||
<!-- Font Awesome for icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
||||
<!-- Load header fix styles to ensure consistent header styling -->
|
||||
<link rel="stylesheet" href="/header-fix.css?v=20250119001">
|
||||
<link rel="stylesheet" href="./header-fix.css?v=20250119001">
|
||||
<!-- Mobile Header specific styles -->
|
||||
<link rel="stylesheet" href="/mobile-header.css?v=20250119002">
|
||||
<link rel="stylesheet" href="./mobile-header.css?v=20250119002">
|
||||
<!-- i18next Local Scripts -->
|
||||
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
<!-- i18n initialization script -->
|
||||
<script src="/js/i18n.js?v=20250119001"></script>
|
||||
<script src="./js/i18n.js?v=20250119001"></script>
|
||||
<!-- Load fix for auth buttons -->
|
||||
|
||||
<script src="/script.js?v=20250119001" defer></script>
|
||||
<script src="./script.js?v=20250119001" defer></script>
|
||||
<script type="module" src="./js/index.js"></script>
|
||||
<style>
|
||||
.auth-container {
|
||||
max-width: 400px;
|
||||
@@ -431,10 +432,10 @@
|
||||
|
||||
|
||||
<!-- Footer Width Fix -->
|
||||
<script src="/footer-fix.js?v=20251024001"></script>
|
||||
<script src="./footer-fix.js?v=20251024001"></script>
|
||||
|
||||
<!-- Footer Content Manager -->
|
||||
<script src="/footer-content.js?v=20250119001"></script>
|
||||
<script src="./footer-content.js?v=20250119001"></script>
|
||||
|
||||
<!-- Powered by Warracker Footer -->
|
||||
<footer class="warracker-footer" id="warrackerFooter">
|
||||
@@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<!-- Authentication redirect script -->
|
||||
<script src="/auth-redirect.js?v=20250119001" data-protected="true"></script>
|
||||
<script src="./auth-redirect.js?v=20250119001" data-protected="true"></script>
|
||||
|
||||
<!-- Include authentication script first to handle login state immediately -->
|
||||
|
||||
@@ -11,38 +11,38 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Settings - Warracker</title>
|
||||
<!-- Favicons -->
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png?v=2">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png?v=2">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/favicon-512x512.png">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="./img/favicon-16x16.png?v=2">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="./img/favicon-32x32.png?v=2">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./img/favicon-512x512.png">
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
<!-- Load the main site styles first -->
|
||||
<link rel="stylesheet" href="/style.css?v=20250119004">
|
||||
<link rel="stylesheet" href="./style.css?v=20250119004">
|
||||
<!-- Font Awesome for icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
||||
<!-- Then load settings-specific styles -->
|
||||
<link rel="stylesheet" href="/settings-styles.css?v=20250119001">
|
||||
<link rel="stylesheet" href="./settings-styles.css?v=20250119001">
|
||||
<!-- Apply theme immediately -->
|
||||
<script src="/theme-loader.js?v=20250119001"></script>
|
||||
<script type="module" src="/js/services/authService.js"></script>
|
||||
<script type="module" src="/js/services/apiService.js"></script>
|
||||
<script src="./theme-loader.js?v=20250119001"></script>
|
||||
<script type="module" src="./js/services/authService.js"></script>
|
||||
<script type="module" src="./js/services/apiService.js"></script>
|
||||
<!-- Load header fix styles last to override any conflicting styles -->
|
||||
<link rel="stylesheet" href="/header-fix.css?v=20250119001">
|
||||
<link rel="stylesheet" href="./header-fix.css?v=20250119001">
|
||||
<!-- Mobile Header specific styles -->
|
||||
<link rel="stylesheet" href="/mobile-header.css?v=20250119002">
|
||||
<link rel="stylesheet" href="./mobile-header.css?v=20250119002">
|
||||
<!-- Load fix for auth buttons -->
|
||||
|
||||
|
||||
<!-- i18next Local Scripts -->
|
||||
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
|
||||
<!-- i18n Configuration -->
|
||||
<script src="/js/i18n.js?v=20250119001"></script>
|
||||
<script src="./js/i18n.js?v=20250119001"></script>
|
||||
|
||||
<!-- Mobile menu logic isolated to avoid collisions on settings -->
|
||||
<script src="/mobile-menu.js?v=20250119002" defer></script>
|
||||
<script src="./mobile-menu.js?v=20250119002" defer></script>
|
||||
|
||||
<!-- Immediate authentication check script -->
|
||||
<script>
|
||||
@@ -1076,52 +1076,17 @@
|
||||
<p style="font-size: 0.8em; color: #666;">User ID: <span id="displayUserId"></span></p>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary close-modal">Cancel</button>
|
||||
<button type="button" id="confirmDeleteUserBtn" class="btn btn-danger" onclick="deleteUser()">Delete</button>
|
||||
<button type="button" id="confirmDeleteUserBtn" class="btn btn-danger">Delete</button>
|
||||
</div>
|
||||
<div style="margin-top: 20px; border-top: 1px solid #ddd; padding-top: 10px;">
|
||||
<p><small>If the delete button doesn't work, try these alternatives:</small></p>
|
||||
<ul style="font-size: 0.8em;">
|
||||
<li><a href="#" id="directDeleteLink" style="color: #dc3545;" onclick="deleteUser(); return false;">Alternative 1: Direct function call</a></li>
|
||||
<li><a href="#" id="directAPILink" style="color: #dc3545;" onclick="directDeleteUserAPI(document.getElementById('deleteUserId').value); return false;">Alternative 2: Direct API call</a></li>
|
||||
<li><a href="#" id="directDeleteLink" style="color: #dc3545;">Alternative 1: Direct function call</a></li>
|
||||
<li><a href="#" id="directAPILink" style="color: #dc3545;">Alternative 2: Direct API call</a></li>
|
||||
<li><button type="submit" class="btn btn-sm btn-danger">Alternative 3: Form Submit</button></li>
|
||||
</ul>
|
||||
<div style="margin-top: 10px;">
|
||||
<button type="button" id="emergencyDeleteBtn" class="btn btn-sm btn-danger"
|
||||
onclick="(function() {
|
||||
const userId = document.getElementById('deleteUserId').value || document.getElementById('displayUserId').textContent;
|
||||
if (!userId) {
|
||||
alert('Error: User ID is missing');
|
||||
return;
|
||||
}
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (!token) {
|
||||
alert('Error: Authentication token is missing');
|
||||
return;
|
||||
}
|
||||
alert('Emergency delete for user ID: ' + userId);
|
||||
fetch('/api/admin/users/' + userId, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
alert('Response status: ' + response.status);
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
alert('User deleted successfully');
|
||||
document.querySelectorAll('.modal-backdrop').forEach(m => m.style.display = 'none');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Failed to delete user: ' + response.status);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Error: ' + error.message);
|
||||
});
|
||||
})(); return false;">
|
||||
Emergency Delete (Inline JS)
|
||||
</button>
|
||||
<button type="button" id="emergencyDeleteBtn" class="btn btn-sm btn-danger">Emergency Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1172,13 +1137,13 @@
|
||||
|
||||
<!-- Scripts -->
|
||||
|
||||
<script src="/settings-new.js?v=20250119001"></script>
|
||||
<script type="module" src="./settings-new.js"></script>
|
||||
|
||||
<!-- Footer Width Fix -->
|
||||
<script src="/footer-fix.js?v=20251024001"></script>
|
||||
<script src="./footer-fix.js?v=20251024001"></script>
|
||||
|
||||
<!-- Footer Content Manager -->
|
||||
<script src="/footer-content.js?v=20250119001"></script>
|
||||
<script src="./footer-content.js?v=20250119001"></script>
|
||||
|
||||
<!-- Powered by Warracker Footer -->
|
||||
<footer class="warracker-footer" id="warrackerFooter">
|
||||
@@ -2,46 +2,47 @@
|
||||
<html>
|
||||
<head>
|
||||
<!-- Authentication redirect script -->
|
||||
<script src="/auth-redirect.js?v=20250119001" data-protected="true"></script>
|
||||
<script src="./auth-redirect.js?v=20250119001" data-protected="true"></script>
|
||||
|
||||
<!-- Include authentication script first to handle login state immediately -->
|
||||
|
||||
|
||||
<!-- File utilities script for secure file handling -->
|
||||
<script src="/file-utils.js?v=20250119001"></script>
|
||||
<script src="./file-utils.js?v=20250119001"></script>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title data-i18n="status.title">Status - Warracker</title>
|
||||
<!-- Favicons -->
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png?v=2">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png?v=2">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/favicon-512x512.png">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="stylesheet" href="/style.css?v=20250119004">
|
||||
<script src="/theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
|
||||
<script type="module" src="/js/store.js"></script>
|
||||
<script type="module" src="/js/services/authService.js"></script>
|
||||
<script type="module" src="/js/services/apiService.js"></script>
|
||||
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="./img/favicon-16x16.png?v=2">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="./img/favicon-32x32.png?v=2">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./img/favicon-512x512.png">
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
<link rel="stylesheet" href="./style.css?v=20250119004">
|
||||
<script src="./theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
|
||||
<script type="module" src="./js/store.js"></script>
|
||||
<script type="module" src="./js/services/authService.js"></script>
|
||||
<script type="module" src="./js/services/apiService.js"></script>
|
||||
<!-- Font Awesome for icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
||||
<!-- Load header fix styles to ensure consistent header styling -->
|
||||
<link rel="stylesheet" href="/header-fix.css?v=20250119001">
|
||||
<link rel="stylesheet" href="./header-fix.css?v=20250119001">
|
||||
<!-- Mobile Header specific styles -->
|
||||
<link rel="stylesheet" href="/mobile-header.css?v=20250119002">
|
||||
<link rel="stylesheet" href="./mobile-header.css?v=20250119002">
|
||||
<!-- Chart.js for visualizations -->
|
||||
<script src="/chart.js?v=20250119001"></script>
|
||||
<script src="./chart.js?v=20250119001"></script>
|
||||
<!-- i18next Local Scripts -->
|
||||
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
|
||||
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
|
||||
<!-- i18n initialization script -->
|
||||
<script src="/js/i18n.js?v=20250119001"></script>
|
||||
<script src="./js/i18n.js?v=20250119001"></script>
|
||||
<!-- Load fix for auth buttons -->
|
||||
|
||||
<script src="/script.js?v=20250119002" defer></script> <!-- Added script.js -->
|
||||
<script type="module" src="/status.js?v=20250119001"></script> <!-- Status page specific functionality -->
|
||||
<script src="./script.js?v=20250119002" defer></script> <!-- Added script.js -->
|
||||
<script type="module" src="./js/index.js"></script>
|
||||
<script type="module" src="./status.js?v=20250119001"></script> <!-- Status page specific functionality -->
|
||||
<style>
|
||||
.user-menu {
|
||||
position: relative;
|
||||
@@ -826,13 +827,13 @@
|
||||
</div>
|
||||
|
||||
|
||||
<script type="module" src="/status.js?v=20250119001"></script>
|
||||
<script type="module" src="./status.js?v=20250119001"></script>
|
||||
|
||||
<!-- Footer Width Fix -->
|
||||
<script src="/footer-fix.js?v=20251024001"></script>
|
||||
<script src="./footer-fix.js?v=20251024001"></script>
|
||||
|
||||
<!-- Footer Content Manager -->
|
||||
<script src="/footer-content.js?v=20250119001"></script>
|
||||
<script src="./footer-content.js?v=20250119001"></script>
|
||||
|
||||
<!-- Powered by Warracker Footer -->
|
||||
<footer class="warracker-footer" id="warrackerFooter">
|
||||
@@ -1,3 +1,5 @@
|
||||
import apiService from './js/services/apiService.js';
|
||||
import authService from './js/services/authService.js';
|
||||
(function() {
|
||||
// DOM Elements
|
||||
const loadingIndicator = document.getElementById('loadingIndicator');
|
||||
@@ -101,17 +103,13 @@
|
||||
async function fetchStatistics() {
|
||||
try {
|
||||
console.log('Checking authentication status... (status.js IIFE)');
|
||||
if (!window.auth || !window.auth.isAuthenticated()) {
|
||||
if (!authService || !authService.isAuthenticated()) {
|
||||
throw new Error('Authentication required.');
|
||||
}
|
||||
const token = window.auth.getToken();
|
||||
const token = authService.getToken();
|
||||
if (!token) {
|
||||
throw new Error('Authentication token not available.');
|
||||
}
|
||||
const options = {
|
||||
method: 'GET',
|
||||
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json'}
|
||||
};
|
||||
|
||||
// Check saved view scope preference to determine which API endpoint to use
|
||||
const savedScope = loadViewScopePreference();
|
||||
@@ -121,17 +119,15 @@
|
||||
const apiUrl = shouldUseGlobalView ? GLOBAL_STATISTICS_API_URL : STATISTICS_API_URL;
|
||||
console.log('Fetching statistics from:', apiUrl, '(Global view preference:', savedScope, ')');
|
||||
|
||||
const response = await fetch(apiUrl, options);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to fetch statistics: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
const response = shouldUseGlobalView
|
||||
? await apiService.request('/api/statistics/global')
|
||||
: await apiService.request('/api/statistics');
|
||||
const json = await response.json();
|
||||
// Update isGlobalView to match the loaded data
|
||||
isGlobalView = shouldUseGlobalView;
|
||||
console.log(`[DEBUG] Set isGlobalView to: ${isGlobalView}`);
|
||||
|
||||
return await response.json();
|
||||
return json;
|
||||
} catch (error) {
|
||||
console.error('Error fetching statistics (status.js IIFE):', error);
|
||||
throw error;
|
||||
@@ -158,13 +154,10 @@
|
||||
|
||||
try {
|
||||
// Check if global view is enabled for this user
|
||||
const token = window.auth.getToken();
|
||||
const token = authService.getToken();
|
||||
if (!token) return;
|
||||
|
||||
const response = await fetch('/api/settings/global-view-status', {
|
||||
method: 'GET',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
const response = await apiService.request('/api/settings/global-view-status', { method: 'GET' });
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
@@ -343,11 +336,8 @@
|
||||
|
||||
try {
|
||||
// Check if global view is still available
|
||||
const token = window.auth.getToken();
|
||||
const response = await fetch('/api/settings/global-view-status', {
|
||||
method: 'GET',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
const token = authService.getToken();
|
||||
const response = await apiService.request('/api/settings/global-view-status', { method: 'GET' });
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
@@ -944,14 +934,9 @@
|
||||
let allWarrantiesForTimeline = [];
|
||||
try {
|
||||
// Try to fetch all warranties for a complete timeline
|
||||
const token = window.auth && window.auth.getToken ? window.auth.getToken() : null;
|
||||
const token = authService && authService.getToken ? authService.getToken() : null;
|
||||
if (token) {
|
||||
const allWarrantiesResponse = await fetch('/api/warranties', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
const allWarrantiesResponse = await apiService.request('/api/warranties', { method: 'GET' });
|
||||
if (allWarrantiesResponse.ok) {
|
||||
allWarrantiesForTimeline = await allWarrantiesResponse.json();
|
||||
} else {
|
||||
@@ -1032,9 +1017,8 @@
|
||||
|
||||
async function loadUserPreferences() {
|
||||
try {
|
||||
if (!window.auth || !window.auth.getToken()) { console.warn('Auth or token not available for prefs in status.js'); return; }
|
||||
const token = window.auth.getToken();
|
||||
const response = await fetch('/api/auth/preferences', { headers: { 'Authorization': `Bearer ${token}` } });
|
||||
if (!authService || !authService.getToken()) { console.warn('Auth or token not available for prefs in status.js'); return; }
|
||||
const response = await apiService.request('/api/auth/preferences', { method: 'GET' });
|
||||
if (response.ok) {
|
||||
const prefs = await response.json();
|
||||
if (prefs && prefs.expiring_soon_days !== undefined) { // Check for undefined specifically
|
||||
@@ -1088,13 +1072,10 @@
|
||||
// ALWAYS fetch fresh details when editing from the status page to ensure modal has latest data.
|
||||
console.log(`[DEBUG status.js] Edit from status page for warranty ${warrantyId}. ALWAYS fetching fresh details from /api/debug/warranty/:id.`);
|
||||
showLoadingSpinner(); // Show spinner for this specific fetch
|
||||
const token = localStorage.getItem('auth_token') || (window.auth && window.auth.getToken());
|
||||
const token = authService.getToken();
|
||||
if (!token) throw new Error('Authentication token not available for fetching fresh details.');
|
||||
|
||||
const response = await fetch(`/api/debug/warranty/${warrantyId}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
|
||||
});
|
||||
const response = await apiService.request(`/api/debug/warranty/${warrantyId}`, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
@@ -4827,18 +4827,18 @@ html.dark-mode .warracker-footer a:hover {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: 360px) {
|
||||
.grid-view .product-photo-thumbnail img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.list-view .product-photo-thumbnail img {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.table-view .product-photo-thumbnail img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.product-photo-thumbnail {
|
||||
margin-right: 5px;
|
||||
@@ -1,35 +0,0 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
export default defineConfig({
|
||||
root: 'src',
|
||||
publicDir: resolve(__dirname, 'public'),
|
||||
build: {
|
||||
outDir: '../dist',
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, 'src/index.html'),
|
||||
login: resolve(__dirname, 'src/login.html'),
|
||||
register: resolve(__dirname, 'src/register.html'),
|
||||
status: resolve(__dirname, 'src/status.html'),
|
||||
settings: resolve(__dirname, 'src/settings-new.html'),
|
||||
resetPassword: resolve(__dirname, 'src/reset-password.html'),
|
||||
resetPasswordRequest: resolve(__dirname, 'src/reset-password-request.html'),
|
||||
authRedirect: resolve(__dirname, 'src/auth-redirect.html'),
|
||||
about: resolve(__dirname, 'src/about.html'),
|
||||
debugExport: resolve(__dirname, 'src/debug-export.html'),
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:5000',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||