mirror of
https://github.com/sassanix/Warracker.git
synced 2026-05-20 08:08:37 -05:00
Fix global warranties view, add Model Number field, and enhance modal tab responsiveness
* **Fixed:** * Global view on Index page now correctly shows warranties from all users, including archived ones. * Added `GET /api/warranties/global/archived` and unified global queries with correlated subqueries to avoid missing or collapsed rows. * Updated frontend logic to merge archived warranties from the new endpoint when Global scope and Status = “All.” * Bumped `script.js` and service worker cache to ensure clients receive updated logic. * Updated files: `backend/warranties_routes.py`, `frontend/script.js`, `frontend/sw.js`, `frontend/index.html`, `frontend/status.html`. * **Added:** * Introduced **Model Number** field to warranties. * Backend: Added `model_number` column, integrated into GET/POST/PUT routes. * Frontend: Added Model Number input in New/Edit modals and display on warranty cards. * Updated files: `backend/migrations/047_add_model_number_to_warranties.sql`, `backend/warranties_routes.py`, `frontend/index.html`, `frontend/status.html`, `frontend/script.js`, `locales/en/translation.json`. * **Enhanced:** * Improved **Add Warranty modal** tab alignment for responsive layouts (≤740px). * Adjusted tab label size and spacing to prevent wrapping while keeping icons and labels visible. * Ensured consistent five-step progress indicator across all breakpoints. * Updated file: `frontend/style.css`.
This commit is contained in:
@@ -1,4 +1,26 @@
|
||||
# Changelog
|
||||
## 0.10.1.15 - 2025-10-09
|
||||
|
||||
|
||||
### Fixed
|
||||
- Global view on Index page now shows warranties from all users as expected:
|
||||
- Backend: Added `GET /api/warranties/global/archived` and unified global queries to return complete sets with correlated subqueries for claim status; no accidental row collapse.
|
||||
- Frontend: When scope is Global and Status is "All", archived warranties are merged from the new global archived endpoint into the main list. Archived view in Global scope now uses the global archived endpoint.
|
||||
- Cache bust: Bumped `script.js` and service worker cache to ensure clients receive the updated logic.
|
||||
- Files: `backend/warranties_routes.py`, `frontend/script.js`, `frontend/sw.js`, `frontend/index.html`, `frontend/status.html`
|
||||
|
||||
### Added
|
||||
- Model Number field added to warranties:
|
||||
- Backend: Added `model_number` column and wired through GET/POST/PUT.
|
||||
- Frontend: New and Edit modals now include a Model Number input.
|
||||
- Cards: When present, Model Number displays on warranty cards (all views).
|
||||
- Files: `backend/migrations/047_add_model_number_to_warranties.sql`, `backend/warranties_routes.py`, `frontend/index.html`, `frontend/status.html`, `frontend/script.js`, `locales/en/translation.json`
|
||||
|
||||
### Enhanced
|
||||
- Add Warranty modal tabs responsive alignment:
|
||||
- Shrink tab label text and spacing at ≤740px to prevent wrapping and keep all five tabs aligned (Product, Warranty, Documents, Tags, Summary).
|
||||
- Maintain icons with labels; no text hiding. Uses five-step progress indicator consistent across breakpoints.
|
||||
- Files: `frontend/style.css`
|
||||
|
||||
## 0.10.1.14 - 2025-10-06
|
||||
|
||||
|
||||
+52
-10
@@ -17,14 +17,40 @@ DB_USER = os.environ.get('DB_USER', 'warranty_user')
|
||||
DB_PASSWORD = os.environ.get('DB_PASSWORD', 'warranty_password')
|
||||
|
||||
connection_pool = None # Global connection pool for this module
|
||||
# Track the PID that created the current pool to detect post-fork reuse
|
||||
pool_pid: Optional[int] = None
|
||||
|
||||
def _close_stale_pool_if_forked(current_pid: int) -> None:
|
||||
"""Close any existing pool if it was created in a different process.
|
||||
|
||||
Gunicorn with preload_app=True forks workers after the app (and pool) may be
|
||||
initialized. Psycopg2 connections/pools are not fork-safe. If we detect that
|
||||
the pool was created in a different PID, we proactively close it in this
|
||||
process so a fresh, per-process pool can be created.
|
||||
"""
|
||||
global connection_pool, pool_pid
|
||||
if connection_pool is not None and pool_pid is not None and pool_pid != current_pid:
|
||||
logger.warning(f"[DB_HANDLER] Detected PID change (pool pid {pool_pid} -> current pid {current_pid}). Closing stale pool and reinitializing...")
|
||||
try:
|
||||
# Close all connections owned by this (forked) process copy of the pool
|
||||
connection_pool.closeall()
|
||||
except Exception as close_err:
|
||||
logger.warning(f"[DB_HANDLER] Error while closing stale pool in forked process: {close_err}")
|
||||
finally:
|
||||
connection_pool = None
|
||||
pool_pid = None
|
||||
|
||||
def init_db_pool(max_retries=5, retry_delay=5):
|
||||
global connection_pool # Ensure we're modifying the global variable in this module
|
||||
global connection_pool, pool_pid # Ensure we're modifying the global variable in this module
|
||||
attempt = 0
|
||||
last_exception = None
|
||||
|
||||
if connection_pool is not None:
|
||||
logger.info("[DB_HANDLER] Database connection pool already initialized.")
|
||||
current_pid = os.getpid()
|
||||
# If a pool exists but was created in a different PID, ensure we drop it first
|
||||
_close_stale_pool_if_forked(current_pid)
|
||||
|
||||
if connection_pool is not None and pool_pid == current_pid:
|
||||
logger.info("[DB_HANDLER] Database connection pool already initialized for this process.")
|
||||
return connection_pool
|
||||
|
||||
while attempt < max_retries:
|
||||
@@ -43,6 +69,7 @@ def init_db_pool(max_retries=5, retry_delay=5):
|
||||
application_name='warracker_optimized' # Identify connections
|
||||
)
|
||||
logger.info("[DB_HANDLER] Database connection pool initialized successfully.")
|
||||
pool_pid = current_pid
|
||||
return connection_pool # Return the pool for external check if needed
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
@@ -58,18 +85,33 @@ def init_db_pool(max_retries=5, retry_delay=5):
|
||||
raise Exception("Unknown error creating database pool")
|
||||
|
||||
def get_db_connection():
|
||||
global connection_pool
|
||||
if connection_pool is None:
|
||||
logger.error("[DB_HANDLER] Database connection pool is None. Attempting to re-initialize.")
|
||||
init_db_pool() # Attempt to initialize it
|
||||
if connection_pool is None: # If still None after attempt
|
||||
logger.critical("[DB_HANDLER] CRITICAL: Database pool re-initialization failed.")
|
||||
global connection_pool, pool_pid
|
||||
current_pid = os.getpid()
|
||||
|
||||
# Detect and clean up any forked/stale pool
|
||||
_close_stale_pool_if_forked(current_pid)
|
||||
|
||||
if connection_pool is None or pool_pid != current_pid:
|
||||
if connection_pool is None:
|
||||
logger.info("[DB_HANDLER] Database connection pool not initialized in this process. Initializing now...")
|
||||
else:
|
||||
logger.warning("[DB_HANDLER] Pool PID mismatch detected. Reinitializing pool for current process...")
|
||||
init_db_pool() # Attempt to initialize it for this PID
|
||||
if connection_pool is None or pool_pid != current_pid: # If still invalid after attempt
|
||||
logger.critical("[DB_HANDLER] CRITICAL: Database pool initialization failed for current process.")
|
||||
raise Exception("Database connection pool is not initialized and could not be re-initialized.")
|
||||
try:
|
||||
return connection_pool.getconn()
|
||||
except Exception as e:
|
||||
logger.error(f"[DB_HANDLER] Error getting connection from pool: {e}")
|
||||
raise
|
||||
# As a last resort, try reinitializing once in case the pool was invalidated
|
||||
try:
|
||||
_close_stale_pool_if_forked(os.getpid())
|
||||
init_db_pool()
|
||||
return connection_pool.getconn()
|
||||
except Exception:
|
||||
# Re-raise original to preserve context
|
||||
raise
|
||||
|
||||
def release_db_connection(conn):
|
||||
global connection_pool
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
-- Migration: Add model_number column to warranties table
|
||||
-- Date: 2025-10-09
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'warranties'
|
||||
AND column_name = 'model_number'
|
||||
) THEN
|
||||
ALTER TABLE warranties ADD COLUMN model_number VARCHAR(255) DEFAULT NULL;
|
||||
RAISE NOTICE 'Added model_number column to warranties table';
|
||||
ELSE
|
||||
RAISE NOTICE 'model_number column already exists in warranties table';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Optional index to speed up searches/filtering by model number
|
||||
CREATE INDEX IF NOT EXISTS idx_warranties_model_number ON warranties(model_number);
|
||||
|
||||
|
||||
+132
-11
@@ -65,7 +65,7 @@ def get_warranties():
|
||||
w.purchase_price, w.user_id, w.created_at, w.updated_at, w.is_lifetime, w.vendor, w.warranty_type,
|
||||
w.warranty_duration_years, w.warranty_duration_months, w.warranty_duration_days, w.product_photo_path, w.currency,
|
||||
w.paperless_invoice_id, w.paperless_manual_id, w.paperless_photo_id, w.paperless_other_id,
|
||||
w.invoice_url, w.manual_url, w.other_document_url,
|
||||
w.invoice_url, w.manual_url, w.other_document_url, w.model_number,
|
||||
CASE
|
||||
WHEN COUNT(c.id) = 0 THEN 'NO_CLAIMS'
|
||||
WHEN BOOL_OR(c.status IN ('Submitted', 'In Progress')) THEN 'OPEN'
|
||||
@@ -134,7 +134,7 @@ def get_archived_warranties():
|
||||
w.purchase_price, w.user_id, w.created_at, w.updated_at, w.is_lifetime, w.vendor, w.warranty_type,
|
||||
w.warranty_duration_years, w.warranty_duration_months, w.warranty_duration_days, w.product_photo_path, w.currency,
|
||||
w.paperless_invoice_id, w.paperless_manual_id, w.paperless_photo_id, w.paperless_other_id,
|
||||
w.invoice_url, w.manual_url, w.other_document_url,
|
||||
w.invoice_url, w.manual_url, w.other_document_url, w.model_number,
|
||||
CASE
|
||||
WHEN COUNT(c.id) = 0 THEN 'NO_CLAIMS'
|
||||
WHEN BOOL_OR(c.status IN ('Submitted', 'In Progress')) THEN 'OPEN'
|
||||
@@ -249,6 +249,7 @@ def add_warranty():
|
||||
notes = request.form.get('notes', '')
|
||||
vendor = request.form.get('vendor', None)
|
||||
warranty_type = request.form.get('warranty_type', None)
|
||||
model_number = request.form.get('model_number', None)
|
||||
|
||||
# Get URL fields for documents
|
||||
invoice_url = request.form.get('invoice_url', None)
|
||||
@@ -440,16 +441,16 @@ def add_warranty():
|
||||
invoice_path, manual_path, other_document_path, product_url, purchase_price, user_id, is_lifetime, notes, vendor, warranty_type,
|
||||
warranty_duration_years, warranty_duration_months, warranty_duration_days, product_photo_path, currency,
|
||||
paperless_invoice_id, paperless_manual_id, paperless_photo_id, paperless_other_id,
|
||||
invoice_url, manual_url, other_document_url
|
||||
invoice_url, manual_url, other_document_url, model_number
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
''', (
|
||||
product_name, purchase_date, expiration_date,
|
||||
db_invoice_path, db_manual_path, db_other_document_path, product_url, purchase_price, user_id, is_lifetime, notes, vendor, warranty_type,
|
||||
warranty_duration_years, warranty_duration_months, warranty_duration_days, db_product_photo_path, currency,
|
||||
paperless_invoice_id, paperless_manual_id, paperless_photo_id, paperless_other_id,
|
||||
invoice_url, manual_url, other_document_url
|
||||
invoice_url, manual_url, other_document_url, model_number
|
||||
))
|
||||
warranty_id = cur.fetchone()[0]
|
||||
|
||||
@@ -711,6 +712,7 @@ def update_warranty(warranty_id):
|
||||
notes = request.form.get('notes', None)
|
||||
vendor = request.form.get('vendor', None)
|
||||
warranty_type = request.form.get('warranty_type', None)
|
||||
model_number = request.form.get('model_number', None)
|
||||
|
||||
# Get URL fields for documents
|
||||
invoice_url = request.form.get('invoice_url', None)
|
||||
@@ -1003,7 +1005,8 @@ def update_warranty(warranty_id):
|
||||
'purchase_price': purchase_price,
|
||||
'vendor': vendor,
|
||||
'warranty_type': warranty_type,
|
||||
'currency': currency
|
||||
'currency': currency,
|
||||
'model_number': model_number
|
||||
}
|
||||
sql_fields = []
|
||||
sql_values = []
|
||||
@@ -1537,6 +1540,7 @@ def get_global_warranties():
|
||||
conn = get_db_connection()
|
||||
with conn.cursor() as cur:
|
||||
# Get all warranties from all users with user information (exclude archived for default view)
|
||||
# Use correlated subqueries for claim status to avoid GROUP BY collapsing or miscounting
|
||||
cur.execute('''
|
||||
SELECT
|
||||
w.id, w.product_name, w.purchase_date, w.expiration_date, w.invoice_path, w.manual_path, w.other_document_path,
|
||||
@@ -1546,15 +1550,19 @@ def get_global_warranties():
|
||||
w.invoice_url, w.manual_url, w.other_document_url,
|
||||
u.username, u.email, u.first_name, u.last_name,
|
||||
CASE
|
||||
WHEN COUNT(c.id) = 0 THEN 'NO_CLAIMS'
|
||||
WHEN BOOL_OR(c.status IN ('Submitted', 'In Progress')) THEN 'OPEN'
|
||||
ELSE 'FINISHED'
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM warranty_claims c
|
||||
WHERE c.warranty_id = w.id AND c.status IN ('Submitted', 'In Progress')
|
||||
) THEN 'OPEN'
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM warranty_claims c
|
||||
WHERE c.warranty_id = w.id
|
||||
) THEN 'FINISHED'
|
||||
ELSE 'NO_CLAIMS'
|
||||
END AS claim_status_summary
|
||||
FROM warranties w
|
||||
JOIN users u ON w.user_id = u.id
|
||||
LEFT JOIN warranty_claims c ON w.id = c.warranty_id
|
||||
WHERE w.archived_at IS NULL
|
||||
GROUP BY w.id, u.id
|
||||
ORDER BY u.username, CASE WHEN w.is_lifetime THEN 1 ELSE 0 END, w.expiration_date NULLS LAST, w.product_name
|
||||
''')
|
||||
|
||||
@@ -1614,6 +1622,119 @@ def get_global_warranties():
|
||||
release_db_connection(conn)
|
||||
|
||||
|
||||
@warranties_bp.route('/warranties/global/archived', methods=['GET'])
|
||||
@token_required
|
||||
def get_global_warranties_archived():
|
||||
"""Get archived warranties from all users (public view for all authenticated users)"""
|
||||
conn = None
|
||||
try:
|
||||
# Check if global view is enabled for this user
|
||||
user_is_admin = request.user.get('is_admin', False)
|
||||
|
||||
conn = get_db_connection()
|
||||
with conn.cursor() as cur:
|
||||
# Get both global view settings
|
||||
cur.execute("SELECT key, value FROM site_settings WHERE key IN ('global_view_enabled', 'global_view_admin_only')")
|
||||
settings = {row[0]: row[1] for row in cur.fetchall()}
|
||||
|
||||
# Check if global view is enabled at all
|
||||
global_view_enabled = settings.get('global_view_enabled', 'true').lower() == 'true'
|
||||
if not global_view_enabled:
|
||||
return jsonify({"error": "Global view is disabled by administrator"}), 403
|
||||
|
||||
# Check if global view is restricted to admins only
|
||||
admin_only = settings.get('global_view_admin_only', 'false').lower() == 'true'
|
||||
if admin_only and not user_is_admin:
|
||||
return jsonify({"error": "Global view is restricted to administrators only"}), 403
|
||||
|
||||
# Release the connection since we'll get a new one below
|
||||
release_db_connection(conn)
|
||||
conn = None
|
||||
conn = get_db_connection()
|
||||
with conn.cursor() as cur:
|
||||
# Get archived warranties from all users with user information
|
||||
# Use correlated subqueries for claim status to avoid GROUP BY collapsing or miscounting
|
||||
cur.execute('''
|
||||
SELECT
|
||||
w.id, w.product_name, w.purchase_date, w.expiration_date, w.invoice_path, w.manual_path, w.other_document_path,
|
||||
w.product_url, w.notes, w.purchase_price, w.user_id, w.created_at, w.updated_at, w.is_lifetime,
|
||||
w.vendor, w.warranty_type, w.warranty_duration_years, w.warranty_duration_months, w.warranty_duration_days, w.product_photo_path, w.currency,
|
||||
w.paperless_invoice_id, w.paperless_manual_id, w.paperless_photo_id, w.paperless_other_id,
|
||||
w.invoice_url, w.manual_url, w.other_document_url,
|
||||
u.username, u.email, u.first_name, u.last_name,
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM warranty_claims c
|
||||
WHERE c.warranty_id = w.id AND c.status IN ('Submitted', 'In Progress')
|
||||
) THEN 'OPEN'
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM warranty_claims c
|
||||
WHERE c.warranty_id = w.id
|
||||
) THEN 'FINISHED'
|
||||
ELSE 'NO_CLAIMS'
|
||||
END AS claim_status_summary
|
||||
FROM warranties w
|
||||
JOIN users u ON w.user_id = u.id
|
||||
WHERE w.archived_at IS NOT NULL
|
||||
ORDER BY w.archived_at DESC NULLS LAST, w.updated_at DESC NULLS LAST, w.product_name
|
||||
''')
|
||||
|
||||
warranties = cur.fetchall()
|
||||
columns = [desc[0] for desc in cur.description]
|
||||
warranties_list = []
|
||||
|
||||
for row in warranties:
|
||||
warranty_dict = dict(zip(columns, row))
|
||||
# Convert date objects to ISO format strings for JSON serialization
|
||||
for key, value in warranty_dict.items():
|
||||
if isinstance(value, (datetime, date)):
|
||||
warranty_dict[key] = value.isoformat()
|
||||
# Convert Decimal objects to float for JSON serialization
|
||||
elif isinstance(value, Decimal):
|
||||
warranty_dict[key] = float(value)
|
||||
|
||||
# Get serial numbers for this warranty
|
||||
warranty_id = warranty_dict['id']
|
||||
cur.execute('SELECT serial_number FROM serial_numbers WHERE warranty_id = %s', (warranty_id,))
|
||||
serial_numbers = [row[0] for row in cur.fetchall()]
|
||||
warranty_dict['serial_numbers'] = serial_numbers
|
||||
|
||||
# Get tags for this warranty
|
||||
cur.execute('''
|
||||
SELECT t.id, t.name, t.color
|
||||
FROM tags t
|
||||
JOIN warranty_tags wt ON t.id = wt.tag_id
|
||||
WHERE wt.warranty_id = %s
|
||||
ORDER BY t.name
|
||||
''', (warranty_id,))
|
||||
tags = [{'id': t[0], 'name': t[1], 'color': t[2]} for t in cur.fetchall()]
|
||||
warranty_dict['tags'] = tags
|
||||
|
||||
# Add user display name for better UI
|
||||
first_name = warranty_dict.get('first_name', '').strip() if warranty_dict.get('first_name') else ''
|
||||
last_name = warranty_dict.get('last_name', '').strip() if warranty_dict.get('last_name') else ''
|
||||
username = warranty_dict.get('username', '').strip() if warranty_dict.get('username') else ''
|
||||
|
||||
if first_name and last_name:
|
||||
warranty_dict['user_display_name'] = f"{first_name} {last_name}"
|
||||
elif first_name:
|
||||
warranty_dict['user_display_name'] = first_name
|
||||
elif username:
|
||||
warranty_dict['user_display_name'] = username
|
||||
else:
|
||||
warranty_dict['user_display_name'] = 'Unknown User'
|
||||
|
||||
warranties_list.append(warranty_dict)
|
||||
|
||||
return jsonify(warranties_list)
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving archived global warranties: {e}")
|
||||
return jsonify({"error": "Failed to retrieve archived global warranties"}), 500
|
||||
finally:
|
||||
if conn:
|
||||
release_db_connection(conn)
|
||||
|
||||
|
||||
@warranties_bp.route('/currencies', methods=['GET'])
|
||||
@token_required
|
||||
def get_currencies():
|
||||
|
||||
+2
-2
@@ -323,7 +323,7 @@
|
||||
<!-- Hero Section -->
|
||||
<div class="about-hero">
|
||||
<h1><i class="fas fa-shield-alt"></i> Warracker</h1>
|
||||
<div class="version" id="versionDisplay" data-i18n="about.version">Version v0.10.1.14</div>
|
||||
<div class="version" id="versionDisplay" data-i18n="about.version">Version v0.10.1.15</div>
|
||||
<p class="description" data-i18n="about.description">
|
||||
A comprehensive warranty management system designed to help you track, organize, and manage all your product warranties in one secure, user-friendly platform.
|
||||
</p>
|
||||
@@ -421,7 +421,7 @@
|
||||
// Update version display dynamically
|
||||
const versionDisplay = document.getElementById('versionDisplay');
|
||||
if (versionDisplay && window.i18next) {
|
||||
const currentVersion = '0.10.1.14'; // This should match version-checker.js
|
||||
const currentVersion = '0.10.1.15'; // This should match version-checker.js
|
||||
versionDisplay.textContent = window.i18next.t('about.version') + ' v' + currentVersion;
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -415,6 +415,10 @@
|
||||
<input type="text" name="serial_numbers[]" class="form-control" style="margin-bottom: 8px;" data-i18n-placeholder="warranties.enter_serial_number" placeholder="Enter serial number">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="modelNumber" data-i18n="warranties.model_number_optional">Model Number (Optional)</label>
|
||||
<input type="text" id="modelNumber" name="model_number" class="form-control" data-i18n-placeholder="warranties.model_number_placeholder" placeholder="e.g. SM-G991U1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vendor" data-i18n="warranties.vendor_optional">Vendor (Optional)</label>
|
||||
<input type="text" id="vendor" name="vendor" class="form-control" data-i18n-placeholder="warranties.vendor_placeholder" placeholder="e.g. Amazon, Best Buy, etc.">
|
||||
@@ -819,6 +823,10 @@
|
||||
<!-- Serial number inputs will be added dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editModelNumber" data-i18n="warranties.model_number_optional">Model Number (Optional)</label>
|
||||
<input type="text" id="editModelNumber" name="model_number" class="form-control" data-i18n-placeholder="warranties.model_number_placeholder" placeholder="e.g. SM-G991U1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editVendor" data-i18n="warranties.vendor_optional">Vendor (Optional)</label>
|
||||
<input type="text" id="editVendor" name="vendor" class="form-control" data-i18n-placeholder="warranties.vendor_placeholder" placeholder="e.g. Amazon, Best Buy, etc.">
|
||||
@@ -1318,7 +1326,7 @@
|
||||
</div>
|
||||
|
||||
<script src="auth.js?v=20250119001"></script>
|
||||
<script src="script.js?v=20250119001"></script>
|
||||
<script src="script.js?v=20250119002"></script>
|
||||
<script>
|
||||
// Lightweight UI logic for Filter/Sort popovers (no change to core logic)
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
+33
-4
@@ -936,6 +936,9 @@ let formTabs = []; // Changed from const to let, initialized as empty
|
||||
if (notesModalWarrantyObj.warranty_type) {
|
||||
formData.append('warranty_type', notesModalWarrantyObj.warranty_type);
|
||||
}
|
||||
if (typeof notesModalWarrantyObj.model_number !== 'undefined' && notesModalWarrantyObj.model_number !== null) {
|
||||
formData.append('model_number', notesModalWarrantyObj.model_number);
|
||||
}
|
||||
if (notesModalWarrantyObj.serial_numbers && Array.isArray(notesModalWarrantyObj.serial_numbers)) {
|
||||
notesModalWarrantyObj.serial_numbers.forEach(sn => {
|
||||
if (sn && sn.trim() !== '') {
|
||||
@@ -953,6 +956,11 @@ let formTabs = []; // Changed from const to let, initialized as empty
|
||||
formData.append('tag_ids', JSON.stringify([]));
|
||||
}
|
||||
formData.append('notes', notesValue);
|
||||
// Also include model number value from edit input if present on DOM
|
||||
const editModelNumberInput = document.getElementById('editModelNumber');
|
||||
if (editModelNumberInput && editModelNumberInput.value.trim() !== '') {
|
||||
formData.set('model_number', editModelNumberInput.value.trim());
|
||||
}
|
||||
const response = await fetch(`/api/warranties/${warrantyId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
@@ -2182,10 +2190,10 @@ async function loadWarranties(isAuthenticated) { // Added isAuthenticated parame
|
||||
|
||||
// Use the appropriate API endpoint based on saved preference
|
||||
const baseUrl = window.location.origin;
|
||||
// If status is 'archived', use archived endpoint (user-scoped only for now)
|
||||
// If status is 'archived', use archived endpoint (support global vs personal)
|
||||
const isArchivedView = currentFilters && currentFilters.status === 'archived';
|
||||
const apiUrl = isArchivedView
|
||||
? `${baseUrl}/api/warranties/archived`
|
||||
? (shouldUseGlobalView ? `${baseUrl}/api/warranties/global/archived` : `${baseUrl}/api/warranties/archived`)
|
||||
: (shouldUseGlobalView ? `${baseUrl}/api/warranties/global` : `${baseUrl}/api/warranties`);
|
||||
|
||||
console.log(`[DEBUG] Using API endpoint based on saved preference '${savedScope}', archivedView=${isArchivedView}: ${apiUrl}`);
|
||||
@@ -2235,9 +2243,9 @@ async function loadWarranties(isAuthenticated) { // Added isAuthenticated parame
|
||||
// Optionally merge archived items into the "All" view (only in personal scope)
|
||||
let combinedData = Array.isArray(data) ? data : [];
|
||||
lastLoadedIncludesArchived = false;
|
||||
if (!shouldUseGlobalView && !isArchivedView && currentFilters && currentFilters.status === 'all') {
|
||||
if (!isArchivedView && currentFilters && currentFilters.status === 'all') {
|
||||
try {
|
||||
const archivedUrl = `${baseUrl}/api/warranties/archived`;
|
||||
const archivedUrl = shouldUseGlobalView ? `${baseUrl}/api/warranties/global/archived` : `${baseUrl}/api/warranties/archived`;
|
||||
const archivedResp = await fetch(archivedUrl, options);
|
||||
if (archivedResp.ok) {
|
||||
const archivedData = await archivedResp.json();
|
||||
@@ -2709,6 +2717,7 @@ async function renderWarranties(warrantiesToRender) {
|
||||
</div>
|
||||
` : ''}
|
||||
` : ''}
|
||||
${warranty.model_number ? `<div><i class="fas fa-tag"></i> ${window.i18next ? window.i18next.t('warranties.model_number') : 'Model Number'}: <span>${warranty.model_number}</span></div>` : ''}
|
||||
${warranty.vendor ? `<div><i class="fas fa-store"></i> ${window.i18next ? window.i18next.t('warranties.vendor') : 'Vendor'}: <span>${warranty.vendor}</span></div>` : ''}
|
||||
${warranty.warranty_type ? `<div><i class="fas fa-shield-alt"></i> ${window.i18next ? window.i18next.t('warranties.type') : 'Type'}: <span>${warranty.warranty_type}</span></div>` : ''}
|
||||
</div>
|
||||
@@ -2771,6 +2780,7 @@ async function renderWarranties(warrantiesToRender) {
|
||||
</div>
|
||||
` : ''}
|
||||
` : ''}
|
||||
${warranty.model_number ? `<div><i class=\"fas fa-tag\"></i> ${window.i18next ? window.i18next.t('warranties.model_number') : 'Model Number'}: <span>${warranty.model_number}</span></div>` : ''}
|
||||
${warranty.vendor ? `<div><i class="fas fa-store"></i> ${window.i18next ? window.i18next.t('warranties.vendor') : 'Vendor'}: <span>${warranty.vendor}</span></div>` : ''}
|
||||
${warranty.warranty_type ? `<div><i class="fas fa-shield-alt"></i> ${window.i18next ? window.i18next.t('warranties.type') : 'Type'}: <span>${warranty.warranty_type}</span></div>` : ''}
|
||||
</div>
|
||||
@@ -2833,6 +2843,7 @@ async function renderWarranties(warrantiesToRender) {
|
||||
</div>
|
||||
` : ''}
|
||||
` : ''}
|
||||
${warranty.model_number ? `<div><i class=\"fas fa-tag\"></i> ${window.i18next ? window.i18next.t('warranties.model_number') : 'Model Number'}: <span>${warranty.model_number}</span></div>` : ''}
|
||||
${warranty.vendor ? `<div><i class="fas fa-store"></i> ${window.i18next ? window.i18next.t('warranties.vendor') : 'Vendor'}: <span>${warranty.vendor}</span></div>` : ''}
|
||||
${warranty.warranty_type ? `<div><i class="fas fa-shield-alt"></i> ${window.i18next ? window.i18next.t('warranties.type') : 'Type'}: <span>${warranty.warranty_type}</span></div>` : ''}
|
||||
</div>
|
||||
@@ -3205,6 +3216,10 @@ async function openEditModal(warranty) {
|
||||
// Populate form fields
|
||||
document.getElementById('editProductName').value = warranty.product_name;
|
||||
document.getElementById('editProductUrl').value = warranty.product_url || '';
|
||||
const editModelNumberInput = document.getElementById('editModelNumber');
|
||||
if (editModelNumberInput) {
|
||||
editModelNumberInput.value = warranty.model_number || '';
|
||||
}
|
||||
document.getElementById('editPurchaseDate').value = warranty.purchase_date.split('T')[0];
|
||||
// Populate new duration fields
|
||||
document.getElementById('editWarrantyDurationYears').value = warranty.warranty_duration_years || 0;
|
||||
@@ -3890,6 +3905,11 @@ async function handleFormSubmit(event) { // Made async to properly await paperle
|
||||
|
||||
// Create form data object
|
||||
const formData = new FormData(warrantyForm);
|
||||
// Ensure model_number is included if present
|
||||
const modelNumberInput = document.getElementById('modelNumber');
|
||||
if (modelNumberInput && modelNumberInput.value.trim() !== '') {
|
||||
formData.set('model_number', modelNumberInput.value.trim());
|
||||
}
|
||||
|
||||
// Handle warranty type - use custom value if "other" is selected
|
||||
const warrantyTypeSelect = document.getElementById('warrantyType');
|
||||
@@ -6359,6 +6379,15 @@ function saveWarranty() {
|
||||
formData.append('notes', '');
|
||||
}
|
||||
|
||||
// Add model number to form data (optional)
|
||||
const editModelNumber = document.getElementById('editModelNumber');
|
||||
if (editModelNumber && editModelNumber.value.trim() !== '') {
|
||||
formData.append('model_number', editModelNumber.value.trim());
|
||||
} else {
|
||||
// Explicitly clear if empty
|
||||
formData.append('model_number', '');
|
||||
}
|
||||
|
||||
// Add vendor/retailer to form data
|
||||
const editVendorInput = document.getElementById('editVendor'); // Use the correct ID
|
||||
formData.append('vendor', editVendorInput ? editVendorInput.value.trim() : ''); // Use the correct variable
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<script src="js/i18n.js?v=20250119001"></script>
|
||||
<!-- Load fix for auth buttons -->
|
||||
<script src="fix-auth-buttons-loader.js?v=20250119001"></script>
|
||||
<script src="script.js?v=20250119001" defer></script> <!-- Added script.js -->
|
||||
<script src="script.js?v=20250119002" defer></script> <!-- Added script.js -->
|
||||
<script src="status.js?v=20250119001" defer></script> <!-- Status page specific functionality -->
|
||||
<style>
|
||||
.user-menu {
|
||||
@@ -551,6 +551,10 @@
|
||||
<!-- Serial number inputs will be added dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editModelNumber" data-i18n="warranties.model_number_optional">Model Number (Optional)</label>
|
||||
<input type="text" id="editModelNumber" name="model_number" class="form-control" data-i18n-placeholder="warranties.model_number_placeholder" placeholder="e.g. SM-G991U1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editVendor" data-i18n="warranties.vendor_optional">Vendor (Optional)</label>
|
||||
<input type="text" id="editVendor" name="vendor" class="form-control" data-i18n-placeholder="warranties.vendor_placeholder" placeholder="e.g. Amazon, Best Buy, etc.">
|
||||
|
||||
+24
-35
@@ -304,35 +304,16 @@ header .container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-tabs::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
height: 2px;
|
||||
background-color: var(--secondary-color);
|
||||
transition: all 0.3s ease;
|
||||
width: 25%;
|
||||
z-index: 1;
|
||||
}
|
||||
.form-tabs::before { display: none; }
|
||||
|
||||
.form-tabs[data-step="0"]::before {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.form-tabs[data-step="1"]::before {
|
||||
left: 25%;
|
||||
}
|
||||
|
||||
.form-tabs[data-step="2"]::before {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.form-tabs[data-step="3"]::before {
|
||||
left: 75%;
|
||||
}
|
||||
/* Progress handled via ::after below */
|
||||
|
||||
.form-tab {
|
||||
flex: 1;
|
||||
flex: 1 1 20%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
text-align: center;
|
||||
padding: 15px 10px;
|
||||
cursor: pointer;
|
||||
@@ -340,7 +321,9 @@ header .container {
|
||||
color: var(--dark-gray);
|
||||
transition: color 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
width: 25%; /* Ensure each tab is exactly 25% width */
|
||||
width: 20%; /* Five tabs fit on one row */
|
||||
min-width: 0; /* Allow shrinking without wrapping inner content */
|
||||
white-space: nowrap; /* Prevent label wrapping */
|
||||
}
|
||||
|
||||
.form-tab:hover {
|
||||
@@ -430,16 +413,22 @@ input.invalid {
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.form-tabs[data-step="0"]::after {
|
||||
width: calc(100% / 3);
|
||||
.form-tabs[data-step="0"]::after { width: calc(100% / 5); }
|
||||
.form-tabs[data-step="1"]::after { width: calc(200% / 5); }
|
||||
.form-tabs[data-step="2"]::after { width: calc(300% / 5); }
|
||||
.form-tabs[data-step="3"]::after { width: calc(400% / 5); }
|
||||
.form-tabs[data-step="4"]::after { width: 100%; }
|
||||
|
||||
@media (max-width: 788px) {
|
||||
.form-tab { padding: 12px 6px; }
|
||||
.form-tab i { margin-right: 6px; }
|
||||
}
|
||||
|
||||
.form-tabs[data-step="1"]::after {
|
||||
width: calc(200% / 3);
|
||||
}
|
||||
|
||||
.form-tabs[data-step="2"]::after {
|
||||
width: 100%;
|
||||
/* On narrow screens, shrink labels to keep tabs uniform (keep text visible) */
|
||||
@media (max-width: 740px) {
|
||||
.form-tab { padding: 10px 4px; gap: 4px; }
|
||||
.form-tab i { margin-right: 4px; font-size: 0.95rem; }
|
||||
.form-tab span { display: inline; font-size: 0.85rem; }
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
const CACHE_NAME = 'warracker-cache-v20250119005';
|
||||
const CACHE_NAME = 'warracker-cache-v20250119006';
|
||||
const urlsToCache = [
|
||||
'./',
|
||||
'./index.html',
|
||||
@@ -8,7 +8,7 @@ const urlsToCache = [
|
||||
'./settings-styles.css?v=20250119001',
|
||||
'./header-fix.css?v=20250119001',
|
||||
'./mobile-header.css?v=20250119002',
|
||||
'./script.js?v=20250119001',
|
||||
'./script.js?v=20250119002',
|
||||
'./auth.js?v=20250119001',
|
||||
'./settings-new.js?v=20250119001',
|
||||
'./status.js?v=20250119001',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Version checker for Warracker
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const currentVersion = '0.10.1.14'; // Current version of the application
|
||||
const currentVersion = '0.10.1.15'; // Current version of the application
|
||||
const updateStatus = document.getElementById('updateStatus');
|
||||
const updateLink = document.getElementById('updateLink');
|
||||
const versionDisplay = document.getElementById('versionDisplay');
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
"warranty_ends": "Warranty Ends",
|
||||
"price": "Price",
|
||||
"serial_number": "Serial Number",
|
||||
"model_number": "Model Number",
|
||||
"vendor": "Vendor",
|
||||
"type": "Type",
|
||||
"owner": "Owner",
|
||||
@@ -211,7 +212,9 @@
|
||||
"months_placeholder": "Months",
|
||||
"days_placeholder": "Days",
|
||||
"add_any_notes_placeholder": "Add any notes about this warranty...",
|
||||
"add_serial_number": "Add another serial number"
|
||||
"add_serial_number": "Add another serial number",
|
||||
"model_number_optional": "Model Number (Optional)",
|
||||
"model_number_placeholder": "e.g. SM-G991U1"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Warracker - Settings",
|
||||
|
||||
Reference in New Issue
Block a user